diff --git a/workspaces/frontend/scripts/swagger.version b/workspaces/frontend/scripts/swagger.version index 21ab894be..1b0720f87 100644 --- a/workspaces/frontend/scripts/swagger.version +++ b/workspaces/frontend/scripts/swagger.version @@ -1 +1 @@ -4f0a29dec0d3c9f0d0f02caab4dc84101bfef8b0 +be67e887ad7396cf0078edca36201564a208d1b7 diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/helpers.ts b/workspaces/frontend/src/app/pages/Workspaces/Form/helpers.ts new file mode 100644 index 000000000..9263694a8 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/helpers.ts @@ -0,0 +1,7 @@ +export const isValidDefaultMode = (mode: string): boolean => { + if (mode.length !== 3) { + return false; + } + const permissions = ['0', '4', '5', '6', '7']; + return Array.from(mode).every((char) => permissions.includes(char)); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx index 66e07ab46..5d823288a 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; import { Table, @@ -12,19 +12,17 @@ import { import { Button } from '@patternfly/react-core/dist/esm/components/Button'; import { Modal, - ModalBody, ModalFooter, ModalHeader, ModalVariant, } from '@patternfly/react-core/dist/esm/components/Modal'; -import { ValidatedOptions } from '@patternfly/react-core/helpers'; -import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown'; import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle'; -import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form'; -import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText'; -import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; -import { WorkspacesPodSecretMount } from '~/generated/data-contracts'; +import { SecretsSecretListItem, WorkspacesPodSecretMount } from '~/generated/data-contracts'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; +import { SecretsAttachModal } from './secrets/SecretsAttachModal'; +import { SecretsCreateModal } from './secrets/SecretsCreateModal'; interface WorkspaceFormPropertiesSecretsProps { secrets: WorkspacesPodSecretMount[]; @@ -37,18 +35,30 @@ export const WorkspaceFormPropertiesSecrets: React.FC { - const [isModalOpen, setIsModalOpen] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isAttachModalOpen, setIsAttachModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [formData, setFormData] = useState({ - secretName: '', - mountPath: '', - defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8), - }); + const [editingSecret, setEditingSecret] = useState( + undefined, + ); const [editIndex, setEditIndex] = useState(null); - const [defaultMode, setDefaultMode] = useState(DEFAULT_MODE_OCTAL); const [deleteIndex, setDeleteIndex] = useState(null); - const [isDefaultModeValid, setIsDefaultModeValid] = useState(true); const [dropdownOpen, setDropdownOpen] = useState(null); + const [availableSecrets, setAvailableSecrets] = useState([]); + const [attachedSecrets, setAttachedSecrets] = useState([]); + const [attachedMountPath, setAttachedMountPath] = useState(''); + const [attachedDefaultMode, setAttachedDefaultMode] = useState(DEFAULT_MODE_OCTAL); + + const { api } = useNotebookAPI(); + const { selectedNamespace } = useNamespaceContext(); + + useEffect(() => { + const fetchSecrets = async () => { + const secretsResponse = await api.secrets.listSecrets(selectedNamespace); + setAvailableSecrets(secretsResponse.data); + }; + fetchSecrets(); + }, [api.secrets, selectedNamespace]); const openDeleteModal = useCallback((i: number) => { setIsDeleteModalOpen(true); @@ -57,62 +67,86 @@ export const WorkspaceFormPropertiesSecrets: React.FC { - setFormData(secrets[index]); - setDefaultMode(secrets[index].defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL); + setEditingSecret(secrets[index]); setEditIndex(index); - setIsModalOpen(true); + setIsCreateModalOpen(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 }); + const handleAttachSecrets = useCallback( + (newSecrets: SecretsSecretListItem[], mountPath: string, mode: number) => { + const newAttachedSecrets = newSecrets.map((secret) => ({ + secretName: secret.name, + mountPath, + defaultMode: mode, + })); + const oldAttachedNames = new Set(attachedSecrets.map((s) => s.secretName)); + const secretsWithoutOldAttached = secrets.filter((s) => !oldAttachedNames.has(s.secretName)); + const manualSecretNames = new Set(secretsWithoutOldAttached.map((s) => s.secretName)); + const filteredNewAttached = newAttachedSecrets.filter( + (s) => !manualSecretNames.has(s.secretName), + ); + + // Update both states + setAttachedSecrets(filteredNewAttached); + setSecrets([...secretsWithoutOldAttached, ...filteredNewAttached]); + setAttachedMountPath(mountPath); + setAttachedDefaultMode(mode.toString(8)); + setIsAttachModalOpen(false); + }, + [attachedSecrets, secrets, setSecrets], + ); + + const handleCreateOrEditSubmit = useCallback( + (secret: WorkspacesPodSecretMount) => { + if (editIndex !== null) { + const updated = [...secrets]; + updated[editIndex] = secret; + setSecrets(updated); + } else { + setSecrets([...secrets, secret]); } + setEditingSecret(undefined); + setEditIndex(null); + setIsCreateModalOpen(false); }, - [setFormData, setIsDefaultModeValid, setDefaultMode, formData], + [editIndex, secrets, setSecrets], ); - const clearForm = useCallback(() => { - setFormData({ secretName: '', mountPath: '', defaultMode: 420 }); + const handleCreateModalClose = useCallback(() => { + setEditingSecret(undefined); setEditIndex(null); - setIsModalOpen(false); - setIsDefaultModeValid(true); + setIsCreateModalOpen(false); }, []); - const handleAddOrEditSubmit = useCallback(() => { - if (!formData.secretName || !formData.mountPath) { - return; - } - if (editIndex !== null) { - const updated = [...secrets]; - updated[editIndex] = formData; - setSecrets(updated); - } else { - setSecrets([...secrets, formData]); - } - clearForm(); - }, [clearForm, editIndex, formData, secrets, setSecrets]); + const isAttachedSecret = useCallback( + (secretName: string) => attachedSecrets.some((s) => s.secretName === secretName), + [attachedSecrets], + ); const handleDelete = useCallback(() => { if (deleteIndex === null) { return; } + const secretToDelete = secrets[deleteIndex]; setSecrets(secrets.filter((_, i) => i !== deleteIndex)); + + // If it's an attached secret, also remove from attachedSecrets + if (isAttachedSecret(secretToDelete.secretName)) { + const updatedAttachedSecrets = attachedSecrets.filter( + (s) => s.secretName !== secretToDelete.secretName, + ); + setAttachedSecrets(updatedAttachedSecrets); + if (updatedAttachedSecrets.length === 0) { + setAttachedMountPath(''); + setAttachedDefaultMode(DEFAULT_MODE_OCTAL); + } + } + setDeleteIndex(null); setIsDeleteModalOpen(false); - }, [deleteIndex, secrets, setSecrets]); + }, [deleteIndex, secrets, setSecrets, attachedSecrets, isAttachedSecret]); return ( <> @@ -149,7 +183,9 @@ export const WorkspaceFormPropertiesSecrets: React.FC setDropdownOpen(null)} popperProps={{ position: 'right' }} > - handleEdit(index)}>Edit + {!isAttachedSecret(secret.secretName) && ( + handleEdit(index)}>Edit + )} openDeleteModal(index)}>Remove @@ -159,79 +195,34 @@ export const WorkspaceFormPropertiesSecrets: React.FC )} + - - - -
- - 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) - - - )} - -
-
- - - - -
+ secret.secretName)} + onClose={handleAttachSecrets} + initialMountPath={attachedMountPath} + initialDefaultMode={attachedDefaultMode} + /> + setIsDeleteModalOpen(false)} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsAttachModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsAttachModal.tsx new file mode 100644 index 000000000..d0ebc13a7 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsAttachModal.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, +} from '@patternfly/react-core/dist/esm/components/Modal'; +import { MultiTypeaheadSelect, MultiTypeaheadSelectOption } from '@patternfly/react-templates'; +import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form'; +import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText'; +import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; +import { ValidatedOptions } from '@patternfly/react-core/helpers'; +import { SecretsSecretListItem } from '~/generated/data-contracts'; +import { isValidDefaultMode } from '~/app/pages/Workspaces/Form/helpers'; + +export interface SecretsAttachModalProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + onClose: (secrets: SecretsSecretListItem[], mountPath: string, mode: number) => void; + selectedSecrets: string[]; + availableSecrets: SecretsSecretListItem[]; + initialMountPath?: string; + initialDefaultMode?: string; +} + +export const SecretsAttachModal: React.FC = ({ + isOpen, + setIsOpen, + onClose, + selectedSecrets, + availableSecrets, + initialMountPath = '', + initialDefaultMode = '', +}) => { + const [selected, setSelected] = useState(selectedSecrets); + const [mountPath, setMountPath] = useState(initialMountPath); + const [defaultMode, setDefaultMode] = useState(initialDefaultMode); + const [isDefaultModeValid, setIsDefaultModeValid] = useState(true); + + // Sync state with props when modal opens or props change + useEffect(() => { + if (isOpen) { + setSelected(selectedSecrets); + setMountPath(initialMountPath); + setDefaultMode(initialDefaultMode); + setIsDefaultModeValid(true); + } + }, [isOpen, selectedSecrets, initialMountPath, initialDefaultMode]); + + const handleDefaultModeChange = (val: string) => { + if (val.length <= 3) { + setDefaultMode(val); + const isValid = isValidDefaultMode(val); + setIsDefaultModeValid(val.length === 3 && isValid); + } + }; + + const initialOptions = useMemo( + () => + availableSecrets.map((secret) => ({ + content: secret.name, + value: secret.name, + selected: selectedSecrets.includes(secret.name), + isDisabled: !secret.canMount, + description: `Type: ${secret.type}`, + })), + [availableSecrets, selectedSecrets], + ); + + return ( + setIsOpen(false)} + ouiaId="BasicModal" + aria-labelledby="basic-modal-title" + aria-describedby="modal-box-body-basic" + variant={ModalVariant.medium} + > + + +
+ + `No secret was found for "${filter}"`} + onSelectionChange={(_ev, selections) => setSelected(selections as string[])} + /> + + + setMountPath(val)} + id="mount-path" + /> + + + handleDefaultModeChange(val)} + id="default-mode" + /> + {!isDefaultModeValid && ( + + + Must be a valid UNIX file system permission value (i.e. 644) + + + )} + +
+
+ + + + +
+ ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsCreateModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsCreateModal.tsx new file mode 100644 index 000000000..e941eb61f --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsCreateModal.tsx @@ -0,0 +1,148 @@ +import React, { useEffect, useState } from 'react'; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, +} from '@patternfly/react-core/dist/esm/components/Modal'; +import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form'; +import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText'; +import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; +import { ValidatedOptions } from '@patternfly/react-core/helpers'; +import { WorkspacesPodSecretMount } from '~/generated/data-contracts'; +import { isValidDefaultMode } from '~/app/pages/Workspaces/Form/helpers'; + +const DEFAULT_MODE_OCTAL = (420).toString(8); + +export interface SecretsCreateModalProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + onSubmit: (secret: WorkspacesPodSecretMount) => void; + editSecret?: WorkspacesPodSecretMount; +} + +export const SecretsCreateModal: React.FC = ({ + isOpen, + setIsOpen, + onSubmit, + editSecret, +}) => { + const [formData, setFormData] = useState({ + secretName: '', + mountPath: '', + defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8), + }); + const [defaultMode, setDefaultMode] = useState(DEFAULT_MODE_OCTAL); + const [isDefaultModeValid, setIsDefaultModeValid] = useState(true); + + // Sync state when modal opens or editSecret changes + useEffect(() => { + if (isOpen) { + if (editSecret) { + setFormData(editSecret); + setDefaultMode(editSecret.defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL); + } else { + setFormData({ + secretName: '', + mountPath: '', + defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8), + }); + setDefaultMode(DEFAULT_MODE_OCTAL); + } + setIsDefaultModeValid(true); + } + }, [isOpen, editSecret]); + + const handleDefaultModeInput = (val: string) => { + if (val.length <= 3) { + // 0 no permissions, 4 read only, 5 read + execute, 6 read + write, 7 all permissions + setDefaultMode(val); + const isValid = isValidDefaultMode(val); + setIsDefaultModeValid(val.length === 3 && isValid); + const decimalVal = parseInt(val, 8); + setFormData({ ...formData, defaultMode: decimalVal }); + } + }; + + const handleSubmit = () => { + if (!formData.secretName || !formData.mountPath || !isDefaultModeValid) { + return; + } + onSubmit(formData); + }; + + const handleClose = () => { + setIsOpen(false); + }; + + return ( + + + +
+ + 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) + + + )} + +
+
+ + + + +
+ ); +}; diff --git a/workspaces/frontend/src/generated/Secrets.ts b/workspaces/frontend/src/generated/Secrets.ts new file mode 100644 index 000000000..c40d11a44 --- /dev/null +++ b/workspaces/frontend/src/generated/Secrets.ts @@ -0,0 +1,141 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import { + ApiErrorEnvelope, + ApiSecretCreateEnvelope, + ApiSecretEnvelope, + ApiSecretListEnvelope, + SecretsSecretUpdate, +} from './data-contracts'; +import { ContentType, HttpClient, RequestParams } from './http-client'; + +export class Secrets extends HttpClient { + /** + * @description Provides a list of all secrets that the user has access to in the specified namespace + * + * @tags secrets + * @name ListSecrets + * @summary Returns a list of all secrets in a namespace + * @request GET:/secrets/{namespace} + * @response `200` `ApiSecretListEnvelope` Successful secrets response + * @response `401` `ApiErrorEnvelope` Unauthorized + * @response `403` `ApiErrorEnvelope` Forbidden + * @response `422` `ApiErrorEnvelope` Unprocessable Entity. Validation error. + * @response `500` `ApiErrorEnvelope` Internal server error + */ + listSecrets = (namespace: string, params: RequestParams = {}) => + this.request({ + path: `/secrets/${namespace}`, + method: 'GET', + format: 'json', + ...params, + }); + /** + * @description Creates a new secret in the specified namespace + * + * @tags secrets + * @name CreateSecret + * @summary Creates a new secret + * @request POST:/secrets/{namespace} + * @response `201` `ApiSecretCreateEnvelope` Secret created successfully + * @response `400` `ApiErrorEnvelope` Bad request + * @response `401` `ApiErrorEnvelope` Unauthorized + * @response `403` `ApiErrorEnvelope` Forbidden + * @response `409` `ApiErrorEnvelope` Secret already exists + * @response `413` `ApiErrorEnvelope` Request Entity Too Large. The request body is too large. + * @response `415` `ApiErrorEnvelope` Unsupported Media Type. Content-Type header is not correct. + * @response `422` `ApiErrorEnvelope` Unprocessable Entity. Validation error. + * @response `500` `ApiErrorEnvelope` Internal server error + */ + createSecret = (namespace: string, secret: ApiSecretCreateEnvelope, params: RequestParams = {}) => + this.request({ + path: `/secrets/${namespace}`, + method: 'POST', + body: secret, + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Provides details of a specific secret by name and namespace + * + * @tags secrets + * @name GetSecret + * @summary Returns a specific secret + * @request GET:/secrets/{namespace}/{name} + * @response `200` `ApiSecretEnvelope` Successful secret response + * @response `401` `ApiErrorEnvelope` Unauthorized + * @response `403` `ApiErrorEnvelope` Forbidden + * @response `404` `ApiErrorEnvelope` Secret not found + * @response `422` `ApiErrorEnvelope` Unprocessable Entity. Validation error. + * @response `500` `ApiErrorEnvelope` Internal server error + */ + getSecret = (namespace: string, name: string, params: RequestParams = {}) => + this.request({ + path: `/secrets/${namespace}/${name}`, + method: 'GET', + format: 'json', + ...params, + }); + /** + * @description Updates an existing secret in the specified namespace + * + * @tags secrets + * @name UpdateSecret + * @summary Updates an existing secret + * @request PUT:/secrets/{namespace}/{name} + * @response `200` `ApiSecretEnvelope` Secret updated successfully + * @response `400` `ApiErrorEnvelope` Bad request + * @response `401` `ApiErrorEnvelope` Unauthorized + * @response `403` `ApiErrorEnvelope` Forbidden + * @response `404` `ApiErrorEnvelope` Secret not found + * @response `413` `ApiErrorEnvelope` Request Entity Too Large. The request body is too large. + * @response `415` `ApiErrorEnvelope` Unsupported Media Type. Content-Type header is not correct. + * @response `422` `ApiErrorEnvelope` Unprocessable Entity. Validation error. + * @response `500` `ApiErrorEnvelope` Internal server error + */ + updateSecret = ( + namespace: string, + name: string, + secret: SecretsSecretUpdate, + params: RequestParams = {}, + ) => + this.request({ + path: `/secrets/${namespace}/${name}`, + method: 'PUT', + body: secret, + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Deletes a secret from the specified namespace + * + * @tags secrets + * @name DeleteSecret + * @summary Deletes a secret + * @request DELETE:/secrets/{namespace}/{name} + * @response `204` `void` No Content + * @response `401` `ApiErrorEnvelope` Unauthorized + * @response `403` `ApiErrorEnvelope` Forbidden + * @response `404` `ApiErrorEnvelope` Secret not found + * @response `500` `ApiErrorEnvelope` Internal server error + */ + deleteSecret = (namespace: string, name: string, params: RequestParams = {}) => + this.request({ + path: `/secrets/${namespace}/${name}`, + method: 'DELETE', + type: ContentType.Json, + ...params, + }); +} diff --git a/workspaces/frontend/src/generated/data-contracts.ts b/workspaces/frontend/src/generated/data-contracts.ts index 12a6a45e3..6db10eb07 100644 --- a/workspaces/frontend/src/generated/data-contracts.ts +++ b/workspaces/frontend/src/generated/data-contracts.ts @@ -77,6 +77,18 @@ export interface ApiNamespaceListEnvelope { data: NamespacesNamespace[]; } +export interface ApiSecretCreateEnvelope { + data: SecretsSecretCreate; +} + +export interface ApiSecretEnvelope { + data: SecretsSecretUpdate; +} + +export interface ApiSecretListEnvelope { + data: SecretsSecretListItem[]; +} + export interface ApiValidationError { field: string; message: string; @@ -107,6 +119,13 @@ export interface ApiWorkspaceListEnvelope { data: WorkspacesWorkspace[]; } +export interface CommonAudit { + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; +} + export interface HealthCheckHealthCheck { status: HealthCheckServiceStatus; systemInfo: HealthCheckSystemInfo; @@ -120,6 +139,41 @@ export interface NamespacesNamespace { name: string; } +export interface SecretsSecretCreate { + contents: SecretsSecretData; + immutable: boolean; + name: string; + type: string; +} + +export type SecretsSecretData = Record; + +export interface SecretsSecretListItem { + audit: CommonAudit; + canMount: boolean; + canUpdate: boolean; + immutable: boolean; + mounts?: SecretsSecretMount[]; + name: string; + type: string; +} + +export interface SecretsSecretMount { + group: string; + kind: string; + name: string; +} + +export interface SecretsSecretUpdate { + contents: SecretsSecretData; + immutable: boolean; + type: string; +} + +export interface SecretsSecretValue { + base64?: string; +} + export interface WorkspacekindsImageConfig { default: string; values: WorkspacekindsImageConfigValue[]; diff --git a/workspaces/frontend/src/shared/api/notebookApi.ts b/workspaces/frontend/src/shared/api/notebookApi.ts index ba4efb0bb..bc3e57b4b 100644 --- a/workspaces/frontend/src/shared/api/notebookApi.ts +++ b/workspaces/frontend/src/shared/api/notebookApi.ts @@ -1,5 +1,6 @@ import { Healthcheck } from '~/generated/Healthcheck'; import { Namespaces } from '~/generated/Namespaces'; +import { Secrets } from '~/generated/Secrets'; import { Workspacekinds } from '~/generated/Workspacekinds'; import { Workspaces } from '~/generated/Workspaces'; import { ApiInstance } from '~/shared/api/types'; @@ -9,6 +10,7 @@ export interface NotebookApis { namespaces: ApiInstance; workspaces: ApiInstance; workspaceKinds: ApiInstance; + secrets: ApiInstance; } export const notebookApisImpl = (path: string): NotebookApis => { @@ -19,5 +21,6 @@ export const notebookApisImpl = (path: string): NotebookApis => { namespaces: new Namespaces(commonConfig), workspaces: new Workspaces(commonConfig), workspaceKinds: new Workspacekinds(commonConfig), + secrets: new Secrets(commonConfig), }; }; diff --git a/workspaces/frontend/src/shared/mock/mockBuilder.ts b/workspaces/frontend/src/shared/mock/mockBuilder.ts index cf9655ed3..c717f60b1 100644 --- a/workspaces/frontend/src/shared/mock/mockBuilder.ts +++ b/workspaces/frontend/src/shared/mock/mockBuilder.ts @@ -3,6 +3,7 @@ import { HealthCheckHealthCheck, HealthCheckServiceStatus, NamespacesNamespace, + SecretsSecretListItem, WorkspacekindsRedirectMessageLevel, WorkspacekindsWorkspaceKind, WorkspacesImageConfig, @@ -475,3 +476,20 @@ export const buildMockWorkspaceList = (args: { } return workspaces; }; + +export const buildMockSecret = ( + secret?: Partial, +): SecretsSecretListItem => ({ + name: 'secret-1', + type: 'Opaque', + immutable: false, + canMount: true, + canUpdate: true, + audit: { + createdAt: new Date(2025, 4, 1).toISOString(), + createdBy: 'test', + updatedAt: new Date(2025, 4, 1).toISOString(), + updatedBy: 'test', + }, + ...secret, +}); diff --git a/workspaces/frontend/src/shared/mock/mockNotebookApis.ts b/workspaces/frontend/src/shared/mock/mockNotebookApis.ts index 3947b6ee8..13afb6362 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookApis.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookApis.ts @@ -4,6 +4,8 @@ import { mockAllWorkspaces, mockedHealthCheckResponse, mockNamespaces, + mockSecretCreate, + mockSecretsList, mockWorkspace1, mockWorkspaceKind1, mockWorkspaceKinds, @@ -80,4 +82,22 @@ export const mockNotebookApisImpl = (): NotebookApis => ({ return { data: mockWorkspaceKind1 }; }, }, + secrets: { + listSecrets: async () => ({ + data: mockSecretsList, + }), + createSecret: async () => ({ + data: mockSecretCreate, + }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getSecret: async () => ({ + data: mockSecretCreate, + }), + updateSecret: async () => ({ + data: mockSecretCreate, + }), + deleteSecret: async () => { + await delay(1500); + }, + }, }); diff --git a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts index 8a2cf687a..6571d511a 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts @@ -1,4 +1,5 @@ import { + SecretsSecretCreate, WorkspacekindsWorkspaceKind, WorkspacesWorkspace, WorkspacesWorkspaceKindInfo, @@ -11,6 +12,7 @@ import { buildMockWorkspaceKind, buildMockWorkspaceKindInfo, buildMockWorkspaceList, + buildMockSecret, } from '~/shared/mock/mockBuilder'; // Health @@ -171,3 +173,24 @@ export const mockAllWorkspaces = [ kind: mockWorkspaceKindInfo1, }), ]; + +export const mockSecretCreate: SecretsSecretCreate = { + name: 'secret-1', + type: 'Opaque', + immutable: false, + contents: { + data: { + base64: 'abcd', + }, + }, +}; + +export const mockSecretsList = [ + buildMockSecret({ + name: 'secret-1', + }), + buildMockSecret({ + name: 'secret-2', + canMount: false, + }), +];