Repository: kubeflow/notebooks
Pull request #303: feat(ws): Notebooks v2 add secrets to workspace creation properties
Author: login: thaorell
Labels: name: approved
name: lgtm
name: size/L
name: ok-to-test
Created: 2025-05-01T19:37:14Z | Merged: 2025-05-12T11:22:38Z

 closes: #267
 related: depends on #262

Note: 

- The modal to create a new secret currently takes in octal value as `defaultMode` and stores it as decimal before submitting back to API
- The default mode also includes input validation to ensure the input is a valid unix permission value



[2025_05_01-15_35_11.webm](https://github.com/user-attachments/assets/36d54763-69b2-4b99-9216-80e683d1b6ae)



Diff:
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 000000000..bdd4c4281
--- /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<React.SetStateAction<WorkspaceSecret[]>>;
+}
+
+export const WorkspaceCreationPropertiesSecrets: React.FC<
+  WorkspaceCreationPropertiesSecretsProps
+> = ({ secrets, setSecrets }) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+  const [formData, setFormData] = useState<WorkspaceSecret>({
+    secretName: '',
+    mountPath: '',
+    defaultMode: 420,
+  });
+  const [editIndex, setEditIndex] = useState<number | null>(null);
+  const [defaultMode, setDefaultMode] = useState('644');
+  const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
+  const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
+  const [dropdownOpen, setDropdownOpen] = useState<number | null>(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 && (
+        <Table variant={TableVariant.compact} aria-label="Secrets Table">
+          <Thead>
+            <Tr>
+              <Th>Secret Name</Th>
+              <Th>Mount Path</Th>
+              <Th>Default Mode</Th>
+              <Th />
+            </Tr>
+          </Thead>
+          <Tbody>
+            {secrets.map((secret, index) => (
+              <Tr key={index}>
+                <Td>{secret.secretName}</Td>
+                <Td>{secret.mountPath}</Td>
+                <Td>{secret.defaultMode.toString(8)}</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={() => openDeleteModal(index)}>Remove</DropdownItem>
+                  </Dropdown>
+                </Td>
+              </Tr>
+            ))}
+          </Tbody>
+        </Table>
+      )}
+      <Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
+        Create Secret
+      </Button>
+      <Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
+        <ModalHeader
+          title={editIndex === null ? 'Create Secret' : 'Edit Secret'}
+          labelId="secret-modal-title"
+          description={
+            editIndex === null
+              ? 'Add a secret to securely use API keys, tokens, or other credentials in your workspace.'
+              : ''
+          }
+        />
+        <ModalBody id="secret-modal-box-body">
+          <Form onSubmit={handleAddOrEditSubmit}>
+            <FormGroup label="Secret Name" isRequired fieldId="secret-name">
+              <TextInput
+                name="secretName"
+                isRequired
+                type="text"
+                value={formData.secretName}
+                onChange={(_, val) => setFormData({ ...formData, secretName: val })}
+                id="secret-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 label="Default Mode" isRequired fieldId="default-mode">
+              <TextInput
+                name="defaultMode"
+                isRequired
+                type="text"
+                value={defaultMode}
+                validated={!isDefaultModeValid ? ValidatedOptions.error : undefined}
+                onChange={(_, val) => handleDefaultModeInput(val)}
+                id="default-mode"
+              />
+              {!isDefaultModeValid && (
+                <HelperText>
+                  <HelperTextItem variant="error">
+                    Must be a valid UNIX file system permission value (i.e. 644)
+                  </HelperTextItem>
+                </HelperText>
+              )}
+            </FormGroup>
+          </Form>
+        </ModalBody>
+        <ModalFooter>
+          <Button
+            key="confirm"
+            variant="primary"
+            onClick={handleAddOrEditSubmit}
+            isDisabled={!isDefaultModeValid}
+          >
+            {editIndex !== null ? 'Save' : 'Create'}
+          </Button>
+          <Button key="cancel" variant="link" onClick={clearForm}>
+            Cancel
+          </Button>
+        </ModalFooter>
+      </Modal>
+      <Modal
+        isOpen={isDeleteModalOpen}
+        onClose={() => setIsDeleteModalOpen(false)}
+        variant={ModalVariant.small}
+      >
+        <ModalHeader
+          title="Remove Secret?"
+          description="The secret will be removed from the workspace."
+        />
+        <ModalFooter>
+          <Button key="remove" variant="danger" onClick={handleDelete}>
+            Remove
+          </Button>
+          <Button key="cancel" variant="link" onClick={() => setIsDeleteModalOpen(false)}>
+            Cancel
+          </Button>
+        </ModalFooter>
+      </Modal>
+    </>
+  );
+};
+
+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 a5ff78690..963ba9c39 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<WorkspaceVolumes>({ home: '', data: [] });
+  const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [], secrets: [] });
   const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]);
+  const [secrets, setSecrets] = useState<WorkspaceSecret[]>(
+    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<
                   </div>
                 </div>
               )}
+              <div className="pf-u-mb-0">
+                <ExpandableSection
+                  toggleText="Secrets"
+                  onToggle={() => setIsSecretsExpanded((prev) => !prev)}
+                  isExpanded={isSecretsExpanded}
+                  isIndented
+                >
+                  {isSecretsExpanded && (
+                    <FormGroup fieldId="secrets-table" style={{ marginTop: '1rem' }}>
+                      <WorkspaceCreationPropertiesSecrets
+                        secrets={secrets}
+                        setSecrets={setSecrets}
+                      />
+                    </FormGroup>
+                  )}
+                </ExpandableSection>
+              </div>
+              {!isSecretsExpanded && (
+                <div style={{ paddingLeft: '36px', marginTop: '-10px' }}>
+                  <div>Secrets enable your project to securely access and manage credentials.</div>
+                  <div className="pf-u-font-size-sm">
+                    <strong>{secrets.length} added</strong>
+                  </div>
+                </div>
+              )}
             </Form>
           </div>
         </SplitItem>
diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
index e61217add..45275951a 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 c8d97a521..f775a2ea8 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 {


Reviews:
user: login: paulovmr
body: Thanks @thaorell for the PR! I left a few comments, after those are fixed and we get the dependent PR merged and your branch rebased, I can do a final review.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-05-06T01:02:28Z
user: login: jenny-s51
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T17:26:07Z
user: login: jenny-s51
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T17:33:39Z
user: login: paulovmr
body: /lgtm
state: APPROVED
author_association: NONE
submitted_at: 2025-05-08T18:52:33Z
user: login: jenny-s51
body: Just one quick fix below, otherwise LGTM. Thanks for making these changes @thaorell !
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-08T19:19:03Z
user: login: jenny-s51
body: Thanks for making suggested update Charles ! 

I'm seeing a slight spacing inconsistency between the expandable section and the description text below. The gap between the Volumes expandable section and text is larger than the gap for Secrets. 

 Can we apply a layout to make sure the spacing is consistent for both? The [`Stack` layout with gutter](https://www.patternfly.org/layouts/stack#with-gutter) would be good to use here . WDYT @thaorell ?
<img width="507" alt="Screenshot 2025-05-09 at 11 20 33 AM" src="https://github.com/user-attachments/assets/7e281ac7-282d-4589-8031-24a88c71c82e" />

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T16:10:04Z
user: login: jenny-s51
body: Thanks @thaorell ! Latest changes look good to me - yes, let's merge and I can open a follow-up ticket for a couple additional enhancements we'll want to include here:

1. Applying Stack layout for consistent spacing in these sections. 
2. Persisting the description text when the expandable section is open - currently it gets removed with the `!isVolumesExpanded` flag however we want to expose it in both cases. 

Great work!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-09T19:29:04Z
user: login: ederign
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T11:19:34Z

Review comments:
diff_hunk: @@ -0,0 +1,239 @@
+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 WorkspaceCreationSecretsProps {
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx
user: login: paulovmr
body: ```suggestion
interface WorkspaceCreationPropertiesSecretsProps {
```
created_at: 2025-05-05T12:54:45Z
author_association: NONE
diff_hunk: @@ -20,6 +20,39 @@ export interface WorkspaceImage {
   };
 }
 
+export interface WorkspaceVolume {
+  pvcName: string;
+  mountPath: string;
+  readOnly: boolean;
+}
+
+export interface WorkspaceVolumes {
+  home: string;
+  data: WorkspaceVolume[];
+  secrets: WorkSpaceSecret[];
+}
+
+export interface WorkSpaceSecret {
path: workspaces/frontend/src/shared/types.ts
user: login: paulovmr
body: ```suggestion
export interface WorkspaceSecret {
```
created_at: 2025-05-05T12:55:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,155 @@
+import * as React from 'react';
+import { 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/WorkspaceCreationPropertiesVolumesProps';
+import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume, WorkSpaceSecret } from '~/shared/types';
+import WorkspaceCreationPropertiesSecrets from './WorkspaceCreationPropertiesSecrets';
+
+interface WorkspaceCreationImageSelectionProps {
+  selectedImage: WorkspaceImage | undefined;
+}
+
+const WorkspaceCreationPropertiesSelection: React.FunctionComponent<
+  WorkspaceCreationImageSelectionProps
+> = ({ selectedImage }) => {
+  const [workspaceName, setWorkspaceName] = useState('');
+  const [deferUpdates, setDeferUpdates] = useState(false);
+  const [homeDirectory, setHomeDirectory] = useState('');
+  const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [], secrets: [] });
+  const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]);
+  const [secrets, setSecrets] = useState<WorkSpaceSecret[]>(
+    volumes.secrets.length ? volumes.secrets : [],
+  );
+  const [isVolumesExpanded, setIsVolumesExpanded] = useState(false);
+  const [isSecretsExpanded, setIsSecretsExpanded] = useState(false);
+
+  React.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>
+              )}
+              <div className="pf-u-mb-0">
+                <ExpandableSection
+                  toggleText="Secrets"
+                  onToggle={() => setIsSecretsExpanded((prev) => !prev)}
+                  isExpanded={isSecretsExpanded}
+                  isIndented
+                >
+                  {isSecretsExpanded && (
+                    <FormGroup fieldId="secrets-table" style={{ marginTop: '1rem' }}>
+                      <WorkspaceCreationPropertiesSecrets
+                        secrets={secrets}
+                        setSecrets={setSecrets}
+                      />
+                    </FormGroup>
+                  )}
+                </ExpandableSection>
+                {!isSecretsExpanded && (
+                  <div style={{ paddingLeft: '36px' }}>
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx
user: login: paulovmr
body: Please consider adding a message here, similarly to volumes. @jenny-s51 Any suggestions on the message to use here?
created_at: 2025-05-06T00:57:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+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 WorkspaceCreationSecretsProps {
+  secrets: WorkSpaceSecret[];
+  setSecrets: React.Dispatch<React.SetStateAction<WorkSpaceSecret[]>>;
+}
+
+export const WorkspaceCreationPropertiesSecrets: React.FC<WorkspaceCreationSecretsProps> = ({
+  secrets,
+  setSecrets,
+}) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+  const [formData, setFormData] = useState<WorkSpaceSecret>({
+    secretName: '',
+    mountPath: '',
+    defaultMode: 420,
+  });
+  const [editIndex, setEditIndex] = useState<number | null>(null);
+  const [defaultMode, setDefaultMode] = useState('644');
+  const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
+  const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
+  const [dropdownOpen, setDropdownOpen] = useState<number | null>(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 && (
+        <Table variant={TableVariant.compact} aria-label="Secrets Table">
+          <Thead>
+            <Tr>
+              <Th>Secret Name</Th>
+              <Th>Mount Path</Th>
+              <Th>Default Mode</Th>
+              <Th />
+            </Tr>
+          </Thead>
+          <Tbody>
+            {secrets.map((secret, index) => (
+              <Tr key={index}>
+                <Td>{secret.secretName}</Td>
+                <Td>{secret.mountPath}</Td>
+                <Td>{secret.defaultMode.toString(8)}</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={() => openDeleteModal(index)}>Remove</DropdownItem>
+                  </Dropdown>
+                </Td>
+              </Tr>
+            ))}
+          </Tbody>
+        </Table>
+      )}
+      <Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
+        Create Secrets
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx
user: login: paulovmr
body: ```suggestion
        Create Secret
```
created_at: 2025-05-06T00:58:26Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+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 WorkspaceCreationSecretsProps {
+  secrets: WorkSpaceSecret[];
+  setSecrets: React.Dispatch<React.SetStateAction<WorkSpaceSecret[]>>;
+}
+
+export const WorkspaceCreationPropertiesSecrets: React.FC<WorkspaceCreationSecretsProps> = ({
+  secrets,
+  setSecrets,
+}) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+  const [formData, setFormData] = useState<WorkSpaceSecret>({
+    secretName: '',
+    mountPath: '',
+    defaultMode: 420,
+  });
+  const [editIndex, setEditIndex] = useState<number | null>(null);
+  const [defaultMode, setDefaultMode] = useState('644');
+  const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
+  const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
+  const [dropdownOpen, setDropdownOpen] = useState<number | null>(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 && (
+        <Table variant={TableVariant.compact} aria-label="Secrets Table">
+          <Thead>
+            <Tr>
+              <Th>Secret Name</Th>
+              <Th>Mount Path</Th>
+              <Th>Default Mode</Th>
+              <Th />
+            </Tr>
+          </Thead>
+          <Tbody>
+            {secrets.map((secret, index) => (
+              <Tr key={index}>
+                <Td>{secret.secretName}</Td>
+                <Td>{secret.mountPath}</Td>
+                <Td>{secret.defaultMode.toString(8)}</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={() => openDeleteModal(index)}>Remove</DropdownItem>
+                  </Dropdown>
+                </Td>
+              </Tr>
+            ))}
+          </Tbody>
+        </Table>
+      )}
+      <Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
+        Create Secrets
+      </Button>
+      <Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
+        <ModalHeader
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx
user: login: paulovmr
body: Please consider adding a `description` to the `ModalHeader`, similarly to volumes. @jenny-s51 Any suggestions on the message to use here?
created_at: 2025-05-06T01:01:09Z
author_association: NONE
diff_hunk: @@ -0,0 +1,155 @@
+import * as React from 'react';
+import { 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/WorkspaceCreationPropertiesVolumesProps';
+import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume, WorkSpaceSecret } from '~/shared/types';
+import WorkspaceCreationPropertiesSecrets from './WorkspaceCreationPropertiesSecrets';
+
+interface WorkspaceCreationImageSelectionProps {
+  selectedImage: WorkspaceImage | undefined;
+}
+
+const WorkspaceCreationPropertiesSelection: React.FunctionComponent<
+  WorkspaceCreationImageSelectionProps
+> = ({ selectedImage }) => {
+  const [workspaceName, setWorkspaceName] = useState('');
+  const [deferUpdates, setDeferUpdates] = useState(false);
+  const [homeDirectory, setHomeDirectory] = useState('');
+  const [volumes, setVolumes] = useState<WorkspaceVolumes>({ home: '', data: [], secrets: [] });
+  const [volumesData, setVolumesData] = useState<WorkspaceVolume[]>([]);
+  const [secrets, setSecrets] = useState<WorkSpaceSecret[]>(
+    volumes.secrets.length ? volumes.secrets : [],
+  );
+  const [isVolumesExpanded, setIsVolumesExpanded] = useState(false);
+  const [isSecretsExpanded, setIsSecretsExpanded] = useState(false);
+
+  React.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>
+              )}
+              <div className="pf-u-mb-0">
+                <ExpandableSection
+                  toggleText="Secrets"
+                  onToggle={() => setIsSecretsExpanded((prev) => !prev)}
+                  isExpanded={isSecretsExpanded}
+                  isIndented
+                >
+                  {isSecretsExpanded && (
+                    <FormGroup fieldId="secrets-table" style={{ marginTop: '1rem' }}>
+                      <WorkspaceCreationPropertiesSecrets
+                        secrets={secrets}
+                        setSecrets={setSecrets}
+                      />
+                    </FormGroup>
+                  )}
+                </ExpandableSection>
+                {!isSecretsExpanded && (
+                  <div style={{ paddingLeft: '36px' }}>
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx
user: login: jenny-s51
body: Yes @paulovmr, we’ll want to apply similar verbiage / phrasing here!

“Secrets enable your project to securely access and manage credentials.” would be a good description to add, it aligns with the “Volumes” content and communicates clearly that it will allow the project to work with the necessary credentials. 
created_at: 2025-05-08T17:26:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+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 WorkspaceCreationSecretsProps {
+  secrets: WorkSpaceSecret[];
+  setSecrets: React.Dispatch<React.SetStateAction<WorkSpaceSecret[]>>;
+}
+
+export const WorkspaceCreationPropertiesSecrets: React.FC<WorkspaceCreationSecretsProps> = ({
+  secrets,
+  setSecrets,
+}) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+  const [formData, setFormData] = useState<WorkSpaceSecret>({
+    secretName: '',
+    mountPath: '',
+    defaultMode: 420,
+  });
+  const [editIndex, setEditIndex] = useState<number | null>(null);
+  const [defaultMode, setDefaultMode] = useState('644');
+  const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
+  const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
+  const [dropdownOpen, setDropdownOpen] = useState<number | null>(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 && (
+        <Table variant={TableVariant.compact} aria-label="Secrets Table">
+          <Thead>
+            <Tr>
+              <Th>Secret Name</Th>
+              <Th>Mount Path</Th>
+              <Th>Default Mode</Th>
+              <Th />
+            </Tr>
+          </Thead>
+          <Tbody>
+            {secrets.map((secret, index) => (
+              <Tr key={index}>
+                <Td>{secret.secretName}</Td>
+                <Td>{secret.mountPath}</Td>
+                <Td>{secret.defaultMode.toString(8)}</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={() => openDeleteModal(index)}>Remove</DropdownItem>
+                  </Dropdown>
+                </Td>
+              </Tr>
+            ))}
+          </Tbody>
+        </Table>
+      )}
+      <Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
+        Create Secrets
+      </Button>
+      <Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
+        <ModalHeader
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx
user: login: jenny-s51
body: We can add this description text: "Add a secret to securely use API keys, tokens, or other credentials in your workspace."
created_at: 2025-05-08T17:33:39Z
author_association: MEMBER
diff_hunk: @@ -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<React.SetStateAction<WorkspaceSecret[]>>;
+}
+
+export const WorkspaceCreationPropertiesSecrets: React.FC<
+  WorkspaceCreationPropertiesSecretsProps
+> = ({ secrets, setSecrets }) => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+  const [formData, setFormData] = useState<WorkspaceSecret>({
+    secretName: '',
+    mountPath: '',
+    defaultMode: 420,
+  });
+  const [editIndex, setEditIndex] = useState<number | null>(null);
+  const [defaultMode, setDefaultMode] = useState('644');
+  const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
+  const [isDefaultModeValid, setIsDefaultModeValid] = useState(true);
+  const [dropdownOpen, setDropdownOpen] = useState<number | null>(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 && (
+        <Table variant={TableVariant.compact} aria-label="Secrets Table">
+          <Thead>
+            <Tr>
+              <Th>Secret Name</Th>
+              <Th>Mount Path</Th>
+              <Th>Default Mode</Th>
+              <Th />
+            </Tr>
+          </Thead>
+          <Tbody>
+            {secrets.map((secret, index) => (
+              <Tr key={index}>
+                <Td>{secret.secretName}</Td>
+                <Td>{secret.mountPath}</Td>
+                <Td>{secret.defaultMode.toString(8)}</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={() => openDeleteModal(index)}>Remove</DropdownItem>
+                  </Dropdown>
+                </Td>
+              </Tr>
+            ))}
+          </Tbody>
+        </Table>
+      )}
+      <Button variant="primary" onClick={() => setIsModalOpen(true)} style={{ marginTop: '1rem' }}>
+        Create Secret
+      </Button>
+      <Modal isOpen={isModalOpen} onClose={clearForm} variant={ModalVariant.small}>
+        <ModalHeader
+          title={editIndex === null ? 'Create Secret' : 'Edit Secret'}
+          labelId="secret-modal-title"
+          description={
+            editIndex === null
+              ? 'Add a secret to securely use API keys, tokens, or other credentials in your workspace'
path: workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx
user: login: jenny-s51
body: ```suggestion
              ? 'Add a secret to securely use API keys, tokens, or other credentials in your workspace.'
```
created_at: 2025-05-08T19:17:43Z
author_association: MEMBER

PR comments:
user: login: paulovmr
created_at: 2025-05-07T10:10:46Z
body: /ok-to-test
author_association: NONE
user: login: thaorell
created_at: 2025-05-08T15:57:42Z
body: @paulovmr the only pending items are the messages on the UI. I'll wait for @jenny-s51 's response
author_association: NONE
user: login: thaorell
created_at: 2025-05-08T18:03:21Z
body: [screencast-localhost_9000-2025_05_08-14_02_58.webm](https://github.com/user-attachments/assets/8843ff42-532d-438d-852c-b9658f28b7ae)

author_association: NONE
user: login: thaorell
created_at: 2025-05-09T17:17:29Z
body: @jenny-s51 Thank you for catching that. It was due to a wrongly wrapped `<div>`, and I've corrected it. I do think there's value in using `<Stack>`, and I think it'd be best to create another issue for that. If you do, please feel free to assign me. What do you think?

![Screenshot 2025-05-09 at 1 16 08 PM](https://github.com/user-attachments/assets/7f69f0c4-e130-47d2-837c-472a242b310d)

author_association: NONE
user: login: ederign
created_at: 2025-05-12T11:21:37Z
body: @paulovmr @thaorell on the next iterations (maybe we can check with ux), let's try to incorporate the feedback on the call of having a UI instead of mode code.

Something more modern, but similar to:
![image](https://github.com/user-attachments/assets/b356f876-5cb7-41e0-96ad-0c478cadd415)

author_association: MEMBER
user: login: ederign
created_at: 2025-05-12T11:21:48Z
body: /approve
author_association: MEMBER
user: login: ederign
created_at: 2025-05-12T11:21:51Z
body: /lgtm
author_association: MEMBER
user: login: google-oss-prow[bot]
created_at: 2025-05-12T11:21:53Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeflow/notebooks/pull/303#issuecomment-2872146316" title="Approved">ederign</a>*

The full list of commands accepted by this bot can be found [here](https://oss.gprow.dev/command-help?repo=kubeflow%2Fnotebooks).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[workspaces/frontend/OWNERS](https://github.com/kubeflow/notebooks/blob/notebooks-v2/workspaces/frontend/OWNERS)~~ [ederign]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE