Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,18 @@
"search": "Search",
"components": "Components",
"notSelected": "Not selected",
"btp": "BTP"
"btp": "BTP",
"error": "Error",
"ready": "Ready",
"synced": "Synced",
"healthy": "Healthy",
"installed": "Installed"
},
"errors": {
"installError": "Install error",
"syncError": "Sync error",
"error": "Error",
"notHealthy": "Not healthy"
},
"buttons": {
"viewResource": "View resource",
Expand Down
36 changes: 24 additions & 12 deletions src/components/ControlPlane/FluxList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { FluxRequest } from '../../lib/api/types/flux/listGitRepo';
import { FluxKustomization, KustomizationsResponse } from '../../lib/api/types/flux/listKustomization';
import { useTranslation } from 'react-i18next';
import { timeAgo } from '../../utils/i18n/timeAgo.ts';
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';

import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
import { useMemo } from 'react';
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';

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

const gitReposColumns: AnalyticalTableColumnDefinition[] = useMemo(
Expand All @@ -56,23 +58,26 @@ export default function FluxList() {
{
Header: t('FluxList.tableStatusHeader'),
accessor: 'status',
width: 85,
width: 125,
hAlign: 'Center',
Filter: ({ column }) => <StatusFilter column={column} />,
Cell: (cellData: CellData<FluxRow['isReady']>) =>
Cell: (cellData: CellData<FluxRow>) =>
cellData.cell.row.original?.isReady != null ? (
<ResourceStatusCell
value={cellData.cell.row.original?.isReady}
positiveText={t('common.ready')}
negativeText={t('errors.error')}
isOk={cellData.cell.row.original?.isReady}
transitionTime={
cellData.cell.row.original?.statusUpdateTime ? cellData.cell.row.original?.statusUpdateTime : ''
}
message={cellData.cell.row.original?.readyMessage}
/>
) : null,
},
{
Header: t('yaml.YAML'),
hAlign: 'Center',
width: 85,
width: 75,
accessor: 'yaml',
disableFilters: true,
Cell: (cellData: CellData<KustomizationsResponse['items']>) => (
Expand All @@ -97,24 +102,27 @@ export default function FluxList() {
{
Header: t('FluxList.tableStatusHeader'),
accessor: 'status',
width: 85,
width: 125,
hAlign: 'Center',
Filter: ({ column }) => <StatusFilter column={column} />,
Cell: (cellData: CellData<FluxRow['isReady']>) =>
cellData.cell.row.original?.isReady != null ? (
<ResourceStatusCell
value={cellData.cell.row.original?.isReady}
positiveText={t('common.ready')}
negativeText={t('common.error')}
isOk={cellData.cell.row.original?.isReady}
transitionTime={
cellData.cell.row.original?.statusUpdateTime ? cellData.cell.row.original?.statusUpdateTime : ''
}
message={cellData.cell.row.original?.readyMessage}
/>
) : null,
},

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

const gitReposRows: FluxRow[] =
gitReposData?.items?.map((item) => {
const readyObject = item.status?.conditions?.find((x) => x.type === 'Ready');
return {
name: item.metadata.name,
isReady: item?.status?.conditions?.find((x) => x.type === 'Ready')?.status === 'True',
statusUpdateTime: item.status?.conditions?.find((x) => x.type === 'Ready')?.lastTransitionTime,
isReady: readyObject?.status === 'True',
statusUpdateTime: readyObject?.lastTransitionTime,
revision: shortenCommitHash(item.status.artifact?.revision ?? '-'),
created: timeAgo.format(new Date(item.metadata.creationTimestamp)),
item: item,
readyMessage: readyObject?.message ?? readyObject?.reason ?? '',
};
}) ?? [];

const kustomizationsRows: FluxRow[] =
kustmizationData?.items?.map((item) => {
const readyObject = item.status?.conditions?.find((x) => x.type === 'Ready');
return {
name: item.metadata.name,
isReady: item.status?.conditions?.find((x) => x.type === 'Ready')?.status === 'True',
statusUpdateTime: item.status?.conditions?.find((x) => x.type === 'Ready')?.lastTransitionTime,
isReady: readyObject?.status === 'True',
statusUpdateTime: readyObject?.lastTransitionTime,
created: timeAgo.format(new Date(item.metadata.creationTimestamp)),
item: item,
readyMessage: readyObject?.message ?? readyObject?.reason ?? '',
};
}) ?? [];

Expand Down
128 changes: 67 additions & 61 deletions src/components/ControlPlane/MCPHealthPopoverButton.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,62 @@
import { AnalyticalTable, Icon, Popover, FlexBox, FlexBoxJustifyContent, Button } from '@ui5/webcomponents-react';
import {
AnalyticalTable,
Icon,
Popover,
FlexBox,
FlexBoxJustifyContent,
Button,
PopoverDomRef,
ButtonDomRef,
} from '@ui5/webcomponents-react';
import { AnalyticalTableColumnDefinition } from '@ui5/webcomponents-react/wrappers';
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
import '@ui5/webcomponents-icons/dist/copy';
import { JSX, useRef, useState } from 'react';
import { ControlPlaneStatusType, ReadyStatus } from '../../lib/api/types/crate/controlPlanes';
import { JSX, useRef, useState, ReactNode } from 'react';
import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js';
import {
ControlPlaneStatusType,
ReadyStatus,
ControlPlaneStatusCondition,
} from '../../lib/api/types/crate/controlPlanes';
import ReactTimeAgo from 'react-time-ago';
import { AnimatedHoverTextButton } from '../Helper/AnimatedHoverTextButton.tsx';
import { useTranslation } from 'react-i18next';
import { useLink } from '../../lib/shared/useLink.ts';
import TooltipCell from '../Shared/TooltipCell.tsx';
export default function MCPHealthPopoverButton({
mcpStatus,
projectName,
workspaceName,
mcpName,
}: {
import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base';

interface CellData<T> {
cell: {
value: ReactNode;
};
row: {
original: T;
[key: string]: unknown;
};
[key: string]: unknown;
}

type MCPHealthPopoverButtonProps = {
mcpStatus: ControlPlaneStatusType | undefined;
projectName: string;
workspaceName: string;
mcpName: string;
}) {
const popoverRef = useRef(null);
};

const MCPHealthPopoverButton = ({ mcpStatus, projectName, workspaceName, mcpName }: MCPHealthPopoverButtonProps) => {
const popoverRef = useRef<PopoverDomRef>(null);
const [open, setOpen] = useState(false);
const { githubIssuesSupportTicket } = useLink();

const { t } = useTranslation();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleOpenerClick = (e: any) => {
const handleOpenerClick = (event: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
if (popoverRef.current) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ref = popoverRef.current as any;
ref.opener = e.target;
(popoverRef.current as unknown as { opener: EventTarget | null }).opener = event.target;
setOpen((prev) => !prev);
}
};

const getTicketTitle = () => {
const getTicketTitle = (): string => {
switch (mcpStatus?.status) {
case ReadyStatus.Ready:
return t('MCPHealthPopoverButton.supportTicketTitleReady');
Expand All @@ -49,13 +69,13 @@ export default function MCPHealthPopoverButton({
}
};

const constructGithubIssuesLink = () => {
const constructGithubIssuesLink = (): string => {
const clusterDetails = `${projectName}/${workspaceName}/${mcpName}`;

const statusDetails = mcpStatus?.conditions
? `${t('MCPHealthPopoverButton.statusDetailsLabel')}: ${mcpStatus.status}\n\n${t('MCPHealthPopoverButton.detailsLabel')}\n` +
mcpStatus?.conditions
.map((condition) => {
mcpStatus.conditions
.map((condition: ControlPlaneStatusCondition) => {
let text = `- ${condition.type}: ${condition.status}\n`;
if (condition.reason) text += ` - ${t('MCPHealthPopoverButton.reasonHeader')}: ${condition.reason}\n`;
if (condition.message) text += ` - ${t('MCPHealthPopoverButton.messageHeader')}: ${condition.message}\n`;
Expand All @@ -79,8 +99,7 @@ export default function MCPHealthPopoverButton({
Header: t('MCPHealthPopoverButton.statusHeader'),
accessor: 'status',
width: 50,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Cell: (instance: any) => {
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
const isReady = instance.cell.value === 'True';
return (
<Icon
Expand All @@ -94,38 +113,33 @@ export default function MCPHealthPopoverButton({
Header: t('MCPHealthPopoverButton.typeHeader'),
accessor: 'type',
width: 150,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Cell: (instance: any) => {
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
return <TooltipCell>{instance.cell.value}</TooltipCell>;
},
},
{
Header: t('MCPHealthPopoverButton.messageHeader'),
accessor: 'message',
width: 350,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Cell: (instance: any) => {
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
return <TooltipCell>{instance.cell.value}</TooltipCell>;
},
},
{
Header: t('MCPHealthPopoverButton.reasonHeader'),
accessor: 'reason',
width: 100,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Cell: (instance: any) => {
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
return <TooltipCell>{instance.cell.value}</TooltipCell>;
},
},
{
Header: t('MCPHealthPopoverButton.transitionHeader'),
accessor: 'lastTransitionTime',
width: 110,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Cell: (instance: any) => {
width: 125,
Cell: (instance: CellData<ControlPlaneStatusCondition>) => {
const rawDate = instance.cell.value;
const date = new Date(rawDate);

const date = new Date(rawDate as string);
return (
<TooltipCell>
<ReactTimeAgo date={date} />
Expand All @@ -143,58 +157,50 @@ export default function MCPHealthPopoverButton({
onClick={handleOpenerClick}
/>
<Popover ref={popoverRef} open={open} placement={PopoverPlacement.Bottom}>
{
<StatusTable
status={mcpStatus}
tableColumns={statusTableColumns}
githubIssuesLink={constructGithubIssuesLink()}
/>
}
<StatusTable
status={mcpStatus}
tableColumns={statusTableColumns}
githubIssuesLink={constructGithubIssuesLink()}
/>
</Popover>
</div>
);
}
};

function StatusTable({
status,
tableColumns,
githubIssuesLink,
}: {
export default MCPHealthPopoverButton;

type StatusTableProps = {
status: ControlPlaneStatusType | undefined;
tableColumns: AnalyticalTableColumnDefinition[];
githubIssuesLink: string;
}) {
};

const StatusTable = ({ status, tableColumns, githubIssuesLink }: StatusTableProps) => {
const { t } = useTranslation();

const sortedConditions = status?.conditions ? [...status.conditions].sort((a, b) => (a.type < b.type ? -1 : 1)) : [];

return (
<div style={{ width: 770 }}>
<AnalyticalTable
scaleWidthMode="Default"
columns={tableColumns}
data={
status?.conditions?.sort((a, b) => {
return a.type < b.type ? -1 : 1;
}) ?? []
}
/>
<AnalyticalTable scaleWidthMode="Default" columns={tableColumns} data={sortedConditions} />
<FlexBox justifyContent={FlexBoxJustifyContent.End} style={{ marginTop: '0.5rem' }}>
<a href={githubIssuesLink} target="_blank" rel="noreferrer">
<Button>{t('MCPHealthPopoverButton.createSupportTicketButton')}</Button>
</a>
</FlexBox>
</div>
);
}
};

function getIconForOverallStatus(status: ReadyStatus | undefined): JSX.Element {
const getIconForOverallStatus = (status: ReadyStatus | undefined): JSX.Element => {
switch (status) {
case ReadyStatus.Ready:
return <Icon style={{ color: 'green' }} name="sap-icon://sys-enter" />;
case ReadyStatus.NotReady:
return <Icon style={{ color: 'red' }} name="sap-icon://pending" />;
case ReadyStatus.InDeletion:
return <Icon style={{ color: 'orange' }} name="sap-icon://delete" />;
case undefined:
default:
return <></>;
}
}
};
Loading
Loading