Skip to content

Commit 8e17251

Browse files
authored
feat: Show resource errors popover for MCP (#215)
1 parent 96b60c8 commit 8e17251

File tree

15 files changed

+270
-124
lines changed

15 files changed

+270
-124
lines changed

public/locales/en.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,18 @@
323323
"search": "Search",
324324
"components": "Components",
325325
"notSelected": "Not selected",
326-
"btp": "BTP"
326+
"btp": "BTP",
327+
"error": "Error",
328+
"ready": "Ready",
329+
"synced": "Synced",
330+
"healthy": "Healthy",
331+
"installed": "Installed"
332+
},
333+
"errors": {
334+
"installError": "Install error",
335+
"syncError": "Sync error",
336+
"error": "Error",
337+
"notHealthy": "Not healthy"
327338
},
328339
"buttons": {
329340
"viewResource": "View resource",

src/components/ControlPlane/FluxList.tsx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
66
import { FluxKustomization, KustomizationsResponse } from '../../lib/api/types/flux/listKustomization';
77
import { useTranslation } from 'react-i18next';
88
import { timeAgo } from '../../utils/i18n/timeAgo.ts';
9-
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
9+
1010
import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
1111
import { useMemo } from 'react';
1212
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
13+
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
1314

1415
export default function FluxList() {
1516
const { data: gitReposData, error: repoErr, isLoading: repoIsLoading } = useApiResource(FluxRequest); //404 if component not enabled
@@ -36,6 +37,7 @@ export default function FluxList() {
3637
isReady: boolean;
3738
statusUpdateTime?: string;
3839
item: unknown;
40+
readyMessage: string;
3941
};
4042

4143
const gitReposColumns: AnalyticalTableColumnDefinition[] = useMemo(
@@ -56,23 +58,26 @@ export default function FluxList() {
5658
{
5759
Header: t('FluxList.tableStatusHeader'),
5860
accessor: 'status',
59-
width: 85,
61+
width: 125,
6062
hAlign: 'Center',
6163
Filter: ({ column }) => <StatusFilter column={column} />,
62-
Cell: (cellData: CellData<FluxRow['isReady']>) =>
64+
Cell: (cellData: CellData<FluxRow>) =>
6365
cellData.cell.row.original?.isReady != null ? (
6466
<ResourceStatusCell
65-
value={cellData.cell.row.original?.isReady}
67+
positiveText={t('common.ready')}
68+
negativeText={t('errors.error')}
69+
isOk={cellData.cell.row.original?.isReady}
6670
transitionTime={
6771
cellData.cell.row.original?.statusUpdateTime ? cellData.cell.row.original?.statusUpdateTime : ''
6872
}
73+
message={cellData.cell.row.original?.readyMessage}
6974
/>
7075
) : null,
7176
},
7277
{
7378
Header: t('yaml.YAML'),
7479
hAlign: 'Center',
75-
width: 85,
80+
width: 75,
7681
accessor: 'yaml',
7782
disableFilters: true,
7883
Cell: (cellData: CellData<KustomizationsResponse['items']>) => (
@@ -97,24 +102,27 @@ export default function FluxList() {
97102
{
98103
Header: t('FluxList.tableStatusHeader'),
99104
accessor: 'status',
100-
width: 85,
105+
width: 125,
101106
hAlign: 'Center',
102107
Filter: ({ column }) => <StatusFilter column={column} />,
103108
Cell: (cellData: CellData<FluxRow['isReady']>) =>
104109
cellData.cell.row.original?.isReady != null ? (
105110
<ResourceStatusCell
106-
value={cellData.cell.row.original?.isReady}
111+
positiveText={t('common.ready')}
112+
negativeText={t('common.error')}
113+
isOk={cellData.cell.row.original?.isReady}
107114
transitionTime={
108115
cellData.cell.row.original?.statusUpdateTime ? cellData.cell.row.original?.statusUpdateTime : ''
109116
}
117+
message={cellData.cell.row.original?.readyMessage}
110118
/>
111119
) : null,
112120
},
113121

114122
{
115123
Header: t('yaml.YAML'),
116124
hAlign: 'Center',
117-
width: 85,
125+
width: 75,
118126
accessor: 'yaml',
119127
disableFilters: true,
120128
Cell: (cellData: CellData<FluxRow>) => <YamlViewButton resourceObject={cellData.cell.row.original?.item} />,
@@ -134,24 +142,28 @@ export default function FluxList() {
134142

135143
const gitReposRows: FluxRow[] =
136144
gitReposData?.items?.map((item) => {
145+
const readyObject = item.status?.conditions?.find((x) => x.type === 'Ready');
137146
return {
138147
name: item.metadata.name,
139-
isReady: item?.status?.conditions?.find((x) => x.type === 'Ready')?.status === 'True',
140-
statusUpdateTime: item.status?.conditions?.find((x) => x.type === 'Ready')?.lastTransitionTime,
148+
isReady: readyObject?.status === 'True',
149+
statusUpdateTime: readyObject?.lastTransitionTime,
141150
revision: shortenCommitHash(item.status.artifact?.revision ?? '-'),
142151
created: timeAgo.format(new Date(item.metadata.creationTimestamp)),
143152
item: item,
153+
readyMessage: readyObject?.message ?? readyObject?.reason ?? '',
144154
};
145155
}) ?? [];
146156

147157
const kustomizationsRows: FluxRow[] =
148158
kustmizationData?.items?.map((item) => {
159+
const readyObject = item.status?.conditions?.find((x) => x.type === 'Ready');
149160
return {
150161
name: item.metadata.name,
151-
isReady: item.status?.conditions?.find((x) => x.type === 'Ready')?.status === 'True',
152-
statusUpdateTime: item.status?.conditions?.find((x) => x.type === 'Ready')?.lastTransitionTime,
162+
isReady: readyObject?.status === 'True',
163+
statusUpdateTime: readyObject?.lastTransitionTime,
153164
created: timeAgo.format(new Date(item.metadata.creationTimestamp)),
154165
item: item,
166+
readyMessage: readyObject?.message ?? readyObject?.reason ?? '',
155167
};
156168
}) ?? [];
157169

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,62 @@
1-
import { AnalyticalTable, Icon, Popover, FlexBox, FlexBoxJustifyContent, Button } from '@ui5/webcomponents-react';
1+
import {
2+
AnalyticalTable,
3+
Icon,
4+
Popover,
5+
FlexBox,
6+
FlexBoxJustifyContent,
7+
Button,
8+
PopoverDomRef,
9+
ButtonDomRef,
10+
} from '@ui5/webcomponents-react';
211
import { AnalyticalTableColumnDefinition } from '@ui5/webcomponents-react/wrappers';
312
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
413
import '@ui5/webcomponents-icons/dist/copy';
5-
import { JSX, useRef, useState } from 'react';
6-
import { ControlPlaneStatusType, ReadyStatus } from '../../lib/api/types/crate/controlPlanes';
14+
import { JSX, useRef, useState, ReactNode } from 'react';
15+
import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js';
16+
import {
17+
ControlPlaneStatusType,
18+
ReadyStatus,
19+
ControlPlaneStatusCondition,
20+
} from '../../lib/api/types/crate/controlPlanes';
721
import ReactTimeAgo from 'react-time-ago';
822
import { AnimatedHoverTextButton } from '../Helper/AnimatedHoverTextButton.tsx';
923
import { useTranslation } from 'react-i18next';
1024
import { useLink } from '../../lib/shared/useLink.ts';
1125
import TooltipCell from '../Shared/TooltipCell.tsx';
12-
export default function MCPHealthPopoverButton({
13-
mcpStatus,
14-
projectName,
15-
workspaceName,
16-
mcpName,
17-
}: {
26+
import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base';
27+
28+
interface CellData<T> {
29+
cell: {
30+
value: ReactNode;
31+
};
32+
row: {
33+
original: T;
34+
[key: string]: unknown;
35+
};
36+
[key: string]: unknown;
37+
}
38+
39+
type MCPHealthPopoverButtonProps = {
1840
mcpStatus: ControlPlaneStatusType | undefined;
1941
projectName: string;
2042
workspaceName: string;
2143
mcpName: string;
22-
}) {
23-
const popoverRef = useRef(null);
44+
};
45+
46+
const MCPHealthPopoverButton = ({ mcpStatus, projectName, workspaceName, mcpName }: MCPHealthPopoverButtonProps) => {
47+
const popoverRef = useRef<PopoverDomRef>(null);
2448
const [open, setOpen] = useState(false);
2549
const { githubIssuesSupportTicket } = useLink();
26-
2750
const { t } = useTranslation();
2851

29-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30-
const handleOpenerClick = (e: any) => {
52+
const handleOpenerClick = (event: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
3153
if (popoverRef.current) {
32-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33-
const ref = popoverRef.current as any;
34-
ref.opener = e.target;
54+
(popoverRef.current as unknown as { opener: EventTarget | null }).opener = event.target;
3555
setOpen((prev) => !prev);
3656
}
3757
};
3858

39-
const getTicketTitle = () => {
59+
const getTicketTitle = (): string => {
4060
switch (mcpStatus?.status) {
4161
case ReadyStatus.Ready:
4262
return t('MCPHealthPopoverButton.supportTicketTitleReady');
@@ -49,13 +69,13 @@ export default function MCPHealthPopoverButton({
4969
}
5070
};
5171

52-
const constructGithubIssuesLink = () => {
72+
const constructGithubIssuesLink = (): string => {
5373
const clusterDetails = `${projectName}/${workspaceName}/${mcpName}`;
5474

5575
const statusDetails = mcpStatus?.conditions
5676
? `${t('MCPHealthPopoverButton.statusDetailsLabel')}: ${mcpStatus.status}\n\n${t('MCPHealthPopoverButton.detailsLabel')}\n` +
57-
mcpStatus?.conditions
58-
.map((condition) => {
77+
mcpStatus.conditions
78+
.map((condition: ControlPlaneStatusCondition) => {
5979
let text = `- ${condition.type}: ${condition.status}\n`;
6080
if (condition.reason) text += ` - ${t('MCPHealthPopoverButton.reasonHeader')}: ${condition.reason}\n`;
6181
if (condition.message) text += ` - ${t('MCPHealthPopoverButton.messageHeader')}: ${condition.message}\n`;
@@ -79,8 +99,7 @@ export default function MCPHealthPopoverButton({
7999
Header: t('MCPHealthPopoverButton.statusHeader'),
80100
accessor: 'status',
81101
width: 50,
82-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83-
Cell: (instance: any) => {
102+
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
84103
const isReady = instance.cell.value === 'True';
85104
return (
86105
<Icon
@@ -94,38 +113,33 @@ export default function MCPHealthPopoverButton({
94113
Header: t('MCPHealthPopoverButton.typeHeader'),
95114
accessor: 'type',
96115
width: 150,
97-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
98-
Cell: (instance: any) => {
116+
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
99117
return <TooltipCell>{instance.cell.value}</TooltipCell>;
100118
},
101119
},
102120
{
103121
Header: t('MCPHealthPopoverButton.messageHeader'),
104122
accessor: 'message',
105123
width: 350,
106-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
107-
Cell: (instance: any) => {
124+
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
108125
return <TooltipCell>{instance.cell.value}</TooltipCell>;
109126
},
110127
},
111128
{
112129
Header: t('MCPHealthPopoverButton.reasonHeader'),
113130
accessor: 'reason',
114131
width: 100,
115-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116-
Cell: (instance: any) => {
132+
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
117133
return <TooltipCell>{instance.cell.value}</TooltipCell>;
118134
},
119135
},
120136
{
121137
Header: t('MCPHealthPopoverButton.transitionHeader'),
122138
accessor: 'lastTransitionTime',
123-
width: 110,
124-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
125-
Cell: (instance: any) => {
139+
width: 125,
140+
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
126141
const rawDate = instance.cell.value;
127-
const date = new Date(rawDate);
128-
142+
const date = new Date(rawDate as string);
129143
return (
130144
<TooltipCell>
131145
<ReactTimeAgo date={date} />
@@ -143,58 +157,50 @@ export default function MCPHealthPopoverButton({
143157
onClick={handleOpenerClick}
144158
/>
145159
<Popover ref={popoverRef} open={open} placement={PopoverPlacement.Bottom}>
146-
{
147-
<StatusTable
148-
status={mcpStatus}
149-
tableColumns={statusTableColumns}
150-
githubIssuesLink={constructGithubIssuesLink()}
151-
/>
152-
}
160+
<StatusTable
161+
status={mcpStatus}
162+
tableColumns={statusTableColumns}
163+
githubIssuesLink={constructGithubIssuesLink()}
164+
/>
153165
</Popover>
154166
</div>
155167
);
156-
}
168+
};
157169

158-
function StatusTable({
159-
status,
160-
tableColumns,
161-
githubIssuesLink,
162-
}: {
170+
export default MCPHealthPopoverButton;
171+
172+
type StatusTableProps = {
163173
status: ControlPlaneStatusType | undefined;
164174
tableColumns: AnalyticalTableColumnDefinition[];
165175
githubIssuesLink: string;
166-
}) {
176+
};
177+
178+
const StatusTable = ({ status, tableColumns, githubIssuesLink }: StatusTableProps) => {
167179
const { t } = useTranslation();
168180

181+
const sortedConditions = status?.conditions ? [...status.conditions].sort((a, b) => (a.type < b.type ? -1 : 1)) : [];
182+
169183
return (
170184
<div style={{ width: 770 }}>
171-
<AnalyticalTable
172-
scaleWidthMode="Default"
173-
columns={tableColumns}
174-
data={
175-
status?.conditions?.sort((a, b) => {
176-
return a.type < b.type ? -1 : 1;
177-
}) ?? []
178-
}
179-
/>
185+
<AnalyticalTable scaleWidthMode="Default" columns={tableColumns} data={sortedConditions} />
180186
<FlexBox justifyContent={FlexBoxJustifyContent.End} style={{ marginTop: '0.5rem' }}>
181187
<a href={githubIssuesLink} target="_blank" rel="noreferrer">
182188
<Button>{t('MCPHealthPopoverButton.createSupportTicketButton')}</Button>
183189
</a>
184190
</FlexBox>
185191
</div>
186192
);
187-
}
193+
};
188194

189-
function getIconForOverallStatus(status: ReadyStatus | undefined): JSX.Element {
195+
const getIconForOverallStatus = (status: ReadyStatus | undefined): JSX.Element => {
190196
switch (status) {
191197
case ReadyStatus.Ready:
192198
return <Icon style={{ color: 'green' }} name="sap-icon://sys-enter" />;
193199
case ReadyStatus.NotReady:
194200
return <Icon style={{ color: 'red' }} name="sap-icon://pending" />;
195201
case ReadyStatus.InDeletion:
196202
return <Icon style={{ color: 'orange' }} name="sap-icon://delete" />;
197-
case undefined:
203+
default:
198204
return <></>;
199205
}
200-
}
206+
};

0 commit comments

Comments
 (0)