From 432f9419c75b21ef4e619c7b93b0b9ec90b208f2 Mon Sep 17 00:00:00 2001 From: Charles Thao Date: Wed, 29 Oct 2025 15:06:45 -0400 Subject: [PATCH 1/2] 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 a4e3a4054..d3668aa1e 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,8 +24,10 @@ import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggl import { Form } 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 ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper'; +import { SecretsSecretListItem, WorkspacesPodSecretMount } from '~/generated/data-contracts'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; interface WorkspaceFormPropertiesSecretsProps { secrets: WorkspacesPodSecretMount[]; @@ -50,6 +52,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', + }), +]; From b23fbb3eb1cc218ca35bf7352dbb356e2b3643bb Mon Sep 17 00:00:00 2001 From: Jenny <32821331+jenny-s51@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:46:46 -0500 Subject: [PATCH 2/2] feat: implemet create new secret modal --- .../WorkspaceKinds/Form/EditableLabels.tsx | 19 +- .../WorkspaceFormPropertiesSecrets.tsx | 182 ++---------- .../secrets/SecretKeyValuePairInput.tsx | 77 +++++ .../secrets/SecretsApiCreateModal.tsx | 277 ++++++++++++++++++ .../src/shared/components/PasswordInput.tsx | 50 ++++ .../components/ThemeAwareFormGroupWrapper.tsx | 10 +- 6 files changed, 453 insertions(+), 162 deletions(-) create mode 100644 workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretKeyValuePairInput.tsx create mode 100644 workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretsApiCreateModal.tsx create mode 100644 workspaces/frontend/src/shared/components/PasswordInput.tsx diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx index 5f52ea785..da0b26127 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx @@ -12,6 +12,7 @@ import { css } from '@patternfly/react-styles'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; import { TrashAltIcon } from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon'; import { WorkspacekindsOptionLabel } from '~/generated/data-contracts'; +import ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper'; interface EditableRowInterface { data: WorkspacekindsOptionLabel; @@ -33,14 +34,16 @@ const EditableRow: React.FC = ({ return ( - saveChanges({ ...data, key: (e.target as HTMLInputElement).value })} - placeholder="Enter key" - /> + + saveChanges({ ...data, key: (e.target as HTMLInputElement).value })} + placeholder="Enter key" + /> + void; } -const DEFAULT_MODE_OCTAL = (420).toString(8); - export const WorkspaceFormPropertiesSecrets: React.FC = ({ secrets, setSecrets, }) => { - const [isModalOpen, setIsModalOpen] = useState(false); + const { api } = useNotebookAPI(); + const { selectedNamespace } = useNamespaceContext(); + + const [isApiCreateModalOpen, setIsApiCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [formData, setFormData] = useState({ - secretName: '', - mountPath: '', - defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8), - }); - 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); + // Keep baseline secrets fetching from PR #698 for future attach functionality const [, setAvailableSecrets] = useState([]); - const { api } = useNotebookAPI(); - const { selectedNamespace } = useNamespaceContext(); - useEffect(() => { const fetchSecrets = async () => { const secretsResponse = await api.secrets.listSecrets(selectedNamespace); @@ -70,56 +55,6 @@ export const WorkspaceFormPropertiesSecrets: React.FC { - setFormData(secrets[index]); - setDefaultMode(secrets[index].defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL); - 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(() => { - 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 handleDelete = useCallback(() => { if (deleteIndex === null) { return; @@ -146,7 +81,7 @@ export const WorkspaceFormPropertiesSecrets: React.FC {secret.secretName} {secret.mountPath} - {secret.defaultMode?.toString(8) ?? DEFAULT_MODE_OCTAL} + {secret.defaultMode?.toString(8) ?? '644'} ( @@ -164,7 +99,6 @@ export const WorkspaceFormPropertiesSecrets: React.FC setDropdownOpen(null)} popperProps={{ position: 'right' }} > - handleEdit(index)}>Edit openDeleteModal(index)}>Remove @@ -173,86 +107,28 @@ export const WorkspaceFormPropertiesSecrets: React.FC )} - - - - -
- - setFormData({ ...formData, secretName: val })} - id="secret-name" - /> - - - setFormData({ ...formData, mountPath: val })} - id="mount-path" - /> - - - - Must be a valid UNIX file system permission value (i.e. 644) - - - ) : null - } - > - handleDefaultModeInput(val)} - id="default-mode" - /> - -
-
- - - - -
+
+ +
+ + {/* */} + + + setIsDeleteModalOpen(false)} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretKeyValuePairInput.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretKeyValuePairInput.tsx new file mode 100644 index 000000000..fb5833b8d --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/secrets/SecretKeyValuePairInput.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { Form } from '@patternfly/react-core/dist/esm/components/Form'; +import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; +import { Grid, GridItem } from '@patternfly/react-core/dist/esm/layouts/Grid'; +import { MinusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/minus-circle-icon'; +import ThemeAwareFormGroupWrapper from '~/shared/components/ThemeAwareFormGroupWrapper'; +import PasswordInput from '~/shared/components/PasswordInput'; + +interface SecretKeyValuePairInputProps { + index: number; + keyValue: string; + valueValue: string; + onKeyChange: (value: string) => void; + onValueChange: (value: string) => void; + onRemove: () => void; + canRemove: boolean; +} + +/** + * A composite component for managing a single secret key-value pair + * Handles layout and semantics for Key input, Value input, and Remove button + */ +const SecretKeyValuePairInput: React.FC = ({ + index, + keyValue, + valueValue, + onKeyChange, + onValueChange, + onRemove, + canRemove, +}) => ( + <> + + +
+ + onKeyChange(val)} + /> + +
+
+ + + + + + + + +
+ ); +}; + +export default SecretsApiCreateModal; diff --git a/workspaces/frontend/src/shared/components/PasswordInput.tsx b/workspaces/frontend/src/shared/components/PasswordInput.tsx new file mode 100644 index 000000000..a9adecbb8 --- /dev/null +++ b/workspaces/frontend/src/shared/components/PasswordInput.tsx @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { InputGroup, InputGroupItem } from '@patternfly/react-core/dist/esm/components/InputGroup'; +import { TextInput, TextInputTypes } from '@patternfly/react-core/dist/esm/components/TextInput'; +import { EyeIcon } from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import { EyeSlashIcon } from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +interface PasswordInputProps { + value: string; + onChange: (event: React.FormEvent, value: string) => void; + 'aria-label': string; + isRequired?: boolean; + 'data-testid'?: string; +} + +const PasswordInput: React.FC = ({ + value, + onChange, + 'aria-label': ariaLabel, + isRequired = false, + 'data-testid': dataTestId, +}) => { + const [showPassword, setShowPassword] = useState(false); + + return ( + + + + + + + + + ); +}; + +export default PasswordInput; diff --git a/workspaces/frontend/src/shared/components/ThemeAwareFormGroupWrapper.tsx b/workspaces/frontend/src/shared/components/ThemeAwareFormGroupWrapper.tsx index 020f30803..3f7412884 100644 --- a/workspaces/frontend/src/shared/components/ThemeAwareFormGroupWrapper.tsx +++ b/workspaces/frontend/src/shared/components/ThemeAwareFormGroupWrapper.tsx @@ -6,11 +6,13 @@ import FormFieldset from '~/app/components/FormFieldset'; // Props required by this wrapper component type ThemeAwareFormGroupWrapperProps = { children: React.ReactNode; // The input component - label: string; + label?: string; fieldId: string; isRequired?: boolean; helperTextNode?: React.ReactNode; // The pre-rendered HelperText component or null className?: string; // Optional className for the outer FormGroup + role?: string; // Optional role attribute for accessibility + isInline?: boolean; // Optional isInline prop for FormGroup }; const ThemeAwareFormGroupWrapper: React.FC = ({ @@ -20,6 +22,8 @@ const ThemeAwareFormGroupWrapper: React.FC = ({ isRequired, helperTextNode, className, + role, + isInline, }) => { const { isMUITheme } = useThemeContext(); const hasError = !!helperTextNode; // Determine error state based on helper text presence @@ -34,6 +38,8 @@ const ThemeAwareFormGroupWrapper: React.FC = ({ label={label} isRequired={isRequired} fieldId={fieldId} + role={role} + isInline={isInline} > @@ -50,6 +56,8 @@ const ThemeAwareFormGroupWrapper: React.FC = ({ label={label} isRequired={isRequired} fieldId={fieldId} + role={role} + isInline={isInline} > {children} {helperTextNode}