From bd6a9c9ac068139c25b551185961b5166bf41c37 Mon Sep 17 00:00:00 2001 From: Charles Thao Date: Fri, 27 Jun 2025 10:50:48 -0400 Subject: [PATCH] Refactor Form View to Edit only Signed-off-by: Charles Thao --- .../WorkspaceKinds/Form/WorkspaceKindForm.tsx | 94 +++++++++++-------- .../frontend/src/shared/api/apiUtils.ts | 42 +++++++++ .../src/shared/api/backendApiTypes.ts | 2 +- .../frontend/src/shared/api/notebookApi.ts | 6 +- .../src/shared/api/notebookService.ts | 3 +- 5 files changed, 103 insertions(+), 44 deletions(-) diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx index c95ef8a4..a744cd0f 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx @@ -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('default'); const workspaceKindFileUploadId = 'workspace-kind-form-fileupload-view'; - const handleViewClick = (event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => { - const { id } = event.currentTarget as HTMLElement; - setView( - id === workspaceKindFileUploadId - ? WorkspaceKindFormView.FileUpload - : WorkspaceKindFormView.Form, - ); - }; const [data, setData, resetData] = useGenericObjectState({ 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 | 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 = () => { {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`} - - - - - - + {mode === 'edit' && ( + + + + + + + )} @@ -144,8 +164,8 @@ export const WorkspaceKindForm: React.FC = () => { diff --git a/workspaces/frontend/src/shared/api/apiUtils.ts b/workspaces/frontend/src/shared/api/apiUtils.ts index 3d90488b..e1c5c6da 100644 --- a/workspaces/frontend/src/shared/api/apiUtils.ts +++ b/workspaces/frontend/src/shared/api/apiUtils.ts @@ -171,6 +171,48 @@ export const restDELETE = ( parseJSON: options?.parseJSON, }); +/** POST -- but with YAML content directly in body */ +export const restYAML = ( + host: string, + path: string, + yamlContent: string, + queryParams?: Record, + options?: APIOptions, +): Promise => { + 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 = (response: unknown): response is ResponseBody => { if (typeof response === 'object' && response !== null) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions diff --git a/workspaces/frontend/src/shared/api/backendApiTypes.ts b/workspaces/frontend/src/shared/api/backendApiTypes.ts index f1beda3b..ca42d666 100644 --- a/workspaces/frontend/src/shared/api/backendApiTypes.ts +++ b/workspaces/frontend/src/shared/api/backendApiTypes.ts @@ -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 {} diff --git a/workspaces/frontend/src/shared/api/notebookApi.ts b/workspaces/frontend/src/shared/api/notebookApi.ts index 17aa6844..d57a118f 100644 --- a/workspaces/frontend/src/shared/api/notebookApi.ts +++ b/workspaces/frontend/src/shared/api/notebookApi.ts @@ -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; export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise; -export type CreateWorkspaceKind = ( - opts: APIOptions, - data: RequestData, -) => Promise; +export type CreateWorkspaceKind = (opts: APIOptions, data: string) => Promise; export type UpdateWorkspaceKind = ( opts: APIOptions, kind: string, diff --git a/workspaces/frontend/src/shared/api/notebookService.ts b/workspaces/frontend/src/shared/api/notebookService.ts index 457a310a..81fdbfcc 100644 --- a/workspaces/frontend/src/shared/api/notebookService.ts +++ b/workspaces/frontend/src/shared/api/notebookService.ts @@ -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(response), );