Skip to content

Commit f87efc2

Browse files
feat(okms): add a key/value editor to secret data
ref: #MANAGER-18318 Signed-off-by: Mathieu Mousnier <[email protected]>
1 parent 6f78e31 commit f87efc2

File tree

14 files changed

+690
-116
lines changed

14 files changed

+690
-116
lines changed

packages/manager/apps/okms/public/translations/secret-manager/Messages_fr_FR.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
"okms_activation_in_progress": "Veuillez patienter, création en cours.",
3232
"okms_list": "Liste de domaines OKMS",
3333
"editor": "Éditeur JSON",
34+
"error_duplicate_keys": "Vous ne pouvez pas avoir deux clés identiques.",
35+
"error_empty_keys": "Vous ne pouvez pas avoir de clé vide.",
3436
"error_invalid_json": "JSON non valide",
37+
"error_key_value_conversion": "La valeur ne peut pas être convertie en paires clé-valeur.",
3538
"error_path_allowed_characters": "Le path ne peut contenir que les caractères suivants: A-Z a-z 0-9 . _ : / = @ et -",
3639
"error_path_structure": "Le path ne peut commencer ou finir par '/', ni contenir deux '/' à la suite.",
3740
"error_invalid_duration": "La durée d'expiration doit être une durée (ex: 30s, 5m, 2h, 7d, 7d1h30m10s)",

packages/manager/apps/okms/src/common/components/drawer/DrawerInnerComponents.component.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { OdsButton } from '@ovhcloud/ods-components/react';
55
// Those components will be moved to MRC to get a composable version of the drawer
66

77
export const DrawerContent = ({ children }: PropsWithChildren) => {
8-
return <div className="flex flex-1 flex-col">{children}</div>;
8+
return (
9+
<section className="flex-1 overflow-y-auto outline-none">
10+
{children}
11+
</section>
12+
);
913
};
1014

1115
export type DrawerFooterProps = PropsWithChildren & {
@@ -37,7 +41,7 @@ export const DrawerFooter = ({
3741
secondaryButtonLabel,
3842
}: DrawerFooterProps) => {
3943
return (
40-
<footer className="mb-6 space-x-2 ">
44+
<footer className="my-6 space-x-2 ">
4145
{secondaryButtonLabel && (
4246
<OdsButton
4347
variant={'ghost'}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import { OdsSwitch, OdsSwitchItem } from '@ovhcloud/ods-components/react';
3+
4+
export type SecretValueToggleState = 'key-value' | 'json';
5+
6+
type SecretValueToggleProps = {
7+
state: SecretValueToggleState;
8+
onChange: (state: SecretValueToggleState) => void;
9+
};
10+
11+
export const SecretValueToggle = ({
12+
state,
13+
onChange,
14+
}: SecretValueToggleProps) => {
15+
return (
16+
<OdsSwitch name="secretValueType">
17+
<OdsSwitchItem
18+
isChecked={state === 'key-value'}
19+
onClick={() => onChange('key-value')}
20+
>
21+
Clé valeur
22+
</OdsSwitchItem>
23+
<OdsSwitchItem
24+
isChecked={state === 'json'}
25+
onClick={() => onChange('json')}
26+
>
27+
JSON
28+
</OdsSwitchItem>
29+
</OdsSwitch>
30+
);
31+
};

packages/manager/apps/okms/src/modules/secret-manager/components/form/SecretDataFormField.component.tsx

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import {
4+
useController,
5+
UseControllerProps,
6+
useFormContext,
7+
} from 'react-hook-form';
8+
import {
9+
formatKeyValuePairsFromString,
10+
formatStringFromKeyValuePairs,
11+
isSimpleKeyValueObjectFromString,
12+
} from '@secret-manager/utils/keyValue';
13+
import { OdsButton, OdsFormField } from '@ovhcloud/ods-components/react';
14+
import { KeyValuesEditorItem, KeyValuePair } from './KeyValuesEditorItem';
15+
import { KeyValuesEditorErrorMessage } from './KeyValuesEditorErrorMessage';
16+
17+
type FormFieldInput = {
18+
data: string;
19+
};
20+
21+
export const KeyValuesEditor = <T extends FormFieldInput>({
22+
name,
23+
control,
24+
}: UseControllerProps<T>) => {
25+
const { t } = useTranslation(['secret-manager']);
26+
const { field, fieldState } = useController({ name, control });
27+
const { setError } = useFormContext();
28+
29+
// We store the data in a local array to allow 2 rows with the same key
30+
// Otherwise, an row would disappear while typing as soon as a key appears twice
31+
const [keyValuePairs, setKeyValuePairs] = useState<KeyValuePair[]>(
32+
formatKeyValuePairsFromString(field.value),
33+
);
34+
35+
const updateFormState = (newKeyValuePairs: KeyValuePair[]) => {
36+
// Update local state
37+
setKeyValuePairs(newKeyValuePairs);
38+
39+
// Look for duplicate keys
40+
const keys = new Set(newKeyValuePairs.map(({ key }) => key));
41+
const hasDuplicateKeys = keys.size !== newKeyValuePairs.length;
42+
if (hasDuplicateKeys) {
43+
setError(name, { message: t('error_duplicate_keys') });
44+
return;
45+
}
46+
47+
// Update form state
48+
const data = formatStringFromKeyValuePairs(newKeyValuePairs);
49+
field.onChange(data);
50+
};
51+
52+
const handleItemChange = (index: number, item: KeyValuePair) => {
53+
const newKeyValuePairs = [...keyValuePairs];
54+
newKeyValuePairs[index] = item;
55+
updateFormState(newKeyValuePairs);
56+
};
57+
58+
const handleAddItem = () => {
59+
const newKeyValuePairs = [...keyValuePairs];
60+
newKeyValuePairs.push({ key: '', value: '' });
61+
updateFormState(newKeyValuePairs);
62+
};
63+
64+
const handleDeleteItem = (index: number) => {
65+
const newKeyValuePairs = [...keyValuePairs];
66+
newKeyValuePairs.splice(index, 1);
67+
updateFormState(newKeyValuePairs);
68+
};
69+
70+
const handleItemBlur = () => {
71+
updateFormState(keyValuePairs);
72+
};
73+
74+
const isKeyValueObject = isSimpleKeyValueObjectFromString(field.value);
75+
const isValueEmpty = !field.value;
76+
if (isKeyValueObject || isValueEmpty) {
77+
return (
78+
<div className="space-y-5">
79+
<OdsFormField
80+
className="w-full block space-y-1"
81+
error={fieldState.error?.message}
82+
>
83+
{keyValuePairs.map(({ key, value }, index) => (
84+
<KeyValuesEditorItem
85+
key={`key-value-pair-item-index-${index}`}
86+
index={index}
87+
item={{ key, value }}
88+
onChange={(item) => handleItemChange(index, item)}
89+
onDelete={() => handleDeleteItem(index)}
90+
onBlur={handleItemBlur}
91+
/>
92+
))}
93+
</OdsFormField>
94+
<OdsButton
95+
size="sm"
96+
variant="outline"
97+
icon="plus"
98+
label="Ajouter une ligne"
99+
onClick={handleAddItem}
100+
/>
101+
</div>
102+
);
103+
}
104+
105+
return <KeyValuesEditorErrorMessage />;
106+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { OdsMessage } from '@ovhcloud/ods-components/react';
4+
5+
export const KeyValuesEditorErrorMessage = () => {
6+
const { t } = useTranslation(['secret-manager']);
7+
return (
8+
<OdsMessage color="danger" isDismissible={false}>
9+
{t('error_key_value_conversion')}
10+
</OdsMessage>
11+
);
12+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
import {
3+
OdsFormField,
4+
OdsInput,
5+
OdsButton,
6+
} from '@ovhcloud/ods-components/react';
7+
import {
8+
OdsInputChangeEventDetail,
9+
OdsInputCustomEvent,
10+
} from '@ovhcloud/ods-components';
11+
12+
export type KeyValuePair = {
13+
key: string;
14+
value: string;
15+
};
16+
17+
type KeyValuesEditorItemProps = {
18+
index: number;
19+
item: KeyValuePair;
20+
onChange: (item: KeyValuePair) => void;
21+
onDelete: () => void;
22+
onBlur: () => void;
23+
};
24+
25+
type OdsInputChangeEventHandler = (
26+
event: OdsInputCustomEvent<OdsInputChangeEventDetail>,
27+
) => void;
28+
29+
export const KeyValuesEditorItem = ({
30+
index,
31+
item,
32+
onChange,
33+
onDelete,
34+
onBlur,
35+
}: KeyValuesEditorItemProps) => {
36+
const keyInputName = `key-value-pair-key-input-index-${index}`;
37+
const valueInputName = `key-value-pair-value-input-index-${index}`;
38+
39+
const handleKeyChange: OdsInputChangeEventHandler = (event) => {
40+
onChange({ ...item, key: event.detail.value.toString() });
41+
};
42+
43+
const handleValueChange: OdsInputChangeEventHandler = (event) => {
44+
onChange({ ...item, value: event.detail.value.toString() });
45+
};
46+
47+
return (
48+
<div className="flex gap-2 items-center">
49+
<OdsFormField className="w-full">
50+
<label slot="label">Clé</label>
51+
<OdsInput
52+
name={keyInputName}
53+
value={item.key}
54+
onOdsChange={handleKeyChange}
55+
onOdsBlur={onBlur}
56+
/>
57+
</OdsFormField>
58+
<OdsFormField className="w-full">
59+
<label slot="label">Valeur</label>
60+
61+
<OdsInput
62+
name={valueInputName}
63+
value={item.value}
64+
onOdsChange={handleValueChange}
65+
onOdsBlur={onBlur}
66+
/>
67+
</OdsFormField>
68+
<OdsButton
69+
className="mt-5"
70+
size="sm"
71+
variant="ghost"
72+
icon="trash"
73+
label=""
74+
onClick={onDelete}
75+
/>
76+
</div>
77+
);
78+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React, { useState } from 'react';
2+
import { useController, UseControllerProps } from 'react-hook-form';
3+
import { isSimpleKeyValueObjectFromString } from '@secret-manager/utils/keyValue';
4+
import {
5+
SecretValueToggle,
6+
SecretValueToggleState,
7+
} from '@secret-manager/components/SecretValueToggle';
8+
import { KeyValuesEditor } from '../keyValuesEditor/KeyValuesEditor';
9+
import { SecretDataInput } from './SecretDataInput.component';
10+
11+
type FormFieldInput = {
12+
data: string;
13+
};
14+
15+
export const SecretDataFormField = <T extends FormFieldInput>({
16+
name,
17+
control,
18+
}: UseControllerProps<T>) => {
19+
const { field } = useController({ name, control });
20+
21+
const isKeyValueObject = isSimpleKeyValueObjectFromString(field.value);
22+
const isEmpty = !field.value;
23+
const [toggleState, setToggleState] = useState<SecretValueToggleState>(
24+
isKeyValueObject || isEmpty ? 'key-value' : 'json',
25+
);
26+
27+
return (
28+
<div className="flex flex-col gap-6 pl-1 mt-1">
29+
<div>
30+
<SecretValueToggle state={toggleState} onChange={setToggleState} />
31+
</div>
32+
{toggleState === 'key-value' ? (
33+
<KeyValuesEditor name={name} control={control} />
34+
) : (
35+
<SecretDataInput name={name} control={control} />
36+
)}
37+
</div>
38+
);
39+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
import { useController, UseControllerProps } from 'react-hook-form';
3+
import { OdsFormField, OdsTextarea } from '@ovhcloud/ods-components/react';
4+
import { useTranslation } from 'react-i18next';
5+
import { SECRET_FORM_FIELD_TEST_IDS } from '../form.constants';
6+
7+
type FormFieldInput = {
8+
data: string;
9+
};
10+
11+
export const SecretDataInput = <T extends FormFieldInput>({
12+
name,
13+
control,
14+
}: UseControllerProps<T>) => {
15+
const { t } = useTranslation(['secret-manager']);
16+
const { field, fieldState } = useController({ name, control });
17+
18+
return (
19+
<OdsFormField error={fieldState.error?.message} className="w-full">
20+
<label htmlFor={field.name} slot="label">
21+
{t('editor')}
22+
</label>
23+
<OdsTextarea
24+
id={field.name}
25+
name={field.name}
26+
value={field.value || ''}
27+
onOdsBlur={field.onBlur}
28+
onOdsChange={field.onChange}
29+
isResizable
30+
rows={12}
31+
data-testid={SECRET_FORM_FIELD_TEST_IDS.INPUT_DATA}
32+
/>
33+
</OdsFormField>
34+
);
35+
};

0 commit comments

Comments
 (0)