diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx new file mode 100644 index 00000000..bdd4c428 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx @@ -0,0 +1,243 @@ +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, + Dropdown, + DropdownItem, + MenuToggle, + ModalBody, + ModalFooter, + Form, + FormGroup, + ModalHeader, + ValidatedOptions, + HelperText, + HelperTextItem, +} from '@patternfly/react-core'; +import { WorkspaceSecret } from '~/shared/types'; + +interface WorkspaceCreationPropertiesSecretsProps { + secrets: WorkspaceSecret[]; + setSecrets: React.Dispatch>; +} + +export const WorkspaceCreationPropertiesSecrets: React.FC< + WorkspaceCreationPropertiesSecretsProps +> = ({ secrets, setSecrets }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [formData, setFormData] = useState({ + secretName: '', + mountPath: '', + defaultMode: 420, + }); + const [editIndex, setEditIndex] = useState(null); + const [defaultMode, setDefaultMode] = useState('644'); + const [deleteIndex, setDeleteIndex] = useState(null); + const [isDefaultModeValid, setIsDefaultModeValid] = useState(true); + const [dropdownOpen, setDropdownOpen] = useState(null); + + const openDeleteModal = useCallback((i: number) => { + setIsDeleteModalOpen(true); + setDeleteIndex(i); + }, []); + + const handleEdit = useCallback( + (index: number) => { + setFormData(secrets[index]); + setDefaultMode(secrets[index].defaultMode.toString(8)); + setEditIndex(index); + setIsModalOpen(true); + }, + [secrets], + ); + + const handleDefaultModeInput = useCallback( + (val: string) => { + if (val.length <= 3) { + // 0 no permissions, 4 read only, 5 read + execute, 6 read + write, 7 all permissions + setDefaultMode(val); + const permissions = ['0', '4', '5', '6', '7']; + const isValid = Array.from(val).every((char) => permissions.includes(char)); + if (val.length < 3 || !isValid) { + setIsDefaultModeValid(false); + } else { + setIsDefaultModeValid(true); + } + const decimalVal = parseInt(val, 8); + setFormData({ ...formData, defaultMode: decimalVal }); + } + }, + [setFormData, setIsDefaultModeValid, setDefaultMode, formData], + ); + + const clearForm = useCallback(() => { + setFormData({ secretName: '', mountPath: '', defaultMode: 420 }); + setEditIndex(null); + setIsModalOpen(false); + setIsDefaultModeValid(true); + }, []); + + const handleAddOrEditSubmit = useCallback(() => { + setSecrets((prev) => { + if (editIndex === null) { + return [...prev, formData]; + } + const updated = prev; + updated[editIndex] = formData; + return updated; + }); + clearForm(); + }, [editIndex, setSecrets, clearForm, formData]); + + const handleDelete = useCallback(() => { + if (deleteIndex !== null) { + setSecrets((prev) => { + prev.splice(deleteIndex, 1); + return prev; + }); + setDeleteIndex(null); + setIsDeleteModalOpen(false); + } + }, [setSecrets, deleteIndex]); + + return ( + <> + {secrets.length > 0 && ( + + + + + + + + + + {secrets.map((secret, index) => ( + + + + + + + ))} + +
Secret NameMount PathDefault Mode +
{secret.secretName}{secret.mountPath}{secret.defaultMode.toString(8)} + ( + 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 + +
+ )} + + + + +
+ + setFormData({ ...formData, secretName: val })} + id="secret-name" + /> + + + setFormData({ ...formData, mountPath: val })} + id="mount-path" + /> + + + handleDefaultModeInput(val)} + id="default-mode" + /> + {!isDefaultModeValid && ( + + + Must be a valid UNIX file system permission value (i.e. 644) + + + )} + +
+
+ + + + +
+ setIsDeleteModalOpen(false)} + variant={ModalVariant.small} + > + + + + + + + + ); +}; + +export default WorkspaceCreationPropertiesSecrets; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx index a5ff7869..963ba9c3 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx @@ -13,7 +13,8 @@ import { } 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'; +import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume, WorkspaceSecret } from '~/shared/types'; +import { WorkspaceCreationPropertiesSecrets } from './WorkspaceCreationPropertiesSecrets'; interface WorkspaceCreationPropertiesSelectionProps { selectedImage: WorkspaceImage | undefined; @@ -25,9 +26,13 @@ const WorkspaceCreationPropertiesSelection: React.FunctionComponent< const [workspaceName, setWorkspaceName] = useState(''); const [deferUpdates, setDeferUpdates] = useState(false); const [homeDirectory, setHomeDirectory] = useState(''); - const [volumes, setVolumes] = useState({ home: '', data: [] }); + const [volumes, setVolumes] = useState({ home: '', data: [], secrets: [] }); const [volumesData, setVolumesData] = useState([]); + const [secrets, setSecrets] = useState( + volumes.secrets.length ? volumes.secrets : [], + ); const [isVolumesExpanded, setIsVolumesExpanded] = useState(false); + const [isSecretsExpanded, setIsSecretsExpanded] = useState(false); useEffect(() => { setVolumes((prev) => ({ @@ -115,6 +120,31 @@ const WorkspaceCreationPropertiesSelection: React.FunctionComponent< )} +
+ setIsSecretsExpanded((prev) => !prev)} + isExpanded={isSecretsExpanded} + isIndented + > + {isSecretsExpanded && ( + + + + )} + +
+ {!isSecretsExpanded && ( +
+
Secrets enable your project to securely access and manage credentials.
+
+ {secrets.length} added +
+
+ )} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index e61217ad..45275951 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -89,6 +89,13 @@ export const Workspaces: React.FunctionComponent = () => { readOnly: false, }, ], + secrets: [ + { + secretName: 'Secret-2', + mountPath: '/data', + defaultMode: 420, + }, + ], }, endpoints: [ { @@ -144,6 +151,13 @@ export const Workspaces: React.FunctionComponent = () => { readOnly: false, }, ], + secrets: [ + { + secretName: 'workspace-secret', + mountPath: '/secrets/my-secret', + defaultMode: 420, + }, + ], }, endpoints: [ { diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index c8d97a52..f775a2ea 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -29,6 +29,13 @@ export interface WorkspaceVolume { export interface WorkspaceVolumes { home: string; data: WorkspaceVolume[]; + secrets: WorkspaceSecret[]; +} + +export interface WorkspaceSecret { + defaultMode: number; + secretName: string; + mountPath: string; } export interface WorkspaceProperties {