From c0cf3252fa9f246382b2c078fe0c190feb8b348c Mon Sep 17 00:00:00 2001 From: Charles Thao Date: Wed, 29 Oct 2025 15:06:45 -0400 Subject: [PATCH] Generate frontend client endpoints for Secrets API Signed-off-by: Charles Thao --- workspaces/frontend/scripts/swagger.version | 2 +- .../WorkspaceFormPropertiesSecrets.tsx | 18 ++- workspaces/frontend/src/generated/Secrets.ts | 141 ++++++++++++++++++ .../frontend/src/generated/data-contracts.ts | 54 +++++++ .../frontend/src/shared/api/notebookApi.ts | 3 + .../frontend/src/shared/mock/mockBuilder.ts | 18 +++ .../src/shared/mock/mockNotebookApis.ts | 20 +++ .../shared/mock/mockNotebookServiceData.ts | 22 +++ 8 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 workspaces/frontend/src/generated/Secrets.ts 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/properties/WorkspaceFormPropertiesSecrets.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx index 66e07ab46..5cffe5871 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, @@ -24,7 +24,9 @@ import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggl 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'; interface WorkspaceFormPropertiesSecretsProps { secrets: WorkspacesPodSecretMount[]; @@ -49,6 +51,18 @@ export const WorkspaceFormPropertiesSecrets: React.FC(null); const [isDefaultModeValid, setIsDefaultModeValid] = useState(true); const [dropdownOpen, setDropdownOpen] = useState(null); + const [, setAvailableSecrets] = useState([]); + + 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); 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..176d77a5e 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,23 @@ export const mockAllWorkspaces = [ kind: mockWorkspaceKindInfo1, }), ]; + +export const mockSecretCreate: SecretsSecretCreate = { + name: 'secret-1', + type: 'Opaque', + immutable: false, + contents: { + username: { + base64: 'abcd', + }, + }, +}; + +export const mockSecretsList = [ + buildMockSecret({ + name: 'secret-1', + }), + buildMockSecret({ + name: 'secret-2', + }), +];