Skip to content

Commit 8ae81f0

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

File tree

3 files changed

+158
-45
lines changed

3 files changed

+158
-45
lines changed

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

Lines changed: 45 additions & 28 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';
@@ -400,14 +400,25 @@ export function SectionGrid(props: SectionGridProps) {
400400

401401
export interface DataFieldProps extends BaseTextFieldProps {
402402
disableLabel?: boolean;
403+
onSave?: (newValue: string) => void;
404+
onChange?: (newValue: string) => void;
403405
}
404406

405407
export function DataField(props: DataFieldProps) {
406-
const { disableLabel, label, value } = props;
408+
const { disableLabel, label, value, onSave, onChange } = props;
407409
// Make sure we reload after a theme change
408410
useTheme();
409411
const themeName = getThemeName();
410412

413+
const [data, setData] = React.useState(value as string);
414+
415+
const handleChange = (newValue: string | undefined) => {
416+
if (newValue !== undefined) {
417+
setData(newValue);
418+
onChange?.(newValue);
419+
}
420+
};
421+
411422
function handleEditorDidMount(editor: any) {
412423
const editorElement: HTMLElement | null = editor.getDomNode();
413424
if (!editorElement) {
@@ -428,40 +439,46 @@ export function DataField(props: DataFieldProps) {
428439
if (language !== 'json') {
429440
language = 'yaml';
430441
}
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}>
442+
443+
const editorComponent = (
444+
<Editor
445+
value={data}
446+
language={language}
447+
onChange={handleChange}
448+
onMount={handleEditorDidMount}
449+
options={{ lineNumbers: 'off', automaticLayout: true }}
450+
theme={themeName === 'dark' ? 'vs-dark' : 'light'}
451+
/>
452+
);
453+
454+
const content = (
455+
<Box borderTop={0} border={1}>
456+
{!disableLabel && (
447457
<Box display="flex">
448458
<Box width="10%" borderTop={1} height={'1px'}></Box>
449459
<Box pb={1} mt={-1} px={0.5}>
450460
<InputLabel>{label}</InputLabel>
451461
</Box>
452462
<Box width="100%" borderTop={1} height={'1px'}></Box>
453463
</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>
464+
)}
465+
<Box mt={1} px={1} pb={1}>
466+
{editorComponent}
463467
</Box>
464-
</>
468+
</Box>
469+
);
470+
471+
return (
472+
<Box>
473+
{content}
474+
{onSave && (
475+
<Box mt={1} display="flex" justifyContent="flex-end">
476+
<Button variant="contained" color="primary" onClick={() => onSave && onSave(data)}>
477+
Save
478+
</Button>
479+
</Box>
480+
)}
481+
</Box>
465482
);
466483
}
467484

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)