Skip to content

Commit 1b67a4b

Browse files
authored
Remove formik Field components from UI layer (#1064)
* Remove formik Field components from UI layer * Nix the formik decorator
1 parent 734b5ce commit 1b67a4b

17 files changed

+121
-190
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { FieldAttributes } from 'formik'
2+
import { Field } from 'formik'
3+
4+
import type { CheckboxProps } from '@oxide/ui'
5+
import { Checkbox } from '@oxide/ui'
6+
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
type CheckboxFieldProps = CheckboxProps & Omit<FieldAttributes<any>, 'type'>
9+
10+
/** Formik Field version of Checkbox */
11+
export const CheckboxField = (props: CheckboxFieldProps) => (
12+
<Field type="checkbox" as={Checkbox} {...props} />
13+
)

app/components/form/fields/ListboxField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cn from 'classnames'
22
import { useField } from 'formik'
33

4-
import { FieldLabel, Listbox, TextFieldHint } from '@oxide/ui'
4+
import { FieldLabel, Listbox, TextInputHint } from '@oxide/ui'
55

66
export type ListboxFieldProps = {
77
name: string
@@ -35,7 +35,7 @@ export function ListboxField({
3535
<FieldLabel id={`${id}-label`} tip={description} optional={!required}>
3636
{label}
3737
</FieldLabel>
38-
{helpText && <TextFieldHint id={`${id}-help-text`}>{helpText}</TextFieldHint>}
38+
{helpText && <TextInputHint id={`${id}-help-text`}>{helpText}</TextInputHint>}
3939
</div>
4040
<Listbox
4141
defaultValue={value}

app/components/form/fields/RadioField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cn from 'classnames'
22

33
import type { RadioGroupProps } from '@oxide/ui'
4-
import { FieldLabel, RadioGroup, TextFieldHint } from '@oxide/ui'
4+
import { FieldLabel, RadioGroup, TextInputHint } from '@oxide/ui'
55

66
// TODO: Centralize these docstrings perhaps on the `FieldLabel` component?
77
export interface RadioFieldProps extends Omit<RadioGroupProps, 'name'> {
@@ -48,7 +48,7 @@ export function RadioField({
4848
</FieldLabel>
4949
)}
5050
{/* TODO: Figure out where this hint field def should live */}
51-
{helpText && <TextFieldHint id={`${id}-help-text`}>{helpText}</TextFieldHint>}
51+
{helpText && <TextInputHint id={`${id}-help-text`}>{helpText}</TextInputHint>}
5252
</div>
5353
<RadioGroup
5454
name={name}

app/components/form/fields/TagsField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, FieldLabel, TextFieldHint } from '@oxide/ui'
1+
import { Button, FieldLabel, TextInputHint } from '@oxide/ui'
22
import { capitalize } from '@oxide/util'
33

44
export interface TagsFieldProps {
@@ -17,7 +17,7 @@ export function TagsField(props: TagsFieldProps) {
1717
{title || capitalize(name)}
1818
</FieldLabel>
1919
{/* TODO: Should TextFieldHint be grouped with FieldLabel? */}
20-
{hint && <TextFieldHint id={`${id}-hint`}>{hint}</TextFieldHint>}
20+
{hint && <TextInputHint id={`${id}-hint`}>{hint}</TextInputHint>}
2121
<Button
2222
variant="default"
2323
size="sm"

app/components/form/fields/TextField.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import cn from 'classnames'
2+
import type { FieldValidator } from 'formik'
3+
import { useField } from 'formik'
24

35
import type {
46
TextAreaProps as UITextAreaProps,
5-
TextFieldBaseProps as UITextFieldProps,
7+
TextInputBaseProps as UITextFieldProps,
68
} from '@oxide/ui'
7-
import { TextFieldError } from '@oxide/ui'
8-
import { TextFieldHint } from '@oxide/ui'
9-
import { FieldLabel, TextField as UITextField } from '@oxide/ui'
9+
import { TextInputError } from '@oxide/ui'
10+
import { TextInputHint } from '@oxide/ui'
11+
import { FieldLabel, TextInput as UITextField } from '@oxide/ui'
1012
import { capitalize } from '@oxide/util'
1113

12-
import { useFieldError } from '../../../hooks/useFieldError'
13-
1414
export interface TextFieldProps extends UITextFieldProps {
1515
id: string
1616
/** Will default to id if not provided */
1717
name?: string
18+
/** HTML type attribute, defaults to text */
19+
type?: string
1820
/** Will default to name if not provided */
1921
label?: string
2022
/**
@@ -35,37 +37,41 @@ export interface TextFieldProps extends UITextFieldProps {
3537
description?: string
3638
placeholder?: string
3739
units?: string
40+
validate?: FieldValidator
3841
}
3942

4043
export function TextField({
4144
id,
4245
name = id,
46+
type = 'text',
4347
label = capitalize(name),
4448
units,
49+
validate,
4550
...props
4651
}: TextFieldProps & UITextAreaProps) {
4752
const { description, helpText, required } = props
48-
const error = useFieldError(name)
53+
const [field, meta] = useField({ name, validate, type })
4954
return (
5055
<div className="max-w-lg">
5156
<div className="mb-2">
5257
<FieldLabel id={`${id}-label`} tip={description} optional={!required}>
5358
{label} {units && <span className="ml-1 text-secondary">({units})</span>}
5459
</FieldLabel>
5560
</div>
56-
{helpText && <TextFieldHint id={`${id}-help-text`}>{helpText}</TextFieldHint>}
61+
{helpText && <TextInputHint id={`${id}-help-text`}>{helpText}</TextInputHint>}
5762
<UITextField
5863
id={id}
59-
name={name}
6064
title={label}
61-
error={!!error}
65+
type={type}
66+
error={!!meta.error}
6267
aria-labelledby={cn(`${id}-label`, {
6368
[`${id}-help-text`]: !!description,
6469
})}
6570
aria-describedby={description ? `${id}-label-tip` : undefined}
6671
{...props}
72+
{...field}
6773
/>
68-
<TextFieldError name={name} />
74+
<TextInputError>{meta.error}</TextInputError>
6975
</div>
7076
)
7177
}

app/components/form/fields/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './CheckboxField'
12
export * from './DescriptionField'
23
export * from './DiskSizeField'
34
export * from './DisksTableField'

app/forms/firewall-rules-create.tsx

Lines changed: 50 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,18 @@ import {
99
useApiQueryClient,
1010
} from '@oxide/api'
1111
import type { ErrorResponse, VpcFirewallRule, VpcFirewallRuleUpdate } from '@oxide/api'
12+
import { Button, Delete10Icon, Divider, Radio, Table } from '@oxide/ui'
13+
1214
import {
13-
Button,
1415
CheckboxField,
15-
Delete10Icon,
16-
Divider,
17-
FieldLabel,
18-
NumberTextField,
19-
Radio,
20-
RadioGroup,
21-
Table,
16+
DescriptionField,
17+
Form,
18+
ListboxField,
19+
NameField,
20+
RadioField,
21+
SideModalForm,
2222
TextField,
23-
TextFieldError,
24-
TextFieldHint,
25-
} from '@oxide/ui'
26-
27-
import { Form, ListboxField, SideModalForm } from 'app/components/form'
23+
} from 'app/components/form'
2824
import { useParams } from 'app/hooks'
2925

3026
import type { CreateSideModalFormProps } from '.'
@@ -104,43 +100,20 @@ export const CommonFields = ({ error }: { error: ErrorResponse | null }) => {
104100
{/* omitting value prop makes it a boolean value. beautiful */}
105101
{/* TODO: better text or heading or tip or something on this checkbox */}
106102
<CheckboxField name="enabled">Enabled</CheckboxField>
107-
<div className="space-y-0.5">
108-
<FieldLabel id="rule-name-label" htmlFor="rule-name">
109-
Name
110-
</FieldLabel>
111-
<TextField id="rule-name" name="name" />
112-
</div>
113-
<div className="space-y-0.5">
114-
<FieldLabel id="rule-description-label" htmlFor="rule-description">
115-
Description {/* TODO: indicate optional */}
116-
</FieldLabel>
117-
<TextField id="rule-description" name="description" />
118-
</div>
103+
<NameField id="rule-name" />
104+
<DescriptionField id="rule-description" />
119105

120106
<Divider />
121107

122-
<div className="space-y-0.5">
123-
<FieldLabel id="priority-label" htmlFor="priority">
124-
Priority
125-
</FieldLabel>
126-
<TextFieldHint id="priority-hint">Must be 0&ndash;65535</TextFieldHint>
127-
<NumberTextField id="priority" name="priority" aria-describedby="priority-hint" />
128-
<TextFieldError name="priority" />
129-
</div>
130-
<fieldset>
131-
<legend>Action</legend>
132-
<RadioGroup column name="action">
133-
<Radio value="allow">Allow</Radio>
134-
<Radio value="deny">Deny</Radio>
135-
</RadioGroup>
136-
</fieldset>
137-
<fieldset>
138-
<legend>Direction of traffic</legend>
139-
<RadioGroup column name="direction">
140-
<Radio value="inbound">Incoming</Radio>
141-
<Radio value="outbound">Outgoing</Radio>
142-
</RadioGroup>
143-
</fieldset>
108+
<TextField type="number" id="priority" helpText="Must be 0&ndash;65535" />
109+
<RadioField id="action" label="Action" column>
110+
<Radio value="allow">Allow</Radio>
111+
<Radio value="deny">Deny</Radio>
112+
</RadioField>
113+
<RadioField id="direction" label="Direction of traffic" column>
114+
<Radio value="inbound">Incoming</Radio>
115+
<Radio value="outbound">Outgoing</Radio>
116+
</RadioField>
144117

145118
<Divider />
146119

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

165135
<div className="flex justify-end">
166136
{/* TODO does this clear out the form or the existing targets? */}
@@ -238,19 +208,15 @@ export const CommonFields = ({ error }: { error: ErrorResponse | null }) => {
238208
{ value: 'internet_gateway', label: 'Internet Gateway' },
239209
]}
240210
/>
241-
<div className="space-y-0.5">
242-
{/* For everything but IP this is a name, but for IP it's an IP.
211+
{/* For everything but IP this is a name, but for IP it's an IP.
243212
So we should probably have the label on this field change when the
244213
host type changes. Also need to confirm that it's just an IP and
245214
not a block. */}
246-
<FieldLabel id="hostValue-label" htmlFor="hostValue">
247-
Value
248-
</FieldLabel>
249-
<TextFieldHint id="hostValue-hint">
250-
For IP, an address. For the rest, a name. [TODO: copy]
251-
</TextFieldHint>
252-
<TextField id="hostValue" name="hostValue" aria-describedby="hostValue-hint" />
253-
</div>
215+
<TextField
216+
id="hostValue"
217+
label="Value"
218+
helpText="For IP, an address. For the rest, a name. [TODO: copy]"
219+
/>
254220

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

315281
<Divider />
316282

317-
<div className="space-y-0.5">
318-
<FieldLabel id="portRange-label" htmlFor="portRange">
319-
Port filter
320-
</FieldLabel>
321-
<TextFieldHint id="portRange-hint">
322-
A single port (1234) or a range (1234-2345)
323-
</TextFieldHint>
324-
<TextField id="portRange" name="portRange" aria-describedby="portRange-hint" />
325-
<TextFieldError name="portRange" />
326-
<div className="flex justify-end">
327-
<Button variant="ghost" color="secondary" className="mr-2.5">
328-
Clear
329-
</Button>
330-
<Button
331-
variant="default"
332-
onClick={() => {
333-
const portRange = values.portRange.trim()
334-
// TODO: show error instead of ignoring the click
335-
if (!parsePortRange(portRange)) return
336-
setFieldValue('ports', [...values.ports, portRange])
337-
setFieldValue('portRange', '')
338-
}}
339-
>
340-
Add port filter
341-
</Button>
342-
</div>
283+
<TextField
284+
id="portRange"
285+
label="Port filter"
286+
helpText="A single port (1234) or a range (1234-2345)"
287+
/>
288+
<div className="flex justify-end">
289+
<Button variant="ghost" color="secondary" className="mr-2.5">
290+
Clear
291+
</Button>
292+
<Button
293+
variant="default"
294+
onClick={() => {
295+
const portRange = values.portRange.trim()
296+
// TODO: show error instead of ignoring the click
297+
if (!parsePortRange(portRange)) return
298+
setFieldValue('ports', [...values.ports, portRange])
299+
setFieldValue('portRange', '')
300+
}}
301+
>
302+
Add port filter
303+
</Button>
343304
</div>
344305
<Table className="w-full">
345306
<Table.Header>

app/forms/instance-create.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
Success16Icon,
1818
Tab,
1919
Tabs,
20-
TextFieldHint,
20+
TextInputHint,
2121
} from '@oxide/ui'
2222
import { GiB } from '@oxide/util'
2323

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

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

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

214214
<Tab>Custom</Tab>
215215
<Tab.Panel>
216-
<TextFieldHint id="hw-custom-help-text" className="mb-12 max-w-xl text-sans-md">
216+
<TextInputHint id="hw-custom-help-text" className="mb-12 max-w-xl text-sans-md">
217217
Custom instances...
218-
</TextFieldHint>
218+
</TextInputHint>
219219
<RadioField id="hw-custom" name="type" label="">
220220
{renderLargeRadioCards('custom')}
221221
</RadioField>

0 commit comments

Comments
 (0)