Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove formik Field components from UI layer #1064

Merged
merged 2 commits into from
Jul 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/components/form/fields/CheckboxField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { FieldAttributes } from 'formik'
import { Field } from 'formik'

import type { CheckboxProps } from '@oxide/ui'
import { Checkbox } from '@oxide/ui'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CheckboxFieldProps = CheckboxProps & Omit<FieldAttributes<any>, 'type'>

/** Formik Field version of Checkbox */
export const CheckboxField = (props: CheckboxFieldProps) => (
<Field type="checkbox" as={Checkbox} {...props} />
)
4 changes: 2 additions & 2 deletions app/components/form/fields/ListboxField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cn from 'classnames'
import { useField } from 'formik'

import { FieldLabel, Listbox, TextFieldHint } from '@oxide/ui'
import { FieldLabel, Listbox, TextInputHint } from '@oxide/ui'

export type ListboxFieldProps = {
name: string
Expand Down Expand Up @@ -35,7 +35,7 @@ export function ListboxField({
<FieldLabel id={`${id}-label`} tip={description} optional={!required}>
{label}
</FieldLabel>
{helpText && <TextFieldHint id={`${id}-help-text`}>{helpText}</TextFieldHint>}
{helpText && <TextInputHint id={`${id}-help-text`}>{helpText}</TextInputHint>}
</div>
<Listbox
defaultValue={value}
Expand Down
4 changes: 2 additions & 2 deletions app/components/form/fields/RadioField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cn from 'classnames'

import type { RadioGroupProps } from '@oxide/ui'
import { FieldLabel, RadioGroup, TextFieldHint } from '@oxide/ui'
import { FieldLabel, RadioGroup, TextInputHint } from '@oxide/ui'

// TODO: Centralize these docstrings perhaps on the `FieldLabel` component?
export interface RadioFieldProps extends Omit<RadioGroupProps, 'name'> {
Expand Down Expand Up @@ -48,7 +48,7 @@ export function RadioField({
</FieldLabel>
)}
{/* TODO: Figure out where this hint field def should live */}
{helpText && <TextFieldHint id={`${id}-help-text`}>{helpText}</TextFieldHint>}
{helpText && <TextInputHint id={`${id}-help-text`}>{helpText}</TextInputHint>}
</div>
<RadioGroup
name={name}
Expand Down
4 changes: 2 additions & 2 deletions app/components/form/fields/TagsField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, FieldLabel, TextFieldHint } from '@oxide/ui'
import { Button, FieldLabel, TextInputHint } from '@oxide/ui'
import { capitalize } from '@oxide/util'

export interface TagsFieldProps {
Expand All @@ -17,7 +17,7 @@ export function TagsField(props: TagsFieldProps) {
{title || capitalize(name)}
</FieldLabel>
{/* TODO: Should TextFieldHint be grouped with FieldLabel? */}
{hint && <TextFieldHint id={`${id}-hint`}>{hint}</TextFieldHint>}
{hint && <TextInputHint id={`${id}-hint`}>{hint}</TextInputHint>}
<Button
variant="default"
size="sm"
Expand Down
28 changes: 17 additions & 11 deletions app/components/form/fields/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import cn from 'classnames'
import type { FieldValidator } from 'formik'
import { useField } from 'formik'

import type {
TextAreaProps as UITextAreaProps,
TextFieldBaseProps as UITextFieldProps,
TextInputBaseProps as UITextFieldProps,
} from '@oxide/ui'
import { TextFieldError } from '@oxide/ui'
import { TextFieldHint } from '@oxide/ui'
import { FieldLabel, TextField as UITextField } from '@oxide/ui'
import { TextInputError } from '@oxide/ui'
import { TextInputHint } from '@oxide/ui'
import { FieldLabel, TextInput as UITextField } from '@oxide/ui'
import { capitalize } from '@oxide/util'

import { useFieldError } from '../../../hooks/useFieldError'

export interface TextFieldProps extends UITextFieldProps {
id: string
/** Will default to id if not provided */
name?: string
/** HTML type attribute, defaults to text */
type?: string
Comment on lines +18 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this configurable so we could pass down number, etc

/** Will default to name if not provided */
label?: string
/**
Expand All @@ -35,37 +37,41 @@ export interface TextFieldProps extends UITextFieldProps {
description?: string
placeholder?: string
units?: string
validate?: FieldValidator
}

export function TextField({
id,
name = id,
type = 'text',
label = capitalize(name),
units,
validate,
...props
}: TextFieldProps & UITextAreaProps) {
const { description, helpText, required } = props
const error = useFieldError(name)
const [field, meta] = useField({ name, validate, type })
return (
<div className="max-w-lg">
<div className="mb-2">
<FieldLabel id={`${id}-label`} tip={description} optional={!required}>
{label} {units && <span className="ml-1 text-secondary">({units})</span>}
</FieldLabel>
</div>
{helpText && <TextFieldHint id={`${id}-help-text`}>{helpText}</TextFieldHint>}
{helpText && <TextInputHint id={`${id}-help-text`}>{helpText}</TextInputHint>}
<UITextField
id={id}
name={name}
title={label}
error={!!error}
type={type}
error={!!meta.error}
aria-labelledby={cn(`${id}-label`, {
[`${id}-help-text`]: !!description,
})}
aria-describedby={description ? `${id}-label-tip` : undefined}
{...props}
{...field}
/>
<TextFieldError name={name} />
<TextInputError>{meta.error}</TextInputError>
</div>
)
}
1 change: 1 addition & 0 deletions app/components/form/fields/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './CheckboxField'
export * from './DescriptionField'
export * from './DiskSizeField'
export * from './DisksTableField'
Expand Down
139 changes: 50 additions & 89 deletions app/forms/firewall-rules-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,18 @@ import {
useApiQueryClient,
} from '@oxide/api'
import type { ErrorResponse, VpcFirewallRule, VpcFirewallRuleUpdate } from '@oxide/api'
import { Button, Delete10Icon, Divider, Radio, Table } from '@oxide/ui'

import {
Button,
CheckboxField,
Delete10Icon,
Divider,
FieldLabel,
NumberTextField,
Radio,
RadioGroup,
Table,
DescriptionField,
Form,
ListboxField,
NameField,
RadioField,
SideModalForm,
TextField,
TextFieldError,
TextFieldHint,
} from '@oxide/ui'

import { Form, ListboxField, SideModalForm } from 'app/components/form'
} from 'app/components/form'
import { useParams } from 'app/hooks'

import type { CreateSideModalFormProps } from '.'
Expand Down Expand Up @@ -104,43 +100,20 @@ export const CommonFields = ({ error }: { error: ErrorResponse | null }) => {
{/* omitting value prop makes it a boolean value. beautiful */}
{/* TODO: better text or heading or tip or something on this checkbox */}
<CheckboxField name="enabled">Enabled</CheckboxField>
<div className="space-y-0.5">
<FieldLabel id="rule-name-label" htmlFor="rule-name">
Name
</FieldLabel>
<TextField id="rule-name" name="name" />
</div>
<div className="space-y-0.5">
<FieldLabel id="rule-description-label" htmlFor="rule-description">
Description {/* TODO: indicate optional */}
</FieldLabel>
<TextField id="rule-description" name="description" />
</div>
<NameField id="rule-name" />
<DescriptionField id="rule-description" />

<Divider />

<div className="space-y-0.5">
<FieldLabel id="priority-label" htmlFor="priority">
Priority
</FieldLabel>
<TextFieldHint id="priority-hint">Must be 0&ndash;65535</TextFieldHint>
<NumberTextField id="priority" name="priority" aria-describedby="priority-hint" />
<TextFieldError name="priority" />
</div>
<fieldset>
<legend>Action</legend>
<RadioGroup column name="action">
<Radio value="allow">Allow</Radio>
<Radio value="deny">Deny</Radio>
</RadioGroup>
</fieldset>
<fieldset>
<legend>Direction of traffic</legend>
<RadioGroup column name="direction">
<Radio value="inbound">Incoming</Radio>
<Radio value="outbound">Outgoing</Radio>
</RadioGroup>
</fieldset>
<TextField type="number" id="priority" helpText="Must be 0&ndash;65535" />
<RadioField id="action" label="Action" column>
<Radio value="allow">Allow</Radio>
<Radio value="deny">Deny</Radio>
</RadioField>
<RadioField id="direction" label="Direction of traffic" column>
<Radio value="inbound">Incoming</Radio>
<Radio value="outbound">Outgoing</Radio>
</RadioField>

<Divider />

Expand All @@ -155,12 +128,9 @@ export const CommonFields = ({ error }: { error: ErrorResponse | null }) => {
{ value: 'instance', label: 'Instance' },
]}
/>
<div className="space-y-0.5">
<FieldLabel id="targetValue-label" htmlFor="targetValue">
Target name
</FieldLabel>
<TextField id="targetValue" name="targetValue" />
</div>
{/* TODO: This is set as optional which is kind of wrong. This section represents an inlined
subform which means it likely should be a custom field */}
<NameField id="targetValue" name="targetValue" label="Target name" required={false} />

<div className="flex justify-end">
{/* TODO does this clear out the form or the existing targets? */}
Expand Down Expand Up @@ -238,19 +208,15 @@ export const CommonFields = ({ error }: { error: ErrorResponse | null }) => {
{ value: 'internet_gateway', label: 'Internet Gateway' },
]}
/>
<div className="space-y-0.5">
{/* For everything but IP this is a name, but for IP it's an IP.
{/* For everything but IP this is a name, but for IP it's an IP.
So we should probably have the label on this field change when the
host type changes. Also need to confirm that it's just an IP and
not a block. */}
<FieldLabel id="hostValue-label" htmlFor="hostValue">
Value
</FieldLabel>
<TextFieldHint id="hostValue-hint">
For IP, an address. For the rest, a name. [TODO: copy]
</TextFieldHint>
<TextField id="hostValue" name="hostValue" aria-describedby="hostValue-hint" />
</div>
<TextField
id="hostValue"
label="Value"
helpText="For IP, an address. For the rest, a name. [TODO: copy]"
/>

<div className="flex justify-end">
<Button variant="ghost" color="secondary" className="mr-2.5">
Expand Down Expand Up @@ -314,32 +280,27 @@ export const CommonFields = ({ error }: { error: ErrorResponse | null }) => {

<Divider />

<div className="space-y-0.5">
<FieldLabel id="portRange-label" htmlFor="portRange">
Port filter
</FieldLabel>
<TextFieldHint id="portRange-hint">
A single port (1234) or a range (1234-2345)
</TextFieldHint>
<TextField id="portRange" name="portRange" aria-describedby="portRange-hint" />
<TextFieldError name="portRange" />
<div className="flex justify-end">
<Button variant="ghost" color="secondary" className="mr-2.5">
Clear
</Button>
<Button
variant="default"
onClick={() => {
const portRange = values.portRange.trim()
// TODO: show error instead of ignoring the click
if (!parsePortRange(portRange)) return
setFieldValue('ports', [...values.ports, portRange])
setFieldValue('portRange', '')
}}
>
Add port filter
</Button>
</div>
<TextField
id="portRange"
label="Port filter"
helpText="A single port (1234) or a range (1234-2345)"
/>
<div className="flex justify-end">
<Button variant="ghost" color="secondary" className="mr-2.5">
Clear
</Button>
<Button
variant="default"
onClick={() => {
const portRange = values.portRange.trim()
// TODO: show error instead of ignoring the click
if (!parsePortRange(portRange)) return
setFieldValue('ports', [...values.ports, portRange])
setFieldValue('portRange', '')
}}
>
Add port filter
</Button>
</div>
<Table className="w-full">
<Table.Header>
Expand Down
18 changes: 9 additions & 9 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
Success16Icon,
Tab,
Tabs,
TextFieldHint,
TextInputHint,
} from '@oxide/ui'
import { GiB } from '@oxide/util'

Expand Down Expand Up @@ -182,40 +182,40 @@ export default function CreateInstanceForm({
<Tabs id="choose-cpu-ram" fullWidth aria-labelledby="hardware">
<Tab>General Purpose</Tab>
<Tab.Panel>
<TextFieldHint id="hw-gp-help-text" className="mb-12 max-w-xl text-sans-md">
<TextInputHint id="hw-gp-help-text" className="mb-12 max-w-xl text-sans-md">
General purpose instances provide a good balance of CPU, memory, and high
performance storage; well suited for a wide range of use cases.
</TextFieldHint>
</TextInputHint>
<RadioField id="hw-general-purpose" name="type" label="">
{renderLargeRadioCards('general')}
</RadioField>
</Tab.Panel>

<Tab>CPU Optimized</Tab>
<Tab.Panel>
<TextFieldHint id="hw-cpu-help-text" className="mb-12 max-w-xl text-sans-md">
<TextInputHint id="hw-cpu-help-text" className="mb-12 max-w-xl text-sans-md">
CPU optimized instances provide a good balance of...
</TextFieldHint>
</TextInputHint>
<RadioField id="hw-cpu-optimized" name="type" label="">
{renderLargeRadioCards('cpuOptimized')}
</RadioField>
</Tab.Panel>

<Tab>Memory optimized</Tab>
<Tab.Panel>
<TextFieldHint id="hw-mem-help-text" className="mb-12 max-w-xl text-sans-md">
<TextInputHint id="hw-mem-help-text" className="mb-12 max-w-xl text-sans-md">
CPU optimized instances provide a good balance of...
</TextFieldHint>
</TextInputHint>
<RadioField id="hw-mem-optimized" name="type" label="">
{renderLargeRadioCards('memoryOptimized')}
</RadioField>
</Tab.Panel>

<Tab>Custom</Tab>
<Tab.Panel>
<TextFieldHint id="hw-custom-help-text" className="mb-12 max-w-xl text-sans-md">
<TextInputHint id="hw-custom-help-text" className="mb-12 max-w-xl text-sans-md">
Custom instances...
</TextFieldHint>
</TextInputHint>
<RadioField id="hw-custom" name="type" label="">
{renderLargeRadioCards('custom')}
</RadioField>
Expand Down
Loading