Skip to content

Commit 74157b5

Browse files
authored
Merge pull request #3132 from skoeva/edit-fields-2
frontend: Allow direct editing of resource fields
2 parents d22ecc0 + 447b6be commit 74157b5

File tree

5 files changed

+239
-56
lines changed

5 files changed

+239
-56
lines changed

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

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Icon } from '@iconify/react';
22
import Editor from '@monaco-editor/react';
3-
import { InputLabel } from '@mui/material';
3+
import { Button, InputLabel } from '@mui/material';
44
import Box from '@mui/material/Box';
55
import Divider from '@mui/material/Divider';
66
import Grid, { GridProps, GridSize } from '@mui/material/Grid';
@@ -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';
@@ -400,14 +399,25 @@ export function SectionGrid(props: SectionGridProps) {
400399

401400
export interface DataFieldProps extends BaseTextFieldProps {
402401
disableLabel?: boolean;
402+
onSave?: (newValue: string) => void;
403+
onChange?: (newValue: string) => void;
403404
}
404405

405406
export function DataField(props: DataFieldProps) {
406-
const { disableLabel, label, value } = props;
407+
const { disableLabel, label, value, onSave, onChange } = props;
407408
// Make sure we reload after a theme change
408409
useTheme();
409410
const themeName = getThemeName();
410411

412+
const [data, setData] = React.useState(value as string);
413+
414+
const handleChange = (newValue: string | undefined) => {
415+
if (newValue !== undefined) {
416+
setData(newValue);
417+
onChange?.(newValue);
418+
}
419+
};
420+
411421
function handleEditorDidMount(editor: any) {
412422
const editorElement: HTMLElement | null = editor.getDomNode();
413423
if (!editorElement) {
@@ -428,40 +438,46 @@ export function DataField(props: DataFieldProps) {
428438
if (language !== 'json') {
429439
language = 'yaml';
430440
}
431-
if (disableLabel === true) {
432-
return (
433-
<Box borderTop={0} border={1}>
434-
<Editor
435-
value={value as string}
436-
language={language}
437-
onMount={handleEditorDidMount}
438-
options={{ readOnly: true, lineNumbers: 'off', automaticLayout: true }}
439-
theme={themeName === 'dark' ? 'vs-dark' : 'light'}
440-
/>
441-
</Box>
442-
);
443-
}
444-
return (
445-
<>
446-
<Box borderTop={0} border={1}>
441+
442+
const editorComponent = (
443+
<Editor
444+
value={data}
445+
language={language}
446+
onChange={handleChange}
447+
onMount={handleEditorDidMount}
448+
options={{ lineNumbers: 'off', automaticLayout: true }}
449+
theme={themeName === 'dark' ? 'vs-dark' : 'light'}
450+
/>
451+
);
452+
453+
const content = (
454+
<Box borderTop={0} border={1}>
455+
{!disableLabel && (
447456
<Box display="flex">
448457
<Box width="10%" borderTop={1} height={'1px'}></Box>
449458
<Box pb={1} mt={-1} px={0.5}>
450459
<InputLabel>{label}</InputLabel>
451460
</Box>
452461
<Box width="100%" borderTop={1} height={'1px'}></Box>
453462
</Box>
454-
<Box mt={1} px={1} pb={1}>
455-
<Editor
456-
value={value as string}
457-
language={language}
458-
onMount={handleEditorDidMount}
459-
options={{ readOnly: true, lineNumbers: 'off', automaticLayout: true }}
460-
theme={themeName === 'dark' ? 'vs-dark' : 'light'}
461-
/>
462-
</Box>
463+
)}
464+
<Box mt={1} px={1} pb={1}>
465+
{editorComponent}
463466
</Box>
464-
</>
467+
</Box>
468+
);
469+
470+
return (
471+
<Box>
472+
{content}
473+
{onSave && (
474+
<Box mt={1} display="flex" justifyContent="flex-end">
475+
<Button variant="contained" color="primary" onClick={() => onSave && onSave(data)}>
476+
Save
477+
</Button>
478+
</Box>
479+
)}
480+
</Box>
465481
);
466482
}
467483

@@ -489,12 +505,12 @@ export function SecretField(props: InputProps) {
489505
</Grid>
490506
<Grid item xs>
491507
<Input
492-
readOnly
508+
readOnly={!showPassword}
493509
type="password"
494510
fullWidth
495511
multiline={showPassword}
496512
maxRows="20"
497-
value={showPassword ? Base64.decode(value as string) : '******'}
513+
value={showPassword ? (value as string) : '******'}
498514
{...other}
499515
/>
500516
</Grid>

frontend/src/components/configmap/Details.tsx

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import { Box, Button } from '@mui/material';
2+
import _ from 'lodash';
3+
import React from 'react';
14
import { useTranslation } from 'react-i18next';
5+
import { useDispatch } from 'react-redux';
26
import { useParams } from 'react-router-dom';
37
import ConfigMap from '../../lib/k8s/configMap';
8+
import { clusterAction } from '../../redux/clusterActionSlice';
9+
import { AppDispatch } from '../../redux/stores/store';
410
import EmptyContent from '../common/EmptyContent';
511
import { DataField, DetailsGrid } from '../common/Resource';
612
import { SectionBox } from '../common/SectionBox';
@@ -14,6 +20,7 @@ export default function ConfigDetails(props: {
1420
const params = useParams<{ namespace: string; name: string }>();
1521
const { name = params.name, namespace = params.namespace, cluster } = props;
1622
const { t } = useTranslation(['translation']);
23+
const dispatch: AppDispatch = useDispatch();
1724

1825
return (
1926
<DetailsGrid
@@ -27,19 +34,70 @@ export default function ConfigDetails(props: {
2734
{
2835
id: 'headlamp.configmap-data',
2936
section: () => {
30-
const itemData = item?.data || [];
31-
const mainRows: NameValueTableRow[] = Object.entries(itemData).map(
32-
(item: unknown[]) => ({
33-
name: item[0] as string,
34-
value: <DataField label={item[0] as string} disableLabel value={item[1]} />,
35-
})
36-
);
37+
const [data, setData] = React.useState(() => _.cloneDeep(item.data));
38+
const [isDirty, setIsDirty] = React.useState(false);
39+
const lastDataRef = React.useRef(_.cloneDeep(item.data));
40+
41+
const handleFieldChange = (key: string, newValue: string) => {
42+
setData(prev => ({ ...prev, [key]: newValue }));
43+
setIsDirty(true);
44+
};
45+
46+
React.useEffect(() => {
47+
const newData = _.cloneDeep(item.data);
48+
if (!isDirty && !_.isEqual(newData, lastDataRef.current)) {
49+
setData(newData);
50+
lastDataRef.current = newData;
51+
}
52+
}, [item.data, isDirty]);
53+
54+
const handleSave = () => {
55+
const updatedConfigMap = { ...item.jsonData, data };
56+
dispatch(
57+
clusterAction(() => item.update(updatedConfigMap), {
58+
startMessage: t('translation|Applying changes to {{ itemName }}…', {
59+
itemName: item.metadata.name,
60+
}),
61+
cancelledMessage: t('translation|Cancelled changes to {{ itemName }}.', {
62+
itemName: item.metadata.name,
63+
}),
64+
successMessage: t('translation|Applied changes to {{ itemName }}.', {
65+
itemName: item.metadata.name,
66+
}),
67+
errorMessage: t('translation|Failed to apply changes to {{ itemName }}.', {
68+
itemName: item.metadata.name,
69+
}),
70+
})
71+
);
72+
lastDataRef.current = _.cloneDeep(data);
73+
setIsDirty(false);
74+
};
75+
76+
const mainRows: NameValueTableRow[] = Object.entries(data).map((item: unknown[]) => ({
77+
name: item[0] as string,
78+
value: (
79+
<DataField
80+
label={item[0] as string}
81+
disableLabel
82+
value={item[1]}
83+
onChange={(newValue: string) => handleFieldChange(item[0] as string, newValue)}
84+
/>
85+
),
86+
}));
87+
3788
return (
3889
<SectionBox title={t('translation|Data')}>
3990
{mainRows.length === 0 ? (
4091
<EmptyContent>{t('No data in this config map')}</EmptyContent>
4192
) : (
42-
<NameValueTable rows={mainRows} />
93+
<>
94+
<NameValueTable rows={mainRows} />
95+
<Box mt={2} display="flex" justifyContent="flex-end">
96+
<Button variant="contained" color="primary" onClick={handleSave}>
97+
{t('translation|Save')}
98+
</Button>
99+
</Box>
100+
</>
43101
)}
44102
</SectionBox>
45103
);

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

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,19 @@
215215
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-deb4a-MuiGrid-root"
216216
>
217217
<div
218-
class="MuiBox-root css-1rsqta2"
218+
class="MuiBox-root css-0"
219219
>
220220
<div
221-
class="mock-monaco-editor"
222-
/>
221+
class="MuiBox-root css-1rsqta2"
222+
>
223+
<div
224+
class="MuiBox-root css-1y5kcuw"
225+
>
226+
<div
227+
class="mock-monaco-editor"
228+
/>
229+
</div>
230+
</div>
223231
</div>
224232
</dd>
225233
<dt
@@ -231,11 +239,19 @@
231239
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-deb4a-MuiGrid-root"
232240
>
233241
<div
234-
class="MuiBox-root css-1rsqta2"
242+
class="MuiBox-root css-0"
235243
>
236244
<div
237-
class="mock-monaco-editor"
238-
/>
245+
class="MuiBox-root css-1rsqta2"
246+
>
247+
<div
248+
class="MuiBox-root css-1y5kcuw"
249+
>
250+
<div
251+
class="mock-monaco-editor"
252+
/>
253+
</div>
254+
</div>
239255
</div>
240256
</dd>
241257
<dt
@@ -247,14 +263,36 @@
247263
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12 MuiGrid-grid-sm-8 css-1xrovmc-MuiGrid-root"
248264
>
249265
<div
250-
class="MuiBox-root css-1rsqta2"
266+
class="MuiBox-root css-0"
251267
>
252268
<div
253-
class="mock-monaco-editor"
254-
/>
269+
class="MuiBox-root css-1rsqta2"
270+
>
271+
<div
272+
class="MuiBox-root css-1y5kcuw"
273+
>
274+
<div
275+
class="mock-monaco-editor"
276+
/>
277+
</div>
278+
</div>
255279
</div>
256280
</dd>
257281
</dl>
282+
<div
283+
class="MuiBox-root css-10igwnb"
284+
>
285+
<button
286+
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"
287+
tabindex="0"
288+
type="button"
289+
>
290+
Save
291+
<span
292+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
293+
/>
294+
</button>
295+
</div>
258296
</div>
259297
</div>
260298
</div>

0 commit comments

Comments
 (0)