Skip to content

Commit c40a2e0

Browse files
authored
feat(ws): prepare frontend for Edit Workspace (#370)
* Add action to Edit workspace button Signed-off-by: Guilherme Caponetto <[email protected]> * Rename WorkspaceCreate* -> WorkspaceForm* Signed-off-by: Guilherme Caponetto <[email protected]> * Enable type-safe navigation Signed-off-by: Guilherme Caponetto <[email protected]> * Fix some a11y warnings in the console Signed-off-by: Guilherme Caponetto <[email protected]> * Prepare submit code for Workspace update Signed-off-by: Guilherme Caponetto <[email protected]> * Final adjustments Signed-off-by: Guilherme Caponetto <[email protected]> * Remove edit action while it is not fully supported Signed-off-by: Guilherme Caponetto <[email protected]> --------- Signed-off-by: Guilherme Caponetto <[email protected]>
1 parent 91b1987 commit c40a2e0

30 files changed

+690
-208
lines changed

workspaces/frontend/src/app/AppRoutes.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as React from 'react';
22
import { Route, Routes } from 'react-router-dom';
3+
import { AppRoutePaths } from '~/app/routes';
4+
import { WorkspaceForm } from '~/app/pages/Workspaces/Form/WorkspaceForm';
35
import { NotFound } from './pages/notFound/NotFound';
46
import { Debug } from './pages/Debug/Debug';
57
import { Workspaces } from './pages/Workspaces/Workspaces';
6-
import { WorkspaceCreation } from './pages/Workspaces/Creation/WorkspaceCreation';
78
import '~/shared/style/MUI-theme.scss';
89
import { WorkspaceKinds } from './pages/WorkspaceKinds/WorkspaceKinds';
910

@@ -41,15 +42,15 @@ export const useAdminDebugSettings = (): NavDataItem[] => {
4142
},
4243
{
4344
label: 'Workspace Kinds',
44-
path: '/workspacekinds',
45+
path: AppRoutePaths.workspaceKinds,
4546
},
4647
];
4748
};
4849

4950
export const useNavData = (): NavDataItem[] => [
5051
{
5152
label: 'Notebooks',
52-
path: '/workspaces',
53+
path: AppRoutePaths.workspaces,
5354
},
5455
...useAdminDebugSettings(),
5556
];
@@ -59,9 +60,10 @@ const AppRoutes: React.FC = () => {
5960

6061
return (
6162
<Routes>
62-
<Route path="/workspaces/create" element={<WorkspaceCreation />} />
63-
<Route path="/workspacekinds" element={<WorkspaceKinds />} />
64-
<Route path="/workspaces" element={<Workspaces />} />
63+
<Route path={AppRoutePaths.workspaceCreate} element={<WorkspaceForm />} />
64+
<Route path={AppRoutePaths.workspaceEdit} element={<WorkspaceForm />} />
65+
<Route path={AppRoutePaths.workspaces} element={<Workspaces />} />
66+
<Route path={AppRoutePaths.workspaceKinds} element={<WorkspaceKinds />} />
6567
<Route path="/" element={<Workspaces />} />
6668
<Route path="*" element={<NotFound />} />
6769
{

workspaces/frontend/src/app/error/ErrorBoundary.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
2+
import { Link } from 'react-router-dom';
23
import { Button, Split, SplitItem, Title } from '@patternfly/react-core';
34
import { TimesIcon } from '@patternfly/react-icons';
5+
import { AppRoutePaths } from '~/app/routes';
46
import ErrorDetails from '~/app/error/ErrorDetails';
57
import UpdateState from '~/app/error/UpdateState';
68

@@ -57,7 +59,11 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
5759
An error occurred
5860
</Title>
5961
<p className="pf-v6-u-mb-md">
60-
Try{' '}
62+
Try going back to the{' '}
63+
<Link reloadDocument to={AppRoutePaths.root}>
64+
HOME PAGE
65+
</Link>{' '}
66+
or{' '}
6167
<Button
6268
data-testid="reload-link"
6369
variant="link"
@@ -66,7 +72,7 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
6672
>
6773
reloading
6874
</Button>{' '}
69-
the page if there was a recent update.
75+
this page if there was a recent update.
7076
</p>
7177
</SplitItem>
7278
<SplitItem>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useLocation, matchPath } from 'react-router-dom';
2+
import { AppRouteKey, AppRoutePaths } from '~/app/routes';
3+
4+
export function useCurrentRouteKey(): AppRouteKey | undefined {
5+
const location = useLocation();
6+
const { pathname } = location;
7+
8+
const matchEntries = Object.entries(AppRoutePaths) as [AppRouteKey, string][];
9+
10+
for (const [routeKey, pattern] of matchEntries) {
11+
const match = matchPath({ path: pattern, end: true }, pathname);
12+
if (match) {
13+
return routeKey;
14+
}
15+
}
16+
17+
return undefined;
18+
}

workspaces/frontend/src/app/hooks/useGenericObjectState.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type GenericObjectState<T> = [
99
data: T,
1010
setData: UpdateObjectAtPropAndValue<T>,
1111
resetDefault: () => void,
12+
replace: (newValue: T) => void,
1213
];
1314

1415
const useGenericObjectState = <T>(defaultData: T | (() => T)): GenericObjectState<T> => {
@@ -28,7 +29,11 @@ const useGenericObjectState = <T>(defaultData: T | (() => T)): GenericObjectStat
2829
setValue(defaultDataRef.current);
2930
}, []);
3031

31-
return [value, setPropValue, resetToDefault];
32+
const replace = React.useCallback((newValue: T) => {
33+
setValue(newValue);
34+
}, []);
35+
36+
return [value, setPropValue, resetToDefault, replace];
3237
};
3338

3439
export default useGenericObjectState;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as React from 'react';
2+
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
3+
import { WorkspaceFormData } from '~/app/types';
4+
import useFetchState, {
5+
FetchState,
6+
FetchStateCallbackPromise,
7+
} from '~/shared/utilities/useFetchState';
8+
9+
const EMPTY_FORM_DATA: WorkspaceFormData = {
10+
kind: undefined,
11+
image: undefined,
12+
podConfig: undefined,
13+
properties: {
14+
deferUpdates: false,
15+
homeDirectory: '',
16+
volumes: [],
17+
secrets: [],
18+
workspaceName: '',
19+
},
20+
};
21+
22+
const useWorkspaceFormData = (args: {
23+
namespace: string | undefined;
24+
workspaceName: string | undefined;
25+
}): FetchState<WorkspaceFormData> => {
26+
const { api, apiAvailable } = useNotebookAPI();
27+
28+
const call = React.useCallback<FetchStateCallbackPromise<WorkspaceFormData>>(
29+
async (opts) => {
30+
if (!apiAvailable) {
31+
throw new Error('API not yet available');
32+
}
33+
34+
if (!args.namespace || !args.workspaceName) {
35+
return EMPTY_FORM_DATA;
36+
}
37+
38+
const workspace = await api.getWorkspace(opts, args.namespace, args.workspaceName);
39+
const workspaceKind = await api.getWorkspaceKind(opts, workspace.workspaceKind.name);
40+
const imageConfig = workspace.podTemplate.options.imageConfig.current;
41+
const podConfig = workspace.podTemplate.options.podConfig.current;
42+
43+
return {
44+
kind: workspaceKind,
45+
image: {
46+
id: imageConfig.id,
47+
displayName: imageConfig.displayName,
48+
description: imageConfig.description,
49+
hidden: false,
50+
labels: [],
51+
},
52+
podConfig: {
53+
id: podConfig.id,
54+
displayName: podConfig.displayName,
55+
description: podConfig.description,
56+
hidden: false,
57+
labels: [],
58+
},
59+
properties: {
60+
workspaceName: workspace.name,
61+
deferUpdates: workspace.deferUpdates,
62+
volumes: workspace.podTemplate.volumes.data.map((volume) => ({ ...volume })),
63+
secrets: workspace.podTemplate.volumes.secrets?.map((secret) => ({ ...secret })) ?? [],
64+
homeDirectory: workspace.podTemplate.volumes.home?.mountPath ?? '',
65+
},
66+
};
67+
},
68+
[api, apiAvailable, args.namespace, args.workspaceName],
69+
);
70+
71+
return useFetchState(call, EMPTY_FORM_DATA);
72+
};
73+
74+
export default useWorkspaceFormData;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
2+
import { useCurrentRouteKey } from '~/app/hooks/useCurrentRouteKey';
3+
import { useTypedLocation } from '~/app/routerHelper';
4+
import { AppRouteKey, RouteStateMap } from '~/app/routes';
5+
6+
type WorkspaceFormLocationState = RouteStateMap['workspaceEdit'] | RouteStateMap['workspaceCreate'];
7+
8+
interface WorkspaceFormLocationData {
9+
mode: 'edit' | 'create';
10+
namespace: string;
11+
workspaceName?: string;
12+
}
13+
14+
function getRouteStateIfMatch<K extends AppRouteKey>(
15+
expectedRoute: K,
16+
actualRoute: AppRouteKey,
17+
state: unknown,
18+
): Partial<RouteStateMap[K]> | undefined {
19+
if (expectedRoute !== actualRoute || typeof state !== 'object' || state === null) {
20+
return undefined;
21+
}
22+
23+
return state as Partial<RouteStateMap[K]>;
24+
}
25+
26+
export function useWorkspaceFormLocationData(): WorkspaceFormLocationData {
27+
const { selectedNamespace } = useNamespaceContext();
28+
const location = useTypedLocation<'workspaceEdit' | 'workspaceCreate'>();
29+
const routeKey = useCurrentRouteKey();
30+
const rawState = location.state as WorkspaceFormLocationState | undefined;
31+
32+
if (routeKey === 'workspaceEdit') {
33+
const editState = getRouteStateIfMatch('workspaceEdit', routeKey, rawState);
34+
const namespace = editState?.namespace ?? selectedNamespace;
35+
const workspaceName = editState?.workspaceName;
36+
37+
if (!workspaceName) {
38+
throw new Error('Workspace name is required for edit mode');
39+
}
40+
41+
return {
42+
mode: 'edit',
43+
namespace,
44+
workspaceName,
45+
};
46+
}
47+
48+
if (routeKey === 'workspaceCreate') {
49+
const createState = getRouteStateIfMatch('workspaceCreate', routeKey, rawState);
50+
const namespace = createState?.namespace ?? selectedNamespace;
51+
52+
return {
53+
mode: 'create',
54+
namespace,
55+
};
56+
}
57+
58+
throw new Error('Unknown workspace form route');
59+
}

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -509,19 +509,21 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
509509
<Table aria-label="Sortable table" ouiaId="SortableTable">
510510
<Thead>
511511
<Tr>
512-
<Th />
513-
{Object.values(columnNames).map((columnName, index) => (
514-
<Th
515-
key={`${columnName}-col-name`}
516-
sort={
517-
columnName === 'Name' || columnName === 'Status'
518-
? getSortParams(index)
519-
: undefined
520-
}
521-
>
522-
{columnName}
523-
</Th>
524-
))}
512+
<Th aria-label="WorkspaceKind Icon" />
513+
{Object.values(columnNames)
514+
.filter((name) => name !== '')
515+
.map((columnName, index) => (
516+
<Th
517+
key={`${columnName}-col-name`}
518+
sort={
519+
columnName === 'Name' || columnName === 'Status'
520+
? getSortParams(index)
521+
: undefined
522+
}
523+
>
524+
{columnName}
525+
</Th>
526+
))}
525527
<Th screenReaderText="Primary action" />
526528
</Tr>
527529
</Thead>

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import { WorkspaceDetailsPodTemplate } from '~/app/pages/Workspaces/Details/Work
2121
type WorkspaceDetailsProps = {
2222
workspace: Workspace;
2323
onCloseClick: React.MouseEventHandler;
24-
onEditClick: React.MouseEventHandler;
24+
// TODO: Uncomment when edit action is fully supported
25+
// onEditClick: React.MouseEventHandler;
2526
onDeleteClick: React.MouseEventHandler;
2627
};
2728

2829
export const WorkspaceDetails: React.FunctionComponent<WorkspaceDetailsProps> = ({
2930
workspace,
3031
onCloseClick,
31-
onEditClick,
32+
// onEditClick,
3233
onDeleteClick,
3334
}) => {
3435
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
@@ -44,7 +45,7 @@ export const WorkspaceDetails: React.FunctionComponent<WorkspaceDetailsProps> =
4445
<DrawerPanelContent data-testid="workspaceDetails">
4546
<DrawerHead>
4647
<Title headingLevel="h6">{workspace.name}</Title>
47-
<WorkspaceDetailsActions onEditClick={onEditClick} onDeleteClick={onDeleteClick} />
48+
<WorkspaceDetailsActions /*onEditClick={onEditClick}*/ onDeleteClick={onDeleteClick} />
4849
<DrawerActions>
4950
<DrawerCloseButton onClick={onCloseClick} />
5051
</DrawerActions>

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import {
99
} from '@patternfly/react-core';
1010

1111
interface WorkspaceDetailsActionsProps {
12-
onEditClick: React.MouseEventHandler;
12+
// TODO: Uncomment when edit action is fully supported
13+
// onEditClick: React.MouseEventHandler;
1314
onDeleteClick: React.MouseEventHandler;
1415
}
1516

1617
export const WorkspaceDetailsActions: React.FC<WorkspaceDetailsActionsProps> = ({
17-
onEditClick,
18+
// onEditClick,
1819
onDeleteClick,
1920
}) => {
2021
const [isOpen, setOpen] = React.useState(false);
@@ -41,14 +42,15 @@ export const WorkspaceDetailsActions: React.FC<WorkspaceDetailsActionsProps> = (
4142
)}
4243
>
4344
<DropdownList>
44-
<DropdownItem
45+
{/* TODO: Uncomment when edit action is fully supported */}
46+
{/* <DropdownItem
4547
id="workspace-details-action-edit-button"
4648
aria-label="Edit workspace"
4749
key="edit-workspace-button"
4850
onClick={onEditClick}
4951
>
5052
Edit
51-
</DropdownItem>
53+
</DropdownItem> */}
5254
<DropdownItem
5355
id="workspace-details-action-delete-button"
5456
aria-label="Delete workspace"

0 commit comments

Comments
 (0)