Skip to content

Commit

Permalink
feat(ux): add dynamic labels & helper text for settings + search form (
Browse files Browse the repository at this point in the history
  • Loading branch information
lance authored Mar 11, 2024
2 parents 2923195 + df9c964 commit 9ba1464
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 84 deletions.
221 changes: 143 additions & 78 deletions src/modules/components/SearchForm.tsx
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;
Expand All @@ -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&apos;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,
Expand All @@ -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.</>,
},
};

Expand Down Expand Up @@ -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>
);
}
20 changes: 19 additions & 1 deletion src/modules/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
TextInput,
Form,
FormGroup,
Popover,
} from "@patternfly/react-core";
import { HelpIcon } from "@patternfly/react-icons";
import styles from "@patternfly/react-styles/css/components/Form/form";

export function Settings({
open,
Expand Down Expand Up @@ -65,6 +68,19 @@ export function Settings({
<Form id="settings-form">
<FormGroup
label="Override Rekor Endpoint"
labelIcon={
<Popover bodyContent={"Specify your private Rekor endpoint URL."}>
<button
type="button"
aria-label="More info for name field"
onClick={e => e.preventDefault()}
aria-describedby="form-group-label-info"
className={styles.formGroupLabelHelp}
>
<HelpIcon />
</button>
</Popover>
}
isRequired
fieldId="rekor-endpoint-override"
>
Expand All @@ -73,7 +89,9 @@ export function Settings({
type="text"
onChange={handleChangeBaseUrl}
placeholder={
baseUrl === undefined ? "https://rekor.sigstore.dev" : baseUrl
baseUrl === undefined
? "https://private.rekor.example.com"
: baseUrl
}
aria-label="override rekor endpoint"
/>
Expand Down
8 changes: 3 additions & 5 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CogIcon, GithubIcon } from "@patternfly/react-icons";
import Link from "next/link";
import Image from "next/image";
import NOSSRWrapper from "../modules/utils/noSSR";
import logo from "/public/Logo-Red_Hat-Trusted_Artifact_Signer-A-Reverse-RGB.svg";

const Home: NextPage = () => {
const [settingsOpen, setSettingsOpen] = useState(false);
Expand All @@ -31,12 +32,9 @@ const Home: NextPage = () => {
header={
<Masthead>
<MastheadMain>
<Link
href={"/"}
className={"pf-v5-c-masthead_brand"}
>
<Link href={"/"}>
<Image
src={"/Logo-Red_Hat-Trusted_Artifact_Signer-A-Reverse-RGB.svg"}
src={logo}
alt={"Red Hat Trusted Artifact Signer logo"}
priority={true}
width={127}
Expand Down

0 comments on commit 9ba1464

Please sign in to comment.