Skip to content

Commit 4ebbfd3

Browse files
committed
frontend: Allow direct editing of secret data
Signed-off-by: Evangelos Skopelitis <eskopelitis@microsoft.com>
1 parent 8ae81f0 commit 4ebbfd3

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

frontend/src/components/common/Resource/Resource.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { BaseTextFieldProps } from '@mui/material/TextField';
1111
import Typography from '@mui/material/Typography';
1212
import { useTheme } from '@mui/system';
1313
import { Location } from 'history';
14-
import { Base64 } from 'js-base64';
1514
import _, { has } from 'lodash';
1615
import React, { PropsWithChildren, ReactNode } from 'react';
1716
import { useTranslation } from 'react-i18next';
@@ -506,12 +505,12 @@ export function SecretField(props: InputProps) {
506505
</Grid>
507506
<Grid item xs>
508507
<Input
509-
readOnly
508+
readOnly={!showPassword}
510509
type="password"
511510
fullWidth
512511
multiline={showPassword}
513512
maxRows="20"
514-
value={showPassword ? Base64.decode(value as string) : '******'}
513+
value={showPassword ? (value as string) : '******'}
515514
{...other}
516515
/>
517516
</Grid>

frontend/src/components/secret/Details.tsx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import { Box, Button } from '@mui/material';
2+
import { Base64 } from 'js-base64';
3+
import _ from 'lodash';
4+
import React from 'react';
15
import { useTranslation } from 'react-i18next';
6+
import { useDispatch } from 'react-redux';
27
import { useParams } from 'react-router-dom';
38
import Secret from '../../lib/k8s/secret';
9+
import { clusterAction } from '../../redux/clusterActionSlice';
10+
import { AppDispatch } from '../../redux/stores/store';
411
import { EmptyContent } from '../common';
512
import { DetailsGrid, SecretField } from '../common/Resource';
613
import { SectionBox } from '../common/SectionBox';
@@ -14,6 +21,7 @@ export default function SecretDetails(props: {
1421
const params = useParams<{ namespace: string; name: string }>();
1522
const { name = params.name, namespace = params.namespace, cluster } = props;
1623
const { t } = useTranslation();
24+
const dispatch: AppDispatch = useDispatch();
1725

1826
return (
1927
<DetailsGrid
@@ -35,19 +43,68 @@ export default function SecretDetails(props: {
3543
{
3644
id: 'headlamp.secrets-data',
3745
section: () => {
38-
const itemData = item?.data || {};
39-
const mainRows: NameValueTableRow[] = Object.entries(itemData).map(
40-
(item: unknown[]) => ({
41-
name: item[0] as string,
42-
value: <SecretField value={item[1]} />,
43-
})
44-
);
46+
const initialData = _.mapValues(item.data, (v: string) => Base64.decode(v));
47+
const [data, setData] = React.useState(initialData);
48+
const lastDataRef = React.useRef(initialData);
49+
50+
React.useEffect(() => {
51+
const newData = _.mapValues(item.data, (v: string) => Base64.decode(v));
52+
if (!_.isEqual(newData, lastDataRef.current)) {
53+
if (_.isEqual(data, lastDataRef.current)) {
54+
setData(newData);
55+
lastDataRef.current = newData;
56+
}
57+
}
58+
}, [item.data]);
59+
60+
const handleFieldChange = (key: string, newValue: string) => {
61+
setData(prev => ({ ...prev, [key]: newValue }));
62+
};
63+
64+
const handleSave = () => {
65+
const encodedData = _.mapValues(data, (v: string) => Base64.encode(v));
66+
const updatedSecret = { ...item.jsonData, data: encodedData };
67+
dispatch(
68+
clusterAction(() => item.update(updatedSecret), {
69+
startMessage: t('translation|Applying changes to {{ itemName }}…', {
70+
itemName: item.metadata.name,
71+
}),
72+
cancelledMessage: t('translation|Cancelled changes to {{ itemName }}.', {
73+
itemName: item.metadata.name,
74+
}),
75+
successMessage: t('translation|Applied changes to {{ itemName }}.', {
76+
itemName: item.metadata.name,
77+
}),
78+
errorMessage: t('translation|Failed to apply changes to {{ itemName }}.', {
79+
itemName: item.metadata.name,
80+
}),
81+
})
82+
);
83+
lastDataRef.current = _.cloneDeep(data);
84+
};
85+
86+
const mainRows: NameValueTableRow[] = Object.entries(data).map((item: unknown[]) => ({
87+
name: item[0] as string,
88+
value: (
89+
<SecretField
90+
value={item[1]}
91+
onChange={e => handleFieldChange(item[0] as string, e.target.value)}
92+
/>
93+
),
94+
}));
4595
return (
4696
<SectionBox title={t('translation|Data')}>
4797
{mainRows.length === 0 ? (
4898
<EmptyContent>{t('No data in this secret')}</EmptyContent>
4999
) : (
50-
<NameValueTable rows={mainRows} />
100+
<>
101+
<NameValueTable rows={mainRows} />
102+
<Box mt={2} display="flex" justifyContent="flex-end">
103+
<Button variant="contained" color="primary" onClick={handleSave}>
104+
{t('translation|Save')}
105+
</Button>
106+
</Box>
107+
</>
51108
)}
52109
</SectionBox>
53110
);

frontend/src/components/secret/__snapshots__/Details.WithBase.stories.storyshot

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,20 @@
344344
</div>
345345
</dd>
346346
</dl>
347+
<div
348+
class="MuiBox-root css-10igwnb"
349+
>
350+
<button
351+
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary MuiButton-disableElevation MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary MuiButton-disableElevation css-gn8fa3-MuiButtonBase-root-MuiButton-root"
352+
tabindex="0"
353+
type="button"
354+
>
355+
Save
356+
<span
357+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
358+
/>
359+
</button>
360+
</div>
347361
</div>
348362
</div>
349363
</div>

0 commit comments

Comments
 (0)