Skip to content

Commit f190a19

Browse files
committed
feat(artifacts): add route and setup component
feat(artifacts): update mock and query feat(artifact): add form control and field validation fix: re-add hook state fix(ci): type issues with react-syntax-highlighter enhancement(artifact): improve search results enhancement(artifact): add metadata accordion fix: use paths constant for route fix: lazy load artifacts chore: remove accordion, use content for titles fix: add type for artifact uri fix: fallback for uri type assertion chore: remove gcTime setting fix: remove unnecessary query dependency
1 parent 1683f24 commit f190a19

File tree

9 files changed

+314
-19
lines changed

9 files changed

+314
-19
lines changed

client/src/app/Routes.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ import { Navigate, useRoutes } from "react-router-dom";
55
import { Bullseye, Spinner } from "@patternfly/react-core";
66
import { ErrorFallback } from "./components/ErrorFallback";
77

8+
const Artifacts = lazy(() => import("./pages/Artifacts"));
89
const TrustRoot = lazy(() => import("./pages/TrustRoot"));
910
const RekorSearch = lazy(() => import("./pages/RekorSearch"));
1011

1112
export const Paths = {
12-
trustRoot: "/trust-root",
13+
artifacts: "/artifacts",
1314
rekorSearch: "/rekor-search",
15+
trustRoot: "/trust-root",
1416
} as const;
1517

1618
export const AppRoutes = () => {
1719
const allRoutes = useRoutes([
1820
{ path: "/", element: <Navigate to={Paths.trustRoot} /> },
1921
{ path: Paths.trustRoot, element: <TrustRoot /> },
22+
{ path: Paths.artifacts, element: <Artifacts /> },
2023
{ path: Paths.rekorSearch, element: <RekorSearch /> },
2124
]);
2225

client/src/app/layout/sidebar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ export const SidebarApp: React.FC = () => {
2323
>
2424
Trust root
2525
</NavLink>
26+
<NavLink
27+
to={Paths.artifacts}
28+
className={({ isActive }) => {
29+
return css(LINK_CLASS, isActive ? ACTIVE_LINK_CLASS : "");
30+
}}
31+
>
32+
Artifacts
33+
</NavLink>
2634
</li>
2735
<li className={nav.navItem}>
2836
<NavLink
Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,148 @@
1-
import { Content, PageSection, Tab, TabContent, Tabs, TabTitleText } from "@patternfly/react-core";
2-
import { ExternalLinkAltIcon } from "@patternfly/react-icons";
3-
4-
import { LoadingWrapper } from "@app/components/LoadingWrapper";
1+
import { Fragment, useRef, useState } from "react";
2+
import {
3+
Button,
4+
Content,
5+
Flex,
6+
FlexItem,
7+
Form,
8+
FormGroup,
9+
FormGroupLabelHelp,
10+
FormHelperText,
11+
HelperText,
12+
HelperTextItem,
13+
PageSection,
14+
Popover,
15+
TextInput,
16+
} from "@patternfly/react-core";
517
import { useFetchArtifactsImageData } from "@app/queries/artifacts";
18+
import { LoadingWrapper } from "@app/components/LoadingWrapper";
19+
import { ArtifactResults } from "./components/ArtifactResults";
20+
import { Controller, useForm } from "react-hook-form";
21+
22+
import { ExclamationCircleIcon } from "@patternfly/react-icons";
23+
24+
const PLACEHOLDER_URI = "docker.io/library/nginx:latest";
25+
26+
interface FormInputs {
27+
searchInput: string;
28+
}
29+
30+
export const Artifacts = () => {
31+
const [artifactUri, setArtifactUri] = useState<string | null>(null);
32+
const labelHelpRef = useRef(null);
33+
34+
const {
35+
artifact,
36+
isFetching: isFetchingArtifactMetadata,
37+
fetchError: fetchErrorArtifactMetadata,
38+
} = useFetchArtifactsImageData({ uri: artifactUri });
39+
40+
const {
41+
control,
42+
handleSubmit,
43+
watch,
44+
formState: { errors },
45+
} = useForm<FormInputs>({
46+
mode: "all",
47+
reValidateMode: "onChange",
48+
defaultValues: {
49+
searchInput: "",
50+
},
51+
});
52+
53+
const onSubmit = (data: FormInputs) => {
54+
const uri = data.searchInput?.trim();
55+
if (!uri) return;
56+
setArtifactUri(uri);
57+
};
58+
59+
const query = watch("searchInput");
60+
const isEmpty = query.trim().length === 0;
661

7-
export const Artifacts = () => {};
62+
return (
63+
<Fragment>
64+
<PageSection variant="default">
65+
<Content>
66+
<h1>Artifacts</h1>
67+
<p>Search for an artifact.</p>
68+
</Content>
69+
</PageSection>
70+
<PageSection>
71+
<Form onSubmit={(e) => void handleSubmit(onSubmit)(e)}>
72+
<Flex>
73+
<Flex direction={{ default: "column" }} flex={{ default: "flex_3" }}>
74+
<FlexItem>
75+
<Controller
76+
name="searchInput"
77+
control={control}
78+
rules={{ required: { value: true, message: "A value is required" } }}
79+
render={({ field, fieldState }) => (
80+
<FormGroup
81+
label="URI"
82+
labelHelp={
83+
<Popover
84+
triggerRef={labelHelpRef}
85+
headerContent={<div>URI of the container image</div>}
86+
bodyContent={<div>e.g., {PLACEHOLDER_URI}</div>}
87+
>
88+
<FormGroupLabelHelp ref={labelHelpRef} aria-label="More info for URI field" />
89+
</Popover>
90+
}
91+
isRequired
92+
fieldId="uri"
93+
>
94+
<TextInput
95+
aria-label={`uri input field`}
96+
{...field}
97+
type="text"
98+
name="searchInput"
99+
id="uri"
100+
aria-describedby="uri-helper"
101+
aria-invalid={errors.searchInput ? "true" : "false"}
102+
placeholder={PLACEHOLDER_URI}
103+
validated={fieldState.invalid ? "error" : "default"}
104+
/>
105+
{fieldState.invalid && (
106+
<FormHelperText>
107+
<HelperText>
108+
<HelperTextItem icon={<ExclamationCircleIcon />} variant={"error"}>
109+
{fieldState.invalid ? fieldState.error?.message : <span>A value is required</span>}
110+
</HelperTextItem>
111+
</HelperText>
112+
</FormHelperText>
113+
)}
114+
</FormGroup>
115+
)}
116+
></Controller>
117+
</FlexItem>
118+
</Flex>
119+
<Flex
120+
direction={{ default: "column" }}
121+
alignSelf={{ default: "alignSelfFlexStart" }}
122+
flex={{ default: "flex_1" }}
123+
>
124+
<FlexItem style={{ marginTop: "2em" }}>
125+
<Button
126+
variant="primary"
127+
id="search-form-button"
128+
isBlock={true}
129+
isDisabled={isEmpty}
130+
type="submit"
131+
spinnerAriaLabel="Loading"
132+
spinnerAriaLabelledBy="search-form-button"
133+
>
134+
Search
135+
</Button>
136+
</FlexItem>
137+
</Flex>
138+
</Flex>
139+
</Form>
140+
</PageSection>
141+
<PageSection>
142+
<LoadingWrapper isFetching={isFetchingArtifactMetadata} fetchError={fetchErrorArtifactMetadata}>
143+
{artifact && <ArtifactResults artifact={artifact} />}
144+
</LoadingWrapper>
145+
</PageSection>
146+
</Fragment>
147+
);
148+
};
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Fragment } from "react";
2+
import type { PrismTheme } from "types/prism-theme";
3+
import type { ImageMetadataResponse } from "@app/client";
4+
import {
5+
Card,
6+
CardBody,
7+
Content,
8+
ContentVariants,
9+
Divider,
10+
Flex,
11+
FlexItem,
12+
Grid,
13+
GridItem,
14+
Panel,
15+
} from "@patternfly/react-core";
16+
import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/prism";
17+
import { atomDark as darkTheme } from "react-syntax-highlighter/dist/esm/styles/prism";
18+
19+
export interface IArtifactResultsProps {
20+
artifact: ImageMetadataResponse;
21+
}
22+
23+
export const ArtifactResults = ({ artifact }: IArtifactResultsProps) => {
24+
return (
25+
<div style={{ margin: "2em auto" }}>
26+
<p>Showing 1 of 1</p>
27+
<Card style={{ margin: "1.5em auto 2em", overflowY: "hidden" }}>
28+
<CardBody>
29+
<Content
30+
component={ContentVariants.h4}
31+
style={{ margin: "1.25em auto", overflow: "hidden", textOverflow: "ellipsis" }}
32+
>
33+
Artifact: {artifact.image}
34+
</Content>
35+
<Divider />
36+
<Panel style={{ marginTop: "1.25em" }}>
37+
<Content component={ContentVariants.h6} style={{ margin: "1em auto" }}>
38+
Digest
39+
</Content>
40+
<SyntaxHighlighter language="text" style={darkTheme as unknown as PrismTheme}>
41+
{artifact.digest}
42+
</SyntaxHighlighter>
43+
</Panel>
44+
<Panel style={{ margin: "0.75em auto" }}>
45+
<Fragment>
46+
<Grid>
47+
<GridItem span={4}>
48+
<Flex style={{ padding: "1em" }}>
49+
<FlexItem>
50+
<Content component={ContentVariants.h6}>Media Type</Content>
51+
<p
52+
style={{
53+
overflow: "hidden",
54+
textOverflow: "ellipsis",
55+
display: "flex",
56+
alignItems: "center",
57+
justifyContent: "start",
58+
}}
59+
>
60+
{artifact.metadata.mediaType}
61+
</p>
62+
</FlexItem>
63+
</Flex>
64+
</GridItem>
65+
<GridItem span={2}>
66+
<Flex style={{ padding: "1em" }}>
67+
<FlexItem>
68+
<Content component={ContentVariants.h6}>Size</Content>
69+
<p
70+
style={{
71+
overflow: "hidden",
72+
textOverflow: "ellipsis",
73+
display: "flex",
74+
alignItems: "center",
75+
justifyContent: "start",
76+
}}
77+
>
78+
{artifact.metadata.size}
79+
</p>
80+
</FlexItem>
81+
</Flex>
82+
</GridItem>
83+
<GridItem span={3}>
84+
<Flex style={{ padding: "1em" }}>
85+
<FlexItem>
86+
<Content component={ContentVariants.h6}>Created</Content>
87+
<p
88+
style={{
89+
overflow: "hidden",
90+
textOverflow: "ellipsis",
91+
display: "flex",
92+
alignItems: "center",
93+
justifyContent: "start",
94+
}}
95+
>
96+
{artifact.metadata.created}
97+
</p>
98+
</FlexItem>
99+
</Flex>
100+
</GridItem>
101+
<GridItem span={3}>
102+
<Flex style={{ padding: "1em" }}>
103+
<Divider orientation={{ default: "vertical" }} style={{ margin: "auto 1em" }} />
104+
<FlexItem>
105+
<Content component={ContentVariants.h6}>Labels</Content>
106+
<p
107+
style={{
108+
overflow: "hidden",
109+
textOverflow: "ellipsis",
110+
display: "flex",
111+
alignItems: "center",
112+
justifyContent: "start",
113+
}}
114+
>
115+
{artifact.metadata.labels?.maintainer}
116+
</p>
117+
</FlexItem>
118+
</Flex>
119+
</GridItem>
120+
</Grid>
121+
</Fragment>
122+
</Panel>
123+
</CardBody>
124+
</Card>
125+
</div>
126+
);
127+
};

client/src/app/pages/RekorSearch/components/HashedRekord.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import type { PrismTheme } from "types/prism-theme";
12
import { dump } from "js-yaml";
23
import { Link } from "react-router-dom";
3-
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4-
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
4+
import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/prism";
5+
import { atomDark as darkTheme } from "react-syntax-highlighter/dist/cjs/styles/prism";
56
import { type RekorSchema } from "rekor";
67
import { decodex509 } from "../x509/decode";
78
import { Panel } from "@patternfly/react-core";
@@ -34,17 +35,17 @@ export function HashedRekordViewer({ hashedRekord }: { hashedRekord: RekorSchema
3435
Hash
3536
</Link>
3637
</h5>
37-
<SyntaxHighlighter language="text" style={atomDark}>
38+
<SyntaxHighlighter language="text" style={darkTheme as unknown as PrismTheme}>
3839
{`${hashedRekord.data.hash?.algorithm}:${hashedRekord.data.hash?.value}`}
3940
</SyntaxHighlighter>
4041

4142
<h5 style={{ margin: "1em auto" }}>Signature</h5>
42-
<SyntaxHighlighter language="text" style={atomDark}>
43+
<SyntaxHighlighter language="text" style={darkTheme as unknown as PrismTheme}>
4344
{hashedRekord.signature.content ?? ""}
4445
</SyntaxHighlighter>
4546

4647
<h5 style={{ margin: "1em auto" }}>{publicKey.title}</h5>
47-
<SyntaxHighlighter language="yaml" style={atomDark}>
48+
<SyntaxHighlighter language="yaml" style={darkTheme as unknown as PrismTheme}>
4849
{publicKey.content}
4950
</SyntaxHighlighter>
5051
</Panel>

client/src/app/queries/artifacts.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@ import { artifactsImageDataMock } from "./mocks/artifacts.mock";
77

88
export const ArtifactsKey = "Artifacts";
99

10-
export const useFetchArtifactsImageData = () => {
10+
export const useFetchArtifactsImageData = ({ uri }: { uri: string | null | undefined }) => {
11+
const enabled = typeof uri === "string" && uri.trim().length > 0;
1112
const { data, isLoading, error, refetch } = useMockableQuery<ImageMetadataResponse | null, AxiosError<_Error>>(
1213
{
13-
queryKey: [ArtifactsKey, "image"],
14+
queryKey: [ArtifactsKey, "image", uri ?? ""],
1415
queryFn: async () => {
15-
const response = await getApiV1ArtifactsImage({ client, query: { uri: "" } });
16+
const response = await getApiV1ArtifactsImage({ client, query: { uri: uri! } });
1617
return response.data ?? null;
1718
},
19+
// only run when we have a string
20+
enabled,
21+
refetchOnWindowFocus: false,
1822
},
1923
artifactsImageDataMock
2024
);
2125

22-
return { config: data, isFetching: isLoading, fetchError: error, refetch };
26+
return { artifact: data, isFetching: isLoading, fetchError: error, refetch };
2327
};

0 commit comments

Comments
 (0)