Skip to content

Commit 9ba1464

Browse files
authored
feat(ux): add dynamic labels & helper text for settings + search form (#28)
2 parents 2923195 + df9c964 commit 9ba1464

File tree

3 files changed

+165
-84
lines changed

3 files changed

+165
-84
lines changed

src/modules/components/SearchForm.tsx

+143-78
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import {
22
Button,
3+
Flex,
4+
FlexItem,
35
Form,
6+
FormGroup,
47
FormHelperText,
58
FormSelect,
69
FormSelectOption,
7-
Grid,
8-
GridItem,
910
HelperText,
1011
HelperTextItem,
12+
Popover,
1113
TextInput,
1214
} from "@patternfly/react-core";
13-
import { ReactNode, useEffect } from "react";
15+
import { ReactNode, useEffect, useState } from "react";
1416
import { Controller, RegisterOptions, useForm } from "react-hook-form";
1517
import { Attribute, ATTRIBUTES } from "../api/rekor_api";
16-
import { ExclamationCircleIcon } from "@patternfly/react-icons";
18+
import { ExclamationCircleIcon, HelpIcon } from "@patternfly/react-icons";
19+
import styles from "@patternfly/react-styles/css/components/Form/form";
1720

1821
export interface FormProps {
1922
defaultValues?: FormInputs;
@@ -34,67 +37,80 @@ type Rules = Omit<
3437
interface InputConfig {
3538
name: string;
3639
helperText?: ReactNode;
40+
placeholder?: string;
3741
rules: Rules;
42+
tooltipText?: ReactNode;
3843
}
3944

4045
const inputConfigByAttribute: Record<FormInputs["attribute"], InputConfig> = {
4146
email: {
4247
name: "Email",
48+
placeholder: "jdoe@example.com",
4349
rules: {
4450
pattern: {
4551
value: /\S+@\S+\.\S+/,
4652
message: "Entered value does not match the email format: 'S+@S+.S+'",
4753
},
4854
},
55+
tooltipText: <>Search by the signer&apos;s email address.</>,
4956
},
5057
hash: {
5158
name: "Hash",
59+
placeholder:
60+
"sha256:8ceb4ab8127731473a9ec81140cb6849cf8e970cda31baef099df48ba3264441",
5261
rules: {
5362
pattern: {
5463
value: /^(sha256:)?[0-9a-fA-F]{64}$|^(sha1:)?[0-9a-fA-F]{40}$/,
5564
message:
5665
"Entered value does not match the hash format: '^(sha256:)?[0-9a-fA-F]{64}$|^(sha1:)?[0-9a-fA-F]{40}$'",
5766
},
5867
},
68+
tooltipText: <>Search by the SHA1 or SHA2 hash value.</>,
5969
},
6070
commitSha: {
6171
name: "Commit SHA",
6272
helperText: (
6373
<>
6474
Only compatible with{" "}
6575
<a
66-
href="https://github.com/sigstore/gitsign"
76+
href="https://access.redhat.com/documentation/en-us/red_hat_trusted_artifact_signer/2024-q1/html/deployment_guide/verify_the_trusted_artifact_signer_installation#signing-and-verifying-commits-by-using-gitsign-from-the-command-line-interface_deploy"
6777
target="_blank"
6878
rel="noopener noreferrer"
6979
style={{
7080
textDecoration: "underline",
7181
}}
7282
>
73-
sigstore/gitsign
83+
gitsign
7484
</a>{" "}
7585
entries
7686
</>
7787
),
88+
placeholder: "6d78e27dfcf83eaad6ef73c4695d1ddc663f5555",
7889
rules: {
7990
pattern: {
8091
value: /^[0-9a-fA-F]{40}$/,
8192
message:
8293
"Entered value does not match the commit SHA format: '^[0-9a-fA-F]{40}$'",
8394
},
8495
},
96+
tooltipText: <>Search by the commit hash.</>,
8597
},
8698
uuid: {
8799
name: "Entry UUID",
100+
placeholder:
101+
"24296fb24b8ad77a71b9c1374e207537bafdd75b4f591dcee10f3f697f150d7cc5d0b725eea641e7",
88102
rules: {
89103
pattern: {
90104
value: /^[0-9a-fA-F]{64}|[0-9a-fA-F]{80}$/,
91105
message:
92106
"Entered value does not match the entry UUID format: '^[0-9a-fA-F]{64}|[0-9a-fA-F]{80}$'",
93107
},
94108
},
109+
tooltipText: <>Search by the universally unique identifier value.</>,
95110
},
96111
logIndex: {
97112
name: "Log Index",
113+
placeholder: "1234567",
98114
rules: {
99115
min: {
100116
value: 0,
@@ -105,6 +121,7 @@ const inputConfigByAttribute: Record<FormInputs["attribute"], InputConfig> = {
105121
message: "Entered value must be of type int64",
106122
},
107123
},
124+
tooltipText: <>Search by the log index number.</>,
108125
},
109126
};
110127

@@ -148,81 +165,129 @@ export function SearchForm({ defaultValues, onSubmit, isLoading }: FormProps) {
148165

149166
return (
150167
<Form onSubmit={handleSubmit(onSubmit)}>
151-
<Grid hasGutter={true}>
152-
<GridItem sm={4}>
153-
<Controller
154-
name="attribute"
155-
control={control}
156-
render={({ field }) => (
157-
<FormSelect
158-
id="rekor-search-type"
159-
{...field}
160-
label="Attribute"
161-
>
162-
{ATTRIBUTES.map(attribute => (
163-
<FormSelectOption
164-
label={inputConfigByAttribute[attribute].name}
165-
key={attribute}
166-
value={attribute}
167-
/>
168-
))}
169-
</FormSelect>
170-
)}
171-
/>
172-
</GridItem>
173-
<GridItem
174-
sm={8}
175-
md={6}
168+
<Flex>
169+
<Flex
170+
direction={{ default: "column" }}
171+
flex={{ default: "flex_3" }}
176172
>
177-
<Controller
178-
name="value"
179-
control={control}
180-
rules={rules}
181-
render={({ field, fieldState }) => (
182-
<>
183-
<TextInput
184-
aria-label={`${inputConfigByAttribute[watchAttribute].name} input field`}
185-
{...field}
186-
label={inputConfigByAttribute[watchAttribute].name}
187-
placeholder={inputConfigByAttribute[watchAttribute].name}
188-
type={"email"}
189-
validated={fieldState.invalid ? "error" : "default"}
190-
/>
191-
{fieldState.invalid && (
192-
<FormHelperText>
193-
<HelperText>
194-
<HelperTextItem
195-
icon={<ExclamationCircleIcon />}
196-
variant={fieldState.invalid ? "error" : "success"}
173+
<FlexItem>
174+
<Controller
175+
name="attribute"
176+
control={control}
177+
render={({ field }) => (
178+
<FormGroup
179+
label={"Attribute"}
180+
fieldId={"rekor-search-attribute"}
181+
labelIcon={
182+
<Popover
183+
bodyContent={
184+
inputConfigByAttribute[watchAttribute].tooltipText
185+
}
186+
position={"right"}
187+
>
188+
<button
189+
type="button"
190+
aria-label="More info for attribute field"
191+
onClick={e => e.preventDefault()}
192+
aria-describedby="attribute-info"
193+
className={styles.formGroupLabelHelp}
197194
>
198-
{fieldState.invalid
199-
? fieldState.error?.message
200-
: inputConfigByAttribute[watchAttribute].helperText}
201-
</HelperTextItem>
202-
</HelperText>
203-
</FormHelperText>
204-
)}
205-
</>
206-
)}
207-
/>
208-
</GridItem>
209-
<GridItem
210-
sm={12}
211-
md={2}
195+
<HelpIcon />
196+
</button>
197+
</Popover>
198+
}
199+
>
200+
<FormSelect
201+
id="rekor-search-attribute"
202+
{...field}
203+
label="Attribute"
204+
>
205+
{ATTRIBUTES.map(attribute => (
206+
<FormSelectOption
207+
label={inputConfigByAttribute[attribute].name}
208+
key={attribute}
209+
value={attribute}
210+
/>
211+
))}
212+
</FormSelect>
213+
</FormGroup>
214+
)}
215+
/>
216+
</FlexItem>
217+
</Flex>
218+
<Flex
219+
direction={{ default: "column" }}
220+
flex={{ default: "flex_3" }}
221+
>
222+
<FlexItem>
223+
<Controller
224+
name="value"
225+
control={control}
226+
rules={rules}
227+
render={({ field, fieldState }) => (
228+
<FormGroup
229+
label={inputConfigByAttribute[watchAttribute].name}
230+
labelInfo={inputConfigByAttribute[watchAttribute].helperText}
231+
fieldId={`rekor-search-${inputConfigByAttribute[
232+
watchAttribute
233+
].name.toLowerCase()}`}
234+
>
235+
<TextInput
236+
aria-label={`${inputConfigByAttribute[watchAttribute].name} input field`}
237+
{...field}
238+
id={`rekor-search-${inputConfigByAttribute[
239+
watchAttribute
240+
].name.toLowerCase()}`}
241+
label={inputConfigByAttribute[watchAttribute].name}
242+
placeholder={
243+
inputConfigByAttribute[watchAttribute].placeholder
244+
}
245+
type={
246+
inputConfigByAttribute[watchAttribute].name === "email"
247+
? "email"
248+
: "text"
249+
}
250+
validated={fieldState.invalid ? "error" : "default"}
251+
/>
252+
{fieldState.invalid && (
253+
<FormHelperText>
254+
<HelperText>
255+
<HelperTextItem
256+
icon={<ExclamationCircleIcon />}
257+
variant={fieldState.invalid ? "error" : "success"}
258+
>
259+
{fieldState.invalid
260+
? fieldState.error?.message
261+
: inputConfigByAttribute[watchAttribute].helperText}
262+
</HelperTextItem>
263+
</HelperText>
264+
</FormHelperText>
265+
)}
266+
</FormGroup>
267+
)}
268+
/>
269+
</FlexItem>
270+
</Flex>
271+
<Flex
272+
direction={{ default: "column" }}
273+
alignSelf={{ default: "alignSelfFlexStart" }}
274+
flex={{ default: "flex_1" }}
212275
>
213-
<Button
214-
variant="primary"
215-
id="search-form-button"
216-
isBlock={true}
217-
isLoading={isLoading}
218-
type="submit"
219-
spinnerAriaLabel={"Loading"}
220-
spinnerAriaLabelledBy={"search-form-button"}
221-
>
222-
Search
223-
</Button>
224-
</GridItem>
225-
</Grid>
276+
<FlexItem style={{ marginTop: "2em" }}>
277+
<Button
278+
variant="primary"
279+
id="search-form-button"
280+
isBlock={true}
281+
isLoading={isLoading}
282+
type="submit"
283+
spinnerAriaLabel={"Loading"}
284+
spinnerAriaLabelledBy={"search-form-button"}
285+
>
286+
Search
287+
</Button>
288+
</FlexItem>
289+
</Flex>
290+
</Flex>
226291
</Form>
227292
);
228293
}

src/modules/components/Settings.tsx

+19-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
TextInput,
88
Form,
99
FormGroup,
10+
Popover,
1011
} from "@patternfly/react-core";
12+
import { HelpIcon } from "@patternfly/react-icons";
13+
import styles from "@patternfly/react-styles/css/components/Form/form";
1114

1215
export function Settings({
1316
open,
@@ -65,6 +68,19 @@ export function Settings({
6568
<Form id="settings-form">
6669
<FormGroup
6770
label="Override Rekor Endpoint"
71+
labelIcon={
72+
<Popover bodyContent={"Specify your private Rekor endpoint URL."}>
73+
<button
74+
type="button"
75+
aria-label="More info for name field"
76+
onClick={e => e.preventDefault()}
77+
aria-describedby="form-group-label-info"
78+
className={styles.formGroupLabelHelp}
79+
>
80+
<HelpIcon />
81+
</button>
82+
</Popover>
83+
}
6884
isRequired
6985
fieldId="rekor-endpoint-override"
7086
>
@@ -73,7 +89,9 @@ export function Settings({
7389
type="text"
7490
onChange={handleChangeBaseUrl}
7591
placeholder={
76-
baseUrl === undefined ? "https://rekor.sigstore.dev" : baseUrl
92+
baseUrl === undefined
93+
? "https://private.rekor.example.com"
94+
: baseUrl
7795
}
7896
aria-label="override rekor endpoint"
7997
/>

src/pages/index.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { CogIcon, GithubIcon } from "@patternfly/react-icons";
2222
import Link from "next/link";
2323
import Image from "next/image";
2424
import NOSSRWrapper from "../modules/utils/noSSR";
25+
import logo from "/public/Logo-Red_Hat-Trusted_Artifact_Signer-A-Reverse-RGB.svg";
2526

2627
const Home: NextPage = () => {
2728
const [settingsOpen, setSettingsOpen] = useState(false);
@@ -31,12 +32,9 @@ const Home: NextPage = () => {
3132
header={
3233
<Masthead>
3334
<MastheadMain>
34-
<Link
35-
href={"/"}
36-
className={"pf-v5-c-masthead_brand"}
37-
>
35+
<Link href={"/"}>
3836
<Image
39-
src={"/Logo-Red_Hat-Trusted_Artifact_Signer-A-Reverse-RGB.svg"}
37+
src={logo}
4038
alt={"Red Hat Trusted Artifact Signer logo"}
4139
priority={true}
4240
width={127}

0 commit comments

Comments
 (0)