forked from sigstore/rekor-search-ui
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ux): add dynamic labels & helper text for settings + search form (…
…#28)
- Loading branch information
Showing
3 changed files
with
165 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,22 @@ | ||
import { | ||
Button, | ||
Flex, | ||
FlexItem, | ||
Form, | ||
FormGroup, | ||
FormHelperText, | ||
FormSelect, | ||
FormSelectOption, | ||
Grid, | ||
GridItem, | ||
HelperText, | ||
HelperTextItem, | ||
Popover, | ||
TextInput, | ||
} from "@patternfly/react-core"; | ||
import { ReactNode, useEffect } from "react"; | ||
import { ReactNode, useEffect, useState } from "react"; | ||
import { Controller, RegisterOptions, useForm } from "react-hook-form"; | ||
import { Attribute, ATTRIBUTES } from "../api/rekor_api"; | ||
import { ExclamationCircleIcon } from "@patternfly/react-icons"; | ||
import { ExclamationCircleIcon, HelpIcon } from "@patternfly/react-icons"; | ||
import styles from "@patternfly/react-styles/css/components/Form/form"; | ||
|
||
export interface FormProps { | ||
defaultValues?: FormInputs; | ||
|
@@ -34,67 +37,80 @@ type Rules = Omit< | |
interface InputConfig { | ||
name: string; | ||
helperText?: ReactNode; | ||
placeholder?: string; | ||
rules: Rules; | ||
tooltipText?: ReactNode; | ||
} | ||
|
||
const inputConfigByAttribute: Record<FormInputs["attribute"], InputConfig> = { | ||
email: { | ||
name: "Email", | ||
placeholder: "[email protected]", | ||
rules: { | ||
pattern: { | ||
value: /\S+@\S+\.\S+/, | ||
message: "Entered value does not match the email format: 'S+@S+.S+'", | ||
}, | ||
}, | ||
tooltipText: <>Search by the signer's email address.</>, | ||
}, | ||
hash: { | ||
name: "Hash", | ||
placeholder: | ||
"sha256:8ceb4ab8127731473a9ec81140cb6849cf8e970cda31baef099df48ba3264441", | ||
rules: { | ||
pattern: { | ||
value: /^(sha256:)?[0-9a-fA-F]{64}$|^(sha1:)?[0-9a-fA-F]{40}$/, | ||
message: | ||
"Entered value does not match the hash format: '^(sha256:)?[0-9a-fA-F]{64}$|^(sha1:)?[0-9a-fA-F]{40}$'", | ||
}, | ||
}, | ||
tooltipText: <>Search by the SHA1 or SHA2 hash value.</>, | ||
}, | ||
commitSha: { | ||
name: "Commit SHA", | ||
helperText: ( | ||
<> | ||
Only compatible with{" "} | ||
<a | ||
href="https://github.com/sigstore/gitsign" | ||
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" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
style={{ | ||
textDecoration: "underline", | ||
}} | ||
> | ||
sigstore/gitsign | ||
gitsign | ||
</a>{" "} | ||
entries | ||
</> | ||
), | ||
placeholder: "6d78e27dfcf83eaad6ef73c4695d1ddc663f5555", | ||
rules: { | ||
pattern: { | ||
value: /^[0-9a-fA-F]{40}$/, | ||
message: | ||
"Entered value does not match the commit SHA format: '^[0-9a-fA-F]{40}$'", | ||
}, | ||
}, | ||
tooltipText: <>Search by the commit hash.</>, | ||
}, | ||
uuid: { | ||
name: "Entry UUID", | ||
placeholder: | ||
"24296fb24b8ad77a71b9c1374e207537bafdd75b4f591dcee10f3f697f150d7cc5d0b725eea641e7", | ||
rules: { | ||
pattern: { | ||
value: /^[0-9a-fA-F]{64}|[0-9a-fA-F]{80}$/, | ||
message: | ||
"Entered value does not match the entry UUID format: '^[0-9a-fA-F]{64}|[0-9a-fA-F]{80}$'", | ||
}, | ||
}, | ||
tooltipText: <>Search by the universally unique identifier value.</>, | ||
}, | ||
logIndex: { | ||
name: "Log Index", | ||
placeholder: "1234567", | ||
rules: { | ||
min: { | ||
value: 0, | ||
|
@@ -105,6 +121,7 @@ const inputConfigByAttribute: Record<FormInputs["attribute"], InputConfig> = { | |
message: "Entered value must be of type int64", | ||
}, | ||
}, | ||
tooltipText: <>Search by the log index number.</>, | ||
}, | ||
}; | ||
|
||
|
@@ -148,81 +165,129 @@ export function SearchForm({ defaultValues, onSubmit, isLoading }: FormProps) { | |
|
||
return ( | ||
<Form onSubmit={handleSubmit(onSubmit)}> | ||
<Grid hasGutter={true}> | ||
<GridItem sm={4}> | ||
<Controller | ||
name="attribute" | ||
control={control} | ||
render={({ field }) => ( | ||
<FormSelect | ||
id="rekor-search-type" | ||
{...field} | ||
label="Attribute" | ||
> | ||
{ATTRIBUTES.map(attribute => ( | ||
<FormSelectOption | ||
label={inputConfigByAttribute[attribute].name} | ||
key={attribute} | ||
value={attribute} | ||
/> | ||
))} | ||
</FormSelect> | ||
)} | ||
/> | ||
</GridItem> | ||
<GridItem | ||
sm={8} | ||
md={6} | ||
<Flex> | ||
<Flex | ||
direction={{ default: "column" }} | ||
flex={{ default: "flex_3" }} | ||
> | ||
<Controller | ||
name="value" | ||
control={control} | ||
rules={rules} | ||
render={({ field, fieldState }) => ( | ||
<> | ||
<TextInput | ||
aria-label={`${inputConfigByAttribute[watchAttribute].name} input field`} | ||
{...field} | ||
label={inputConfigByAttribute[watchAttribute].name} | ||
placeholder={inputConfigByAttribute[watchAttribute].name} | ||
type={"email"} | ||
validated={fieldState.invalid ? "error" : "default"} | ||
/> | ||
{fieldState.invalid && ( | ||
<FormHelperText> | ||
<HelperText> | ||
<HelperTextItem | ||
icon={<ExclamationCircleIcon />} | ||
variant={fieldState.invalid ? "error" : "success"} | ||
<FlexItem> | ||
<Controller | ||
name="attribute" | ||
control={control} | ||
render={({ field }) => ( | ||
<FormGroup | ||
label={"Attribute"} | ||
fieldId={"rekor-search-attribute"} | ||
labelIcon={ | ||
<Popover | ||
bodyContent={ | ||
inputConfigByAttribute[watchAttribute].tooltipText | ||
} | ||
position={"right"} | ||
> | ||
<button | ||
type="button" | ||
aria-label="More info for attribute field" | ||
onClick={e => e.preventDefault()} | ||
aria-describedby="attribute-info" | ||
className={styles.formGroupLabelHelp} | ||
> | ||
{fieldState.invalid | ||
? fieldState.error?.message | ||
: inputConfigByAttribute[watchAttribute].helperText} | ||
</HelperTextItem> | ||
</HelperText> | ||
</FormHelperText> | ||
)} | ||
</> | ||
)} | ||
/> | ||
</GridItem> | ||
<GridItem | ||
sm={12} | ||
md={2} | ||
<HelpIcon /> | ||
</button> | ||
</Popover> | ||
} | ||
> | ||
<FormSelect | ||
id="rekor-search-attribute" | ||
{...field} | ||
label="Attribute" | ||
> | ||
{ATTRIBUTES.map(attribute => ( | ||
<FormSelectOption | ||
label={inputConfigByAttribute[attribute].name} | ||
key={attribute} | ||
value={attribute} | ||
/> | ||
))} | ||
</FormSelect> | ||
</FormGroup> | ||
)} | ||
/> | ||
</FlexItem> | ||
</Flex> | ||
<Flex | ||
direction={{ default: "column" }} | ||
flex={{ default: "flex_3" }} | ||
> | ||
<FlexItem> | ||
<Controller | ||
name="value" | ||
control={control} | ||
rules={rules} | ||
render={({ field, fieldState }) => ( | ||
<FormGroup | ||
label={inputConfigByAttribute[watchAttribute].name} | ||
labelInfo={inputConfigByAttribute[watchAttribute].helperText} | ||
fieldId={`rekor-search-${inputConfigByAttribute[ | ||
watchAttribute | ||
].name.toLowerCase()}`} | ||
> | ||
<TextInput | ||
aria-label={`${inputConfigByAttribute[watchAttribute].name} input field`} | ||
{...field} | ||
id={`rekor-search-${inputConfigByAttribute[ | ||
watchAttribute | ||
].name.toLowerCase()}`} | ||
label={inputConfigByAttribute[watchAttribute].name} | ||
placeholder={ | ||
inputConfigByAttribute[watchAttribute].placeholder | ||
} | ||
type={ | ||
inputConfigByAttribute[watchAttribute].name === "email" | ||
? "email" | ||
: "text" | ||
} | ||
validated={fieldState.invalid ? "error" : "default"} | ||
/> | ||
{fieldState.invalid && ( | ||
<FormHelperText> | ||
<HelperText> | ||
<HelperTextItem | ||
icon={<ExclamationCircleIcon />} | ||
variant={fieldState.invalid ? "error" : "success"} | ||
> | ||
{fieldState.invalid | ||
? fieldState.error?.message | ||
: inputConfigByAttribute[watchAttribute].helperText} | ||
</HelperTextItem> | ||
</HelperText> | ||
</FormHelperText> | ||
)} | ||
</FormGroup> | ||
)} | ||
/> | ||
</FlexItem> | ||
</Flex> | ||
<Flex | ||
direction={{ default: "column" }} | ||
alignSelf={{ default: "alignSelfFlexStart" }} | ||
flex={{ default: "flex_1" }} | ||
> | ||
<Button | ||
variant="primary" | ||
id="search-form-button" | ||
isBlock={true} | ||
isLoading={isLoading} | ||
type="submit" | ||
spinnerAriaLabel={"Loading"} | ||
spinnerAriaLabelledBy={"search-form-button"} | ||
> | ||
Search | ||
</Button> | ||
</GridItem> | ||
</Grid> | ||
<FlexItem style={{ marginTop: "2em" }}> | ||
<Button | ||
variant="primary" | ||
id="search-form-button" | ||
isBlock={true} | ||
isLoading={isLoading} | ||
type="submit" | ||
spinnerAriaLabel={"Loading"} | ||
spinnerAriaLabelledBy={"search-form-button"} | ||
> | ||
Search | ||
</Button> | ||
</FlexItem> | ||
</Flex> | ||
</Flex> | ||
</Form> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters