Skip to content

Commit f3496d7

Browse files
author
Andres Aristizabal
authored
Merge pull request #1746 from redpanda-data/front-end/secrets/add-scope
frontend: add scopes selection to secret creation and update modals
2 parents f8f24fb + 9733634 commit f3496d7

6 files changed

Lines changed: 115 additions & 8 deletions

File tree

frontend/src/components/pages/secrets/create-secret-modal.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ describe('CreateSecretModal', () => {
5757
fireEvent.change(screen.getByTestId('secret-id-field'), { target: { value: secretId } });
5858
fireEvent.change(screen.getByTestId('secret-value-field'), { target: { value: secretValue } });
5959

60+
fireEvent.click(screen.getByRole('combobox'));
61+
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
62+
await fireEvent.click(screen.getByText('Redpanda Connect'));
63+
6064
fireEvent.click(screen.getByTestId('add-label-button'));
6165

6266
fireEvent.change(screen.getByTestId('secret-labels-field-key-0'), { target: { value: 'environment' } });

frontend/src/components/pages/secrets/create-secret-modal.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import {
22
Button,
33
ButtonGroup,
4+
FormErrorMessage,
5+
FormField,
46
Modal,
57
ModalBody,
68
ModalCloseButton,
79
ModalContent,
810
ModalFooter,
911
ModalHeader,
1012
ModalOverlay,
13+
Select,
1114
Stack,
1215
Text,
16+
UnorderedList,
17+
isMultiValue,
1318
} from '@redpanda-data/ui';
1419
import { formOptions } from '@tanstack/react-form';
1520
import { useAppForm } from 'components/form/form';
@@ -40,12 +45,23 @@ export const CreateSecretModal = ({ isOpen, onClose, customSecretSchema, helperT
4045
form.reset();
4146
};
4247

48+
// Form type
49+
interface Secret {
50+
id: string;
51+
value: string;
52+
labels: string[];
53+
scopes: Scope[];
54+
}
55+
56+
const defaultValues: Secret = {
57+
id: '',
58+
value: '',
59+
labels: [],
60+
scopes: [],
61+
};
62+
4363
const formOpts = formOptions({
44-
defaultValues: {
45-
id: '',
46-
value: '',
47-
labels: [],
48-
},
64+
defaultValues: defaultValues,
4965
validators: {
5066
onChange: finalSchema,
5167
},
@@ -61,7 +77,7 @@ export const CreateSecretModal = ({ isOpen, onClose, customSecretSchema, helperT
6177
id: value.id,
6278
// @ts-ignore js-base64 does not play nice with TypeScript 5: Type 'Uint8Array<ArrayBufferLike>' is not assignable to type 'Uint8Array<ArrayBuffer>'.
6379
secretData: base64ToUInt8Array(encodeBase64(value.value)),
64-
scopes: [Scope.REDPANDA_CONNECT],
80+
scopes: value.scopes || [],
6581
labels: labelsMap,
6682
});
6783

@@ -108,6 +124,41 @@ export const CreateSecretModal = ({ isOpen, onClose, customSecretSchema, helperT
108124
<field.PasswordField label="Value" data-testid="secret-value-field" helperText={helperText} />
109125
)}
110126
</form.AppField>
127+
<form.AppField name="scopes">
128+
{({ state, handleChange, handleBlur }) => (
129+
<FormField label="Scopes" errorText=" " isInvalid={state.meta.errors?.length > 0}>
130+
<Select
131+
placeholder="Select scopes"
132+
data-testid="secret-scopes-field"
133+
onChange={(nextValue) => {
134+
if (isMultiValue(nextValue) && nextValue) {
135+
handleChange(nextValue.map(({ value }) => value));
136+
}
137+
}}
138+
options={[
139+
{ label: 'Redpanda Connect', value: Scope.REDPANDA_CONNECT },
140+
{ label: 'Redpanda Cluster', value: Scope.REDPANDA_CLUSTER },
141+
]}
142+
isMulti
143+
onBlur={handleBlur}
144+
/>
145+
{
146+
// Display error messages like tanstack/react-form fields.
147+
state?.meta.errors?.length > 0 && (
148+
<FormErrorMessage>
149+
<UnorderedList>
150+
{state.meta.errors?.map((error) => (
151+
<li key={error.path}>
152+
<Text color="red.500">{error.message}</Text>
153+
</li>
154+
))}
155+
</UnorderedList>
156+
</FormErrorMessage>
157+
)
158+
}
159+
</FormField>
160+
)}
161+
</form.AppField>
111162
<form.AppField name="labels" mode="array">
112163
{(field) => (
113164
<field.KeyValueField

frontend/src/components/pages/secrets/form/secret-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ export const secretSchema = (customValueSchema?: z.ZodTypeAny) =>
2525
return (label.key === '' && label.value === '') || (label.key !== '' && label.value !== '');
2626
});
2727
}, 'Both key and value must be provided for a label'),
28+
scopes: z.array(z.number()).min(1, 'At least one scope is required'),
2829
});

frontend/src/components/pages/secrets/secrets-store-page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const getScopeDisplayValue = (scope: Scope) => {
4848
switch (scope) {
4949
case Scope.REDPANDA_CONNECT:
5050
return 'RP Connect';
51+
case Scope.REDPANDA_CLUSTER:
52+
return 'Cluster';
5153
case Scope.UNSPECIFIED:
5254
return 'Unspecified';
5355
default:
@@ -186,7 +188,7 @@ export const SecretsStorePage = () => {
186188
header: 'Scope',
187189
id: 'scope',
188190
cell: ({ row: { original } }) =>
189-
original?.scopes.map((scope) => <Text key={scope}>{getScopeDisplayValue(scope)}</Text>),
191+
original?.scopes.map((scope) => getScopeDisplayValue(scope)).join(', ') ?? 'No scopes',
190192
size: 50,
191193
},
192194
{

frontend/src/components/pages/secrets/update-secret-modal.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ describe('UpdateSecretModal', () => {
9696

9797
fireEvent.change(screen.getByTestId('secret-value-field'), { target: { value: updatedSecretValue } });
9898

99+
fireEvent.click(screen.getByRole('combobox'));
100+
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
101+
fireEvent.click(screen.getByText('Redpanda Connect'));
102+
99103
fireEvent.click(screen.getByTestId('add-label-button'));
100104

101105
fireEvent.change(screen.getByTestId('secret-labels-field-key-1'), { target: { value: 'environment' } });

frontend/src/components/pages/secrets/update-secret-modal.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import {
22
Button,
33
ButtonGroup,
4+
FormErrorMessage,
5+
FormField,
46
Modal,
57
ModalBody,
68
ModalCloseButton,
79
ModalContent,
810
ModalFooter,
911
ModalHeader,
1012
ModalOverlay,
13+
Select,
1114
Stack,
15+
Text,
16+
UnorderedList,
17+
isMultiValue,
1218
} from '@redpanda-data/ui';
1319
import { formOptions } from '@tanstack/react-form';
1420
import { useAppForm } from 'components/form/form';
@@ -54,6 +60,7 @@ export const UpdateSecretModal = ({ isOpen, onClose, secretId }: UpdateSecretMod
5460
defaultValues: {
5561
id: secretId,
5662
value: '',
63+
scopes: matchingSecret?.scopes ?? [],
5764
labels: existingLabels.length > 0 ? existingLabels : [],
5865
},
5966
validators: {
@@ -71,7 +78,7 @@ export const UpdateSecretModal = ({ isOpen, onClose, secretId }: UpdateSecretMod
7178
id: value.id,
7279
// @ts-ignore js-base64 does not play nice with TypeScript 5: Type 'Uint8Array<ArrayBufferLike>' is not assignable to type 'Uint8Array<ArrayBuffer>'.
7380
secretData: base64ToUInt8Array(encodeBase64(value.value)),
74-
scopes: [Scope.REDPANDA_CONNECT],
81+
scopes: value.scopes || [],
7582
labels: labelsMap,
7683
});
7784

@@ -82,6 +89,11 @@ export const UpdateSecretModal = ({ isOpen, onClose, secretId }: UpdateSecretMod
8289

8390
const form = useAppForm({ ...formOpts });
8491

92+
const scopeOptions = [
93+
{ label: 'Redpanda Connect', value: Scope.REDPANDA_CONNECT },
94+
{ label: 'Redpanda Cluster', value: Scope.REDPANDA_CLUSTER },
95+
];
96+
8597
return (
8698
<Modal isOpen={isOpen} onClose={handleClose} onEsc={handleClose} size="lg">
8799
<ModalOverlay />
@@ -102,6 +114,39 @@ export const UpdateSecretModal = ({ isOpen, onClose, secretId }: UpdateSecretMod
102114
{(field) => <field.PasswordField label="Value" data-testid="secret-value-field" />}
103115
</form.AppField>
104116

117+
<form.AppField name="scopes">
118+
{({ state, handleChange, handleBlur }) => (
119+
<FormField label="Scopes" errorText=" " isInvalid={state.meta.errors?.length > 0}>
120+
<Select
121+
placeholder="Select scopes"
122+
onChange={(nextValue) => {
123+
if (isMultiValue(nextValue) && nextValue) {
124+
handleChange(nextValue.map(({ value }) => value));
125+
}
126+
}}
127+
options={scopeOptions}
128+
defaultValue={scopeOptions.filter((so) => matchingSecret?.scopes?.some((s) => so.value === s))}
129+
isMulti
130+
onBlur={handleBlur}
131+
/>
132+
{
133+
// Display error messages like tanstack/react-form fields.
134+
state?.meta.errors?.length > 0 && (
135+
<FormErrorMessage>
136+
<UnorderedList>
137+
{state.meta.errors?.map((error) => (
138+
<li key={error.path}>
139+
<Text color="red.500">{error.message}</Text>
140+
</li>
141+
))}
142+
</UnorderedList>
143+
</FormErrorMessage>
144+
)
145+
}
146+
</FormField>
147+
)}
148+
</form.AppField>
149+
105150
<form.AppField name="labels" mode="array">
106151
{(field) => (
107152
<field.KeyValueField

0 commit comments

Comments
 (0)