Forms on ReUI controls: radio-group installed, native checkboxes and radios replaced

Radios (PanelForm) is ReUI's RadioGroup; Check takes rich text; a bare Tick covers
the table-cell ticks and the list-row switch. Settings IPv6 mode, Change Theme and all
Zones modals no longer draw their own native inputs. Matrix and the data-grid's own
selection stay as they are.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-30 15:06:22 +07:00
1 parent 7f60dc9960
commit d2b05a1037
22 files changed
+458 -493

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { cn } from "@/lib/utils"
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return (
<RadioGroupPrimitive
data-slot="radio-group"
className={cn("grid w-full gap-2", className)}
{...props}
/>
)
}
function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
return (
<RadioPrimitive.Root
data-slot="radio-group-item"
className={cn(
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
className
)}
{...props}
>
<RadioPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center"
>
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
</RadioPrimitive.Indicator>
</RadioPrimitive.Root>
)
}
export { RadioGroup, RadioGroupItem }
+22 -21
View File
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Button } from '../../ui/Button' import { Button } from '../../ui/Button'
import { Tick } from '../../ui/Tick'
import { Dialog } from '../../ui/Dialog' import { Dialog } from '../../ui/Dialog'
import { SectionHeader } from '../../ui/SectionHeader' import { SectionHeader } from '../../ui/SectionHeader'
import { SectionIndex } from '../../ui/SectionIndex' import { SectionIndex } from '../../ui/SectionIndex'
@@ -564,31 +565,31 @@ function PermissionsTable({
<tr key={f.name}> <tr key={f.name}>
<td className={styles.who}>{f.name}</td> <td className={styles.who}>{f.name}</td>
<td> <td>
<input <span className={styles.chkPerm}>
type="checkbox" <Tick
className={styles.chkPerm} label={`${section} · ${f.name} · View`}
aria-label={`${section} · ${f.name} · View`} checked={f.canView}
checked={f.canView} onChange={(v) => set(f, { canView: v })}
onChange={(e) => set(f, { canView: e.target.checked })} />
/> </span>
</td> </td>
<td> <td>
<input <span className={styles.chkPerm}>
type="checkbox" <Tick
className={styles.chkPerm} label={`${section} · ${f.name} · Modify`}
aria-label={`${section} · ${f.name} · Modify`} checked={f.canModify}
checked={f.canModify} onChange={(v) => set(f, { canModify: v })}
onChange={(e) => set(f, { canModify: e.target.checked })} />
/> </span>
</td> </td>
<td> <td>
<input <span className={styles.chkPerm}>
type="checkbox" <Tick
className={styles.chkPerm} label={`${section} · ${f.name} · Delete`}
aria-label={`${section} · ${f.name} · Delete`} checked={f.canDelete}
checked={f.canDelete} onChange={(v) => set(f, { canDelete: v })}
onChange={(e) => set(f, { canDelete: e.target.checked })} />
/> </span>
</td> </td>
<td className={styles.tdel}> <td className={styles.tdel}>
<Button onClick={() => onChange(rows.filter((x) => x.name !== f.name))}>Remove</Button> <Button onClick={() => onChange(rows.filter((x) => x.name !== f.name))}>Remove</Button>
+1 -1
View File
@@ -63,7 +63,7 @@ row's name goes in `aria-label` instead, as the table checkboxes of Zones do.
It takes BOTH classes `Check` gives its own DOM: `check` on the label (the 26 px It takes BOTH classes `Check` gives its own DOM: `check` on the label (the 26 px
target and the input's alignment) and `toggle` for the switch drawing. target and the input's alignment) and `toggle` for the switch drawing.
*/ */
.switch { composes: check toggle from '../../ui/Check.module.css'; } .switch { composes: check from '../../ui/Check.module.css'; }
.name { font-weight: 600; } .name { font-weight: 600; }
/* The URL wraps after its separators (`ui/UrlText`), not at whatever letter meets the /* The URL wraps after its separators (`ui/UrlText`), not at whatever letter meets the
+4 -3
View File
@@ -9,6 +9,7 @@ import { DataTable } from '../../ui/DataTable'
import { RowAction } from '../../ui/Table' import { RowAction } from '../../ui/Table'
import { Field, Input, Select } from '../../ui/Field' import { Field, Input, Select } from '../../ui/Field'
import { Button } from '../../ui/Button' import { Button } from '../../ui/Button'
import { Tick } from '../../ui/Tick'
import { PermissionButton } from '../../ui/PermissionButton' import { PermissionButton } from '../../ui/PermissionButton'
import { Confirm } from '../../ui/Confirm' import { Confirm } from '../../ui/Confirm'
import { Notifier } from '../../ui/Notifier' import { Notifier } from '../../ui/Notifier'
@@ -768,9 +769,9 @@ export function BlockLists({
return ( return (
canToggle(l) && ( canToggle(l) && (
<label className={styles.switch}> <label className={styles.switch}>
<input <Tick
type="checkbox" toggle
aria-label={`Enabled ${what}`} label={`Enabled ${what}`}
checked={l.enabled} checked={l.enabled}
disabled={off} disabled={off}
onChange={() => setLines((all) => all.map((x, j) => (j === i ? toggleLine(x) : x)))} onChange={() => setLines((all) => all.map((x, j) => (j === i ? toggleLine(x) : x)))}
+2 -2
View File
@@ -25,7 +25,7 @@ describe('Change Theme (index.html:3818-3862)', () => {
open() open()
const dialog = screen.getByRole('dialog', { name: 'Change Theme' }) const dialog = screen.getByRole('dialog', { name: 'Change Theme' })
const group = within(dialog).getByRole('radiogroup', { name: 'Theme' }) const group = within(dialog).getByRole('radiogroup', { name: 'Theme' })
expect(within(group).getAllByRole('radio').map((r) => r.closest('label')?.textContent?.trim())).toEqual([ expect(within(group).getAllByRole('radio').map((r) => r.getAttribute('aria-label'))).toEqual([
'Use System Theme (default)', 'Use System Theme (default)',
'Light Theme', 'Light Theme',
'Dark Theme', 'Dark Theme',
@@ -64,7 +64,7 @@ describe('Change Theme (index.html:3818-3862)', () => {
if (stored != null) localStorage.setItem('theme', stored) if (stored != null) localStorage.setItem('theme', stored)
open() open()
expect(radio(label)).toBeChecked() expect(radio(label)).toBeChecked()
expect(screen.getAllByRole('radio').filter((r) => (r as HTMLInputElement).checked)).toHaveLength(1) expect(screen.getAllByRole('radio').filter((r) => r.getAttribute('aria-checked') === 'true')).toHaveLength(1)
}) })
it('a stored amber checks none, since Amber is not offered', () => { it('a stored amber checks none, since Amber is not offered', () => {
+2 -1
View File
@@ -42,9 +42,10 @@ export function ChangeTheme({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange} title="Change Theme"> <Dialog open={open} onOpenChange={onOpenChange} title="Change Theme">
<GroupRow modal label="Theme" role="radiogroup"> <GroupRow modal label="Theme">
<Radios <Radios
name="rdChangeTheme" name="rdChangeTheme"
label="Theme"
value={checked} value={checked}
options={OPTIONS} options={OPTIONS}
onChange={(v) => setChoice(v as OfferedTheme)} onChange={(v) => setChoice(v as OfferedTheme)}
+3 -3
View File
@@ -199,13 +199,13 @@ describe('the Edit Zone File dialog', () => {
draw() draw()
let dialog = await openFromList(user) let dialog = await openFromList(user)
await user.click(await within(dialog).findByLabelText('Overwrite SOA Serial')) await user.click(await within(dialog).findByLabelText('Overwrite SOA Serial'))
expect(within(dialog).getByLabelText('Overwrite SOA Serial')).toHaveProperty('checked', true) expect(within(dialog).getByLabelText('Overwrite SOA Serial')).toBeChecked()
await user.click(within(dialog).getAllByRole('button', { name: 'Close' }).at(-1)!) await user.click(within(dialog).getAllByRole('button', { name: 'Close' }).at(-1)!)
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
dialog = await openFromList(user) dialog = await openFromList(user)
expect(await within(dialog).findByLabelText('Overwrite SOA Serial')).toHaveProperty('checked', false) expect(await within(dialog).findByLabelText('Overwrite SOA Serial')).not.toBeChecked()
}) })
it('Save sends the editor as text to zones/import with overwriteZone=true and the checkbox', async () => { it('Save sends the editor as text to zones/import with overwriteZone=true and the checkbox', async () => {
@@ -297,7 +297,7 @@ describe('Import Zone (v15.5)', () => {
const menu = await rowMenu(user, 'primary.test') const menu = await rowMenu(user, 'primary.test')
await user.click(within(menu).getByRole('menuitem', { name: 'Import Zone' })) await user.click(within(menu).getByRole('menuitem', { name: 'Import Zone' }))
const dialog = await screen.findByRole('dialog', { name: 'Import Zone - primary.test' }) const dialog = await screen.findByRole('dialog', { name: 'Import Zone - primary.test' })
await user.click(within(dialog).getByLabelText('Text Editor', { selector: 'input' })) await user.click(within(dialog).getByRole('radio', { name: 'Text Editor' }))
await user.type(within(dialog).getByRole('textbox', { name: 'Text Editor' }), 'www 3600 IN A 10.0.0.2') await user.type(within(dialog).getByRole('textbox', { name: 'Text Editor' }), 'www 3600 IN A 10.0.0.2')
const listCalls = spy.mock.calls.filter((c) => c[0] === 'zones/list').length const listCalls = spy.mock.calls.filter((c) => c[0] === 'zones/list').length
+7 -8
View File
@@ -16,6 +16,7 @@ import {
type Zone, type Zone,
} from '../../api/zones' } from '../../api/zones'
import { Button } from '../../ui/Button' import { Button } from '../../ui/Button'
import { Tick } from '../../ui/Tick'
import { Loading } from '../../ui/Empty' import { Loading } from '../../ui/Empty'
import { StaleData } from '../StaleData' import { StaleData } from '../StaleData'
import { Field, Input, Select } from '../../ui/Field' import { Field, Input, Select } from '../../ui/Field'
@@ -596,12 +597,11 @@ export function ZoneList({
th: { style: { width: 38 }, className: tbl.checkCell }, th: { style: { width: 38 }, className: tbl.checkCell },
header: ( header: (
<label> <label>
<input <Tick
disabled={stale} disabled={stale}
type="checkbox" label="Select all zones"
aria-label="Select all zones"
checked={allChecked} checked={allChecked}
onChange={(e) => setChecked(e.target.checked ? zones.map((z) => z.name) : [])} onChange={(v) => setChecked(v ? zones.map((z) => z.name) : [])}
/> />
</label> </label>
), ),
@@ -733,15 +733,14 @@ function zoneCells(p: RowProps) {
return { return {
check: ( check: (
<label> <label>
<input <Tick
type="checkbox" label={`Select ${name}`}
aria-label={`Select ${name}`}
/* With stale data there is no selecting: acting in bulk on a list /* With stale data there is no selecting: acting in bulk on a list
that no longer reflects the server is how the wrong zone gets that no longer reflects the server is how the wrong zone gets
deleted. */ deleted. */
disabled={p.stale} disabled={p.stale}
checked={p.checked} checked={p.checked}
onChange={(e) => p.onCheck(e.target.checked)} onChange={p.onCheck}
/> />
</label> </label>
), ),
-3
View File
@@ -52,9 +52,6 @@
.fields { display:flex; flex-direction:column; gap: var(--s-8); } .fields { display:flex; flex-direction:column; gap: var(--s-8); }
.inline { composes: inline from '../../ui/PanelForm.module.css'; } .inline { composes: inline from '../../ui/PanelForm.module.css'; }
.suffix { composes: trailer from '../../ui/text.module.css'; } .suffix { composes: trailer from '../../ui/text.module.css'; }
/* The same row as `ui/Check.module.css`, which is where the minimum height
lives: without it, the radios of "Add Zone", "Import" and "Convert" were 19 px. */
.chk { composes: check from '../../ui/Check.module.css'; }
/* The same role as the `.help` of Settings, DHCP and Administration: same colour /* The same role as the `.help` of Settings, DHCP and Administration: same colour
and same line height. With `--faint` it read at 4.64:1 and the others at 5.90:1. */ and same line height. With `--faint` it read at 4.64:1 and the others at 5.90:1. */
.help { font-size: var(--t-3); color:var(--mute); line-height:1.55; } .help { font-size: var(--t-3); color:var(--mute); line-height:1.55; }
+3 -3
View File
@@ -319,9 +319,9 @@ describe('modals', () => {
await user.click(await screen.findByRole('menuitem', { name: 'Convert Zone' })) await user.click(await screen.findByRole('menuitem', { name: 'Convert Zone' }))
const dialog = await screen.findByRole('dialog') const dialog = await screen.findByRole('dialog')
expect(within(dialog).getByLabelText('Primary Zone')).toHaveProperty('disabled', true) expect(within(dialog).getByRole('radio', { name: 'Primary Zone' })).toBeDisabled()
expect(within(dialog).getByLabelText('Conditional Forwarder Zone')).toHaveProperty('checked', true) expect(within(dialog).getByRole('radio', { name: 'Conditional Forwarder Zone' })).toBeChecked()
expect(within(dialog).getByLabelText('Catalog Zone')).toHaveProperty('disabled', true) expect(within(dialog).getByRole('radio', { name: 'Catalog Zone' })).toBeDisabled()
}) })
it('\"Import Zone\" alerts if no file is chosen', async () => { it('\"Import Zone\" alerts if no file is chosen', async () => {
+63 -93
View File
@@ -8,6 +8,8 @@ import {
type ZoneDetails, type ZoneDetails,
} from '../../../api/records' } from '../../../api/records'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Check } from '../../../ui/Check'
import { Radios } from '../../../ui/PanelForm'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Field, Input, Select, Textarea } from '../../../ui/Field' import { Field, Input, Select, Textarea } from '../../../ui/Field'
import { import {
@@ -277,10 +279,11 @@ export function AddEditRecord(p: AddEditRecordProps) {
{/* "Overwrite" only exists when adding. */} {/* "Overwrite" only exists when adding. */}
{!editing && ( {!editing && (
<label className={styles.chk}> <Check
<input type="checkbox" checked={f.overwrite} onChange={(e) => set('overwrite', e.target.checked)} /> label="Overwrite existing records"
Overwrite existing records checked={f.overwrite}
</label> onChange={(v) => set('overwrite', v)}
/>
)} )}
<Field label="Comments"> <Field label="Comments">
@@ -370,21 +373,19 @@ function TypeFields({ f, set, setF, apps, classes, onApp, onClass, editing }: Fi
return ( return (
<> <>
{text(f.type === 'A' ? 'IPv4 Address' : 'IPv6 Address', 'value', { mono: true })} {text(f.type === 'A' ? 'IPv4 Address' : 'IPv6 Address', 'value', { mono: true })}
<label className={styles.chk}> <Check
<input type="checkbox" checked={f.ptr} onChange={(e) => set('ptr', e.target.checked)} /> label="Add reverse (PTR) record"
Add reverse (PTR) record checked={f.ptr}
</label> onChange={(v) => set('ptr', v)}
<label className={styles.chk}> />
{/* Enabled only while the PTR box is ticked (zone.js:543-546); unticking {/* Enabled only while the PTR box is ticked (zone.js:543-546); unticking
the PTR does not untick it. */} the PTR does not untick it. */}
<input <Check
type="checkbox" label="Create reverse zone if it does not exists"
checked={f.createPtrZone} checked={f.createPtrZone}
disabled={!f.ptr} disabled={!f.ptr}
onChange={(e) => set('createPtrZone', e.target.checked)} onChange={(v) => set('createPtrZone', v)}
/> />
Create reverse zone if it does not exists
</label>
</> </>
) )
@@ -418,14 +419,11 @@ function TypeFields({ f, set, setF, apps, classes, onApp, onClass, editing }: Fi
{text('Retry', 'soaRetry', { mono: true, short: true })} {text('Retry', 'soaRetry', { mono: true, short: true })}
{text('Expire', 'soaExpire', { mono: true, short: true })} {text('Expire', 'soaExpire', { mono: true, short: true })}
{text('Minimum', 'soaMinimum', { mono: true, short: true })} {text('Minimum', 'soaMinimum', { mono: true, short: true })}
<label className={styles.chk}> <Check
<input label="Use Serial Date Scheme"
type="checkbox" checked={f.soaUseSerialDateScheme}
checked={f.soaUseSerialDateScheme} onChange={(v) => set('soaUseSerialDateScheme', v)}
onChange={(e) => set('soaUseSerialDateScheme', e.target.checked)} />
/>
Use Serial Date Scheme
</label>
</> </>
) )
@@ -463,14 +461,11 @@ function TypeFields({ f, set, setF, apps, classes, onApp, onClass, editing }: Fi
/> />
)} )}
</Field> </Field>
<label className={styles.chk}> <Check
<input label="Split text into multiple character strings"
type="checkbox" checked={f.txtSplitText}
checked={f.txtSplitText} onChange={(v) => set('txtSplitText', v)}
onChange={(e) => set('txtSplitText', e.target.checked)} />
/>
Split text into multiple character strings
</label>
</> </>
) )
@@ -629,22 +624,16 @@ MII...
</Button> </Button>
</div> </div>
</div> </div>
<label className={styles.chk}> <Check
<input label="Use Automatic IPv4 Hint"
type="checkbox" checked={f.svcbAutoIpv4Hint}
checked={f.svcbAutoIpv4Hint} onChange={(v) => set('svcbAutoIpv4Hint', v)}
onChange={(e) => set('svcbAutoIpv4Hint', e.target.checked)} />
/> <Check
Use Automatic IPv4 Hint label="Use Automatic IPv6 Hint"
</label> checked={f.svcbAutoIpv6Hint}
<label className={styles.chk}> onChange={(v) => set('svcbAutoIpv6Hint', v)}
<input />
type="checkbox"
checked={f.svcbAutoIpv6Hint}
onChange={(e) => set('svcbAutoIpv6Hint', e.target.checked)}
/>
Use Automatic IPv6 Hint
</label>
</> </>
) )
@@ -670,32 +659,21 @@ MII...
return ( return (
<> <>
<GroupRow modal label="Protocol"> <GroupRow modal label="Protocol">
{FORWARDER_PROTOCOLS.map((x) => ( <Radios
<label key={x.value} className={styles.chk}> name="recordForwarderProtocol"
<input disabled={f.forwarderThisServer}
type="radio" value={f.forwarderProtocol}
name="recordForwarderProtocol" onChange={(v) => set('forwarderProtocol', v as typeof f.forwarderProtocol)}
checked={f.forwarderProtocol === x.value} options={FORWARDER_PROTOCOLS.map((x) => ({ value: x.value, label: x.label }))}
disabled={f.forwarderThisServer} />
onChange={() => set('forwarderProtocol', x.value)}
/>
{x.label}
</label>
))}
</GroupRow> </GroupRow>
{/* `updateAddEditFormForwarderThisServer` (zone.js:5336-5356): ticked, the {/* `updateAddEditFormForwarderThisServer` (zone.js:5336-5356): ticked, the
forwarder is `this-server`, fixed, on UDP, and the proxy goes away. */} forwarder is `this-server`, fixed, on UDP, and the proxy goes away. */}
<label className={styles.chk}> <Check
<input label={'Use "This Server"'}
type="checkbox" checked={f.forwarderThisServer}
checked={f.forwarderThisServer} onChange={(on) => setF((prev) => toggleForwarderThisServer(prev, on))}
onChange={(e) => { />
const on = e.target.checked
setF((prev) => toggleForwarderThisServer(prev, on))
}}
/>
Use &quot;This Server&quot;
</label>
<div className={styles.help}> <div className={styles.help}>
When using &quot;This Server&quot;, if a record does not exists in the zone then the request is When using &quot;This Server&quot;, if a record does not exists in the zone then the request is
forwarded to the DNS Server&apos;s resolver internally. This allows you to override any record for forwarded to the DNS Server&apos;s resolver internally. This allows you to override any record for
@@ -719,27 +697,19 @@ MII...
queried before trying for forwarder with high priority value. Forwarders with the same queried before trying for forwarder with high priority value. Forwarders with the same
priority value will be queried concurrently. priority value will be queried concurrently.
</div> </div>
<label className={styles.chk}> <Check
<input label="Enable DNSSEC Validation"
type="checkbox" checked={f.forwarderDnssecValidation}
checked={f.forwarderDnssecValidation} onChange={(v) => set('forwarderDnssecValidation', v)}
onChange={(e) => set('forwarderDnssecValidation', e.target.checked)} />
/>
Enable DNSSEC Validation
</label>
{!f.forwarderThisServer && ( {!f.forwarderThisServer && (
<GroupRow modal label="Network Proxy"> <GroupRow modal label="Network Proxy">
{PROXY_TYPES.map((x) => ( <Radios
<label key={x.value} className={styles.chk}> name="recordProxyType"
<input value={f.proxyType}
type="radio" onChange={(v) => set('proxyType', v as typeof f.proxyType)}
name="recordProxyType" options={PROXY_TYPES.map((x) => ({ value: x.value, label: x.label }))}
checked={f.proxyType === x.value} />
onChange={() => set('proxyType', x.value)}
/>
{x.label}
</label>
))}
<Field label="Proxy Server Address"> <Field label="Proxy Server Address">
{(id) => ( {(id) => (
<Input <Input
+84 -115
View File
@@ -2,7 +2,9 @@ import { useEffect, useRef, useState } from 'react'
import { createZone, listCatalogs } from '../../../api/zones' import { createZone, listCatalogs } from '../../../api/zones'
import { getTsigKeyNames } from '../../../api/settings' import { getTsigKeyNames } from '../../../api/settings'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Check } from '../../../ui/Check'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Radios } from '../../../ui/PanelForm'
import { Field, Input, Select, Textarea } from '../../../ui/Field' import { Field, Input, Select, Textarea } from '../../../ui/Field'
import type { Notice } from '../types' import type { Notice } from '../types'
import styles from '../Zones.module.css' import styles from '../Zones.module.css'
@@ -167,33 +169,30 @@ export function AddZone({
</Field> </Field>
<GroupRow modal label="Zone Type"> <GroupRow modal label="Zone Type">
{ADD_TYPES.map((t) => ( {/* A link inside an option stays inside its sentence: wrapped in a span, the
<label key={t.value} className={styles.chk}> link and the brackets are text again and not three boxes, which is what
<input grants the link the target-size exception. */}
type="radio" <Radios
name="addZoneType" name="addZoneType"
checked={f.type === t.value} value={f.type}
onChange={() => changeType(t.value)} onChange={(v) => changeType(v as AddZoneKind)}
/> options={ADD_TYPES.map((t) => ({
{/* value: t.value,
`.chk` is `display:flex`, so every child of the label is an item: name: t.label,
without this `span` the link and the brackets split into three boxes label: (
("Secondary ROOT Zone ( RFC 8806 )"), and the link stopped being <span>
inside a sentence, which is what grants it the target-size {t.label}
exception. Wrapped, it is text again. {t.reference && (
*/} <>
<span> {' ('}
{t.label} <External href={t.reference.href}>{t.reference.text}</External>
{t.reference && ( {')'}
<> </>
{' ('} )}
<External href={t.reference.href}>{t.reference.text}</External> </span>
{')'} ),
</> }))}
)} />
</span>
</label>
))}
</GroupRow> </GroupRow>
{v.catalog && catalogs.length > 0 && ( {v.catalog && catalogs.length > 0 && (
@@ -213,14 +212,11 @@ export function AddZone({
{v.initializeForwarderCheckbox && ( {v.initializeForwarderCheckbox && (
<GroupRow modal label="Conditional Forwarder"> <GroupRow modal label="Conditional Forwarder">
<label className={styles.chk}> <Check
<input label="Initialize Forwarder (FWD) Record"
type="checkbox" checked={f.initializeForwarder}
checked={f.initializeForwarder} onChange={(v) => set('initializeForwarder', v)}
onChange={(e) => set('initializeForwarder', e.target.checked)} />
/>
Initialize Forwarder (FWD) Record
</label>
</GroupRow> </GroupRow>
)} )}
@@ -240,14 +236,11 @@ export function AddZone({
{v.soaSerial && ( {v.soaSerial && (
<GroupRow modal label="Zone Serial"> <GroupRow modal label="Zone Serial">
<label className={styles.chk}> <Check
<input label="Use SOA Serial Date Scheme"
type="checkbox" checked={f.useSoaSerialDateScheme}
checked={f.useSoaSerialDateScheme} onChange={(v) => set('useSoaSerialDateScheme', v)}
onChange={(e) => set('useSoaSerialDateScheme', e.target.checked)} />
/>
Use SOA Serial Date Scheme
</label>
</GroupRow> </GroupRow>
)} )}
@@ -280,17 +273,12 @@ export function AddZone({
{v.transferProtocol && ( {v.transferProtocol && (
<GroupRow modal label="Zone Transfer Protocol"> <GroupRow modal label="Zone Transfer Protocol">
{TRANSFER_PROTOCOLS.map((p) => ( <Radios
<label key={p.value} className={styles.chk}> name="addZoneTransferProtocol"
<input value={f.zoneTransferProtocol}
type="radio" onChange={(v) => set('zoneTransferProtocol', v as typeof f.zoneTransferProtocol)}
name="addZoneTransferProtocol" options={TRANSFER_PROTOCOLS.map((p) => ({ value: p.value, label: p.label }))}
checked={f.zoneTransferProtocol === p.value} />
onChange={() => set('zoneTransferProtocol', p.value)}
/>
{p.label}
</label>
))}
</GroupRow> </GroupRow>
)} )}
@@ -311,37 +299,30 @@ export function AddZone({
{v.validateZone && ( {v.validateZone && (
<GroupRow modal label="Zone Validation"> <GroupRow modal label="Zone Validation">
<label className={styles.chk}> <Check
<input label="Use ZONEMD to Validate Zone"
type="checkbox" text={
checked={f.validateZone} <>
onChange={(e) => set('validateZone', e.target.checked)} Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone
/> </>
Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone }
</label> checked={f.validateZone}
<div className={styles.help}> onChange={(v) => set('validateZone', v)}
When enabled, the secondary zone will be validated using the ZONEMD record after every help="When enabled, the secondary zone will be validated using the ZONEMD record after every zone transfer. The zone will get disabled if the validation fails. The zone must be DNSSEC signed for the validation to work."
zone transfer. The zone will get disabled if the validation fails. The zone must be DNSSEC />
signed for the validation to work.
</div>
</GroupRow> </GroupRow>
)} )}
{v.forwarderFields && ( {v.forwarderFields && (
<> <>
<GroupRow modal label="Protocol"> <GroupRow modal label="Protocol">
{FORWARDER_PROTOCOLS.map((p) => ( <Radios
<label key={p.value} className={styles.chk}> name="addZoneForwarderProtocol"
<input disabled={f.useThisServer}
type="radio" value={f.forwarderProtocol}
name="addZoneForwarderProtocol" onChange={(v) => set('forwarderProtocol', v as typeof f.forwarderProtocol)}
disabled={f.useThisServer} options={FORWARDER_PROTOCOLS.map((p) => ({ value: p.value, label: p.label }))}
checked={f.forwarderProtocol === p.value} />
onChange={() => set('forwarderProtocol', p.value)}
/>
{p.label}
</label>
))}
</GroupRow> </GroupRow>
{/* Two help paragraphs: upstream's two, one about "This Server" and {/* Two help paragraphs: upstream's two, one about "This Server" and
@@ -373,52 +354,40 @@ export function AddZone({
value={f.useThisServer ? 'this-server' : f.forwarder} value={f.useThisServer ? 'this-server' : f.forwarder}
onChange={(e) => set('forwarder', e.target.value)} onChange={(e) => set('forwarder', e.target.value)}
/> />
<label className={styles.chk}> <Check
<input label={'Use "This Server"'}
type="checkbox" checked={f.useThisServer}
checked={f.useThisServer} onChange={(on) => {
onChange={(e) => { /* `updateAddZoneFormForwarderThisServer` (zone.js:2979-2998):
/* `updateAddZoneFormForwarderThisServer` (zone.js:2979-2998): ticked, the protocol goes to UDP; unticked, the field is emptied. */
ticked, the protocol goes to UDP; unticked, the field is emptied. */ setF((prev) =>
const on = e.target.checked on
setF((prev) => ? { ...prev, useThisServer: true, forwarderProtocol: 'Udp' }
on : { ...prev, useThisServer: false, forwarder: '' },
? { ...prev, useThisServer: true, forwarderProtocol: 'Udp' } )
: { ...prev, useThisServer: false, forwarder: '' }, }}
) />
}}
/>
Use &quot;This Server&quot;
</label>
</> </>
)} )}
</Row> </Row>
<GroupRow modal label="DNSSEC"> <GroupRow modal label="DNSSEC">
<label className={styles.chk}> <Check
<input label="Enable DNSSEC Validation"
type="checkbox" checked={f.dnssecValidation}
checked={f.dnssecValidation} onChange={(v) => set('dnssecValidation', v)}
onChange={(e) => set('dnssecValidation', e.target.checked)} />
/>
Enable DNSSEC Validation
</label>
</GroupRow> </GroupRow>
{/* "this-server" takes no proxy: upstream hides the whole block. */} {/* "this-server" takes no proxy: upstream hides the whole block. */}
{!f.useThisServer && ( {!f.useThisServer && (
<GroupRow modal label="Network Proxy"> <GroupRow modal label="Network Proxy">
{PROXY_TYPES.map((p) => ( <Radios
<label key={p.value} className={styles.chk}> name="addZoneProxyType"
<input value={f.proxyType}
type="radio" onChange={(v) => set('proxyType', v as typeof f.proxyType)}
name="addZoneProxyType" options={PROXY_TYPES.map((p) => ({ value: p.value, label: p.label }))}
checked={f.proxyType === p.value} />
onChange={() => set('proxyType', p.value)}
/>
{p.label}
</label>
))}
<Field label="Proxy Server Address"> <Field label="Proxy Server Address">
{(id) => ( {(id) => (
<Input <Input
+11 -12
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { convertZone } from '../../../api/zones' import { convertZone } from '../../../api/zones'
import { Alert } from '../../../ui/Alert' import { Alert } from '../../../ui/Alert'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Radios } from '../../../ui/PanelForm'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import type { Notice } from '../types' import type { Notice } from '../types'
import styles from '../Zones.module.css' import styles from '../Zones.module.css'
@@ -111,18 +112,16 @@ export function ConvertZone({
<div className={styles.fields}> <div className={styles.fields}>
<GroupRow modal label="Convert To"> <GroupRow modal label="Convert To">
{(['Primary', 'Forwarder', 'Catalog'] as ConversionTarget[]).map((d) => ( <Radios
<label key={d} className={styles.chk}> name="convertTo"
<input value={target ?? ''}
type="radio" onChange={(v) => setTarget(v as ConversionTarget)}
name="convertTo" options={(['Primary', 'Forwarder', 'Catalog'] as ConversionTarget[]).map((d) => ({
disabled={!table.enabled2.includes(d)} value: d,
checked={target === d} label: LABELS[d],
onChange={() => setTarget(d)} disabled: !table.enabled2.includes(d),
/> }))}
{LABELS[d]} />
</label>
))}
</GroupRow> </GroupRow>
<Alert type="info" title="Note!"> <Alert type="info" title="Note!">
+21 -33
View File
@@ -21,6 +21,7 @@ import {
} from '../../../api/dnssec' } from '../../../api/dnssec'
import { Alert } from '../../../ui/Alert' import { Alert } from '../../../ui/Alert'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Radios } from '../../../ui/PanelForm'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Field, Input, Select, Textarea } from '../../../ui/Field' import { Field, Input, Select, Textarea } from '../../../ui/Field'
import { Loading } from '../../../ui/Empty' import { Loading } from '../../../ui/Empty'
@@ -535,25 +536,20 @@ export function DnssecProperties({
)} )}
<div className={frm.mrowCtl}> <div className={frm.mrowCtl}>
{GENERATIONS.map((g) => ( <Radios
<label key={g.value} className={styles.chk}> name="addKeyGeneration"
<input value={newKey.generation}
type="radio" onChange={(v) =>
name="addKeyGeneration" // zone.js:501-526: the rollover follows, and the PEM is cleared.
checked={newKey.generation === g.value} setNewKey((k) => ({
onChange={() => ...k,
// zone.js:501-526: the rollover follows, and the PEM is cleared. generation: v as typeof k.generation,
setNewKey((k) => ({ pem: '',
...k, rolloverDays: addKeyRollover(k.keyType, v as typeof k.generation),
generation: g.value, }))
pem: '', }
rolloverDays: addKeyRollover(k.keyType, g.value), options={GENERATIONS.map((g) => ({ value: g.value, label: g.label }))}
})) />
}
/>
{g.label}
</label>
))}
</div> </div>
{newKey.algorithm === 'RSA' && newKey.generation === 'Automatic' && ( {newKey.algorithm === 'RSA' && newKey.generation === 'Automatic' && (
@@ -635,20 +631,12 @@ MII...
<div className={styles.group}> <div className={styles.group}>
<div className={styles.groupTitle}>Proof of Non-Existence</div> <div className={styles.groupTitle}>Proof of Non-Existence</div>
{NX_PROOFS.map((n) => ( <Radios
<div key={n.value}> name="propsNxProof"
<label className={styles.chk}> value={nxProof}
<input onChange={(v) => setNxProof(v as NxProof)}
type="radio" options={NX_PROOFS.map((n) => ({ value: n.value, label: n.label, help: n.help }))}
name="propsNxProof" />
checked={nxProof === n.value}
onChange={() => setNxProof(n.value as NxProof)}
/>
{n.label}
</label>
<div className={styles.help}>{n.help}</div>
</div>
))}
{nxProof === 'NSEC3' && ( {nxProof === 'NSEC3' && (
<> <>
+7 -12
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { exportZoneText, saveZoneFile } from '../../../api/zones' import { exportZoneText, saveZoneFile } from '../../../api/zones'
import { Alert } from '../../../ui/Alert' import { Alert } from '../../../ui/Alert'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Check } from '../../../ui/Check'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Loading } from '../../../ui/Empty' import { Loading } from '../../../ui/Empty'
import { LabeledTextarea } from '../../../ui/Field' import { LabeledTextarea } from '../../../ui/Field'
@@ -132,18 +133,12 @@ export function EditZoneFile({
</div> </div>
<div className={frm.mrowCtl}> <div className={frm.mrowCtl}>
<label className={styles.chk}> <Check
<input label="Overwrite SOA Serial"
type="checkbox" checked={overwriteSoaSerial}
checked={overwriteSoaSerial} onChange={setOverwriteSoaSerial}
onChange={(e) => setOverwriteSoaSerial(e.target.checked)} help="Enable this option to overwrite existing SOA record serial with the SOA record serial specified in the zone file editor."
/> />
Overwrite SOA Serial
</label>
<div className={styles.help}>
Enable this option to overwrite existing SOA record serial with the SOA record serial
specified in the zone file editor.
</div>
</div> </div>
<Alert type="info" title="Note!"> <Alert type="info" title="Note!">
+29 -43
View File
@@ -2,6 +2,8 @@ import { useEffect, useRef, useState } from 'react'
import { importZone } from '../../../api/zones' import { importZone } from '../../../api/zones'
import { Alert } from '../../../ui/Alert' import { Alert } from '../../../ui/Alert'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Radios } from '../../../ui/PanelForm'
import { Check } from '../../../ui/Check'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { LabeledTextarea } from '../../../ui/Field' import { LabeledTextarea } from '../../../ui/Field'
import type { Notice } from '../types' import type { Notice } from '../types'
@@ -107,52 +109,36 @@ export function ImportZone({
<div className={styles.fields}> <div className={styles.fields}>
<GroupRow modal label="Import Options"> <GroupRow modal label="Import Options">
<label className={styles.chk}> <Check
<input type="checkbox" checked={overwrite} onChange={(e) => setOverwrite(e.target.checked)} /> label="Overwrite Existing Records"
Overwrite Existing Records checked={overwrite}
</label> onChange={setOverwrite}
<div className={styles.help}> help="Enable this option to overwrite existing records for the record types being imported."
Enable this option to overwrite existing records for the record types being imported. />
</div> <Check
<label className={styles.chk}> label="Overwrite Zone"
<input type="checkbox" checked={overwriteZone} onChange={(e) => setOverwriteZone(e.target.checked)} /> checked={overwriteZone}
Overwrite Zone onChange={setOverwriteZone}
</label> help="Enable this option to delete all existing records from the zone before importing new records."
<div className={styles.help}> />
Enable this option to delete all existing records from the zone before importing new records. <Check
</div> label="Overwrite SOA Serial"
<label className={styles.chk}> checked={overwriteSoaSerial}
<input onChange={setOverwriteSoaSerial}
type="checkbox" help="Enable this option to overwrite existing SOA record serial with the imported SOA record serial."
checked={overwriteSoaSerial} />
onChange={(e) => setOverwriteSoaSerial(e.target.checked)}
/>
Overwrite SOA Serial
</label>
<div className={styles.help}>
Enable this option to overwrite existing SOA record serial with the imported SOA record serial.
</div>
</GroupRow> </GroupRow>
<GroupRow modal label="Import Type"> <GroupRow modal label="Import Type">
<label className={styles.chk}> <Radios
<input name="importType"
type="radio" value={mode}
name="importType" onChange={(v) => setMode(v as typeof mode)}
checked={mode === 'File'} options={[
onChange={() => setMode('File')} { value: 'File', label: 'Zone File' },
/> { value: 'Text', label: 'Text Editor' },
Zone File ]}
</label> />
<label className={styles.chk}>
<input
type="radio"
name="importType"
checked={mode === 'Text'}
onChange={() => setMode('Text')}
/>
Text Editor
</label>
</GroupRow> </GroupRow>
{/* Both stay mounted and the other one is hidden, as upstream does {/* Both stay mounted and the other one is hidden, as upstream does
+19 -36
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { signZone, type Algorithm, type NxProof } from '../../../api/dnssec' import { signZone, type Algorithm, type NxProof } from '../../../api/dnssec'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Radios } from '../../../ui/PanelForm'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Field, Input, Select, Textarea } from '../../../ui/Field' import { Field, Input, Select, Textarea } from '../../../ui/Field'
import { import {
@@ -155,17 +156,12 @@ export function SignZone({
<div className={styles.fields}> <div className={styles.fields}>
<GroupRow modal label="DNSKEY Algorithm"> <GroupRow modal label="DNSKEY Algorithm">
{ALGORITHMS.map((a) => ( <Radios
<label key={a.value} className={styles.chk}> name="signAlgorithm"
<input value={f.algorithm}
type="radio" onChange={(v) => changeAlgorithm(v as Algorithm)}
name="signAlgorithm" options={ALGORITHMS.map((a) => ({ value: a.value, label: a.label }))}
checked={f.algorithm === a.value} />
onChange={() => changeAlgorithm(a.value as Algorithm)}
/>
{a.label}
</label>
))}
</GroupRow> </GroupRow>
{isRsa ? ( {isRsa ? (
@@ -237,20 +233,12 @@ export function SignZone({
/> />
<GroupRow modal label="Proof of Non-Existence"> <GroupRow modal label="Proof of Non-Existence">
{NX_PROOFS.map((n) => ( <Radios
<div key={n.value}> name="signNxProof"
<label className={styles.chk}> value={f.nxProof}
<input onChange={(v) => set('nxProof', v as NxProof)}
type="radio" options={NX_PROOFS.map((n) => ({ value: n.value, label: n.label, help: n.help }))}
name="signNxProof" />
checked={f.nxProof === n.value}
onChange={() => set('nxProof', n.value as NxProof)}
/>
{n.label}
</label>
<div className={styles.help}>{n.help}</div>
</div>
))}
</GroupRow> </GroupRow>
{f.nxProof === 'NSEC3' && ( {f.nxProof === 'NSEC3' && (
@@ -378,17 +366,12 @@ function SigningKey({
return ( return (
<div className={styles.group}> <div className={styles.group}>
<div className={styles.groupTitle}>{title}</div> <div className={styles.groupTitle}>{title}</div>
{GENERATIONS.map((g) => ( <Radios
<label key={g.value} className={styles.chk}> name={name}
<input value={generation}
type="radio" onChange={onGeneration}
name={name} options={GENERATIONS.map((g) => ({ value: g.value, label: g.label }))}
checked={generation === g.value} />
onChange={() => onGeneration(g.value)}
/>
{g.label}
</label>
))}
{/* The size only applies to RSA and only with automatic generation. */} {/* The size only applies to RSA and only with automatic generation. */}
{isRsa && generation === 'Automatic' && ( {isRsa && generation === 'Automatic' && (
+50 -80
View File
@@ -2,6 +2,8 @@ import { useEffect, useState, type ReactNode } from 'react'
import { getZoneOptions, setZoneOptions, type ZoneOptions as Response } from '../../../api/zones' import { getZoneOptions, setZoneOptions, type ZoneOptions as Response } from '../../../api/zones'
import { Alert } from '../../../ui/Alert' import { Alert } from '../../../ui/Alert'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Check } from '../../../ui/Check'
import { Radios } from '../../../ui/PanelForm'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Field, Input, Select, Textarea } from '../../../ui/Field' import { Field, Input, Select, Textarea } from '../../../ui/Field'
import { Loading } from '../../../ui/Empty' import { Loading } from '../../../ui/Empty'
@@ -188,46 +190,31 @@ export function ZoneOptions({
<div className={styles.help}>Select a Catalog zone to register as its member zone.</div> <div className={styles.help}>Select a Catalog zone to register as its member zone.</div>
{e.overrideQueryAccess && ( {e.overrideQueryAccess && (
<div> <Check
<label className={styles.chk}> label="Override Query Access Option"
<input disabled={e.overrideLocked}
type="checkbox" checked={f.overrideCatalogQueryAccess}
disabled={e.overrideLocked} onChange={(v) => set('overrideCatalogQueryAccess', v)}
checked={f.overrideCatalogQueryAccess} help="Enable to override Query Access option in the Catalog zone."
onChange={(ev) => set('overrideCatalogQueryAccess', ev.target.checked)} />
/>
Override Query Access Option
</label>
<div className={styles.help}>Enable to override Query Access option in the Catalog zone.</div>
</div>
)} )}
{e.overrideZoneTransfer && ( {e.overrideZoneTransfer && (
<div> <Check
<label className={styles.chk}> label="Override Zone Transfer Option"
<input disabled={e.overrideLocked}
type="checkbox" checked={f.overrideCatalogZoneTransfer}
disabled={e.overrideLocked} onChange={(v) => set('overrideCatalogZoneTransfer', v)}
checked={f.overrideCatalogZoneTransfer} help="Enable to override Zone Transfer option in the Catalog zone."
onChange={(ev) => set('overrideCatalogZoneTransfer', ev.target.checked)} />
/>
Override Zone Transfer Option
</label>
<div className={styles.help}>Enable to override Zone Transfer option in the Catalog zone.</div>
</div>
)} )}
{e.overrideNotify && ( {e.overrideNotify && (
<div> <Check
<label className={styles.chk}> label="Override Notify Option"
<input disabled={e.overrideLocked}
type="checkbox" checked={f.overrideCatalogNotify}
disabled={e.overrideLocked} onChange={(v) => set('overrideCatalogNotify', v)}
checked={f.overrideCatalogNotify} help="Enable to override Notify option in the Catalog zone."
onChange={(ev) => set('overrideCatalogNotify', ev.target.checked)} />
/>
Override Notify Option
</label>
<div className={styles.help}>Enable to override Notify option in the Catalog zone.</div>
</div>
)} )}
<Alert type="info" title="Note!"> <Alert type="info" title="Note!">
When a zone becomes a member of a Catalog zone, all of the Catalog zone&apos;s Options are When a zone becomes a member of a Catalog zone, all of the Catalog zone&apos;s Options are
@@ -264,18 +251,13 @@ export function ZoneOptions({
{e.xfrProtocol && ( {e.xfrProtocol && (
<GroupRow modal label="Zone Transfer Protocol"> <GroupRow modal label="Zone Transfer Protocol">
{XFR_PROTOCOLS.map((x) => ( <Radios
<label key={x.value} className={styles.chk}> name="zoneOptionsXfr"
<input disabled={e.primaryServerLocked}
type="radio" value={f.primaryZoneTransferProtocol}
name="zoneOptionsXfr" onChange={(v) => set('primaryZoneTransferProtocol', v as typeof f.primaryZoneTransferProtocol)}
disabled={e.primaryServerLocked} options={XFR_PROTOCOLS.map((x) => ({ value: x.value, label: x.label }))}
checked={f.primaryZoneTransferProtocol === x.value} />
onChange={() => set('primaryZoneTransferProtocol', x.value)}
/>
{x.label}
</label>
))}
</GroupRow> </GroupRow>
)} )}
@@ -300,23 +282,19 @@ export function ZoneOptions({
)} )}
{e.validateZone && ( {e.validateZone && (
<div> <Check
<label className={styles.chk}> label="Use ZONEMD to Validate Zone"
<input text={
type="checkbox" <>
disabled={e.primaryServerLocked} Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone
checked={f.validateZone} </>
onChange={(ev) => set('validateZone', ev.target.checked)} }
/> disabled={e.primaryServerLocked}
Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone checked={f.validateZone}
</label> onChange={(v) => set('validateZone', v)}
{/* index.html:5402 in v15.5.1. */} /* index.html:5402 in v15.5.1. */
<div className={styles.help}> help="When enabled, the secondary zone will be validated using the ZONEMD record after every zone transfer. The zone will get disabled if the validation fails. The zone must be DNSSEC signed for the validation to work."
When enabled, the secondary zone will be validated using the ZONEMD record after every />
zone transfer. The zone will get disabled if the validation fails. The zone must be DNSSEC
signed for the validation to work.
</div>
</div>
)} )}
</> </>
)} )}
@@ -616,21 +594,13 @@ function Criterion({
return ( return (
<> <>
<div className={frm.mrowCtl}> <div className={frm.mrowCtl}>
{options.map((o) => ( <Radios
<div key={o.value}> name={name}
<label className={styles.chk}> disabled={locked}
<input value={value}
type="radio" onChange={onChanged2}
name={name} options={options.map((o) => ({ value: o.value, label: o.label, help: o.help }))}
disabled={locked} />
checked={value === o.value}
onChange={() => onChanged2(o.value)}
/>
{o.label}
</label>
<div className={styles.help}>{o.help}</div>
</div>
))}
</div> </div>
<Field label={listLabel}> <Field label={listLabel}>
{(id) => ( {(id) => (
+4 -7
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { getZonePermissions, serializePermissions, setZonePermissions } from '../../../api/zones' import { getZonePermissions, serializePermissions, setZonePermissions } from '../../../api/zones'
import { Button } from '../../../ui/Button' import { Button } from '../../../ui/Button'
import { Tick } from '../../../ui/Tick'
import { Dialog } from '../../../ui/Dialog' import { Dialog } from '../../../ui/Dialog'
import { Field, Select } from '../../../ui/Field' import { Field, Select } from '../../../ui/Field'
import { Loading } from '../../../ui/Empty' import { Loading } from '../../../ui/Empty'
@@ -191,13 +192,9 @@ function PermissionsTable({
th: { style: { width: 70 } }, th: { style: { width: 70 } },
header: ['View', 'Modify', 'Delete'][n], header: ['View', 'Modify', 'Delete'][n],
cell: (f: (typeof rows)[number], i: number) => ( cell: (f: (typeof rows)[number], i: number) => (
<input <span className={styles.chkPerm}>
type="checkbox" <Tick label={`${key} for ${f.name}`} checked={f[key]} onChange={(v) => change(i, key, v)} />
className={styles.chkPerm} </span>
aria-label={`${key} for ${f.name}`}
checked={f[key]}
onChange={(e) => change(i, key, e.target.checked)}
/>
), ),
})), })),
{ {
+13 -2
View File
@@ -25,8 +25,12 @@ export function Check({
help, help,
disabled, disabled,
toggle = false, toggle = false,
text,
}: { }: {
/** The control's accessible name, and the visible text unless `text` replaces it. */
label: string label: string
/** Visible text with markup in it (a link), when `label` alone would lose it. */
text?: ReactNode
checked: boolean checked: boolean
onChange: (v: boolean) => void onChange: (v: boolean) => void
help?: ReactNode help?: ReactNode
@@ -52,8 +56,15 @@ export function Check({
) : ( ) : (
<Checkbox nativeButton render={<button type="button" />} {...common} /> <Checkbox nativeButton render={<button type="button" />} {...common} />
)} )}
<span className={styles.text} onClick={() => !disabled && onChange(!checked)}> <span
{label} className={styles.text}
onClick={(e) => {
// A link inside the text is followed, not a click on the box.
if (disabled || (e.target as HTMLElement).closest('a')) return
onChange(!checked)
}}
>
{text ?? label}
</span> </span>
</div> </div>
{help && <div className={toggle ? `${styles.help} ${styles.helpSwitch}` : styles.help}>{help}</div>} {help && <div className={toggle ? `${styles.help} ${styles.helpSwitch}` : styles.help}>{help}</div>}
+40 -15
View File
@@ -1,4 +1,5 @@
import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react' import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Alert } from './Alert' import { Alert } from './Alert'
import { Button } from './Button' import { Button } from './Button'
import { Empty } from './Empty' import { Empty } from './Empty'
@@ -242,14 +243,21 @@ export function Trailer({ children }: { children: ReactNode }) {
export interface RadioOption { export interface RadioOption {
value: string value: string
label: string /** The option's text; with markup in it (a link), give `name` as well. */
label: ReactNode
/** The option's accessible name when `label` is not a string. */
name?: string
help?: ReactNode help?: ReactNode
disabled?: boolean
} }
/* /*
The radio group. It shares its row and its help with `ui/Check`: they are the The radio group, ReUI's `RadioGroup`. It shares its row and its help with `ui/Check`:
same control with different cardinality, and when each screen wrote its own, a they are the same control with different cardinality, and when each screen wrote
radio's help and a checkbox's help did not land on the same left edge. its own, a radio's help and a checkbox's help did not land on the same left edge.
The item carries its own name and the text beside it only forwards the click, as in
`Check`: a wrapping <label> would name the hidden input as well.
*/ */
export function Radios({ export function Radios({
name, name,
@@ -257,32 +265,49 @@ export function Radios({
options, options,
onChange, onChange,
disabled, disabled,
label,
}: { }: {
name: string name: string
value: string value: string
options: RadioOption[] options: RadioOption[]
onChange: (v: string) => void onChange: (v: string) => void
disabled?: boolean disabled?: boolean
/** The group's accessible name, when its heading is not a `GroupRow` label. */
label?: string
}) { }) {
return ( return (
<> <RadioGroup
name={name}
aria-label={label}
value={value}
disabled={disabled}
onValueChange={(v) => onChange(String(v))}
className="gap-0"
>
{options.map((o) => ( {options.map((o) => (
<div key={o.value}> <div key={o.value}>
<label className={check.check}> <div className={check.check}>
<input <RadioGroupItem
type="radio"
name={name}
value={o.value} value={o.value}
checked={value === o.value} nativeButton
disabled={disabled} render={<button type="button" />}
onChange={() => onChange(o.value)} disabled={o.disabled}
aria-label={o.name ?? (typeof o.label === 'string' ? o.label : o.value)}
/> />
<span className={check.text}>{o.label}</span> <span
</label> className={check.text}
onClick={(e) => {
if (disabled || o.disabled || (e.target as HTMLElement).closest('a')) return
onChange(o.value)
}}
>
{o.label}
</span>
</div>
{o.help && <div className={check.help}>{o.help}</div>} {o.help && <div className={check.help}>{o.help}</div>}
</div> </div>
))} ))}
</> </RadioGroup>
) )
} }
+38
View File
@@ -0,0 +1,38 @@
import { Checkbox } from '@/components/ui/checkbox'
import { Switch } from '@/components/ui/switch'
/*
The bare control, with no text beside it: a table cell's tick (a row selection, a
permission), or the enable switch of a list row. It is `ui/Check` without the row and
its label, and it is named the same way: through `aria-label`, announced as a
checkbox either way (a switch too), so the tests and the keyboard keep one role.
*/
export function Tick({
label,
checked,
onChange,
disabled,
toggle = false,
indeterminate,
}: {
/** The accessible name: there is no visible text. */
label: string
checked: boolean
onChange: (v: boolean) => void
disabled?: boolean
/** A setting that stays put (a switch), not a selection. */
toggle?: boolean
indeterminate?: boolean
}) {
const common = {
checked,
disabled,
'aria-label': label,
onCheckedChange: (v: boolean) => onChange(v),
}
return toggle ? (
<Switch role="checkbox" nativeButton render={<button type="button" />} {...common} />
) : (
<Checkbox nativeButton render={<button type="button" />} indeterminate={indeterminate} {...common} />
)
}