Skip to content

Commit f1ad1a1

Browse files
feat: Add Trust Root Page / Root Details Page (#7)
Signed-off-by: Carlos Feria <[email protected]>
1 parent 287f7c8 commit f1ad1a1

File tree

14 files changed

+292
-8
lines changed

14 files changed

+292
-8
lines changed

client/src/app/Constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
import ENV from "./env";
22

33
export const isAuthRequired = ENV.AUTH_REQUIRED !== "false";
4+
5+
export const RENDER_DATE_FORMAT = "MMM DD, YYYY";

client/src/app/Routes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { ErrorFallback } from "./components/ErrorFallback";
88
const Overview = lazy(() => import("./pages/Overview"));
99
const Certificates = lazy(() => import("./pages/Certificates"));
1010
const TrustRoots = lazy(() => import("./pages/TrustRoots"));
11+
const TrustRoot = lazy(() => import("./pages/TrustRoot"));
1112

1213
export const AppRoutes = () => {
1314
const allRoutes = useRoutes([
1415
{ path: "/", element: <Overview /> },
1516
{ path: "/certificates", element: <Certificates /> },
1617
{ path: "/trust-roots", element: <TrustRoots /> },
18+
{ path: "/trust-root", element: <TrustRoot /> },
1719
]);
1820

1921
return (
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from "react";
2+
3+
import { Icon, type IconComponentProps } from "@patternfly/react-core";
4+
5+
import { CheckCircleIcon, MinusIcon, TimesCircleIcon, WarningTriangleIcon } from "@patternfly/react-icons";
6+
7+
interface ICertificateStatusIconProps {
8+
status: string;
9+
iconProps?: IconComponentProps;
10+
}
11+
12+
export const CertificateStatusIcon: React.FC<ICertificateStatusIconProps> = ({ status, iconProps }) => {
13+
const statusLowerCase = status.toLocaleLowerCase();
14+
15+
if (statusLowerCase === "active" || statusLowerCase === "valid") {
16+
return (
17+
<Icon status="success" {...iconProps}>
18+
<CheckCircleIcon />
19+
</Icon>
20+
);
21+
} else if (statusLowerCase === "expired") {
22+
return (
23+
<Icon status="danger" {...iconProps}>
24+
<TimesCircleIcon />
25+
</Icon>
26+
);
27+
} else if (statusLowerCase === "expiring") {
28+
return (
29+
<Icon status="warning" {...iconProps}>
30+
<WarningTriangleIcon />
31+
</Icon>
32+
);
33+
} else {
34+
return (
35+
<Icon size="xl">
36+
<MinusIcon />
37+
</Icon>
38+
);
39+
}
40+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type React from "react";
2+
3+
import type { AxiosError } from "axios";
4+
5+
import { Bullseye, Spinner } from "@patternfly/react-core";
6+
7+
import { StateError } from "./StateError";
8+
9+
export const LoadingWrapper = (props: {
10+
isFetching: boolean;
11+
fetchError?: AxiosError | null;
12+
isFetchingState?: React.ReactNode;
13+
fetchErrorState?: (error: AxiosError) => React.ReactNode;
14+
children: React.ReactNode;
15+
}) => {
16+
if (props.isFetching) {
17+
return (
18+
props.isFetchingState ?? (
19+
<Bullseye>
20+
<Spinner />
21+
</Bullseye>
22+
)
23+
);
24+
}
25+
if (props.fetchError) {
26+
return props.fetchErrorState ? props.fetchErrorState(props.fetchError) : <StateError />;
27+
}
28+
return props.children;
29+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type React from "react";
2+
3+
import { EmptyState, EmptyStateBody, EmptyStateVariant } from "@patternfly/react-core";
4+
import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon";
5+
6+
export const StateError: React.FC = () => {
7+
return (
8+
<EmptyState
9+
status="danger"
10+
headingLevel="h4"
11+
titleText="Unable to connect"
12+
icon={ExclamationCircleIcon}
13+
variant={EmptyStateVariant.sm}
14+
>
15+
<EmptyStateBody>There was an error retrieving data. Check your connection and try again.</EmptyStateBody>
16+
</EmptyState>
17+
);
18+
};

client/src/app/dayjs.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import dayjs from "dayjs";
2+
import arraySupport from "dayjs/plugin/arraySupport";
3+
import customParseFormat from "dayjs/plugin/customParseFormat";
4+
import duration from "dayjs/plugin/duration";
5+
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
6+
import relativeTime from "dayjs/plugin/relativeTime";
7+
import timezone from "dayjs/plugin/timezone";
8+
import utc from "dayjs/plugin/utc";
9+
10+
dayjs.extend(utc);
11+
dayjs.extend(timezone);
12+
dayjs.extend(customParseFormat);
13+
dayjs.extend(isSameOrBefore);
14+
dayjs.extend(arraySupport);
15+
dayjs.extend(relativeTime);
16+
dayjs.extend(duration);

client/src/app/layout/sidebar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ export const SidebarApp: React.FC = () => {
4343
Trust Roots
4444
</NavLink>
4545
</li>
46+
<li className={nav.navItem}>
47+
<NavLink
48+
to="/trust-root"
49+
className={({ isActive }) => {
50+
return css(LINK_CLASS, isActive ? ACTIVE_LINK_CLASS : "");
51+
}}
52+
>
53+
Trust root
54+
</NavLink>
55+
</li>
4656
</NavList>
4757
</Nav>
4858
);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { Fragment } from "react";
2+
3+
import { Content, PageSection, Tab, TabContent, Tabs, TabTitleText } from "@patternfly/react-core";
4+
import { ExternalLinkAltIcon } from "@patternfly/react-icons";
5+
6+
import { LoadingWrapper } from "@app/components/LoadingWrapper";
7+
import { useFetchTrustRootMetadataInfo } from "@app/queries/trust";
8+
9+
import { Certificates } from "./components/Certificates";
10+
import { RootDetails } from "./components/RootDetails";
11+
12+
export const TrustRoots: React.FC = () => {
13+
const { rootMetadataList, isFetching, fetchError } = useFetchTrustRootMetadataInfo();
14+
15+
// Tab refs
16+
const certificatesTabRef = React.createRef<HTMLElement>();
17+
const rootDetailsTabRef = React.createRef<HTMLElement>();
18+
19+
const [activeTabKey, setActiveTabKey] = React.useState<string | number>(0);
20+
21+
const handleTabClick = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, tabIndex: string | number) => {
22+
setActiveTabKey(tabIndex);
23+
};
24+
25+
return (
26+
<Fragment>
27+
<PageSection variant="default">
28+
<Content>
29+
<h1>Trust Root</h1>
30+
<p>This information represents the update framework.</p>
31+
<p>
32+
<a href={rootMetadataList?.["repo-url"]} target="_blank" rel="noreferrer">
33+
{rootMetadataList?.["repo-url"]} <ExternalLinkAltIcon />
34+
</a>
35+
</p>
36+
</Content>
37+
</PageSection>
38+
<PageSection variant="default">
39+
<Tabs
40+
mountOnEnter
41+
activeKey={activeTabKey}
42+
onSelect={handleTabClick}
43+
aria-label="Tabs that contain the SBOM information"
44+
role="region"
45+
>
46+
<Tab
47+
eventKey={0}
48+
title={<TabTitleText>Certificates</TabTitleText>}
49+
tabContentId="certificatesTabSection"
50+
tabContentRef={certificatesTabRef}
51+
/>
52+
<Tab
53+
eventKey={1}
54+
title={<TabTitleText>Root details</TabTitleText>}
55+
tabContentId="rootDetailsTabSection"
56+
tabContentRef={rootDetailsTabRef}
57+
/>
58+
</Tabs>
59+
</PageSection>
60+
<PageSection>
61+
<TabContent eventKey={0} id="certificatesTabSection" ref={certificatesTabRef} aria-label="Certificates">
62+
<Certificates />
63+
</TabContent>
64+
<TabContent eventKey={1} id="rootDetailsTabSection" ref={rootDetailsTabRef} aria-label="Root details" hidden>
65+
<LoadingWrapper isFetching={isFetching} fetchError={fetchError}>
66+
{rootMetadataList && <RootDetails rootMetadataList={rootMetadataList} />}
67+
</LoadingWrapper>
68+
</TabContent>
69+
</PageSection>
70+
</Fragment>
71+
);
72+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from "react";
2+
3+
export const Certificates: React.FC = () => {
4+
return <>Certificates</>;
5+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from "react";
2+
3+
import {
4+
Card,
5+
CardBody,
6+
CardTitle,
7+
DescriptionList,
8+
DescriptionListDescription,
9+
DescriptionListGroup,
10+
DescriptionListTermHelpText,
11+
} from "@patternfly/react-core";
12+
13+
import type { RootMetadataInfoList } from "@app/client";
14+
import { CertificateStatusIcon } from "@app/components/CertificateStatusIcon";
15+
import { capitalizeFirstLetter, formatDate, universalComparator } from "@app/utils/utils";
16+
17+
interface IRootDetailsProps {
18+
rootMetadataList: RootMetadataInfoList;
19+
}
20+
21+
export const RootDetails: React.FC<IRootDetailsProps> = ({ rootMetadataList }) => {
22+
const latestMetadataInfo = React.useMemo(() => {
23+
// Sort:desc of metadata by version
24+
const metadataInfo = [...rootMetadataList.data]
25+
.sort((a, b) => universalComparator(a.version, b.version, "en"))
26+
.reverse();
27+
return metadataInfo[0] ? metadataInfo[0] : null;
28+
}, [rootMetadataList]);
29+
30+
return (
31+
<Card isPlain>
32+
<CardTitle>Metadata</CardTitle>
33+
<CardBody>
34+
<DescriptionList
35+
aria-label="Metadata"
36+
columnModifier={{
37+
default: "2Col",
38+
}}
39+
>
40+
<DescriptionListGroup>
41+
<DescriptionListTermHelpText>Version</DescriptionListTermHelpText>
42+
<DescriptionListDescription>{latestMetadataInfo?.version}</DescriptionListDescription>
43+
</DescriptionListGroup>
44+
<DescriptionListGroup>
45+
<DescriptionListTermHelpText>Expires</DescriptionListTermHelpText>
46+
<DescriptionListDescription>{formatDate(latestMetadataInfo?.expires)}</DescriptionListDescription>
47+
</DescriptionListGroup>
48+
<DescriptionListGroup>
49+
<DescriptionListTermHelpText>Status</DescriptionListTermHelpText>
50+
<DescriptionListDescription>
51+
{latestMetadataInfo && <CertificateStatusIcon status={latestMetadataInfo?.status} />}{" "}
52+
{capitalizeFirstLetter(latestMetadataInfo?.status ?? "")}
53+
</DescriptionListDescription>
54+
</DescriptionListGroup>
55+
</DescriptionList>
56+
</CardBody>
57+
</Card>
58+
);
59+
};

0 commit comments

Comments
 (0)