Skip to content

Commit 11b6f9b

Browse files
Add Managed Resources (#2)
1 parent 3cfa84d commit 11b6f9b

File tree

6 files changed

+165
-8
lines changed

6 files changed

+165
-8
lines changed

public/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
"tableStatusHeader": "Status",
2020
"tableCreatedHeader": "Created"
2121
},
22+
"ManagedResources": {
23+
"headerManagedResources": "Resources",
24+
"tableHeaderKind": "Kind",
25+
"tableHeaderName": "Name",
26+
"tableHeaderCreated": "Created",
27+
"tableHeaderSynced": "Synced",
28+
"tableHeaderReady": "Ready"
29+
},
2230
"ControlPlaneListToolbar": {
2331
"buttonText": "Workspace"
2432
},
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useTranslation } from 'react-i18next';
2+
import { AnalyticalTable, AnalyticalTableColumnDefinition, AnalyticalTableScaleWidthMode, Icon, Title } from '@ui5/webcomponents-react';
3+
import useResource from '../../lib/api/useApiResource';
4+
import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources';
5+
import { timeAgo } from '../../utils/i18n/timeAgo';
6+
import IllustratedError from '../Shared/IllustratedError';
7+
import '@ui5/webcomponents-icons/dist/sys-enter-2';
8+
import '@ui5/webcomponents-icons/dist/sys-cancel-2';
9+
10+
interface CellData<T> {
11+
cell: {
12+
value: T | null; // null for grouping rows
13+
row: {
14+
original?: ResourceRow; // missing for grouping rows
15+
}
16+
};
17+
}
18+
19+
type ResourceRow = {
20+
kind: string
21+
name: string
22+
created: string;
23+
synced: boolean;
24+
syncedTransitionTime: string;
25+
ready: boolean;
26+
readyTransitionTime: string;
27+
}
28+
29+
export function ManagedResources() {
30+
const { t } = useTranslation();
31+
32+
let {data: managedResources, error, isLoading} = useResource(ManagedResourcesRequest, {
33+
refreshInterval: 30000 // Resources are quite expensive to fetch, so we refresh every 30 seconds
34+
});
35+
36+
const columns: AnalyticalTableColumnDefinition[] = [
37+
{
38+
Header: t('ManagedResources.tableHeaderKind'),
39+
accessor: 'kind',
40+
},
41+
{
42+
Header: t('ManagedResources.tableHeaderName'),
43+
accessor: 'name',
44+
},
45+
{
46+
Header: t('ManagedResources.tableHeaderCreated'),
47+
accessor: 'created',
48+
},
49+
{
50+
Header: t('ManagedResources.tableHeaderSynced'),
51+
accessor: 'synced',
52+
Cell: (cellData: CellData<ResourceRow['synced']>) => cellData.cell.row.original?.synced != null ? <ResourceStatusCell value={cellData.cell.row.original?.synced} transitionTime={cellData.cell.row.original?.syncedTransitionTime} /> : null
53+
},
54+
{
55+
Header: t('ManagedResources.tableHeaderReady'),
56+
accessor: 'ready',
57+
Cell: (cellData: CellData<ResourceRow['ready']>) => cellData.cell.row.original?.ready != null ? <ResourceStatusCell value={cellData.cell.row.original?.ready} transitionTime={cellData.cell.row.original?.readyTransitionTime} /> : null
58+
},
59+
];
60+
61+
const rows: ResourceRow[] = managedResources?.flatMap((managedResource) =>
62+
managedResource.items?.map((item) => {
63+
const conditionSynced = item.status.conditions?.find((condition) => condition.type === 'Synced');
64+
const conditionReady = item.status.conditions?.find((condition) => condition.type === 'Ready');
65+
66+
return {
67+
kind: item.kind,
68+
name: item.metadata.name,
69+
created: timeAgo.format(new Date(item.metadata.creationTimestamp)),
70+
synced: conditionSynced?.status === "True",
71+
syncedTransitionTime: conditionSynced?.lastTransitionTime ?? "",
72+
ready: conditionReady?.status === "True",
73+
readyTransitionTime: conditionReady?.lastTransitionTime ?? "",
74+
}
75+
})
76+
) ?? [];
77+
78+
79+
return (
80+
<>
81+
<Title level='H4'>{t('ManagedResources.headerManagedResources')}</Title>
82+
83+
{error && <IllustratedError error={error}/>}
84+
85+
{!error &&
86+
<AnalyticalTable
87+
columns={columns}
88+
data={rows}
89+
minRows={1}
90+
groupBy={['kind']}
91+
scaleWidthMode={AnalyticalTableScaleWidthMode.Smart}
92+
loading={isLoading}
93+
filterable
94+
// Prevent the table from resetting when the data changes
95+
retainColumnWidth
96+
reactTableOptions={{
97+
autoResetHiddenColumns: false,
98+
autoResetPage: false,
99+
autoResetExpanded: false,
100+
autoResetGroupBy: false,
101+
autoResetSelectedRows: false,
102+
autoResetSortBy: false,
103+
autoResetFilters: false,
104+
autoResetRowState: false,
105+
autoResetResize: false
106+
}}
107+
/>
108+
}
109+
</>
110+
)
111+
}
112+
113+
114+
interface ResourceStatusCellProps {
115+
value: boolean;
116+
transitionTime: string;
117+
}
118+
119+
function ResourceStatusCell({ value, transitionTime }: ResourceStatusCellProps) {
120+
return <Icon
121+
design={value ? 'Positive' : 'Negative'}
122+
name={value ? 'sys-enter-2' : 'sys-cancel-2'}
123+
showTooltip={true}
124+
accessibleName={timeAgo.format(new Date(transitionTime))}
125+
/>
126+
}

src/components/ControlPlane/ProvidersList.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import IllustratedError from "../Shared/IllustratedError.tsx";
55
import useResource from "../../lib/api/useApiResource";
66
import { ListProviders } from "../../lib/api/types/crossplane/listProviders";
77
import { useTranslation } from 'react-i18next';
8+
import { ManagedResources } from './ManagedResources';
89

910
export default function ProvidersList() {
1011
const { data, error, isLoading } = useResource(ListProviders);
@@ -45,11 +46,8 @@ export default function ProvidersList() {
4546
columns={columns}
4647
data={[]}
4748
/>
48-
<Title level="H4">Resources</Title>
49-
<ConfiguredAnalyticstable
50-
columns={columns}
51-
data={[]}
52-
/>
49+
50+
<ManagedResources />
5351
</>
5452
);
5553
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Resource } from "../resource";
2+
3+
export type ManagedResourcesResponse = [{
4+
items: [{
5+
kind: string;
6+
metadata: {
7+
name: string;
8+
creationTimestamp: string;
9+
};
10+
status: {
11+
conditions: [{
12+
type: "Ready" | "Synced" | unknown;
13+
status: "True" | "False";
14+
lastTransitionTime: string;
15+
}]
16+
};
17+
}];
18+
}];
19+
20+
export const ManagedResourcesRequest: Resource<ManagedResourcesResponse> = {
21+
path: "/managed",
22+
};

src/main.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,21 @@ import App from "./App";
55
import { ThemeProvider } from "@ui5/webcomponents-react";
66
import { AuthProvider } from "react-oidc-context";
77
import { LoadCrateKubeConfig } from "./lib/oidc/crate.ts";
8-
import TimeAgo from "javascript-time-ago";
9-
import en from "javascript-time-ago/locale/en";
108
import { SWRConfig } from "swr";
119
import { ToastProvider } from "./context/ToastContext.tsx";
1210
import { CopyButtonProvider } from './context/CopyButtonContext.tsx';
1311
import { FrontendConfigProvider, LoadFrontendConfig } from "./context/FrontendConfigContext.tsx";
1412
import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes
1513
import { DarkModeSystemSwitcher } from "./components/Core/DarkModeSystemSwitcher.tsx";
1614
import ".././i18n";
15+
import "./utils/i18n/timeAgo";
1716
import { useTranslation } from "react-i18next";
1817

1918
(async () => {
2019
try {
2120
const frontendConfig = await LoadFrontendConfig();
2221
const authconfig = await LoadCrateKubeConfig(frontendConfig.backendUrl);
2322

24-
TimeAgo.addDefaultLocale(en);
2523
ReactDOM.createRoot(document.getElementById("root")!).render(
2624
<React.StrictMode>
2725
<FrontendConfigProvider config={frontendConfig}>

src/utils/i18n/timeAgo.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import en from 'javascript-time-ago/locale/en';
2+
import TimeAgo from 'javascript-time-ago';
3+
4+
TimeAgo.addDefaultLocale(en);
5+
export const timeAgo = new TimeAgo('en-US');

0 commit comments

Comments
 (0)