feat: refactor Form View to Edit only (#451)

Signed-off-by: Charles Thao <cthao@redhat.com>
This commit is contained in:
Charles Thao 2025-06-27 14:31:18 -04:00 committed by GitHub
parent 28f2471bb5
commit ca8e94c5c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 {
Button,
Content,
@ -13,6 +13,7 @@ import {
} from '@patternfly/react-core';
import { useTypedNavigate } from '~/app/routerHelper';
import useGenericObjectState from '~/app/hooks/useGenericObjectState';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceKindFormData } from '~/app/types';
import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload';
import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties';
@ -27,6 +28,7 @@ export type ValidationStatus = 'success' | 'error' | 'default';
export const WorkspaceKindForm: React.FC = () => {
const navigate = useTypedNavigate();
const { api } = useNotebookAPI();
// TODO: Detect mode by route
const [mode] = useState('create');
const [yamlValue, setYamlValue] = useState('');
@ -35,14 +37,6 @@ export const WorkspaceKindForm: React.FC = () => {
const [validated, setValidated] = useState<ValidationStatus>('default');
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>({
properties: {
displayName: '',
@ -59,16 +53,41 @@ export const WorkspaceKindForm: React.FC = () => {
},
});
const handleCreate = useCallback(() => {
// TODO: Complete handleCreate with API call to create a new WS kind
if (!Object.keys(data).length) {
return;
}
const handleViewClick = useCallback(
(event: React.MouseEvent<unknown> | React.KeyboardEvent | MouseEvent) => {
const { id } = event.currentTarget as HTMLElement;
setView(
id === workspaceKindFileUploadId
? WorkspaceKindFormView.FileUpload
: WorkspaceKindFormView.Form,
);
},
[],
);
const handleSubmit = useCallback(async () => {
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(() => {
navigate('workspaceKindCreate');
navigate('workspaceKinds');
}, [navigate]);
return (
@ -83,29 +102,30 @@ export const WorkspaceKindForm: React.FC = () => {
</Content>
<Content component={ContentVariants.p}>
{view === WorkspaceKindFormView.FileUpload
? `Please upload a Workspace Kind YAML file. Select 'Form View' to view
and edit the workspace kind's information`
? `Please upload or drag and drop a Workspace Kind YAML file.`
: `View and edit the Workspace Kind's information. Some fields may not be
represented in this form`}
</Content>
</FlexItem>
<FlexItem>
<ToggleGroup className="workspace-kind-form-header" aria-label="Toggle form view">
<ToggleGroupItem
text="YAML Upload"
buttonId={workspaceKindFileUploadId}
isSelected={view === WorkspaceKindFormView.FileUpload}
onChange={handleViewClick}
/>
<ToggleGroupItem
text="Form View"
buttonId="workspace-kind-form-form-view"
isSelected={view === WorkspaceKindFormView.Form}
onChange={handleViewClick}
isDisabled={yamlValue === '' || validated === 'error'}
/>
</ToggleGroup>
</FlexItem>
{mode === 'edit' && (
<FlexItem>
<ToggleGroup className="workspace-kind-form-header" aria-label="Toggle form view">
<ToggleGroupItem
text="YAML Upload"
buttonId={workspaceKindFileUploadId}
isSelected={view === WorkspaceKindFormView.FileUpload}
onChange={handleViewClick}
/>
<ToggleGroupItem
text="Form View"
buttonId="workspace-kind-form-form-view"
isSelected={view === WorkspaceKindFormView.Form}
onChange={handleViewClick}
isDisabled={yamlValue === '' || validated === 'error'}
/>
</ToggleGroup>
</FlexItem>
)}
</Flex>
</Stack>
</PageSection>
@ -144,8 +164,8 @@ export const WorkspaceKindForm: React.FC = () => {
<Button
variant="primary"
ouiaId="Primary"
onClick={handleCreate}
isDisabled={!isSubmitting}
onClick={handleSubmit}
isDisabled={!canSubmit}
>
{mode === 'create' ? 'Create' : 'Edit'}
</Button>

View File

@ -171,6 +171,48 @@ export const restDELETE = <T>(
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> => {
if (typeof response === 'object' && response !== null) {
// 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
export interface WorkspaceKindCreate {}
export type WorkspaceKindCreate = string;
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface WorkspaceKindUpdate {}

View File

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

View File

@ -5,6 +5,7 @@ import {
restGET,
restPATCH,
restUPDATE,
restYAML,
} from '~/shared/api/apiUtils';
import { handleRestFailures } from '~/shared/api/errorUtils';
import {
@ -96,7 +97,7 @@ export const getWorkspaceKind: GetWorkspaceKindAPI = (hostPath) => (opts, kind)
);
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),
);