feat(ws): Add properties tile to new workspace creation (#262)

* feat(ws): Add properties tile to new workspace creation

Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>

* feat(ws): Add properties tile to new workspace creation

Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>

* feat(ws): Add properties tile to new workspace creation

Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>

* feat(ws): Add properties tile to new workspace creation

Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>

---------

Signed-off-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>
Co-authored-by: Hen Schwartz (EXT-Nokia) <hen.schwartz@nokia.com>
This commit is contained in:
henschwartz 2025-05-08 16:30:40 +03:00 committed by GitHub
parent 1e8280519a
commit c3a6f54ae2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 372 additions and 23 deletions

View File

@ -16,9 +16,14 @@ import { useNavigate } from 'react-router-dom';
import { CheckIcon } from '@patternfly/react-icons';
import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection';
import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection';
import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/WorkspaceCreationPropertiesSelection';
import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection';
import { WorkspaceCreationPodConfigSelection } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection';
import { WorkspaceImage, WorkspaceKind, WorkspacePodConfig } from '~/shared/types';
import {
WorkspaceImage,
WorkspaceKind,
WorkspacePodConfig,
WorkspaceProperties,
} from '~/shared/types';
enum WorkspaceCreationSteps {
KindSelection,
@ -34,6 +39,7 @@ const WorkspaceCreation: React.FunctionComponent = () => {
const [selectedKind, setSelectedKind] = useState<WorkspaceKind | undefined>();
const [selectedImage, setSelectedImage] = useState<WorkspaceImage | undefined>();
const [selectedPodConfig, setSelectedPodConfig] = useState<WorkspacePodConfig | undefined>();
const [, setSelectedProperties] = useState<WorkspaceProperties | undefined>();
const getStepVariant = useCallback(
(step: WorkspaceCreationSteps) => {
@ -64,6 +70,7 @@ const WorkspaceCreation: React.FunctionComponent = () => {
setSelectedKind(newWorkspaceKind);
setSelectedImage(undefined);
setSelectedPodConfig(undefined);
setSelectedProperties(undefined);
}, []);
return (
@ -169,7 +176,7 @@ const WorkspaceCreation: React.FunctionComponent = () => {
/>
)}
{currentStep === WorkspaceCreationSteps.Properties && (
<WorkspaceCreationPropertiesSelection />
<WorkspaceCreationPropertiesSelection selectedImage={selectedImage} />
)}
</PageSection>
<PageSection isFilled={false} stickyOnBreakpoint={{ default: 'bottom' }}>

View File

@ -1,10 +0,0 @@
import * as React from 'react';
import { Content } from '@patternfly/react-core';
const WorkspaceCreationPropertiesSelection: React.FunctionComponent = () => (
<Content>
<p>Configure properties for your workspace.</p>
</Content>
);
export { WorkspaceCreationPropertiesSelection };

View File

@ -0,0 +1,126 @@
import * as React from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
TextInput,
Checkbox,
Form,
FormGroup,
ExpandableSection,
Divider,
Split,
SplitItem,
Content,
} from '@patternfly/react-core';
import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails';
import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes';
import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume } from '~/shared/types';
interface WorkspaceCreationPropertiesSelectionProps {
selectedImage: WorkspaceImage | undefined;
}
const WorkspaceCreationPropertiesSelection: React.FunctionComponent<
WorkspaceCreationPropertiesSelectionProps
> = ({ selectedImage }) => {
const [workspaceName, setWorkspaceName] = useState('');
const [deferUpdates, setDeferUpdates] = useState(false);
const [homeDirectory, setHomeDirectory] = useState('');
const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [] });
const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]);
const [isVolumesExpanded, setIsVolumesExpanded] = useState(false);
useEffect(() => {
setVolumes((prev) => ({
...prev,
data: volumesData,
}));
}, [volumesData]);
const imageDetailsContent = useMemo(
() => <WorkspaceCreationImageDetails workspaceImage={selectedImage} />,
[selectedImage],
);
return (
<Content style={{ height: '100%' }}>
<p>Configure properties for your workspace.</p>
<Divider />
<Split hasGutter>
<SplitItem style={{ minWidth: '200px' }}>{imageDetailsContent}</SplitItem>
<SplitItem isFilled>
<div className="pf-u-p-lg pf-u-max-width-xl">
<Form>
<FormGroup
label="Workspace Name"
isRequired
fieldId="workspace-name"
style={{ width: 520 }}
>
<TextInput
isRequired
type="text"
value={workspaceName}
onChange={(_, value) => setWorkspaceName(value)}
id="workspace-name"
/>
</FormGroup>
<FormGroup fieldId="defer-updates">
<Checkbox
label="Defer Updates"
isChecked={deferUpdates}
onChange={() => setDeferUpdates((prev) => !prev)}
id="defer-updates"
/>
</FormGroup>
<Divider />
<div className="pf-u-mb-0">
<ExpandableSection
toggleText="Volumes"
onToggle={() => setIsVolumesExpanded((prev) => !prev)}
isExpanded={isVolumesExpanded}
isIndented
>
{isVolumesExpanded && (
<>
<FormGroup
label="Home Directory"
fieldId="home-directory"
style={{ width: 500 }}
>
<TextInput
value={homeDirectory}
onChange={(_, value) => {
setHomeDirectory(value);
setVolumes((prev) => ({ ...prev, home: value }));
}}
id="home-directory"
/>
</FormGroup>
<FormGroup fieldId="volumes-table" style={{ marginTop: '1rem' }}>
<WorkspaceCreationPropertiesVolumes
volumes={volumesData}
setVolumes={setVolumesData}
/>
</FormGroup>
</>
)}
</ExpandableSection>
</div>
{!isVolumesExpanded && (
<div style={{ paddingLeft: '36px', marginTop: '-10px' }}>
<div>Workspace volumes enable your project data to persist.</div>
<div className="pf-u-font-size-sm">
<strong>{volumes.data.length} added</strong>
</div>
</div>
)}
</Form>
</div>
</SplitItem>
</Split>
</Content>
);
};
export { WorkspaceCreationPropertiesSelection };

View File

@ -0,0 +1,207 @@
import React, { useCallback, useState } from 'react';
import { EllipsisVIcon } from '@patternfly/react-icons';
import { Table, Thead, Tbody, Tr, Th, Td, TableVariant } from '@patternfly/react-table';
import {
Button,
Modal,
ModalVariant,
TextInput,
Switch,
Dropdown,
DropdownItem,
MenuToggle,
ModalBody,
ModalFooter,
Form,
FormGroup,
ModalHeader,
} from '@patternfly/react-core';
import { WorkspaceVolume } from '~/shared/types';
interface WorkspaceCreationPropertiesVolumesProps {
volumes: WorkspaceVolume[];
setVolumes: React.Dispatch<React.SetStateAction<WorkspaceVolume[]>>;
}
export const WorkspaceCreationPropertiesVolumes: React.FC<
WorkspaceCreationPropertiesVolumesProps
> = ({ volumes, setVolumes }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [formData, setFormData] = useState<WorkspaceVolume>({
pvcName: '',
mountPath: '',
readOnly: false,
});
const [editIndex, setEditIndex] = useState<number | null>(null);
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
const resetForm = useCallback(() => {
setFormData({ pvcName: '', mountPath: '', readOnly: false });
setEditIndex(null);
setIsModalOpen(false);
}, []);
const handleAddOrEdit = useCallback(() => {
if (!formData.pvcName || !formData.mountPath) {
return;
}
if (editIndex !== null) {
const updated = [...volumes];
updated[editIndex] = formData;
setVolumes(updated);
} else {
setVolumes([...volumes, formData]);
}
resetForm();
}, [formData, editIndex, volumes, setVolumes, resetForm]);
const handleEdit = useCallback(
(index: number) => {
setFormData(volumes[index]);
setEditIndex(index);
setIsModalOpen(true);
},
[volumes],
);
const openDetachModal = useCallback((index: number) => {
setDeleteIndex(index);
setIsDeleteModalOpen(true);
}, []);
const handleDelete = useCallback(() => {
if (deleteIndex !== null) {
setVolumes(volumes.filter((_, i) => i !== deleteIndex));
setIsDeleteModalOpen(false);
setDeleteIndex(null);
}
}, [deleteIndex, volumes, setVolumes]);
return (
<>
{volumes.length > 0 && (
<Table variant={TableVariant.compact} aria-label="Volumes Table">
<Thead>
<Tr>
<Th>PVC Name</Th>
<Th>Mount Path</Th>
<Th>Read-only Access</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{volumes.map((volume, index) => (
<Tr key={index}>
<Td>{volume.pvcName}</Td>
<Td>{volume.mountPath}</Td>
<Td>{volume.readOnly ? 'Enabled' : 'Disabled'}</Td>
<Td isActionCell>
<Dropdown
toggle={(toggleRef) => (
<MenuToggle
ref={toggleRef}
isExpanded={dropdownOpen === index}
onClick={() => setDropdownOpen(dropdownOpen === index ? null : index)}
variant="plain"
aria-label="plain kebab"
>
<EllipsisVIcon />
</MenuToggle>
)}
isOpen={dropdownOpen === index}
onSelect={() => setDropdownOpen(null)}
popperProps={{ position: 'right' }}
>
<DropdownItem onClick={() => handleEdit(index)}>Edit</DropdownItem>
<DropdownItem onClick={() => openDetachModal(index)}>Detach</DropdownItem>
</Dropdown>
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
<Button
variant="primary"
onClick={() => setIsModalOpen(true)}
style={{ marginTop: '1rem' }}
className="pf-u-mt-md"
>
Create Volume
</Button>
<Modal isOpen={isModalOpen} onClose={resetForm} variant={ModalVariant.small}>
<ModalHeader
title={editIndex !== null ? 'Edit Volume' : 'Create Volume'}
description="Add a volume and optionally connect it with an existing workspace."
/>
<ModalBody>
<Form>
<FormGroup label="PVC Name" isRequired fieldId="pvc-name">
<TextInput
name="pvcName"
isRequired
type="text"
value={formData.pvcName}
onChange={(_, val) => setFormData({ ...formData, pvcName: val })}
id="pvc-name"
/>
</FormGroup>
<FormGroup label="Mount Path" isRequired fieldId="mount-path">
<TextInput
name="mountPath"
isRequired
type="text"
value={formData.mountPath}
onChange={(_, val) => setFormData({ ...formData, mountPath: val })}
id="mount-path"
/>
</FormGroup>
<FormGroup fieldId="readonly-access">
<Switch
id="readonly-access-switch"
label="Enable read-only access"
isChecked={formData.readOnly}
onChange={() => setFormData({ ...formData, readOnly: !formData.readOnly })}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
key="confirm"
onClick={handleAddOrEdit}
isDisabled={!formData.pvcName || !formData.mountPath}
>
{editIndex !== null ? 'Save' : 'Create'}
</Button>
<Button key="cancel" variant="link" onClick={resetForm}>
Cancel
</Button>
</ModalFooter>
</Modal>
<Modal
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
variant={ModalVariant.small}
>
<ModalHeader
title="Detach Volume?"
description="The volume and all of its resources will be detached from the workspace."
/>
<ModalFooter>
<Button key="detach" variant="danger" onClick={handleDelete}>
Detach
</Button>
<Button key="cancel" variant="link" onClick={() => setIsDeleteModalOpen(false)}>
Cancel
</Button>
</ModalFooter>
</Modal>
</>
);
};
export default WorkspaceCreationPropertiesVolumesProps;

View File

@ -752,8 +752,8 @@
}
.mui-theme .pf-v6-c-modal-box {
--pf-v6-c-modal-box--BorderRadius: 0;
border: 2px solid var(--mui-palette-common-black);
--pf-v6-c-modal-box--BorderRadius: var(--mui-shape-borderRadius);
--pf-v6-c-modal-box--BoxShadow: var(--mui-shadows-24);
}
.mui-theme .pf-v6-c-button.pf-m-plain {

View File

@ -20,6 +20,32 @@ export interface WorkspaceImage {
};
}
export interface WorkspaceVolume {
pvcName: string;
mountPath: string;
readOnly: boolean;
}
export interface WorkspaceVolumes {
home: string;
data: WorkspaceVolume[];
}
export interface WorkspaceProperties {
workspaceName: string;
deferUpdates: boolean;
homeDirectory: string;
volumes: boolean;
isVolumesExpanded: boolean;
redirect?: {
to: string;
message: {
text: string;
level: string;
};
};
}
export interface WorkspacePodConfig {
id: string;
displayName: string;
@ -133,14 +159,7 @@ export interface Workspace {
labels: string[];
annotations: string[];
};
volumes: {
home: string;
data: {
pvcName: string;
mountPath: string;
readOnly: boolean;
}[];
};
volumes: WorkspaceVolumes;
endpoints: {
displayName: string;
port: string;