diff --git a/workspaces/frontend/src/app/components/ValidationErrorAlert.tsx b/workspaces/frontend/src/app/components/ValidationErrorAlert.tsx index e2691b2a..417969b9 100644 --- a/workspaces/frontend/src/app/components/ValidationErrorAlert.tsx +++ b/workspaces/frontend/src/app/components/ValidationErrorAlert.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Alert, List, ListItem } from '@patternfly/react-core'; import { ValidationError } from '~/shared/api/backendApiTypes'; +import { ErrorEnvelopeException } from '~/shared/api/apiUtils'; interface ValidationErrorAlertProps { title: string; - errors: ValidationError[]; + errors: (ValidationError | ErrorEnvelopeException)[]; } export const ValidationErrorAlert: React.FC = ({ title, errors }) => { diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx index f738e956..98b1d4df 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx @@ -71,9 +71,18 @@ type ColumnNames = { [K in keyof T]: string }; interface EditableLabelsProps { rows: WorkspaceOptionLabel[]; setRows: (value: WorkspaceOptionLabel[]) => void; + title?: string; + description?: string; + buttonLabel?: string; } -export const EditableLabels: React.FC = ({ rows, setRows }) => { +export const EditableLabels: React.FC = ({ + rows, + setRows, + title = 'Labels', + description, + buttonLabel = 'Label', +}) => { const columnNames: ColumnNames = { key: 'Key', value: 'Value', @@ -86,12 +95,15 @@ export const EditableLabels: React.FC = ({ rows, setRows }) header={ -
Labels are key/value pairs that are attached to Kubernetes objects.
+
+ {description || + 'Labels are key/value pairs that are attached to Kubernetes objects.'} +
{rows.length} added
@@ -141,7 +153,7 @@ export const EditableLabels: React.FC = ({ rows, setRows }) ]); }} > - Add Label + {`Add ${buttonLabel}`} ); diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx index feb1ba68..0972c68d 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx @@ -1,8 +1,10 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Content, ContentVariants, + EmptyState, + EmptyStateBody, Flex, FlexItem, PageGroup, @@ -11,18 +13,22 @@ import { StackItem, } from '@patternfly/react-core'; import { t_global_spacer_sm as SmallPadding } from '@patternfly/react-tokens'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { ValidationErrorAlert } from '~/app/components/ValidationErrorAlert'; -import { useTypedNavigate } from '~/app/routerHelper'; +import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName'; +import { WorkspaceKind, ValidationError } from '~/shared/api/backendApiTypes'; +import { useTypedNavigate, useTypedParams } from '~/app/routerHelper'; import { useCurrentRouteKey } from '~/app/hooks/useCurrentRouteKey'; import useGenericObjectState from '~/app/hooks/useGenericObjectState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { WorkspaceKindFormData } from '~/app/types'; import { ErrorEnvelopeException } from '~/shared/api/apiUtils'; -import { ValidationError } from '~/shared/api/backendApiTypes'; import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload'; import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties'; import { WorkspaceKindFormImage } from './image/WorkspaceKindFormImage'; import { WorkspaceKindFormPodConfig } from './podConfig/WorkspaceKindFormPodConfig'; +import { WorkspaceKindFormPodTemplate } from './podTemplate/WorkspaceKindFormPodTemplate'; +import { EMPTY_WORKSPACE_KIND_FORM_DATA } from './helpers'; export enum WorkspaceKindFormView { Form, @@ -30,6 +36,19 @@ export enum WorkspaceKindFormView { } export type ValidationStatus = 'success' | 'error' | 'default'; +export type FormMode = 'edit' | 'create'; + +const convertToFormData = (initialData: WorkspaceKind): WorkspaceKindFormData => { + const { podTemplate, ...properties } = initialData; + const { options, ...spec } = podTemplate; + const { podConfig, imageConfig } = options; + return { + properties, + podConfig, + imageConfig, + podTemplate: spec, + }; +}; export const WorkspaceKindForm: React.FC = () => { const navigate = useTypedNavigate(); @@ -38,28 +57,23 @@ export const WorkspaceKindForm: React.FC = () => { const [yamlValue, setYamlValue] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [validated, setValidated] = useState('default'); - const mode = useCurrentRouteKey() === 'workspaceKindCreate' ? 'create' : 'edit'; - const [specErrors, setSpecErrors] = useState([]); + const mode: FormMode = useCurrentRouteKey() === 'workspaceKindCreate' ? 'create' : 'edit'; + const [specErrors, setSpecErrors] = useState<(ValidationError | ErrorEnvelopeException)[]>([]); - const [data, setData, resetData] = useGenericObjectState({ - properties: { - displayName: '', - description: '', - deprecated: false, - deprecationMessage: '', - hidden: false, - icon: { url: '' }, - logo: { url: '' }, - }, - imageConfig: { - default: '', - values: [], - }, - podConfig: { - default: '', - values: [], - }, - }); + const { kind } = useTypedParams<'workspaceKindEdit'>(); + const [initialFormData, initialFormDataLoaded, initialFormDataError] = + useWorkspaceKindByName(kind); + + const [data, setData, resetData, replaceData] = useGenericObjectState( + initialFormData ? convertToFormData(initialFormData) : EMPTY_WORKSPACE_KIND_FORM_DATA, + ); + + useEffect(() => { + if (!initialFormDataLoaded || initialFormData === null || mode === 'create') { + return; + } + replaceData(convertToFormData(initialFormData)); + }, [initialFormData, initialFormDataLoaded, mode, replaceData]); const handleSubmit = useCallback(async () => { setIsSubmitting(true); @@ -71,14 +85,20 @@ export const WorkspaceKindForm: React.FC = () => { console.info('New workspace kind created:', JSON.stringify(newWorkspaceKind)); navigate('workspaceKinds'); } + // TODO: Finish when WSKind API is finalized + // const updatedWorkspace = await api.updateWorkspaceKind({}, kind, { data: {} }); + // console.info('Workspace Kind updated:', JSON.stringify(updatedWorkspace)); + // navigate('workspaceKinds'); } catch (err) { if (err instanceof ErrorEnvelopeException) { const validationErrors = err.envelope.error?.cause?.validation_errors; if (validationErrors && validationErrors.length > 0) { - setSpecErrors(validationErrors); + setSpecErrors((prev) => [...prev, ...validationErrors]); setValidated('error'); return; } + setSpecErrors((prev) => [...prev, err]); + setValidated('error'); } // TODO: alert user about error console.error(`Error ${mode === 'edit' ? 'editing' : 'creating'} workspace kind: ${err}`); @@ -88,14 +108,26 @@ export const WorkspaceKindForm: React.FC = () => { }, [navigate, mode, api, yamlValue]); const canSubmit = useMemo( - () => !isSubmitting && yamlValue.length > 0 && validated === 'success', - [yamlValue, isSubmitting, validated], + () => !isSubmitting && validated === 'success', + [isSubmitting, validated], ); const cancel = useCallback(() => { navigate('workspaceKinds'); }, [navigate]); + if (mode === 'edit' && initialFormDataError) { + return ( + + {initialFormDataError.message} + + ); + } return ( <> @@ -159,6 +191,12 @@ export const WorkspaceKindForm: React.FC = () => { setData('podConfig', podConfig); }} /> + { + setData('podTemplate', podTemplate); + }} + /> )} @@ -169,9 +207,10 @@ export const WorkspaceKindForm: React.FC = () => { variant="primary" ouiaId="Primary" onClick={handleSubmit} - isDisabled={!canSubmit} + // TODO: button is always disabled on edit mode. Need to modify when WorkspaceKind edit is finalized + isDisabled={!canSubmit || mode === 'edit'} > - {mode === 'create' ? 'Create' : 'Edit'} + {mode === 'create' ? 'Create' : 'Save'} diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx new file mode 100644 index 00000000..d7887f2f --- /dev/null +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx @@ -0,0 +1,142 @@ +import React, { useMemo, useState } from 'react'; +import { Table, Thead, Tr, Td, Tbody, Th } from '@patternfly/react-table'; +import { + Dropdown, + DropdownItem, + getUniqueId, + Label, + MenuToggle, + PageSection, + Pagination, + PaginationVariant, + Radio, +} from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; + +import { WorkspaceKindImageConfigValue } from '~/app/types'; +import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; + +interface PaginatedTableProps { + rows: WorkspaceKindImageConfigValue[] | WorkspacePodConfigValue[]; + defaultId: string; + setDefaultId: (id: string) => void; + handleEdit: (index: number) => void; + openDeleteModal: (index: number) => void; + ariaLabel: string; +} + +export const WorkspaceKindFormPaginatedTable: React.FC = ({ + rows, + defaultId, + setDefaultId, + handleEdit, + openDeleteModal, + ariaLabel, +}) => { + const [dropdownOpen, setDropdownOpen] = useState(null); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(10); + const rowPages = useMemo(() => { + const pages = []; + for (let i = 0; i < rows.length; i += perPage) { + pages.push(rows.slice(i, i + perPage)); + } + return pages; + }, [perPage, rows]); + + const onSetPage = ( + _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + newPage: number, + ) => { + setPage(newPage); + }; + + const onPerPageSelect = ( + _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + newPerPage: number, + newPage: number, + ) => { + setPerPage(newPerPage); + setPage(newPage); + }; + return ( + + + + + + + + + + + + {rowPages[page - 1].map((row, index) => ( + + + + + + + + ))} + +
Display NameIDDefaultLabels +
{row.displayName}{row.id} + { + console.log(row.id); + setDefaultId(row.id); + }} + aria-label={`Select ${row.id} as default`} + /> + + {row.labels.length > 0 && + row.labels.map((label) => ( + + ))} + + ( + setDropdownOpen(dropdownOpen === index ? null : index)} + variant="plain" + aria-label="plain kebab" + > + + + )} + isOpen={dropdownOpen === index} + onSelect={() => setDropdownOpen(null)} + popperProps={{ position: 'right' }} + > + handleEdit(perPage * (page - 1) + index)}> + Edit + + openDeleteModal(perPage * (page - 1) + index)}> + Remove + + +
+ +
+ ); +}; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts index aad5f622..786670e7 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts @@ -104,6 +104,45 @@ export const emptyPodConfig: WorkspacePodConfigValue = { to: '', }, }; + +export const EMPTY_WORKSPACE_KIND_FORM_DATA = { + properties: { + displayName: '', + description: '', + deprecated: false, + deprecationMessage: '', + hidden: false, + icon: { url: '' }, + logo: { url: '' }, + }, + imageConfig: { + default: '', + values: [], + }, + podConfig: { + default: '', + values: [], + }, + podTemplate: { + podMetadata: { + labels: {}, + annotations: {}, + }, + volumeMounts: { + home: '', + }, + extraVolumeMounts: [], + culling: { + enabled: false, + maxInactiveSeconds: 86400, + activityProbe: { + jupyter: { + lastActivity: true, + }, + }, + }, + }, +}; // convert from k8s resource object {limits: {}, requests{}} to array of {type: '', limit: '', request: ''} for each type of resource (e.g. CPU, memory, nvidia.com/gpu) export const getResources = (currConfig: WorkspaceKindPodConfigValue): PodResourceEntry[] => { const grouped = new Map([ diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImage.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImage.tsx index ca625585..9f50e5a0 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImage.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImage.tsx @@ -2,9 +2,6 @@ import React, { useCallback, useState } from 'react'; import { Button, Content, - Dropdown, - MenuToggle, - DropdownItem, Modal, ModalHeader, ModalFooter, @@ -13,14 +10,13 @@ import { EmptyStateFooter, EmptyStateActions, EmptyStateBody, - Label, - getUniqueId, ExpandableSection, } from '@patternfly/react-core'; -import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table'; -import { PlusCircleIcon, EllipsisVIcon, CubesIcon } from '@patternfly/react-icons'; +import { PlusCircleIcon, CubesIcon } from '@patternfly/react-icons'; import { WorkspaceKindImageConfigData, WorkspaceKindImageConfigValue } from '~/app/types'; import { emptyImage } from '~/app/pages/WorkspaceKinds/Form/helpers'; +import { WorkspaceKindFormPaginatedTable } from '~/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable'; + import { WorkspaceKindFormImageModal } from './WorkspaceKindFormImageModal'; interface WorkspaceKindFormImageProps { @@ -38,7 +34,6 @@ export const WorkspaceKindFormImage: React.FC = ({ const [defaultId, setDefaultId] = useState(imageConfig.default || ''); const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [dropdownOpen, setDropdownOpen] = useState(null); const [editIndex, setEditIndex] = useState(null); const [deleteIndex, setDeleteIndex] = useState(null); const [image, setImage] = useState({ ...emptyImage }); @@ -125,70 +120,17 @@ export const WorkspaceKindFormImage: React.FC = ({ )} {imageConfig.values.length > 0 && (
- - - - - - - - - - - - {imageConfig.values.map((img, index) => ( - - - - - - - - - - ))} - -
Display NameIDDefaultHiddenLabels -
{img.displayName}{img.id} - { - setDefaultId(img.id); - updateImageConfig({ ...imageConfig, default: img.id }); - }} - aria-label={`Select ${img.id} as default`} - /> - {img.hidden ? 'Yes' : 'No'} - {img.labels.length > 0 && - img.labels.map((label) => ( - - ))} - - ( - setDropdownOpen(dropdownOpen === index ? null : index)} - variant="plain" - aria-label="plain kebab" - > - - - )} - isOpen={dropdownOpen === index} - onSelect={() => setDropdownOpen(null)} - popperProps={{ position: 'right' }} - > - handleEdit(index)}>Edit - openDeleteModal(index)}>Remove - -
+ { + updateImageConfig({ ...imageConfig, default: id }); + setDefaultId(id); + }} + handleEdit={handleEdit} + openDeleteModal={openDeleteModal} + /> {addImageBtn}
)} diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageModal.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageModal.tsx index 99a7a22b..607d3aad 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageModal.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageModal.tsx @@ -138,7 +138,7 @@ export const WorkspaceKindFormImageModal: React.FC setImage({ ...image, ports })} /> {mode === 'edit' && ( diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/ResourceInputWrapper.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/ResourceInputWrapper.tsx index 3c8ab7bb..5c0371b3 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/ResourceInputWrapper.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/ResourceInputWrapper.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { FormSelect, FormSelectOption, @@ -6,13 +6,18 @@ import { Split, SplitItem, } from '@patternfly/react-core'; -import { CPU_UNITS, MEMORY_UNITS_FOR_SELECTION, UnitOption } from '~/shared/utilities/valueUnits'; +import { + CPU_UNITS, + MEMORY_UNITS_FOR_SELECTION, + TIME_UNIT_FOR_SELECTION, + UnitOption, +} from '~/shared/utilities/valueUnits'; import { parseResourceValue } from '~/shared/utilities/WorkspaceUtils'; interface ResourceInputWrapperProps { value: string; onChange: (value: string) => void; - type: 'cpu' | 'memory' | 'custom'; + type: 'cpu' | 'memory' | 'time' | 'custom'; min?: number; max?: number; step?: number; @@ -26,6 +31,7 @@ const unitMap: { } = { memory: MEMORY_UNITS_FOR_SELECTION, cpu: CPU_UNITS, + time: TIME_UNIT_FOR_SELECTION, }; const DEFAULT_STEP = 1; @@ -34,7 +40,6 @@ const DEFAULT_UNITS = { memory: 'Mi', cpu: '', }; - export const ResourceInputWrapper: React.FC = ({ value, onChange, @@ -48,22 +53,47 @@ export const ResourceInputWrapper: React.FC = ({ }) => { const [inputValue, setInputValue] = useState(value); const [unit, setUnit] = useState(''); + const isTimeInitialized = useRef(false); useEffect(() => { - if (type === 'custom') { - setInputValue(value); - return; + if (type === 'time') { + // Initialize time only once + if (!isTimeInitialized.current) { + const seconds = parseFloat(value) || 0; + let defaultUnit = 60; // Default to minutes + if (seconds >= 86400) { + defaultUnit = 86400; // Days + } else if (seconds >= 3600) { + defaultUnit = 3600; // Hours + } else if (seconds >= 60) { + defaultUnit = 60; // Minutes + } else { + defaultUnit = 1; // Seconds + } + setUnit(defaultUnit.toString()); + setInputValue((seconds / defaultUnit).toString()); + isTimeInitialized.current = true; + } + } else { + if (type === 'custom') { + setInputValue(value); + return; + } + const [numericValue, extractedUnit] = parseResourceValue(value, type); + setInputValue(String(numericValue || '')); + setUnit(extractedUnit?.unit || DEFAULT_UNITS[type]); } - const [numericValue, extractedUnit] = parseResourceValue(value, type); - setInputValue(String(numericValue || '')); - setUnit(extractedUnit?.unit || DEFAULT_UNITS[type]); - }, [value, type]); + }, [type, value]); const handleInputChange = useCallback( (newValue: string) => { setInputValue(newValue); if (type === 'custom') { onChange(newValue); + } else if (type === 'time') { + const numericValue = parseFloat(newValue) || 0; + const unitMultiplier = parseFloat(unit) || 1; + onChange(String(numericValue * unitMultiplier)); } else { onChange(newValue ? `${newValue}${unit}` : ''); } @@ -73,12 +103,24 @@ export const ResourceInputWrapper: React.FC = ({ const handleUnitChange = useCallback( (newUnit: string) => { - setUnit(newUnit); - if (inputValue) { - onChange(`${inputValue}${newUnit}`); + if (type === 'time') { + const currentValue = parseFloat(inputValue) || 0; + const oldUnitMultiplier = parseFloat(unit) || 1; + const newUnitMultiplier = parseFloat(newUnit) || 1; + // Convert the current value to the new unit + const valueInSeconds = currentValue * oldUnitMultiplier; + const valueInNewUnit = valueInSeconds / newUnitMultiplier; + setUnit(newUnit); + setInputValue(valueInNewUnit.toString()); + onChange(String(valueInSeconds)); + } else { + setUnit(newUnit); + if (inputValue) { + onChange(`${inputValue}${newUnit}`); + } } }, - [inputValue, onChange], + [inputValue, onChange, type, unit], ); const handleIncrement = useCallback(() => { @@ -104,7 +146,13 @@ export const ResourceInputWrapper: React.FC = ({ const unitOptions = useMemo( () => type !== 'custom' - ? unitMap[type].map((u) => ) + ? unitMap[type].map((u) => ( + + )) : [], [type], ); @@ -136,6 +184,7 @@ export const ResourceInputWrapper: React.FC = ({ onChange={(_, v) => handleUnitChange(v)} id={`${ariaLabel}-unit-select`} isDisabled={isDisabled} + className="workspace-kind-unit-select" > {unitOptions} diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormPodConfig.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormPodConfig.tsx index 400d1da0..5f18678f 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormPodConfig.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormPodConfig.tsx @@ -2,9 +2,6 @@ import React, { useCallback, useState } from 'react'; import { Button, Content, - Dropdown, - MenuToggle, - DropdownItem, Modal, ModalHeader, ModalFooter, @@ -14,14 +11,11 @@ import { EmptyStateActions, ExpandableSection, EmptyStateBody, - Label, - getUniqueId, } from '@patternfly/react-core'; -import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table'; -import { PlusCircleIcon, EllipsisVIcon, CubesIcon } from '@patternfly/react-icons'; +import { PlusCircleIcon, CubesIcon } from '@patternfly/react-icons'; import { emptyPodConfig } from '~/app/pages/WorkspaceKinds/Form/helpers'; import { WorkspaceKindPodConfigValue, WorkspaceKindPodConfigData } from '~/app/types'; - +import { WorkspaceKindFormPaginatedTable } from '~/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable'; import { WorkspaceKindFormPodConfigModal } from './WorkspaceKindFormPodConfigModal'; interface WorkspaceKindFormPodConfigProps { @@ -37,7 +31,6 @@ export const WorkspaceKindFormPodConfig: React.FC(null); const [editIndex, setEditIndex] = useState(null); const [deleteIndex, setDeleteIndex] = useState(null); const [currConfig, setCurrConfig] = useState({ ...emptyPodConfig }); @@ -128,69 +121,17 @@ export const WorkspaceKindFormPodConfig: React.FC 0 && ( <> - - - - - - - - - - - - {podConfig.values.map((config, index) => ( - - - - - - - - - ))} - -
Display NameIDDefaultHiddenLabels -
{config.displayName}{config.id} - { - setDefaultId(config.id); - updatePodConfig({ ...podConfig, default: config.id }); - }} - aria-label={`Select ${config.id} as default`} - /> - {config.hidden ? 'Yes' : 'No'} - {config.labels.length > 0 && - config.labels.map((label) => ( - - ))} - - ( - setDropdownOpen(dropdownOpen === index ? null : index)} - variant="plain" - aria-label="plain kebab" - > - - - )} - isOpen={dropdownOpen === index} - onSelect={() => setDropdownOpen(null)} - popperProps={{ position: 'right' }} - > - handleEdit(index)}>Edit - openDeleteModal(index)}>Remove - -
+ { + updatePodConfig({ ...podConfig, default: id }); + setDefaultId(id); + }} + handleEdit={handleEdit} + openDeleteModal={openDeleteModal} + /> {addConfigBtn} )} diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormResource.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormResource.tsx index 8127f3c4..08106117 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormResource.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podConfig/WorkspaceKindFormResource.tsx @@ -312,6 +312,7 @@ export const WorkspaceKindFormResource: React.FC onChange={(_event, value) => handleChange(res.id, 'type', value)} /> + diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx index 9feb7374..7fb0b118 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx @@ -14,7 +14,7 @@ import { Switch, TextInput, } from '@patternfly/react-core'; -import { EllipsisVIcon } from '@patternfly/react-icons'; +import { EllipsisVIcon, PlusCircleIcon } from '@patternfly/react-icons'; import { Table, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes'; @@ -126,9 +126,10 @@ export const WorkspaceFormPropertiesVolumes: React.FC )}