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:
parent
1e8280519a
commit
c3a6f54ae2
|
@ -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' }}>
|
||||
|
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue