Skip to content

Commit 870ab14

Browse files
feat: add overview tab to trust root page (#20)
Signed-off-by: Carlos Feria <[email protected]>
1 parent 9301269 commit 870ab14

File tree

3 files changed

+154
-19
lines changed

3 files changed

+154
-19
lines changed

client/src/app/pages/TrustRoot/TrustRoot.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,27 @@ import { Content, PageSection, Tab, TabContent, Tabs, TabTitleText } from "@patt
44
import { ExternalLinkAltIcon } from "@patternfly/react-icons";
55

66
import { LoadingWrapper } from "@app/components/LoadingWrapper";
7-
import { useFetchTrustRootMetadataInfo } from "@app/queries/trust";
7+
import { useFetchTrustRootMetadataInfo, useFetchTrustTargetCertificates } from "@app/queries/trust";
88

9+
import { CertificatesTable } from "./components/Certificates";
10+
import { Overview } from "./components/Overview";
911
import { RootDetails } from "./components/RootDetails";
10-
import { Certificates } from "./components/Certificates";
1112

1213
export const TrustRoots: React.FC = () => {
13-
const { rootMetadataList, isFetching, fetchError } = useFetchTrustRootMetadataInfo();
14+
const {
15+
rootMetadataList,
16+
isFetching: isFetchingRootMetadata,
17+
fetchError: fetchErrorRootMetadata,
18+
} = useFetchTrustRootMetadataInfo();
19+
20+
const {
21+
certificates,
22+
isFetching: isFetchingCertificates,
23+
fetchError: fetchErrorCertificates,
24+
} = useFetchTrustTargetCertificates();
1425

1526
// Tab refs
27+
const overviewTabRef = React.createRef<HTMLElement>();
1628
const certificatesTabRef = React.createRef<HTMLElement>();
1729
const rootDetailsTabRef = React.createRef<HTMLElement>();
1830

@@ -45,24 +57,41 @@ export const TrustRoots: React.FC = () => {
4557
>
4658
<Tab
4759
eventKey={0}
60+
title={<TabTitleText>Overview</TabTitleText>}
61+
tabContentId="overviewTabSection"
62+
tabContentRef={overviewTabRef}
63+
/>
64+
<Tab
65+
eventKey={1}
4866
title={<TabTitleText>Certificates</TabTitleText>}
4967
tabContentId="certificatesTabSection"
5068
tabContentRef={certificatesTabRef}
5169
/>
5270
<Tab
53-
eventKey={1}
71+
eventKey={2}
5472
title={<TabTitleText>Root details</TabTitleText>}
5573
tabContentId="rootDetailsTabSection"
5674
tabContentRef={rootDetailsTabRef}
5775
/>
5876
</Tabs>
5977
</PageSection>
6078
<PageSection>
61-
<TabContent eventKey={0} id="certificatesTabSection" ref={certificatesTabRef} aria-label="Certificates">
62-
<Certificates />
79+
<TabContent eventKey={0} id="overviewTabSection" ref={overviewTabRef} aria-label="Overview">
80+
<Overview
81+
certificates={certificates?.data ?? []}
82+
isFetching={isFetchingCertificates}
83+
fetchError={fetchErrorCertificates}
84+
/>
85+
</TabContent>
86+
<TabContent eventKey={1} id="certificatesTabSection" ref={certificatesTabRef} aria-label="Certificates" hidden>
87+
<CertificatesTable
88+
certificates={certificates?.data ?? []}
89+
isFetching={isFetchingCertificates}
90+
fetchError={fetchErrorCertificates}
91+
/>
6392
</TabContent>
64-
<TabContent eventKey={1} id="rootDetailsTabSection" ref={rootDetailsTabRef} aria-label="Root details" hidden>
65-
<LoadingWrapper isFetching={isFetching} fetchError={fetchError}>
93+
<TabContent eventKey={2} id="rootDetailsTabSection" ref={rootDetailsTabRef} aria-label="Root details" hidden>
94+
<LoadingWrapper isFetching={isFetchingRootMetadata} fetchError={fetchErrorRootMetadata}>
6695
{rootMetadataList && <RootDetails rootMetadataList={rootMetadataList} />}
6796
</LoadingWrapper>
6897
</TabContent>

client/src/app/pages/TrustRoot/components/Certificates.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,15 @@ import { SimplePagination } from "@app/components/SimplePagination";
2626
import { ConditionalTableBody } from "@app/components/TableControls/ConditionalTableBody";
2727
import { useWithUiId } from "@app/hooks/query-utils";
2828
import { usePFToolbarTable } from "@app/hooks/usePFToolbarTable";
29-
import { useFetchTrustTargetCertificates } from "@app/queries/trust";
3029
import { formatDate, stringMatcher } from "@app/utils/utils";
3130

32-
export const Certificates: React.FC = () => {
33-
const { certificates, isFetching, fetchError } = useFetchTrustTargetCertificates();
34-
35-
return <CerticatesTable certificates={certificates?.data ?? []} isFetching={isFetching} fetchError={fetchError} />;
36-
};
37-
38-
interface ICerticatesTableProps {
31+
interface ICertificatesTableProps {
3932
certificates: CertificateInfo[];
4033
isFetching: boolean;
4134
fetchError: AxiosError<_Error> | null;
4235
}
4336

44-
export const CerticatesTable: React.FC<ICerticatesTableProps> = ({ certificates, isFetching, fetchError }) => {
37+
export const CertificatesTable: React.FC<ICertificatesTableProps> = ({ certificates, isFetching, fetchError }) => {
4538
const items = useWithUiId(certificates, (item) => `${item.type}-${item.issuer}-${item.subject}-${item.target}`);
4639

4740
const tableState = usePFToolbarTable({
@@ -161,7 +154,6 @@ export const CerticatesTable: React.FC<ICerticatesTableProps> = ({ certificates,
161154
<Th screenReaderText="Row expansion" />
162155
<Th>Issuer</Th>
163156
<Th>Subject</Th>
164-
<Th>Issuer</Th>
165157
<Th>Target</Th>
166158
<Th>Type</Th>
167159
<Th>Status</Th>
@@ -171,7 +163,7 @@ export const CerticatesTable: React.FC<ICerticatesTableProps> = ({ certificates,
171163
</Thead>
172164
<ConditionalTableBody
173165
isNoData={currentPageItems.length === 0}
174-
numRenderedColumns={6}
166+
numRenderedColumns={8}
175167
isLoading={isFetching}
176168
isError={!!fetchError}
177169
>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from "react";
2+
3+
import type { AxiosError } from "axios";
4+
import dayjs from "dayjs";
5+
6+
import { ChartDonut, ChartThemeColor } from "@patternfly/react-charts/victory";
7+
import {
8+
Card,
9+
CardBody,
10+
CardTitle,
11+
Divider,
12+
EmptyState,
13+
EmptyStateBody,
14+
EmptyStateVariant,
15+
Flex,
16+
FlexItem,
17+
List,
18+
ListItem,
19+
} from "@patternfly/react-core";
20+
import { InfoAltIcon } from "@patternfly/react-icons";
21+
22+
import type { _Error, CertificateInfo } from "@app/client";
23+
import { LoadingWrapper } from "@app/components/LoadingWrapper";
24+
25+
interface IOverviewProps {
26+
certificates: CertificateInfo[];
27+
isFetching: boolean;
28+
fetchError: AxiosError<_Error> | null;
29+
}
30+
31+
export const Overview: React.FC<IOverviewProps> = ({ certificates, isFetching, fetchError }) => {
32+
const chartDonutData = React.useMemo(() => {
33+
return certificates.reduce(
34+
(prev, current) => {
35+
return {
36+
...prev,
37+
[current.status]: (prev[current.status] ?? 0) + 1,
38+
};
39+
},
40+
{} as Record<string, number>
41+
);
42+
}, [certificates]);
43+
44+
const totalCertificates = React.useMemo(() => {
45+
return Object.values(chartDonutData).reduce((prev, current) => prev + current, 0);
46+
}, [chartDonutData]);
47+
48+
const expiringSoonCertificates = React.useMemo(() => {
49+
return certificates.reduce((prev, current) => {
50+
const daysBeforeExpiration = dayjs().diff(dayjs(current.expiration), "days");
51+
if (daysBeforeExpiration >= 0 && daysBeforeExpiration <= 7) {
52+
prev.push(current);
53+
}
54+
return prev;
55+
}, [] as CertificateInfo[]);
56+
}, [certificates]);
57+
58+
return (
59+
<LoadingWrapper isFetching={isFetching} fetchError={fetchError}>
60+
<Flex direction={{ default: "column", md: "row" }}>
61+
<FlexItem alignSelf={{ default: "alignSelfStretch" }} flex={{ md: "flex_1" }}>
62+
<Card isPlain>
63+
<CardTitle>Certificate health</CardTitle>
64+
<CardBody>
65+
<div style={{ height: "230px", width: "350px" }}>
66+
<ChartDonut
67+
constrainToVisibleArea
68+
data={Object.entries(chartDonutData).map(([key, value]) => ({ x: key, y: value }))}
69+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
70+
labels={({ datum }) => `${datum.x}: ${datum.y}`}
71+
legendData={Object.entries(chartDonutData).map(([key, value]) => ({ name: `${key}: ${value}` }))}
72+
legendOrientation="vertical"
73+
legendPosition="right"
74+
name="Certificates"
75+
padding={{
76+
bottom: 20,
77+
left: 20,
78+
right: 140, // Adjusted to accommodate legend
79+
top: 20,
80+
}}
81+
subTitle="Certificates"
82+
title={totalCertificates.toString()}
83+
themeColor={ChartThemeColor.multiOrdered}
84+
width={350}
85+
/>
86+
</div>
87+
</CardBody>
88+
</Card>
89+
</FlexItem>
90+
<Divider orientation={{ md: "vertical" }} inset={{ default: "inset3xl" }} />
91+
<FlexItem alignSelf={{ default: "alignSelfStretch" }} flex={{ md: "flex_1" }}>
92+
<Card isPlain>
93+
<CardTitle>Expiring soon</CardTitle>
94+
<CardBody>
95+
{expiringSoonCertificates.length > 0 ? (
96+
<List>
97+
{expiringSoonCertificates.map((item) => (
98+
<ListItem key={`${item.type}-${item.issuer}-${item.subject}-${item.target}`}>
99+
{item.issuer}
100+
</ListItem>
101+
))}
102+
</List>
103+
) : (
104+
<EmptyState variant={EmptyStateVariant.xs} icon={InfoAltIcon}>
105+
<EmptyStateBody>There are no certificates expiring soon.</EmptyStateBody>
106+
</EmptyState>
107+
)}
108+
</CardBody>
109+
</Card>
110+
</FlexItem>
111+
</Flex>
112+
</LoadingWrapper>
113+
);
114+
};

0 commit comments

Comments
 (0)