diff --git a/public/locales/en.json b/public/locales/en.json index 364b6741..6cf67f34 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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", diff --git a/src/components/ControlPlane/FluxList.tsx b/src/components/ControlPlane/FluxList.tsx index a8adfe4f..ea9a9287 100644 --- a/src/components/ControlPlane/FluxList.tsx +++ b/src/components/ControlPlane/FluxList.tsx @@ -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 @@ -36,6 +37,7 @@ export default function FluxList() { isReady: boolean; statusUpdateTime?: string; item: unknown; + readyMessage: string; }; const gitReposColumns: AnalyticalTableColumnDefinition[] = useMemo( @@ -56,23 +58,26 @@ export default function FluxList() { { Header: t('FluxList.tableStatusHeader'), accessor: 'status', - width: 85, + width: 125, hAlign: 'Center', Filter: ({ column }) => , - Cell: (cellData: CellData) => + Cell: (cellData: CellData) => cellData.cell.row.original?.isReady != null ? ( ) : null, }, { Header: t('yaml.YAML'), hAlign: 'Center', - width: 85, + width: 75, accessor: 'yaml', disableFilters: true, Cell: (cellData: CellData) => ( @@ -97,16 +102,19 @@ export default function FluxList() { { Header: t('FluxList.tableStatusHeader'), accessor: 'status', - width: 85, + width: 125, hAlign: 'Center', Filter: ({ column }) => , Cell: (cellData: CellData) => cellData.cell.row.original?.isReady != null ? ( ) : null, }, @@ -114,7 +122,7 @@ export default function FluxList() { { Header: t('yaml.YAML'), hAlign: 'Center', - width: 85, + width: 75, accessor: 'yaml', disableFilters: true, Cell: (cellData: CellData) => , @@ -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 ?? '', }; }) ?? []; diff --git a/src/components/ControlPlane/MCPHealthPopoverButton.tsx b/src/components/ControlPlane/MCPHealthPopoverButton.tsx index 230e9022..8ed4d7e7 100644 --- a/src/components/ControlPlane/MCPHealthPopoverButton.tsx +++ b/src/components/ControlPlane/MCPHealthPopoverButton.tsx @@ -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 { + 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(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) => { 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'); @@ -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`; @@ -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) => { const isReady = instance.cell.value === 'True'; return ( { + Cell: (instance: CellData) => { return {instance.cell.value}; }, }, @@ -103,8 +121,7 @@ export default function MCPHealthPopoverButton({ Header: t('MCPHealthPopoverButton.messageHeader'), accessor: 'message', width: 350, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Cell: (instance: any) => { + Cell: (instance: CellData) => { return {instance.cell.value}; }, }, @@ -112,20 +129,17 @@ export default function MCPHealthPopoverButton({ Header: t('MCPHealthPopoverButton.reasonHeader'), accessor: 'reason', width: 100, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Cell: (instance: any) => { + Cell: (instance: CellData) => { return {instance.cell.value}; }, }, { 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) => { const rawDate = instance.cell.value; - const date = new Date(rawDate); - + const date = new Date(rawDate as string); return ( @@ -143,40 +157,32 @@ export default function MCPHealthPopoverButton({ onClick={handleOpenerClick} /> - { - - } + ); -} +}; -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 ( ); -} +}; -function getIconForOverallStatus(status: ReadyStatus | undefined): JSX.Element { +const getIconForOverallStatus = (status: ReadyStatus | undefined): JSX.Element => { switch (status) { case ReadyStatus.Ready: return ; @@ -194,7 +200,7 @@ function getIconForOverallStatus(status: ReadyStatus | undefined): JSX.Element { return ; case ReadyStatus.InDeletion: return ; - case undefined: + default: return <>; } -} +}; diff --git a/src/components/ControlPlane/ManagedResources.tsx b/src/components/ControlPlane/ManagedResources.tsx index 851b488d..2763cc58 100644 --- a/src/components/ControlPlane/ManagedResources.tsx +++ b/src/components/ControlPlane/ManagedResources.tsx @@ -12,10 +12,11 @@ import IllustratedError from '../Shared/IllustratedError'; import '@ui5/webcomponents-icons/dist/sys-enter-2'; import '@ui5/webcomponents-icons/dist/sys-cancel-2'; import { resourcesInterval } from '../../lib/shared/constants'; -import { ResourceStatusCell } from '../Shared/ResourceStatusCell'; + import { YamlViewButton } from '../Yaml/YamlViewButton.tsx'; import { useMemo } from 'react'; import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx'; +import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx'; interface CellData { cell: { @@ -35,6 +36,8 @@ type ResourceRow = { ready: boolean; readyTransitionTime: string; item: unknown; + conditionReadyMessage: string; + conditionSyncedMessage: string; }; export function ManagedResources() { @@ -66,13 +69,16 @@ export function ManagedResources() { Header: t('ManagedResources.tableHeaderSynced'), accessor: 'synced', hAlign: 'Center', - width: 85, + width: 125, Filter: ({ column }) => , Cell: (cellData: CellData) => cellData.cell.row.original?.synced != null ? ( ) : null, }, @@ -80,20 +86,23 @@ export function ManagedResources() { Header: t('ManagedResources.tableHeaderReady'), accessor: 'ready', hAlign: 'Center', - width: 85, + width: 125, Filter: ({ column }) => , Cell: (cellData: CellData) => cellData.cell.row.original?.ready != null ? ( ) : null, }, { Header: t('yaml.YAML'), hAlign: 'Center', - width: 85, + width: 75, accessor: 'yaml', disableFilters: true, Cell: (cellData: CellData) => @@ -122,6 +131,8 @@ export function ManagedResources() { ready: conditionReady?.status === 'True', readyTransitionTime: conditionReady?.lastTransitionTime ?? '', item: item, + conditionSyncedMessage: conditionSynced?.message ?? conditionSynced?.reason ?? '', + conditionReadyMessage: conditionReady?.message ?? conditionReady?.reason ?? '', }; }), ) ?? []; diff --git a/src/components/ControlPlane/Providers.tsx b/src/components/ControlPlane/Providers.tsx index 353fb017..b758d619 100644 --- a/src/components/ControlPlane/Providers.tsx +++ b/src/components/ControlPlane/Providers.tsx @@ -12,12 +12,13 @@ import IllustratedError from '../Shared/IllustratedError'; import { ProvidersListRequest } from '../../lib/api/types/crossplane/listProviders'; import { resourcesInterval } from '../../lib/shared/constants'; import { timeAgo } from '../../utils/i18n/timeAgo'; -import { ResourceStatusCell } from '../Shared/ResourceStatusCell'; + import { YamlViewButton } from '../Yaml/YamlViewButton.tsx'; import '@ui5/webcomponents-icons/dist/sys-enter-2'; import '@ui5/webcomponents-icons/dist/sys-cancel-2'; import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx'; +import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx'; interface CellData { cell: { @@ -33,8 +34,10 @@ type ProvidersRow = { version: string; healthy: string; healthyTransitionTime: string; + healthyMessage: string; installed: string; installedTransitionTime: string; + installedMessage: string; created: string; item: unknown; }; @@ -68,14 +71,17 @@ export function Providers() { Header: t('Providers.tableHeaderInstalled'), accessor: 'installed', hAlign: 'Center', - width: 85, + width: 125, Filter: ({ column }) => , filter: 'equals', Cell: (cellData: CellData) => cellData.cell.row.original?.installed != null ? ( ) : null, }, @@ -83,21 +89,24 @@ export function Providers() { Header: t('Providers.tableHeaderHealthy'), accessor: 'healthy', hAlign: 'Center', - width: 85, + width: 125, Filter: ({ column }) => , filter: 'equals', Cell: (cellData: CellData) => cellData.cell.row.original?.installed != null ? ( ) : null, }, { Header: t('yaml.YAML'), hAlign: 'Center', - width: 85, + width: 75, accessor: 'yaml', disableFilters: true, Cell: (cellData: CellData) => ( @@ -121,6 +130,8 @@ export function Providers() { healthyTransitionTime: healthy?.lastTransitionTime ?? '', version: item.spec.package.match(/\d+(\.\d+)+/g)?.toString() ?? '', item: item, + healthyMessage: healthy?.message ?? healthy?.reason ?? '', + installedMessage: installed?.message ?? installed?.reason ?? '', }; }) ?? []; diff --git a/src/components/ControlPlane/ProvidersConfig.tsx b/src/components/ControlPlane/ProvidersConfig.tsx index b6891320..a4756116 100644 --- a/src/components/ControlPlane/ProvidersConfig.tsx +++ b/src/components/ControlPlane/ProvidersConfig.tsx @@ -74,7 +74,7 @@ export function ProvidersConfig() { { Header: t('yaml.YAML'), hAlign: 'Center', - width: 85, + width: 75, accessor: 'yaml', disableFilters: true, Cell: (cellData: CellData) => diff --git a/src/components/Helper/AnimatedHoverTextButton.tsx b/src/components/Helper/AnimatedHoverTextButton.tsx index 8bd7b310..ed768bae 100644 --- a/src/components/Helper/AnimatedHoverTextButton.tsx +++ b/src/components/Helper/AnimatedHoverTextButton.tsx @@ -1,21 +1,21 @@ -import { Button, FlexBox, FlexBoxAlignItems, Text } from '@ui5/webcomponents-react'; +import { Button, ButtonDomRef, FlexBox, FlexBoxAlignItems, Text } from '@ui5/webcomponents-react'; import '@ui5/webcomponents-icons/dist/copy'; -import { JSX, useState } from 'react'; +import { JSX, RefObject, useState } from 'react'; import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; +import type { Ui5CustomEvent } from '@ui5/webcomponents-react-base'; +import type { ButtonClickEventDetail } from '@ui5/webcomponents/dist/Button.js'; -export function AnimatedHoverTextButton({ - text, - icon, - onClick, -}: { +type HoverTextButtonProps = { text: string; icon: JSX.Element; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onClick: (e: any) => void; -}) { + ref?: RefObject; + onClick: (event: Ui5CustomEvent) => void; +}; +export const AnimatedHoverTextButton = ({ text, icon, onClick, ref }: HoverTextButtonProps) => { const [hover, setHover] = useState(false); return ( ); -} +}; diff --git a/src/components/Projects/ProjectsList.tsx b/src/components/Projects/ProjectsList.tsx index 454771ca..0b43eb1e 100644 --- a/src/components/Projects/ProjectsList.tsx +++ b/src/components/Projects/ProjectsList.tsx @@ -1,4 +1,4 @@ -import { AnalyticalTable, AnalyticalTableColumnDefinition, Link } from '@ui5/webcomponents-react'; +import { AnalyticalTable, Link } from '@ui5/webcomponents-react'; import { CopyButton } from '../Shared/CopyButton.tsx'; import useLuigiNavigate from '../Shared/useLuigiNavigate.tsx'; @@ -13,12 +13,26 @@ import { YamlViewButtonWithLoader } from '../Yaml/YamlViewButtonWithLoader.tsx'; import { useMemo } from 'react'; import { ProjectsListItemMenu } from './ProjectsListItemMenu.tsx'; +type ProjectListRow = { + projectName: string; + nameSpace: string; +}; + +type ProjectListCellInstance = { + cell: { + value: string; + row: { + original: T; + }; + }; +}; + export default function ProjectsList() { const navigate = useLuigiNavigate(); const { data, error } = useApiResource(ListProjectNames, { refreshInterval: 3000, }); - const stabilizedData = useMemo( + const stabilizedData = useMemo( () => data?.map((projectName) => { return { @@ -28,13 +42,12 @@ export default function ProjectsList() { }) ?? [], [data], ); - const stabilizedColumns: AnalyticalTableColumnDefinition[] = useMemo( + const stabilizedColumns = useMemo( () => [ { Header: t('ProjectsListView.title'), accessor: 'projectName', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Cell: (instance: any) => ( + Cell: (instance: ProjectListCellInstance) => ( ( + Cell: (instance: ProjectListCellInstance) => (
@@ -75,16 +86,15 @@ export default function ProjectsList() { { Header: t('yaml.YAML'), accessor: 'yaml', - width: 85, + width: 75, disableFilters: true, - hAlign: 'Center', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Cell: (instance: any) => ( + hAlign: 'Center' as const, + Cell: (instance: ProjectListCellInstance) => (
@@ -100,9 +110,8 @@ export default function ProjectsList() { accessor: 'options', width: 60, disableFilters: true, - hAlign: 'Center', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Cell: (instance: any) => ( + hAlign: 'Center' as const, + Cell: (instance: ProjectListCellInstance) => (
{ + const btnRef = useRef(null); + const [popoverIsOpen, setPopoverIsOpen] = useState(false); -export function ResourceStatusCell({ value, transitionTime }: StatusCellProps) { + const handleClose = () => { + setPopoverIsOpen(false); + }; + const handleOpen = () => { + setPopoverIsOpen(true); + }; return ( - + + + } + text={isOk ? positiveText : negativeText} + onClick={handleOpen} + /> + + {message} + + + + + {timeAgo.format(new Date(transitionTime))} + + + + ); -} +}; diff --git a/src/components/Yaml/YamlViewButton.tsx b/src/components/Yaml/YamlViewButton.tsx index 5e2add13..2e6b7d78 100644 --- a/src/components/Yaml/YamlViewButton.tsx +++ b/src/components/Yaml/YamlViewButton.tsx @@ -16,6 +16,7 @@ export const YamlViewButton: FC = ({ resourceObject }) => { const [isOpen, setIsOpen] = useState(false); const { t } = useTranslation(); const resource = resourceObject as Resource; + const yamlString = useMemo(() => { return stringify(removeManagedFieldsProperty(resource)); }, [resource]); diff --git a/src/lib/api/types/crossplane/listManagedResources.ts b/src/lib/api/types/crossplane/listManagedResources.ts index f3e1e1ec..2ebd01e9 100644 --- a/src/lib/api/types/crossplane/listManagedResources.ts +++ b/src/lib/api/types/crossplane/listManagedResources.ts @@ -15,6 +15,8 @@ export type ManagedResourcesResponse = [ type: 'Ready' | 'Synced' | unknown; status: 'True' | 'False'; lastTransitionTime: string; + message?: string; + reason?: string; }, ]; }; diff --git a/src/lib/api/types/crossplane/listProviders.ts b/src/lib/api/types/crossplane/listProviders.ts index 6d4702ab..c20a807e 100644 --- a/src/lib/api/types/crossplane/listProviders.ts +++ b/src/lib/api/types/crossplane/listProviders.ts @@ -17,6 +17,8 @@ export type ProvidersListResponse = { type: 'Healthy' | 'Installed' | unknown; status: 'True' | 'False'; lastTransitionTime: string; + message?: string; + reason?: string; }, ]; }; diff --git a/src/lib/api/types/flux/listGitRepo.ts b/src/lib/api/types/flux/listGitRepo.ts index 882b6ac0..b9fc6676 100644 --- a/src/lib/api/types/flux/listGitRepo.ts +++ b/src/lib/api/types/flux/listGitRepo.ts @@ -20,6 +20,8 @@ export type GitReposResponse = { status: string; type: string; lastTransitionTime: string; + message?: string; + reason?: string; }, ]; }; diff --git a/src/lib/api/types/flux/listKustomization.ts b/src/lib/api/types/flux/listKustomization.ts index 0efff18e..2f003594 100644 --- a/src/lib/api/types/flux/listKustomization.ts +++ b/src/lib/api/types/flux/listKustomization.ts @@ -17,9 +17,11 @@ export type KustomizationsResponse = { }; conditions: [ { + message: string; status: string; type: string; lastTransitionTime: string; + reason: string; }, ]; };