Refactor Form View to Edit only

Signed-off-by: Charles Thao <cthao@redhat.com>
This commit is contained in:
Charles Thao 2025-06-27 10:50:48 -04:00
parent 28f2471bb5
commit bd6a9c9ac0
5 changed files with 103 additions and 44 deletions

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { import {
Button, Button,
Content, Content,
@ -13,6 +13,7 @@ import {
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { useTypedNavigate } from '~/app/routerHelper'; import { useTypedNavigate } from '~/app/routerHelper';
import useGenericObjectState from '~/app/hooks/useGenericObjectState'; import useGenericObjectState from '~/app/hooks/useGenericObjectState';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceKindFormData } from '~/app/types'; import { WorkspaceKindFormData } from '~/app/types';
import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload'; import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload';
import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties'; import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties';
@ -27,6 +28,7 @@ export type ValidationStatus = 'success' | 'error' | 'default';
export const WorkspaceKindForm: React.FC = () => { export const WorkspaceKindForm: React.FC = () => {
const navigate = useTypedNavigate(); const navigate = useTypedNavigate();
const { api } = useNotebookAPI();
// TODO: Detect mode by route // TODO: Detect mode by route
const [mode] = useState('create'); const [mode] = useState('create');
const [yamlValue, setYamlValue] = useState(''); const [yamlValue, setYamlValue] = useState('');
@ -35,14 +37,6 @@ export const WorkspaceKindForm: React.FC = () => {
const [validated, setValidated] = useState<ValidationStatus>('default'); const [validated, setValidated] = useState<ValidationStatus>('default');
const workspaceKindFileUploadId = 'workspace-kind-form-fileupload-view'; const workspaceKindFileUploadId = 'workspace-kind-form-fileupload-view';
const handleViewClick = (event: React.MouseEvent<unknown> | React.KeyboardEvent | MouseEvent) => {
const { id } = event.currentTarget as HTMLElement;
setView(
id === workspaceKindFileUploadId
? WorkspaceKindFormView.FileUpload
: WorkspaceKindFormView.Form,
);
};
const [data, setData, resetData] = useGenericObjectState<WorkspaceKindFormData>({ const [data, setData, resetData] = useGenericObjectState<WorkspaceKindFormData>({
properties: { properties: {
displayName: '', displayName: '',
@ -59,16 +53,41 @@ export const WorkspaceKindForm: React.FC = () => {
}, },
}); });
const handleCreate = useCallback(() => { const handleViewClick = useCallback(
// TODO: Complete handleCreate with API call to create a new WS kind (event: React.MouseEvent<unknown> | React.KeyboardEvent | MouseEvent) => {
if (!Object.keys(data).length) { const { id } = event.currentTarget as HTMLElement;
return; setView(
} id === workspaceKindFileUploadId
? WorkspaceKindFormView.FileUpload
: WorkspaceKindFormView.Form,
);
},
[],
);
const handleSubmit = useCallback(async () => {
setIsSubmitting(true); setIsSubmitting(true);
}, [data]); // TODO: Complete handleCreate with API call to create a new WS kind
try {
if (mode === 'create') {
const newWorkspaceKind = await api.createWorkspaceKind({}, yamlValue);
console.info('New workspace kind created:', JSON.stringify(newWorkspaceKind));
}
} catch (err) {
console.error(`Error ${mode === 'edit' ? 'editing' : 'creating'} workspace kind: ${err}`);
} finally {
setIsSubmitting(false);
}
navigate('workspaceKinds');
}, [navigate, mode, api, yamlValue]);
const canSubmit = useMemo(
() => !isSubmitting && yamlValue.length > 0 && validated === 'success',
[yamlValue, isSubmitting, validated],
);
const cancel = useCallback(() => { const cancel = useCallback(() => {
navigate('workspaceKindCreate'); navigate('workspaceKinds');
}, [navigate]); }, [navigate]);
return ( return (
@ -83,29 +102,30 @@ export const WorkspaceKindForm: React.FC = () => {
</Content> </Content>
<Content component={ContentVariants.p}> <Content component={ContentVariants.p}>
{view === WorkspaceKindFormView.FileUpload {view === WorkspaceKindFormView.FileUpload
? `Please upload a Workspace Kind YAML file. Select 'Form View' to view ? `Please upload or drag and drop a Workspace Kind YAML file.`
and edit the workspace kind's information`
: `View and edit the Workspace Kind's information. Some fields may not be : `View and edit the Workspace Kind's information. Some fields may not be
represented in this form`} represented in this form`}
</Content> </Content>
</FlexItem> </FlexItem>
<FlexItem> {mode === 'edit' && (
<ToggleGroup className="workspace-kind-form-header" aria-label="Toggle form view"> <FlexItem>
<ToggleGroupItem <ToggleGroup className="workspace-kind-form-header" aria-label="Toggle form view">
text="YAML Upload" <ToggleGroupItem
buttonId={workspaceKindFileUploadId} text="YAML Upload"
isSelected={view === WorkspaceKindFormView.FileUpload} buttonId={workspaceKindFileUploadId}
onChange={handleViewClick} isSelected={view === WorkspaceKindFormView.FileUpload}
/> onChange={handleViewClick}
<ToggleGroupItem />
text="Form View" <ToggleGroupItem
buttonId="workspace-kind-form-form-view" text="Form View"
isSelected={view === WorkspaceKindFormView.Form} buttonId="workspace-kind-form-form-view"
onChange={handleViewClick} isSelected={view === WorkspaceKindFormView.Form}
isDisabled={yamlValue === '' || validated === 'error'} onChange={handleViewClick}
/> isDisabled={yamlValue === '' || validated === 'error'}
</ToggleGroup> />
</FlexItem> </ToggleGroup>
</FlexItem>
)}
</Flex> </Flex>
</Stack> </Stack>
</PageSection> </PageSection>
@ -144,8 +164,8 @@ export const WorkspaceKindForm: React.FC = () => {
<Button <Button
variant="primary" variant="primary"
ouiaId="Primary" ouiaId="Primary"
onClick={handleCreate} onClick={handleSubmit}
isDisabled={!isSubmitting} isDisabled={!canSubmit}
> >
{mode === 'create' ? 'Create' : 'Edit'} {mode === 'create' ? 'Create' : 'Edit'}
</Button> </Button>

View File

@ -171,6 +171,48 @@ export const restDELETE = <T>(
parseJSON: options?.parseJSON, parseJSON: options?.parseJSON,
}); });
/** POST -- but with YAML content directly in body */
export const restYAML = <T>(
host: string,
path: string,
yamlContent: string,
queryParams?: Record<string, unknown>,
options?: APIOptions,
): Promise<T> => {
const { method, ...otherOptions } = mergeRequestInit(options, { method: 'POST' });
const sanitizedQueryParams = queryParams
? Object.entries(queryParams).reduce((acc, [key, value]) => {
if (value) {
return { ...acc, [key]: value };
}
return acc;
}, {})
: null;
const searchParams = sanitizedQueryParams
? new URLSearchParams(sanitizedQueryParams).toString()
: null;
return fetch(`${host}${path}${searchParams ? `?${searchParams}` : ''}`, {
...otherOptions,
headers: {
...otherOptions.headers,
...(DEV_MODE && { [AUTH_HEADER]: localStorage.getItem(AUTH_HEADER) }),
'Content-Type': 'application/vnd.kubeflow-notebooks.manifest+yaml',
},
method,
body: yamlContent,
}).then((response) =>
response.text().then((fetchedData) => {
if (options?.parseJSON !== false) {
return JSON.parse(fetchedData);
}
return fetchedData;
}),
);
};
export const isNotebookResponse = <T>(response: unknown): response is ResponseBody<T> => { export const isNotebookResponse = <T>(response: unknown): response is ResponseBody<T> => {
if (typeof response === 'object' && response !== null) { if (typeof response === 'object' && response !== null) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions

View File

@ -90,7 +90,7 @@ export interface WorkspaceKindPodTemplate {
} }
// eslint-disable-next-line @typescript-eslint/no-empty-object-type // eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface WorkspaceKindCreate {} export type WorkspaceKindCreate = string;
// eslint-disable-next-line @typescript-eslint/no-empty-object-type // eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface WorkspaceKindUpdate {} export interface WorkspaceKindUpdate {}

View File

@ -4,7 +4,6 @@ import {
Workspace, Workspace,
WorkspaceCreate, WorkspaceCreate,
WorkspaceKind, WorkspaceKind,
WorkspaceKindCreate,
WorkspaceKindPatch, WorkspaceKindPatch,
WorkspaceKindUpdate, WorkspaceKindUpdate,
WorkspacePatch, WorkspacePatch,
@ -63,10 +62,7 @@ export type StartWorkspace = (
// WorkspaceKind // WorkspaceKind
export type ListWorkspaceKinds = (opts: APIOptions) => Promise<WorkspaceKind[]>; export type ListWorkspaceKinds = (opts: APIOptions) => Promise<WorkspaceKind[]>;
export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise<WorkspaceKind>; export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise<WorkspaceKind>;
export type CreateWorkspaceKind = ( export type CreateWorkspaceKind = (opts: APIOptions, data: string) => Promise<WorkspaceKind>;
opts: APIOptions,
data: RequestData<WorkspaceKindCreate>,
) => Promise<WorkspaceKind>;
export type UpdateWorkspaceKind = ( export type UpdateWorkspaceKind = (
opts: APIOptions, opts: APIOptions,
kind: string, kind: string,

View File

@ -5,6 +5,7 @@ import {
restGET, restGET,
restPATCH, restPATCH,
restUPDATE, restUPDATE,
restYAML,
} from '~/shared/api/apiUtils'; } from '~/shared/api/apiUtils';
import { handleRestFailures } from '~/shared/api/errorUtils'; import { handleRestFailures } from '~/shared/api/errorUtils';
import { import {
@ -96,7 +97,7 @@ export const getWorkspaceKind: GetWorkspaceKindAPI = (hostPath) => (opts, kind)
); );
export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) => export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) =>
handleRestFailures(restCREATE(hostPath, `/workspacekinds`, data, {}, opts)).then((response) => handleRestFailures(restYAML(hostPath, `/workspacekinds`, data, {}, opts)).then((response) =>
extractNotebookResponse<WorkspaceKind>(response), extractNotebookResponse<WorkspaceKind>(response),
); );