Skip to content

Commit 0ee177f

Browse files
authored
feat(ws): prepare frontend calls to pause and start endpoints (#346)
Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
1 parent 79fe52d commit 0ee177f

File tree

10 files changed

+291
-44
lines changed

10 files changed

+291
-44
lines changed

workspaces/frontend/src/app/context/useNotebookAPIState.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
listWorkspaces,
1515
patchWorkspace,
1616
patchWorkspaceKind,
17+
pauseWorkspace,
18+
startWorkspace,
1719
updateWorkspace,
1820
updateWorkspaceKind,
1921
} from '~/shared/api/notebookService';
@@ -33,6 +35,8 @@ import {
3335
mockListWorkspaces,
3436
mockPatchWorkspace,
3537
mockPatchWorkspaceKind,
38+
mockPauseWorkspace,
39+
mockStartWorkspace,
3640
mockUpdateWorkspace,
3741
mockUpdateWorkspaceKind,
3842
} from '~/shared/mock/mockNotebookService';
@@ -45,7 +49,7 @@ const useNotebookAPIState = (
4549
hostPath: string | null,
4650
): [apiState: NotebookAPIState, refreshAPIState: () => void] => {
4751
const createApi = React.useCallback(
48-
(path: string) => ({
52+
(path: string): NotebookAPIs => ({
4953
// Health
5054
getHealthCheck: getHealthCheck(path),
5155
// Namespace
@@ -58,6 +62,8 @@ const useNotebookAPIState = (
5862
updateWorkspace: updateWorkspace(path),
5963
patchWorkspace: patchWorkspace(path),
6064
deleteWorkspace: deleteWorkspace(path),
65+
pauseWorkspace: pauseWorkspace(path),
66+
startWorkspace: startWorkspace(path),
6167
// WorkspaceKind
6268
listWorkspaceKinds: listWorkspaceKinds(path),
6369
createWorkspaceKind: createWorkspaceKind(path),
@@ -70,7 +76,7 @@ const useNotebookAPIState = (
7076
);
7177

7278
const createMockApi = React.useCallback(
73-
(path: string) => ({
79+
(path: string): NotebookAPIs => ({
7480
// Health
7581
getHealthCheck: mockGetHealthCheck(path),
7682
// Namespace
@@ -83,6 +89,8 @@ const useNotebookAPIState = (
8389
updateWorkspace: mockUpdateWorkspace(path),
8490
patchWorkspace: mockPatchWorkspace(path),
8591
deleteWorkspace: mockDeleteWorkspace(path),
92+
pauseWorkspace: mockPauseWorkspace(path),
93+
startWorkspace: mockStartWorkspace(path),
8694
// WorkspaceKind
8795
listWorkspaceKinds: mockListWorkspaceKinds(path),
8896
createWorkspaceKind: mockCreateWorkspaceKind(path),

workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
} from '~/app/actions/WorkspaceKindsActions';
4343
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
4444
import useWorkspaces from '~/app/hooks/useWorkspaces';
45+
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
4546
import { WorkspaceConnectAction } from '~/app/pages/Workspaces/WorkspaceConnectAction';
4647
import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal';
4748
import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal';
@@ -89,8 +90,10 @@ export const Workspaces: React.FunctionComponent = () => {
8990
lastActivity: 'Last Activity',
9091
};
9192

93+
const { api } = useNotebookAPI();
9294
const { selectedNamespace } = useNamespaceContext();
93-
const [initialWorkspaces, initialWorkspacesLoaded] = useWorkspaces(selectedNamespace);
95+
const [initialWorkspaces, initialWorkspacesLoaded, , initialWorkspacesRefresh] =
96+
useWorkspaces(selectedNamespace);
9497
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
9598
const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState<string[]>([]);
9699
const [selectedWorkspace, setSelectedWorkspace] = React.useState<Workspace | null>(null);
@@ -322,6 +325,19 @@ export const Workspaces: React.FunctionComponent = () => {
322325
onClose={onCloseActionAlertDialog}
323326
isOpen={isActionAlertModalOpen}
324327
workspace={selectedWorkspace}
328+
onActionDone={() => {
329+
initialWorkspacesRefresh();
330+
}}
331+
onStart={async () => {
332+
if (!selectedWorkspace) {
333+
return;
334+
}
335+
336+
await api.startWorkspace({}, selectedNamespace, selectedWorkspace.name);
337+
}}
338+
onUpdateAndStart={async () => {
339+
// TODO: implement update and stop
340+
}}
325341
/>
326342
);
327343
case ActionType.Restart:
@@ -338,6 +354,18 @@ export const Workspaces: React.FunctionComponent = () => {
338354
onClose={onCloseActionAlertDialog}
339355
isOpen={isActionAlertModalOpen}
340356
workspace={selectedWorkspace}
357+
onActionDone={() => {
358+
initialWorkspacesRefresh();
359+
}}
360+
onStop={async () => {
361+
if (!selectedWorkspace) {
362+
return;
363+
}
364+
await api.pauseWorkspace({}, selectedNamespace, selectedWorkspace.name);
365+
}}
366+
onUpdateAndStop={async () => {
367+
// TODO: implement update and stop
368+
}}
341369
/>
342370
);
343371
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as React from 'react';
2+
import { Button } from '@patternfly/react-core';
3+
4+
type ActionButtonProps = {
5+
action: string;
6+
titleOnLoading: string;
7+
onClick: () => Promise<void>;
8+
} & Omit<React.ComponentProps<typeof Button>, 'onClick'>;
9+
10+
export const ActionButton: React.FC<ActionButtonProps> = ({
11+
action,
12+
titleOnLoading,
13+
onClick,
14+
...props
15+
}) => {
16+
const [isLoading, setIsLoading] = React.useState(false);
17+
18+
const handleClick = React.useCallback(async () => {
19+
setIsLoading(true);
20+
try {
21+
await onClick();
22+
} finally {
23+
setIsLoading(false);
24+
}
25+
}, [onClick]);
26+
27+
return (
28+
<Button
29+
{...props}
30+
spinnerAriaLabel={`Executing action '${action}'`}
31+
spinnerAriaValueText={action}
32+
onClick={handleClick}
33+
isLoading={isLoading}
34+
isDisabled={isLoading || props.isDisabled}
35+
>
36+
{isLoading ? titleOnLoading : props.children}
37+
</Button>
38+
);
39+
};

workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,73 @@ import {
99
} from '@patternfly/react-core';
1010
import { Workspace } from '~/shared/api/backendApiTypes';
1111
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
12+
import { ActionButton } from '~/app/pages/Workspaces/workspaceActions/ActionButton';
1213

1314
interface StartActionAlertProps {
1415
onClose: () => void;
1516
isOpen: boolean;
1617
workspace: Workspace | null;
18+
onStart: () => Promise<void>;
19+
onUpdateAndStart: () => Promise<void>;
20+
onActionDone: () => void;
1721
}
1822

23+
type StartAction = 'start' | 'updateAndStart';
24+
1925
export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
2026
onClose,
2127
isOpen,
2228
workspace,
29+
onStart,
30+
onUpdateAndStart,
31+
onActionDone,
2332
}) => {
24-
const handleClick = (isUpdate = false) => {
25-
if (isUpdate) {
26-
console.log(`Update ${workspace?.name}`);
33+
const [actionOnGoing, setActionOnGoing] = React.useState<StartAction | null>(null);
34+
35+
const executeAction = React.useCallback(
36+
async (args: { action: StartAction; callback: () => Promise<void> }) => {
37+
setActionOnGoing(args.action);
38+
try {
39+
return await args.callback();
40+
} finally {
41+
setActionOnGoing(null);
42+
}
43+
},
44+
[],
45+
);
46+
47+
const handleStart = React.useCallback(async () => {
48+
try {
49+
await executeAction({ action: 'start', callback: onStart });
50+
// TODO: alert user about success
51+
console.info('Workspace started successfully');
52+
onActionDone();
53+
onClose();
54+
} catch (error) {
55+
// TODO: alert user about error
56+
console.error('Error starting workspace:', error);
2757
}
28-
console.log(`Start ${workspace?.name}`);
29-
onClose();
30-
};
58+
}, [executeAction, onActionDone, onClose, onStart]);
59+
60+
// TODO: combine handleStart and handleUpdateAndStart if they end up being similar
61+
const handleUpdateAndStart = React.useCallback(async () => {
62+
try {
63+
await executeAction({ action: 'updateAndStart', callback: onUpdateAndStart });
64+
// TODO: alert user about success
65+
console.info('Workspace updated and started successfully');
66+
onActionDone();
67+
onClose();
68+
} catch (error) {
69+
// TODO: alert user about error
70+
console.error('Error updating and stopping workspace:', error);
71+
}
72+
}, [executeAction, onActionDone, onClose, onUpdateAndStart]);
73+
74+
const shouldShowActionButton = React.useCallback(
75+
(action: StartAction) => !actionOnGoing || actionOnGoing === action,
76+
[actionOnGoing],
77+
);
78+
3179
return (
3280
<Modal
3381
variant="medium"
@@ -44,13 +92,30 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
4492
{workspace && <WorkspaceRedirectInformationView kind={workspace.workspaceKind.name} />}
4593
</ModalBody>
4694
<ModalFooter>
47-
<Button onClick={() => handleClick(true)}>Update and Start</Button>
48-
<Button onClick={() => handleClick(false)} variant="secondary">
49-
Start
50-
</Button>
51-
<Button variant="link" onClick={onClose}>
52-
Cancel
53-
</Button>
95+
{shouldShowActionButton('updateAndStart') && (
96+
<ActionButton
97+
action="Update and Start"
98+
titleOnLoading="Starting ..."
99+
onClick={() => handleUpdateAndStart()}
100+
>
101+
Update and Start
102+
</ActionButton>
103+
)}
104+
{shouldShowActionButton('start') && (
105+
<ActionButton
106+
action="Start"
107+
titleOnLoading="Starting ..."
108+
onClick={() => handleStart()}
109+
variant="secondary"
110+
>
111+
Start
112+
</ActionButton>
113+
)}
114+
{!actionOnGoing && (
115+
<Button variant="link" onClick={onClose}>
116+
Cancel
117+
</Button>
118+
)}
54119
</ModalFooter>
55120
</Modal>
56121
);

workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,74 @@ import {
1010
} from '@patternfly/react-core';
1111
import { Workspace } from '~/shared/api/backendApiTypes';
1212
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
13+
import { ActionButton } from '~/app/pages/Workspaces/workspaceActions/ActionButton';
1314

1415
interface StopActionAlertProps {
1516
onClose: () => void;
1617
isOpen: boolean;
1718
workspace: Workspace | null;
19+
onStop: () => Promise<void>;
20+
onUpdateAndStop: () => Promise<void>;
21+
onActionDone: () => void;
1822
}
1923

24+
type StopAction = 'stop' | 'updateAndStop';
25+
2026
export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
2127
onClose,
2228
isOpen,
2329
workspace,
30+
onStop,
31+
onUpdateAndStop,
32+
onActionDone,
2433
}) => {
2534
const workspacePendingUpdate = workspace?.pendingRestart;
26-
const handleClick = (isUpdate = false) => {
27-
if (isUpdate) {
28-
console.log(`Update ${workspace?.name}`);
35+
const [actionOnGoing, setActionOnGoing] = React.useState<StopAction | null>(null);
36+
37+
const executeAction = React.useCallback(
38+
async (args: { action: StopAction; callback: () => Promise<void> }) => {
39+
setActionOnGoing(args.action);
40+
try {
41+
return await args.callback();
42+
} finally {
43+
setActionOnGoing(null);
44+
}
45+
},
46+
[],
47+
);
48+
49+
const handleStop = React.useCallback(async () => {
50+
try {
51+
await executeAction({ action: 'stop', callback: onStop });
52+
// TODO: alert user about success
53+
console.info('Workspace stopped successfully');
54+
onActionDone();
55+
onClose();
56+
} catch (error) {
57+
// TODO: alert user about error
58+
console.error('Error stopping workspace:', error);
2959
}
30-
console.log(`Stop ${workspace?.name}`);
31-
onClose();
32-
};
60+
}, [executeAction, onActionDone, onClose, onStop]);
61+
62+
// TODO: combine handleStop and handleUpdateAndStop if they end up being similar
63+
const handleUpdateAndStop = React.useCallback(async () => {
64+
try {
65+
await executeAction({ action: 'updateAndStop', callback: onUpdateAndStop });
66+
// TODO: alert user about success
67+
console.info('Workspace updated and stopped successfully');
68+
onActionDone();
69+
onClose();
70+
} catch (error) {
71+
// TODO: alert user about error
72+
console.error('Error updating and stopping workspace:', error);
73+
}
74+
}, [executeAction, onActionDone, onClose, onUpdateAndStop]);
75+
76+
const shouldShowActionButton = React.useCallback(
77+
(action: StopAction) => !actionOnGoing || actionOnGoing === action,
78+
[actionOnGoing],
79+
);
80+
3381
return (
3482
<Modal
3583
variant="medium"
@@ -53,18 +101,32 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
53101
)}
54102
</ModalBody>
55103
<ModalFooter>
56-
{workspacePendingUpdate && (
57-
<Button onClick={() => handleClick(true)}>Update and Stop</Button>
104+
{shouldShowActionButton('updateAndStop') && workspacePendingUpdate && (
105+
<ActionButton
106+
action="Update and Stop"
107+
titleOnLoading="Stopping ..."
108+
onClick={() => handleUpdateAndStop()}
109+
>
110+
Update and Stop
111+
</ActionButton>
112+
)}
113+
114+
{shouldShowActionButton('stop') && (
115+
<ActionButton
116+
action="Stop"
117+
titleOnLoading="Stopping ..."
118+
onClick={() => handleStop()}
119+
variant={workspacePendingUpdate ? 'secondary' : 'primary'}
120+
>
121+
{workspacePendingUpdate ? 'Stop and defer updates' : 'Stop'}
122+
</ActionButton>
123+
)}
124+
125+
{!actionOnGoing && (
126+
<Button variant="link" onClick={onClose}>
127+
Cancel
128+
</Button>
58129
)}
59-
<Button
60-
onClick={() => handleClick(false)}
61-
variant={workspacePendingUpdate ? 'secondary' : 'primary'}
62-
>
63-
{workspacePendingUpdate ? 'Stop and defer updates' : 'Stop'}
64-
</Button>
65-
<Button variant="link" onClick={onClose}>
66-
Cancel
67-
</Button>
68130
</ModalFooter>
69131
</Modal>
70132
);

0 commit comments

Comments
 (0)