diff --git a/public/locales/en.json b/public/locales/en.json index 3bfe811d..1c24f1b6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -19,6 +19,14 @@ "tableStatusHeader": "Status", "tableCreatedHeader": "Created" }, + "ManagedResources": { + "headerManagedResources": "Resources", + "tableHeaderKind": "Kind", + "tableHeaderName": "Name", + "tableHeaderCreated": "Created", + "tableHeaderSynced": "Synced", + "tableHeaderReady": "Ready" + }, "ControlPlaneListToolbar": { "buttonText": "Workspace" }, diff --git a/src/components/ControlPlane/ManagedResources.tsx b/src/components/ControlPlane/ManagedResources.tsx new file mode 100644 index 00000000..32fabc2e --- /dev/null +++ b/src/components/ControlPlane/ManagedResources.tsx @@ -0,0 +1,126 @@ +import { useTranslation } from 'react-i18next'; +import { AnalyticalTable, AnalyticalTableColumnDefinition, AnalyticalTableScaleWidthMode, Icon, Title } from '@ui5/webcomponents-react'; +import useResource from '../../lib/api/useApiResource'; +import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources'; +import { timeAgo } from '../../utils/i18n/timeAgo'; +import IllustratedError from '../Shared/IllustratedError'; +import '@ui5/webcomponents-icons/dist/sys-enter-2'; +import '@ui5/webcomponents-icons/dist/sys-cancel-2'; + +interface CellData { + cell: { + value: T | null; // null for grouping rows + row: { + original?: ResourceRow; // missing for grouping rows + } + }; +} + +type ResourceRow = { + kind: string + name: string + created: string; + synced: boolean; + syncedTransitionTime: string; + ready: boolean; + readyTransitionTime: string; +} + +export function ManagedResources() { + const { t } = useTranslation(); + + let {data: managedResources, error, isLoading} = useResource(ManagedResourcesRequest, { + refreshInterval: 30000 // Resources are quite expensive to fetch, so we refresh every 30 seconds + }); + + const columns: AnalyticalTableColumnDefinition[] = [ + { + Header: t('ManagedResources.tableHeaderKind'), + accessor: 'kind', + }, + { + Header: t('ManagedResources.tableHeaderName'), + accessor: 'name', + }, + { + Header: t('ManagedResources.tableHeaderCreated'), + accessor: 'created', + }, + { + Header: t('ManagedResources.tableHeaderSynced'), + accessor: 'synced', + Cell: (cellData: CellData) => cellData.cell.row.original?.synced != null ? : null + }, + { + Header: t('ManagedResources.tableHeaderReady'), + accessor: 'ready', + Cell: (cellData: CellData) => cellData.cell.row.original?.ready != null ? : null + }, + ]; + + const rows: ResourceRow[] = managedResources?.flatMap((managedResource) => + managedResource.items?.map((item) => { + const conditionSynced = item.status.conditions?.find((condition) => condition.type === 'Synced'); + const conditionReady = item.status.conditions?.find((condition) => condition.type === 'Ready'); + + return { + kind: item.kind, + name: item.metadata.name, + created: timeAgo.format(new Date(item.metadata.creationTimestamp)), + synced: conditionSynced?.status === "True", + syncedTransitionTime: conditionSynced?.lastTransitionTime ?? "", + ready: conditionReady?.status === "True", + readyTransitionTime: conditionReady?.lastTransitionTime ?? "", + } + }) + ) ?? []; + + + return ( + <> + {t('ManagedResources.headerManagedResources')} + + {error && } + + {!error && + + } + + ) +} + + +interface ResourceStatusCellProps { + value: boolean; + transitionTime: string; +} + +function ResourceStatusCell({ value, transitionTime }: ResourceStatusCellProps) { + return +} diff --git a/src/components/ControlPlane/ProvidersList.tsx b/src/components/ControlPlane/ProvidersList.tsx index 98067851..aea26f4d 100644 --- a/src/components/ControlPlane/ProvidersList.tsx +++ b/src/components/ControlPlane/ProvidersList.tsx @@ -5,6 +5,7 @@ import IllustratedError from "../Shared/IllustratedError.tsx"; import useResource from "../../lib/api/useApiResource"; import { ListProviders } from "../../lib/api/types/crossplane/listProviders"; import { useTranslation } from 'react-i18next'; +import { ManagedResources } from './ManagedResources'; export default function ProvidersList() { const { data, error, isLoading } = useResource(ListProviders); @@ -45,11 +46,8 @@ export default function ProvidersList() { columns={columns} data={[]} /> - Resources - + + ); } diff --git a/src/lib/api/types/crossplane/listManagedResources.ts b/src/lib/api/types/crossplane/listManagedResources.ts new file mode 100644 index 00000000..c0deb68a --- /dev/null +++ b/src/lib/api/types/crossplane/listManagedResources.ts @@ -0,0 +1,22 @@ +import { Resource } from "../resource"; + +export type ManagedResourcesResponse = [{ + items: [{ + kind: string; + metadata: { + name: string; + creationTimestamp: string; + }; + status: { + conditions: [{ + type: "Ready" | "Synced" | unknown; + status: "True" | "False"; + lastTransitionTime: string; + }] + }; + }]; +}]; + +export const ManagedResourcesRequest: Resource = { + path: "/managed", +}; diff --git a/src/main.tsx b/src/main.tsx index f3b58e9d..4e31991d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,8 +5,6 @@ import App from "./App"; import { ThemeProvider } from "@ui5/webcomponents-react"; import { AuthProvider } from "react-oidc-context"; import { LoadCrateKubeConfig } from "./lib/oidc/crate.ts"; -import TimeAgo from "javascript-time-ago"; -import en from "javascript-time-ago/locale/en"; import { SWRConfig } from "swr"; import { ToastProvider } from "./context/ToastContext.tsx"; import { CopyButtonProvider } from './context/CopyButtonContext.tsx'; @@ -14,6 +12,7 @@ import { FrontendConfigProvider, LoadFrontendConfig } from "./context/FrontendCo import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes import { DarkModeSystemSwitcher } from "./components/Core/DarkModeSystemSwitcher.tsx"; import ".././i18n"; +import "./utils/i18n/timeAgo"; import { useTranslation } from "react-i18next"; (async () => { @@ -21,7 +20,6 @@ import { useTranslation } from "react-i18next"; const frontendConfig = await LoadFrontendConfig(); const authconfig = await LoadCrateKubeConfig(frontendConfig.backendUrl); - TimeAgo.addDefaultLocale(en); ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/src/utils/i18n/timeAgo.ts b/src/utils/i18n/timeAgo.ts new file mode 100644 index 00000000..cdcba428 --- /dev/null +++ b/src/utils/i18n/timeAgo.ts @@ -0,0 +1,5 @@ +import en from 'javascript-time-ago/locale/en'; +import TimeAgo from 'javascript-time-ago'; + +TimeAgo.addDefaultLocale(en); +export const timeAgo = new TimeAgo('en-US'); \ No newline at end of file