Skip to content

Commit d0dcca5

Browse files
authored
feat: Edit YAMLs of MCP resources (#323)
1 parent a2f9e17 commit d0dcca5

23 files changed

+1049
-368
lines changed

public/locales/en.json

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@
3737
"tableHeaderReady": "Ready",
3838
"tableHeaderDelete": "Delete",
3939
"deleteAction": "Delete resource",
40+
"editAction": "Edit resource",
4041
"deleteDialogTitle": "Delete resource",
4142
"advancedOptions": "Advanced options",
4243
"forceDeletion": "Force deletion",
4344
"forceWarningLine": "Force deletion removes finalizers. Related resources may not be deleted and cleanup may be skipped.",
4445
"deleteStarted": "Deleting {{resourceName}} initialized",
46+
"patchStarted": "Updating {{resourceName}} initialized",
47+
"patchSuccess": "Updated {{resourceName}}",
48+
"patchError": "Failed to update {{resourceName}}",
4549
"actionColumnHeader": " "
4650
},
4751
"ProvidersConfig": {
@@ -373,7 +377,8 @@
373377
"installError": "Install error",
374378
"syncError": "Sync error",
375379
"error": "Error",
376-
"notHealthy": "Not healthy"
380+
"notHealthy": "Not healthy",
381+
"notReady": "Not ready"
377382
},
378383
"buttons": {
379384
"viewResource": "View resource",
@@ -384,11 +389,20 @@
384389
"close": "Close",
385390
"back": "Back",
386391
"cancel": "Cancel",
387-
"update": "Update"
392+
"update": "Update",
393+
"applyChanges": "Apply changes"
388394
},
389395
"yaml": {
390396
"YAML": "File",
391-
"showOnlyImportant": "Show only important fields"
397+
"showOnlyImportant": "Show only important fields",
398+
"panelTitle": "YAML",
399+
"editorTitle": "YAML Editor",
400+
"applySuccess2": "The Managed Control Plane will reconcile this resource shortly.",
401+
"applySuccess": "Update submitted ",
402+
"diffConfirmTitle": "Review changes",
403+
"diffConfirmMessage": "Are you sure that you want to apply these changes?",
404+
"diffNo": "No, go back",
405+
"diffYes": "Yes"
392406
},
393407
"createMCP": {
394408
"dialogTitle": "Create Managed Control Plane",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useRef, useState } from 'react';
2+
import { Button, Menu, MenuItem, MenuDomRef } from '@ui5/webcomponents-react';
3+
import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js';
4+
import type { Ui5CustomEvent, ButtonDomRef } from '@ui5/webcomponents-react';
5+
6+
export type ActionItem<T> = {
7+
key: string;
8+
text: string;
9+
icon?: string;
10+
disabled?: boolean;
11+
onClick: (item: T) => void;
12+
};
13+
14+
export type ActionsMenuProps<T> = {
15+
item: T;
16+
actions: ActionItem<T>[];
17+
buttonIcon?: string;
18+
};
19+
20+
export function ActionsMenu<T>({ item, actions, buttonIcon = 'overflow' }: ActionsMenuProps<T>) {
21+
const popoverRef = useRef<MenuDomRef>(null);
22+
const [open, setOpen] = useState(false);
23+
24+
const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
25+
if (popoverRef.current && e.currentTarget) {
26+
popoverRef.current.opener = e.currentTarget as unknown as HTMLElement;
27+
setOpen((prev) => !prev);
28+
}
29+
};
30+
31+
return (
32+
<>
33+
<Button icon={buttonIcon} design="Transparent" onClick={handleOpenerClick} />
34+
<Menu
35+
ref={popoverRef}
36+
open={open}
37+
onItemClick={(event) => {
38+
const element = event.detail.item as HTMLElement & { disabled?: boolean };
39+
const actionKey = element.dataset.actionKey;
40+
const action = actions.find((a) => a.key === actionKey);
41+
if (action && !action.disabled) {
42+
action.onClick(item);
43+
}
44+
setOpen(false);
45+
}}
46+
>
47+
{actions.map((a) => (
48+
<MenuItem key={a.key} text={a.text} icon={a.icon} data-action-key={a.key} disabled={a.disabled} />
49+
))}
50+
</Menu>
51+
</>
52+
);
53+
}

src/components/ControlPlane/GitRepositories.tsx

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,129 @@ import { AnalyticalTableColumnDefinition, Panel, Title, Toolbar, ToolbarSpacer }
33
import IllustratedError from '../Shared/IllustratedError.tsx';
44
import { useApiResource } from '../../lib/api/useApiResource';
55
import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
6-
import { KustomizationsResponse } from '../../lib/api/types/flux/listKustomization';
76
import { useTranslation } from 'react-i18next';
87
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo.ts';
98

109
import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
11-
import { useMemo } from 'react';
10+
import { Fragment, useCallback, useMemo, useRef } from 'react';
1211
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
1312
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
1413
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
14+
import { useSplitter } from '../Splitter/SplitterContext.tsx';
15+
import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx';
16+
import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts';
17+
import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
18+
import type { GitReposResponse } from '../../lib/api/types/flux/listGitRepo';
19+
import { ActionsMenu, type ActionItem } from './ActionsMenu';
20+
21+
export type GitRepoItem = GitReposResponse['items'][0] & {
22+
apiVersion?: string;
23+
metadata: GitReposResponse['items'][0]['metadata'] & { namespace?: string };
24+
};
25+
26+
interface CellRow<T> {
27+
original: T;
28+
}
1529

1630
export function GitRepositories() {
1731
const { data, error, isLoading } = useApiResource(FluxRequest); //404 if component not enabled
1832
const { t } = useTranslation();
19-
20-
interface CellData<T> {
21-
cell: {
22-
value: T | null; // null for grouping rows
23-
row: {
24-
original?: FluxRow; // missing for grouping rows
25-
};
26-
};
27-
}
33+
const { openInAside } = useSplitter();
34+
const errorDialogRef = useRef<ErrorDialogHandle>(null);
35+
const handlePatch = useHandleResourcePatch(errorDialogRef);
2836

2937
type FluxRow = {
3038
name: string;
3139
created: string;
3240
isReady: boolean;
3341
statusUpdateTime?: string;
34-
item: unknown;
42+
item: GitRepoItem;
3543
readyMessage: string;
44+
revision?: string;
3645
};
3746

38-
const columns: AnalyticalTableColumnDefinition[] = useMemo(
39-
() => [
40-
{
41-
Header: t('FluxList.tableNameHeader'),
42-
accessor: 'name',
43-
minWidth: 250,
44-
},
45-
{
46-
Header: t('FluxList.tableCreatedHeader'),
47-
accessor: 'created',
48-
},
49-
{
50-
Header: t('FluxList.tableVersionHeader'),
51-
accessor: 'revision',
52-
},
53-
{
54-
Header: t('FluxList.tableStatusHeader'),
55-
accessor: 'status',
56-
width: 125,
57-
hAlign: 'Center',
58-
Filter: ({ column }) => <StatusFilter column={column} />,
59-
Cell: (cellData: CellData<FluxRow>) =>
60-
cellData.cell.row.original?.isReady != null ? (
61-
<ResourceStatusCell
62-
positiveText={t('common.ready')}
63-
negativeText={t('errors.error')}
64-
isOk={cellData.cell.row.original?.isReady}
65-
transitionTime={
66-
cellData.cell.row.original?.statusUpdateTime ? cellData.cell.row.original?.statusUpdateTime : ''
67-
}
68-
message={cellData.cell.row.original?.readyMessage}
69-
/>
70-
) : null,
71-
},
72-
{
73-
Header: t('yaml.YAML'),
74-
hAlign: 'Center',
75-
width: 75,
76-
accessor: 'yaml',
77-
disableFilters: true,
78-
Cell: (cellData: CellData<KustomizationsResponse['items']>) => (
79-
<YamlViewButton variant="resource" resource={cellData.cell.row.original?.item as Resource} />
80-
),
81-
},
82-
],
83-
[t],
47+
const openEditPanel = useCallback(
48+
(item: GitRepoItem) => {
49+
const identityKey = `${item.kind}:${item.metadata.namespace ?? ''}:${item.metadata.name}`;
50+
openInAside(
51+
<Fragment key={identityKey}>
52+
<YamlSidePanel
53+
isEdit={true}
54+
resource={item as unknown as Resource}
55+
filename={`${item.kind}_${item.metadata.name}`}
56+
onApply={async (parsed) => await handlePatch(item, parsed)}
57+
/>
58+
</Fragment>,
59+
);
60+
},
61+
[openInAside, handlePatch],
62+
);
63+
64+
const columns = useMemo<AnalyticalTableColumnDefinition[]>(
65+
() =>
66+
[
67+
{
68+
Header: t('FluxList.tableNameHeader'),
69+
accessor: 'name',
70+
minWidth: 250,
71+
},
72+
{
73+
Header: t('FluxList.tableCreatedHeader'),
74+
accessor: 'created',
75+
},
76+
{
77+
Header: t('FluxList.tableVersionHeader'),
78+
accessor: 'revision',
79+
},
80+
{
81+
Header: t('FluxList.tableStatusHeader'),
82+
accessor: 'status',
83+
width: 125,
84+
hAlign: 'Center',
85+
Filter: ({ column }) => <StatusFilter column={column} />,
86+
Cell: ({ row }: { row: CellRow<FluxRow> }) =>
87+
row.original?.isReady != null ? (
88+
<ResourceStatusCell
89+
positiveText={t('common.ready')}
90+
negativeText={t('errors.error')}
91+
isOk={row.original?.isReady}
92+
transitionTime={row.original?.statusUpdateTime ? row.original?.statusUpdateTime : ''}
93+
message={row.original?.readyMessage}
94+
/>
95+
) : null,
96+
},
97+
{
98+
Header: t('yaml.YAML'),
99+
hAlign: 'Center',
100+
width: 75,
101+
accessor: 'yaml',
102+
disableFilters: true,
103+
Cell: ({ row }: { row: CellRow<FluxRow> }) => (
104+
<YamlViewButton variant="resource" resource={row.original.item as unknown as Resource} />
105+
),
106+
},
107+
{
108+
Header: t('ManagedResources.actionColumnHeader'),
109+
hAlign: 'Center',
110+
width: 60,
111+
disableFilters: true,
112+
accessor: 'actions',
113+
Cell: ({ row }: { row: CellRow<FluxRow> }) => {
114+
const item = row.original?.item;
115+
if (!item) return undefined;
116+
const actions: ActionItem<GitRepoItem>[] = [
117+
{
118+
key: 'edit',
119+
text: t('ManagedResources.editAction', 'Edit'),
120+
icon: 'edit',
121+
onClick: openEditPanel,
122+
},
123+
];
124+
return <ActionsMenu item={item} actions={actions} />;
125+
},
126+
},
127+
] as AnalyticalTableColumnDefinition[],
128+
[t, openEditPanel],
84129
);
85130

86131
if (error) {
@@ -102,7 +147,12 @@ export function GitRepositories() {
102147
statusUpdateTime: readyObject?.lastTransitionTime,
103148
revision: shortenCommitHash(item.status.artifact?.revision ?? '-'),
104149
created: formatDateAsTimeAgo(item.metadata.creationTimestamp),
105-
item: item,
150+
item: {
151+
...item,
152+
kind: 'GitRepository',
153+
apiVersion: 'source.toolkit.fluxcd.io/v1',
154+
metadata: { ...item.metadata },
155+
} as GitRepoItem,
106156
readyMessage: readyObject?.message ?? readyObject?.reason ?? '',
107157
};
108158
}) ?? [];
@@ -118,7 +168,10 @@ export function GitRepositories() {
118168
</Toolbar>
119169
}
120170
>
121-
<ConfiguredAnalyticstable columns={columns} isLoading={isLoading} data={rows} />
171+
<>
172+
<ConfiguredAnalyticstable columns={columns} isLoading={isLoading} data={rows} />
173+
<ErrorDialog ref={errorDialogRef} />
174+
</>
122175
</Panel>
123176
);
124177
}

0 commit comments

Comments
 (0)