Skip to content

Commit 565fe35

Browse files
committed
[UI] Fix secrets after review #2882
1 parent c1b7541 commit 565fe35

File tree

5 files changed

+171
-133
lines changed

5 files changed

+171
-133
lines changed

frontend/src/locale/en.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,15 @@
303303
"empty_message_text": "No secrets to display.",
304304
"name": "Secret name",
305305
"value": "Secret value",
306-
"delete_confirm_title": "Delete secrets",
307-
"delete_confirm_message": "Are you sure you want to delete these secrets?"
306+
"create_secret": "Create secret",
307+
"update_secret": "Update secret",
308+
"delete_confirm_title": "Delete secret",
309+
"delete_confirm_message": "Are you sure you want to delete the {{name}} secret?",
310+
"multiple_delete_confirm_title": "Delete secrets",
311+
"multiple_delete_confirm_message": "Are you sure you want to delete {{count}} secrets?",
312+
"validation": {
313+
"secret_name_format": "Invalid secret name"
314+
}
308315
},
309316
"error_notification": "Update project error",
310317
"validation": {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import { useForm } from 'react-hook-form';
3+
import { useTranslation } from 'react-i18next';
4+
5+
import { Box, Button, FormInput, FormTextarea, SpaceBetween } from 'components';
6+
7+
import { TFormValues } from '../types';
8+
9+
export type IFormProps = {
10+
initialValues?: TFormValues;
11+
onSubmit: (values: TFormValues) => void;
12+
onCancel?: () => void;
13+
loading?: boolean;
14+
};
15+
16+
export const SecretForm: React.FC<IFormProps> = ({ initialValues, onSubmit: onSubmitProp, loading, onCancel }) => {
17+
const { t } = useTranslation();
18+
const { handleSubmit, control } = useForm<TFormValues>({
19+
defaultValues: {
20+
...initialValues,
21+
},
22+
});
23+
24+
const onSubmit = (values: TFormValues) => onSubmitProp(values);
25+
26+
return (
27+
<form onSubmit={handleSubmit(onSubmit)}>
28+
<SpaceBetween direction="vertical" size="l">
29+
<FormInput
30+
label={t('projects.edit.secrets.name')}
31+
control={control}
32+
name="name"
33+
disabled={loading || !!initialValues?.id}
34+
rules={{
35+
required: t('validation.required'),
36+
37+
pattern: {
38+
value: /^[A-Za-z0-9-_]{1,200}$/,
39+
message: t('projects.edit.secrets.validation.secret_name_format'),
40+
},
41+
}}
42+
/>
43+
44+
<FormTextarea
45+
rows={6}
46+
label={t('projects.edit.secrets.value')}
47+
control={control}
48+
name="value"
49+
disabled={loading}
50+
rules={{
51+
required: t('validation.required'),
52+
}}
53+
/>
54+
55+
<Box float="right">
56+
<SpaceBetween size="l" direction="horizontal">
57+
{onCancel && (
58+
<Button formAction="none" disabled={loading} onClick={onCancel}>
59+
{t('common.cancel')}
60+
</Button>
61+
)}
62+
63+
<Button variant="primary" formAction="submit" loading={loading} disabled={loading}>
64+
{t('common.save')}
65+
</Button>
66+
</SpaceBetween>
67+
</Box>
68+
</SpaceBetween>
69+
</form>
70+
);
71+
};
Lines changed: 82 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,35 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
2-
import { useFieldArray, useForm } from 'react-hook-form';
1+
import React, { useState } from 'react';
32
import { useTranslation } from 'react-i18next';
43

4+
import { Button, ButtonWithConfirmation, Header, ListEmptyMessage, Modal, Pagination, SpaceBetween, Table } from 'components';
5+
6+
import { useCollection, useNotifications } from 'hooks';
57
import {
6-
Button,
7-
ButtonWithConfirmation,
8-
FormInput,
9-
Header,
10-
ListEmptyMessage,
11-
Pagination,
12-
SpaceBetween,
13-
Table,
14-
} from 'components';
8+
useDeleteSecretsMutation,
9+
useGetAllSecretsQuery,
10+
useLazyGetSecretQuery,
11+
useUpdateSecretMutation,
12+
} from 'services/secrets';
1513

16-
import { useCollection } from 'hooks';
17-
import { useDeleteSecretsMutation, useGetAllSecretsQuery, useUpdateSecretMutation } from 'services/secrets';
14+
import { getServerError } from '../../../libs';
15+
import { SecretForm } from './Form';
1816

19-
import { IProps, TFormSecretValue, TFormValues, TProjectSecretWithIndex } from './types';
17+
import { IProps, TFormValues } from './types';
2018

2119
import styles from './styles.module.scss';
2220

2321
export const ProjectSecrets: React.FC<IProps> = ({ project, loading }) => {
2422
const { t } = useTranslation();
25-
const [editableRowIndex, setEditableRowIndex] = useState<number | null>(null);
23+
const [initialFormValues, setInitialFormValues] = useState<TFormValues | undefined>();
2624
const projectName = project?.project_name ?? '';
25+
const [pushNotification] = useNotifications();
2726

2827
const { data, isLoading, isFetching } = useGetAllSecretsQuery({ project_name: projectName });
2928
const [updateSecret, { isLoading: isUpdating }] = useUpdateSecretMutation();
3029
const [deleteSecret, { isLoading: isDeleting }] = useDeleteSecretsMutation();
30+
const [getSecret, { isLoading: isGettingSecrets }] = useLazyGetSecretQuery();
3131

32-
const { handleSubmit, control, getValues, setValue } = useForm<TFormValues>({
33-
defaultValues: { secrets: [] },
34-
});
35-
36-
useEffect(() => {
37-
if (data) {
38-
setValue(
39-
'secrets',
40-
data.map((s) => ({ ...s, serverId: s.id })),
41-
);
42-
}
43-
}, [data]);
44-
45-
const { fields, append, remove } = useFieldArray({
46-
control,
47-
name: 'secrets',
48-
});
49-
50-
const fieldsWithIndex = useMemo(() => {
51-
return fields.map<TProjectSecretWithIndex>((field, index) => ({ ...field, index }));
52-
}, [fields]);
53-
54-
const { items, paginationProps, collectionProps } = useCollection(fieldsWithIndex, {
32+
const { items, paginationProps, collectionProps } = useCollection(data ?? [], {
5533
filtering: {
5634
empty: (
5735
<ListEmptyMessage
@@ -70,112 +48,72 @@ export const ProjectSecrets: React.FC<IProps> = ({ project, loading }) => {
7048
const names = selectedItems?.map((s) => s.name ?? '');
7149

7250
if (names?.length) {
73-
deleteSecret({ project_name: projectName, names }).then(() => {
74-
selectedItems?.forEach((s) => remove(s.index));
75-
});
51+
deleteSecret({ project_name: projectName, names });
7652
}
7753
};
7854

79-
const removeSecretByIndex = (index: number) => {
80-
const secretData = getValues().secrets?.[index];
81-
82-
if (!secretData || !secretData.name) {
83-
return;
84-
}
85-
86-
deleteSecret({ project_name: projectName, names: [secretData.name] }).then(() => {
87-
remove(index);
88-
});
55+
const removeSecretByName = (name: IProjectSecret['name']) => {
56+
deleteSecret({ project_name: projectName, names: [name] });
8957
};
9058

91-
const saveSecretByIndex = (index: number) => {
92-
const secretData = getValues().secrets?.[index];
93-
94-
if (!secretData || !secretData.name || !secretData.value) {
59+
const updateOrCreateSecret = ({ name, value }: TFormValues) => {
60+
if (!name || !value) {
9561
return;
9662
}
9763

98-
updateSecret({ project_name: projectName, name: secretData.name, value: secretData.value })
64+
updateSecret({ project_name: projectName, name, value })
9965
.unwrap()
100-
.then(() => {
101-
setEditableRowIndex(null);
66+
.then(() => setInitialFormValues(undefined))
67+
.catch((error) => {
68+
pushNotification({
69+
type: 'error',
70+
content: t('common.server_error', { error: getServerError(error) }),
71+
});
10272
});
10373
};
10474

105-
const isDisabledEditableRowActions = loading || isLoading || isFetching || isUpdating;
106-
const isDisabledNotEditableRowActions = loading || isLoading || isFetching || isDeleting;
75+
const editSecret = ({ name }: IProjectSecret) => {
76+
getSecret({ project_name: projectName, name })
77+
.unwrap()
78+
.then((secret) => setInitialFormValues(secret));
79+
};
80+
81+
const closeModal = () => setInitialFormValues(undefined);
82+
83+
const isDisabledActions = loading || isLoading || isFetching || isDeleting || isGettingSecrets;
10784

10885
const COLUMN_DEFINITIONS = [
10986
{
11087
id: 'name',
11188
header: t('projects.edit.secrets.name'),
112-
cell: (field: TFormSecretValue & { index: number }) => {
113-
const isEditable = editableRowIndex === field.index;
114-
115-
return (
116-
<div className={styles.value}>
117-
<div className={styles.valueFieldWrapper}>
118-
<FormInput
119-
key={field.name}
120-
control={control}
121-
name={`secrets.${field.index}.name`}
122-
disabled={loading || !isEditable || !!field.serverId}
123-
/>
124-
</div>
125-
</div>
126-
);
127-
},
89+
cell: (secret: IProjectSecret) => secret.name,
12890
},
12991
{
13092
id: 'value',
13193
header: t('projects.edit.secrets.value'),
132-
cell: (field: TFormSecretValue & { index: number }) => {
133-
const isEditable = editableRowIndex === field.index;
134-
94+
cell: (secret: IProjectSecret) => {
13595
return (
13696
<div className={styles.value}>
137-
<div className={styles.valueFieldWrapper}>
138-
<FormInput
139-
readOnly={!isEditable}
140-
key={field.value}
141-
control={control}
142-
name={`secrets.${field.index}.value`}
143-
disabled={loading || !isEditable}
144-
/>
145-
</div>
97+
<div className={styles.valueFieldWrapper}>************************</div>
14698

14799
<div className={styles.buttonsWrapper}>
148-
{isEditable && (
149-
<Button
150-
disabled={isDisabledEditableRowActions}
151-
formAction="none"
152-
onClick={() => saveSecretByIndex(field.index)}
153-
variant="icon"
154-
iconName="check"
155-
/>
156-
)}
157-
158-
{!isEditable && (
159-
<Button
160-
disabled={isDisabledNotEditableRowActions}
161-
formAction="none"
162-
onClick={() => setEditableRowIndex(field.index)}
163-
variant="icon"
164-
iconName="edit"
165-
/>
166-
)}
167-
168-
{!isEditable && (
169-
<ButtonWithConfirmation
170-
disabled={isDisabledNotEditableRowActions}
171-
formAction="none"
172-
onClick={() => removeSecretByIndex(field.index)}
173-
confirmTitle={t('projects.edit.secrets.delete_confirm_title')}
174-
confirmContent={t('projects.edit.secrets.delete_confirm_message')}
175-
variant="icon"
176-
iconName="remove"
177-
/>
178-
)}
100+
<Button
101+
disabled={isDisabledActions}
102+
formAction="none"
103+
onClick={() => editSecret(secret)}
104+
variant="icon"
105+
iconName="edit"
106+
/>
107+
108+
<ButtonWithConfirmation
109+
disabled={isDisabledActions}
110+
formAction="none"
111+
onClick={() => removeSecretByName(secret.name)}
112+
confirmTitle={t('projects.edit.secrets.delete_confirm_title')}
113+
confirmContent={t('projects.edit.secrets.delete_confirm_message', { name: secret.name })}
114+
variant="icon"
115+
iconName="remove"
116+
/>
179117
</div>
180118
</div>
181119
);
@@ -184,8 +122,7 @@ export const ProjectSecrets: React.FC<IProps> = ({ project, loading }) => {
184122
];
185123

186124
const addSecretHandler = () => {
187-
append({});
188-
setEditableRowIndex(fields.length);
125+
setInitialFormValues({});
189126
};
190127

191128
const renderActions = () => {
@@ -196,11 +133,11 @@ export const ProjectSecrets: React.FC<IProps> = ({ project, loading }) => {
196133

197134
<ButtonWithConfirmation
198135
key="delete"
199-
disabled={isDisabledNotEditableRowActions || !selectedItems?.length}
136+
disabled={isDisabledActions || !selectedItems?.length}
200137
formAction="none"
201138
onClick={deleteSelectedSecrets}
202-
confirmTitle={t('projects.edit.secrets.delete_confirm_title')}
203-
confirmContent={t('projects.edit.secrets.delete_confirm_message')}
139+
confirmTitle={t('projects.edit.secrets.multiple_delete_confirm_title')}
140+
confirmContent={t('projects.edit.secrets.multiple_delete_confirm_message', { count: selectedItems?.length })}
204141
>
205142
{t('common.delete')}
206143
</ButtonWithConfirmation>,
@@ -213,8 +150,10 @@ export const ProjectSecrets: React.FC<IProps> = ({ project, loading }) => {
213150
) : undefined;
214151
};
215152

153+
const isShowModal = !!initialFormValues;
154+
216155
return (
217-
<form onSubmit={handleSubmit(() => {})}>
156+
<>
218157
<Table
219158
{...collectionProps}
220159
selectionType="multi"
@@ -228,6 +167,23 @@ export const ProjectSecrets: React.FC<IProps> = ({ project, loading }) => {
228167
}
229168
pagination={<Pagination {...paginationProps} />}
230169
/>
231-
</form>
170+
171+
<Modal
172+
header={
173+
initialFormValues?.id ? t('projects.edit.secrets.update_secret') : t('projects.edit.secrets.create_secret')
174+
}
175+
visible={isShowModal}
176+
onDismiss={closeModal}
177+
>
178+
{isShowModal && (
179+
<SecretForm
180+
initialValues={initialFormValues}
181+
onSubmit={updateOrCreateSecret}
182+
loading={isLoading || isUpdating}
183+
onCancel={closeModal}
184+
/>
185+
)}
186+
</Modal>
187+
</>
232188
);
233189
};

frontend/src/pages/Project/Secrets/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,4 @@ export interface IProps {
33
project?: IProject;
44
}
55

6-
export type TFormSecretValue = Partial<IProjectSecret & { serverId: IProjectSecret['id'] }>;
7-
export type TProjectSecretWithIndex = TFormSecretValue & { index: number };
8-
export type TFormValues = { secrets: TFormSecretValue[] };
6+
export type TFormValues = Partial<IProjectSecret>;

0 commit comments

Comments
 (0)