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 <[email protected]>
This commit is contained in:
1 parent
7f60dc9960
commit
d2b05a1037
22 files changed
+458
-493
No files matched your search
@@ -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 }
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Button } from '../../ui/Button'
|
||||
import { Tick } from '../../ui/Tick'
|
||||
import { Dialog } from '../../ui/Dialog'
|
||||
import { SectionHeader } from '../../ui/SectionHeader'
|
||||
import { SectionIndex } from '../../ui/SectionIndex'
|
||||
@@ -564,31 +565,31 @@ function PermissionsTable({
|
||||
<tr key={f.name}>
|
||||
<td className={styles.who}>{f.name}</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.chkPerm}
|
||||
aria-label={`${section} · ${f.name} · View`}
|
||||
checked={f.canView}
|
||||
onChange={(e) => set(f, { canView: e.target.checked })}
|
||||
/>
|
||||
<span className={styles.chkPerm}>
|
||||
<Tick
|
||||
label={`${section} · ${f.name} · View`}
|
||||
checked={f.canView}
|
||||
onChange={(v) => set(f, { canView: v })}
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.chkPerm}
|
||||
aria-label={`${section} · ${f.name} · Modify`}
|
||||
checked={f.canModify}
|
||||
onChange={(e) => set(f, { canModify: e.target.checked })}
|
||||
/>
|
||||
<span className={styles.chkPerm}>
|
||||
<Tick
|
||||
label={`${section} · ${f.name} · Modify`}
|
||||
checked={f.canModify}
|
||||
onChange={(v) => set(f, { canModify: v })}
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.chkPerm}
|
||||
aria-label={`${section} · ${f.name} · Delete`}
|
||||
checked={f.canDelete}
|
||||
onChange={(e) => set(f, { canDelete: e.target.checked })}
|
||||
/>
|
||||
<span className={styles.chkPerm}>
|
||||
<Tick
|
||||
label={`${section} · ${f.name} · Delete`}
|
||||
checked={f.canDelete}
|
||||
onChange={(v) => set(f, { canDelete: v })}
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<td className={styles.tdel}>
|
||||
<Button onClick={() => onChange(rows.filter((x) => x.name !== f.name))}>Remove</Button>
|
||||
|
||||
@@ -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
|
||||
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; }
|
||||
/* The URL wraps after its separators (`ui/UrlText`), not at whatever letter meets the
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DataTable } from '../../ui/DataTable'
|
||||
import { RowAction } from '../../ui/Table'
|
||||
import { Field, Input, Select } from '../../ui/Field'
|
||||
import { Button } from '../../ui/Button'
|
||||
import { Tick } from '../../ui/Tick'
|
||||
import { PermissionButton } from '../../ui/PermissionButton'
|
||||
import { Confirm } from '../../ui/Confirm'
|
||||
import { Notifier } from '../../ui/Notifier'
|
||||
@@ -768,9 +769,9 @@ export function BlockLists({
|
||||
return (
|
||||
canToggle(l) && (
|
||||
<label className={styles.switch}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Enabled ${what}`}
|
||||
<Tick
|
||||
toggle
|
||||
label={`Enabled ${what}`}
|
||||
checked={l.enabled}
|
||||
disabled={off}
|
||||
onChange={() => setLines((all) => all.map((x, j) => (j === i ? toggleLine(x) : x)))}
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('Change Theme (index.html:3818-3862)', () => {
|
||||
open()
|
||||
const dialog = screen.getByRole('dialog', { name: 'Change 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)',
|
||||
'Light Theme',
|
||||
'Dark Theme',
|
||||
@@ -64,7 +64,7 @@ describe('Change Theme (index.html:3818-3862)', () => {
|
||||
if (stored != null) localStorage.setItem('theme', stored)
|
||||
open()
|
||||
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', () => {
|
||||
|
||||
@@ -42,9 +42,10 @@ export function ChangeTheme({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} title="Change Theme">
|
||||
<GroupRow modal label="Theme" role="radiogroup">
|
||||
<GroupRow modal label="Theme">
|
||||
<Radios
|
||||
name="rdChangeTheme"
|
||||
label="Theme"
|
||||
value={checked}
|
||||
options={OPTIONS}
|
||||
onChange={(v) => setChoice(v as OfferedTheme)}
|
||||
|
||||
@@ -199,13 +199,13 @@ describe('the Edit Zone File dialog', () => {
|
||||
draw()
|
||||
let dialog = await openFromList(user)
|
||||
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 waitFor(() => expect(screen.queryByRole('dialog')).toBeNull())
|
||||
|
||||
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 () => {
|
||||
@@ -297,7 +297,7 @@ describe('Import Zone (v15.5)', () => {
|
||||
const menu = await rowMenu(user, 'primary.test')
|
||||
await user.click(within(menu).getByRole('menuitem', { name: 'Import Zone' }))
|
||||
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')
|
||||
|
||||
const listCalls = spy.mock.calls.filter((c) => c[0] === 'zones/list').length
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type Zone,
|
||||
} from '../../api/zones'
|
||||
import { Button } from '../../ui/Button'
|
||||
import { Tick } from '../../ui/Tick'
|
||||
import { Loading } from '../../ui/Empty'
|
||||
import { StaleData } from '../StaleData'
|
||||
import { Field, Input, Select } from '../../ui/Field'
|
||||
@@ -596,12 +597,11 @@ export function ZoneList({
|
||||
th: { style: { width: 38 }, className: tbl.checkCell },
|
||||
header: (
|
||||
<label>
|
||||
<input
|
||||
<Tick
|
||||
disabled={stale}
|
||||
type="checkbox"
|
||||
aria-label="Select all zones"
|
||||
label="Select all zones"
|
||||
checked={allChecked}
|
||||
onChange={(e) => setChecked(e.target.checked ? zones.map((z) => z.name) : [])}
|
||||
onChange={(v) => setChecked(v ? zones.map((z) => z.name) : [])}
|
||||
/>
|
||||
</label>
|
||||
),
|
||||
@@ -733,15 +733,14 @@ function zoneCells(p: RowProps) {
|
||||
return {
|
||||
check: (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Select ${name}`}
|
||||
<Tick
|
||||
label={`Select ${name}`}
|
||||
/* 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
|
||||
deleted. */
|
||||
disabled={p.stale}
|
||||
checked={p.checked}
|
||||
onChange={(e) => p.onCheck(e.target.checked)}
|
||||
onChange={p.onCheck}
|
||||
/>
|
||||
</label>
|
||||
),
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
.fields { display:flex; flex-direction:column; gap: var(--s-8); }
|
||||
.inline { composes: inline from '../../ui/PanelForm.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
|
||||
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; }
|
||||
|
||||
@@ -319,9 +319,9 @@ describe('modals', () => {
|
||||
await user.click(await screen.findByRole('menuitem', { name: 'Convert Zone' }))
|
||||
|
||||
const dialog = await screen.findByRole('dialog')
|
||||
expect(within(dialog).getByLabelText('Primary Zone')).toHaveProperty('disabled', true)
|
||||
expect(within(dialog).getByLabelText('Conditional Forwarder Zone')).toHaveProperty('checked', true)
|
||||
expect(within(dialog).getByLabelText('Catalog Zone')).toHaveProperty('disabled', true)
|
||||
expect(within(dialog).getByRole('radio', { name: 'Primary Zone' })).toBeDisabled()
|
||||
expect(within(dialog).getByRole('radio', { name: 'Conditional Forwarder Zone' })).toBeChecked()
|
||||
expect(within(dialog).getByRole('radio', { name: 'Catalog Zone' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('\"Import Zone\" alerts if no file is chosen', async () => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
type ZoneDetails,
|
||||
} from '../../../api/records'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Check } from '../../../ui/Check'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Field, Input, Select, Textarea } from '../../../ui/Field'
|
||||
import {
|
||||
@@ -277,10 +279,11 @@ export function AddEditRecord(p: AddEditRecordProps) {
|
||||
|
||||
{/* "Overwrite" only exists when adding. */}
|
||||
{!editing && (
|
||||
<label className={styles.chk}>
|
||||
<input type="checkbox" checked={f.overwrite} onChange={(e) => set('overwrite', e.target.checked)} />
|
||||
Overwrite existing records
|
||||
</label>
|
||||
<Check
|
||||
label="Overwrite existing records"
|
||||
checked={f.overwrite}
|
||||
onChange={(v) => set('overwrite', v)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Field label="Comments">
|
||||
@@ -370,21 +373,19 @@ function TypeFields({ f, set, setF, apps, classes, onApp, onClass, editing }: Fi
|
||||
return (
|
||||
<>
|
||||
{text(f.type === 'A' ? 'IPv4 Address' : 'IPv6 Address', 'value', { mono: true })}
|
||||
<label className={styles.chk}>
|
||||
<input type="checkbox" checked={f.ptr} onChange={(e) => set('ptr', e.target.checked)} />
|
||||
Add reverse (PTR) record
|
||||
</label>
|
||||
<label className={styles.chk}>
|
||||
{/* Enabled only while the PTR box is ticked (zone.js:543-546); unticking
|
||||
the PTR does not untick it. */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.createPtrZone}
|
||||
disabled={!f.ptr}
|
||||
onChange={(e) => set('createPtrZone', e.target.checked)}
|
||||
/>
|
||||
Create reverse zone if it does not exists
|
||||
</label>
|
||||
<Check
|
||||
label="Add reverse (PTR) record"
|
||||
checked={f.ptr}
|
||||
onChange={(v) => set('ptr', v)}
|
||||
/>
|
||||
{/* Enabled only while the PTR box is ticked (zone.js:543-546); unticking
|
||||
the PTR does not untick it. */}
|
||||
<Check
|
||||
label="Create reverse zone if it does not exists"
|
||||
checked={f.createPtrZone}
|
||||
disabled={!f.ptr}
|
||||
onChange={(v) => set('createPtrZone', v)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -418,14 +419,11 @@ function TypeFields({ f, set, setF, apps, classes, onApp, onClass, editing }: Fi
|
||||
{text('Retry', 'soaRetry', { mono: true, short: true })}
|
||||
{text('Expire', 'soaExpire', { mono: true, short: true })}
|
||||
{text('Minimum', 'soaMinimum', { mono: true, short: true })}
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.soaUseSerialDateScheme}
|
||||
onChange={(e) => set('soaUseSerialDateScheme', e.target.checked)}
|
||||
/>
|
||||
Use Serial Date Scheme
|
||||
</label>
|
||||
<Check
|
||||
label="Use Serial Date Scheme"
|
||||
checked={f.soaUseSerialDateScheme}
|
||||
onChange={(v) => set('soaUseSerialDateScheme', v)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -463,14 +461,11 @@ function TypeFields({ f, set, setF, apps, classes, onApp, onClass, editing }: Fi
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.txtSplitText}
|
||||
onChange={(e) => set('txtSplitText', e.target.checked)}
|
||||
/>
|
||||
Split text into multiple character strings
|
||||
</label>
|
||||
<Check
|
||||
label="Split text into multiple character strings"
|
||||
checked={f.txtSplitText}
|
||||
onChange={(v) => set('txtSplitText', v)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -629,22 +624,16 @@ MII...
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.svcbAutoIpv4Hint}
|
||||
onChange={(e) => set('svcbAutoIpv4Hint', e.target.checked)}
|
||||
/>
|
||||
Use Automatic IPv4 Hint
|
||||
</label>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.svcbAutoIpv6Hint}
|
||||
onChange={(e) => set('svcbAutoIpv6Hint', e.target.checked)}
|
||||
/>
|
||||
Use Automatic IPv6 Hint
|
||||
</label>
|
||||
<Check
|
||||
label="Use Automatic IPv4 Hint"
|
||||
checked={f.svcbAutoIpv4Hint}
|
||||
onChange={(v) => set('svcbAutoIpv4Hint', v)}
|
||||
/>
|
||||
<Check
|
||||
label="Use Automatic IPv6 Hint"
|
||||
checked={f.svcbAutoIpv6Hint}
|
||||
onChange={(v) => set('svcbAutoIpv6Hint', v)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -670,32 +659,21 @@ MII...
|
||||
return (
|
||||
<>
|
||||
<GroupRow modal label="Protocol">
|
||||
{FORWARDER_PROTOCOLS.map((x) => (
|
||||
<label key={x.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="recordForwarderProtocol"
|
||||
checked={f.forwarderProtocol === x.value}
|
||||
disabled={f.forwarderThisServer}
|
||||
onChange={() => set('forwarderProtocol', x.value)}
|
||||
/>
|
||||
{x.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="recordForwarderProtocol"
|
||||
disabled={f.forwarderThisServer}
|
||||
value={f.forwarderProtocol}
|
||||
onChange={(v) => set('forwarderProtocol', v as typeof f.forwarderProtocol)}
|
||||
options={FORWARDER_PROTOCOLS.map((x) => ({ value: x.value, label: x.label }))}
|
||||
/>
|
||||
</GroupRow>
|
||||
{/* `updateAddEditFormForwarderThisServer` (zone.js:5336-5356): ticked, the
|
||||
forwarder is `this-server`, fixed, on UDP, and the proxy goes away. */}
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.forwarderThisServer}
|
||||
onChange={(e) => {
|
||||
const on = e.target.checked
|
||||
setF((prev) => toggleForwarderThisServer(prev, on))
|
||||
}}
|
||||
/>
|
||||
Use "This Server"
|
||||
</label>
|
||||
<Check
|
||||
label={'Use "This Server"'}
|
||||
checked={f.forwarderThisServer}
|
||||
onChange={(on) => setF((prev) => toggleForwarderThisServer(prev, on))}
|
||||
/>
|
||||
<div className={styles.help}>
|
||||
When using "This Server", if a record does not exists in the zone then the request is
|
||||
forwarded to the DNS Server'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
|
||||
priority value will be queried concurrently.
|
||||
</div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.forwarderDnssecValidation}
|
||||
onChange={(e) => set('forwarderDnssecValidation', e.target.checked)}
|
||||
/>
|
||||
Enable DNSSEC Validation
|
||||
</label>
|
||||
<Check
|
||||
label="Enable DNSSEC Validation"
|
||||
checked={f.forwarderDnssecValidation}
|
||||
onChange={(v) => set('forwarderDnssecValidation', v)}
|
||||
/>
|
||||
{!f.forwarderThisServer && (
|
||||
<GroupRow modal label="Network Proxy">
|
||||
{PROXY_TYPES.map((x) => (
|
||||
<label key={x.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="recordProxyType"
|
||||
checked={f.proxyType === x.value}
|
||||
onChange={() => set('proxyType', x.value)}
|
||||
/>
|
||||
{x.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="recordProxyType"
|
||||
value={f.proxyType}
|
||||
onChange={(v) => set('proxyType', v as typeof f.proxyType)}
|
||||
options={PROXY_TYPES.map((x) => ({ value: x.value, label: x.label }))}
|
||||
/>
|
||||
<Field label="Proxy Server Address">
|
||||
{(id) => (
|
||||
<Input
|
||||
|
||||
@@ -2,7 +2,9 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { createZone, listCatalogs } from '../../../api/zones'
|
||||
import { getTsigKeyNames } from '../../../api/settings'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Check } from '../../../ui/Check'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Field, Input, Select, Textarea } from '../../../ui/Field'
|
||||
import type { Notice } from '../types'
|
||||
import styles from '../Zones.module.css'
|
||||
@@ -167,33 +169,30 @@ export function AddZone({
|
||||
</Field>
|
||||
|
||||
<GroupRow modal label="Zone Type">
|
||||
{ADD_TYPES.map((t) => (
|
||||
<label key={t.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="addZoneType"
|
||||
checked={f.type === t.value}
|
||||
onChange={() => changeType(t.value)}
|
||||
/>
|
||||
{/*
|
||||
`.chk` is `display:flex`, so every child of the label is an item:
|
||||
without this `span` the link and the brackets split into three boxes
|
||||
("Secondary ROOT Zone ( RFC 8806 )"), and the link stopped being
|
||||
inside a sentence, which is what grants it the target-size
|
||||
exception. Wrapped, it is text again.
|
||||
*/}
|
||||
<span>
|
||||
{t.label}
|
||||
{t.reference && (
|
||||
<>
|
||||
{' ('}
|
||||
<External href={t.reference.href}>{t.reference.text}</External>
|
||||
{')'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{/* A link inside an option stays inside its sentence: wrapped in a span, the
|
||||
link and the brackets are text again and not three boxes, which is what
|
||||
grants the link the target-size exception. */}
|
||||
<Radios
|
||||
name="addZoneType"
|
||||
value={f.type}
|
||||
onChange={(v) => changeType(v as AddZoneKind)}
|
||||
options={ADD_TYPES.map((t) => ({
|
||||
value: t.value,
|
||||
name: t.label,
|
||||
label: (
|
||||
<span>
|
||||
{t.label}
|
||||
{t.reference && (
|
||||
<>
|
||||
{' ('}
|
||||
<External href={t.reference.href}>{t.reference.text}</External>
|
||||
{')'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
{v.catalog && catalogs.length > 0 && (
|
||||
@@ -213,14 +212,11 @@ export function AddZone({
|
||||
|
||||
{v.initializeForwarderCheckbox && (
|
||||
<GroupRow modal label="Conditional Forwarder">
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.initializeForwarder}
|
||||
onChange={(e) => set('initializeForwarder', e.target.checked)}
|
||||
/>
|
||||
Initialize Forwarder (FWD) Record
|
||||
</label>
|
||||
<Check
|
||||
label="Initialize Forwarder (FWD) Record"
|
||||
checked={f.initializeForwarder}
|
||||
onChange={(v) => set('initializeForwarder', v)}
|
||||
/>
|
||||
</GroupRow>
|
||||
)}
|
||||
|
||||
@@ -240,14 +236,11 @@ export function AddZone({
|
||||
|
||||
{v.soaSerial && (
|
||||
<GroupRow modal label="Zone Serial">
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.useSoaSerialDateScheme}
|
||||
onChange={(e) => set('useSoaSerialDateScheme', e.target.checked)}
|
||||
/>
|
||||
Use SOA Serial Date Scheme
|
||||
</label>
|
||||
<Check
|
||||
label="Use SOA Serial Date Scheme"
|
||||
checked={f.useSoaSerialDateScheme}
|
||||
onChange={(v) => set('useSoaSerialDateScheme', v)}
|
||||
/>
|
||||
</GroupRow>
|
||||
)}
|
||||
|
||||
@@ -280,17 +273,12 @@ export function AddZone({
|
||||
|
||||
{v.transferProtocol && (
|
||||
<GroupRow modal label="Zone Transfer Protocol">
|
||||
{TRANSFER_PROTOCOLS.map((p) => (
|
||||
<label key={p.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="addZoneTransferProtocol"
|
||||
checked={f.zoneTransferProtocol === p.value}
|
||||
onChange={() => set('zoneTransferProtocol', p.value)}
|
||||
/>
|
||||
{p.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="addZoneTransferProtocol"
|
||||
value={f.zoneTransferProtocol}
|
||||
onChange={(v) => set('zoneTransferProtocol', v as typeof f.zoneTransferProtocol)}
|
||||
options={TRANSFER_PROTOCOLS.map((p) => ({ value: p.value, label: p.label }))}
|
||||
/>
|
||||
</GroupRow>
|
||||
)}
|
||||
|
||||
@@ -311,37 +299,30 @@ export function AddZone({
|
||||
|
||||
{v.validateZone && (
|
||||
<GroupRow modal label="Zone Validation">
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.validateZone}
|
||||
onChange={(e) => set('validateZone', e.target.checked)}
|
||||
/>
|
||||
Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone
|
||||
</label>
|
||||
<div className={styles.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.
|
||||
</div>
|
||||
<Check
|
||||
label="Use ZONEMD to Validate Zone"
|
||||
text={
|
||||
<>
|
||||
Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone
|
||||
</>
|
||||
}
|
||||
checked={f.validateZone}
|
||||
onChange={(v) => set('validateZone', v)}
|
||||
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."
|
||||
/>
|
||||
</GroupRow>
|
||||
)}
|
||||
|
||||
{v.forwarderFields && (
|
||||
<>
|
||||
<GroupRow modal label="Protocol">
|
||||
{FORWARDER_PROTOCOLS.map((p) => (
|
||||
<label key={p.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="addZoneForwarderProtocol"
|
||||
disabled={f.useThisServer}
|
||||
checked={f.forwarderProtocol === p.value}
|
||||
onChange={() => set('forwarderProtocol', p.value)}
|
||||
/>
|
||||
{p.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="addZoneForwarderProtocol"
|
||||
disabled={f.useThisServer}
|
||||
value={f.forwarderProtocol}
|
||||
onChange={(v) => set('forwarderProtocol', v as typeof f.forwarderProtocol)}
|
||||
options={FORWARDER_PROTOCOLS.map((p) => ({ value: p.value, label: p.label }))}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
{/* 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}
|
||||
onChange={(e) => set('forwarder', e.target.value)}
|
||||
/>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.useThisServer}
|
||||
onChange={(e) => {
|
||||
/* `updateAddZoneFormForwarderThisServer` (zone.js:2979-2998):
|
||||
ticked, the protocol goes to UDP; unticked, the field is emptied. */
|
||||
const on = e.target.checked
|
||||
setF((prev) =>
|
||||
on
|
||||
? { ...prev, useThisServer: true, forwarderProtocol: 'Udp' }
|
||||
: { ...prev, useThisServer: false, forwarder: '' },
|
||||
)
|
||||
}}
|
||||
/>
|
||||
Use "This Server"
|
||||
</label>
|
||||
<Check
|
||||
label={'Use "This Server"'}
|
||||
checked={f.useThisServer}
|
||||
onChange={(on) => {
|
||||
/* `updateAddZoneFormForwarderThisServer` (zone.js:2979-2998):
|
||||
ticked, the protocol goes to UDP; unticked, the field is emptied. */
|
||||
setF((prev) =>
|
||||
on
|
||||
? { ...prev, useThisServer: true, forwarderProtocol: 'Udp' }
|
||||
: { ...prev, useThisServer: false, forwarder: '' },
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<GroupRow modal label="DNSSEC">
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={f.dnssecValidation}
|
||||
onChange={(e) => set('dnssecValidation', e.target.checked)}
|
||||
/>
|
||||
Enable DNSSEC Validation
|
||||
</label>
|
||||
<Check
|
||||
label="Enable DNSSEC Validation"
|
||||
checked={f.dnssecValidation}
|
||||
onChange={(v) => set('dnssecValidation', v)}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
{/* "this-server" takes no proxy: upstream hides the whole block. */}
|
||||
{!f.useThisServer && (
|
||||
<GroupRow modal label="Network Proxy">
|
||||
{PROXY_TYPES.map((p) => (
|
||||
<label key={p.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="addZoneProxyType"
|
||||
checked={f.proxyType === p.value}
|
||||
onChange={() => set('proxyType', p.value)}
|
||||
/>
|
||||
{p.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="addZoneProxyType"
|
||||
value={f.proxyType}
|
||||
onChange={(v) => set('proxyType', v as typeof f.proxyType)}
|
||||
options={PROXY_TYPES.map((p) => ({ value: p.value, label: p.label }))}
|
||||
/>
|
||||
<Field label="Proxy Server Address">
|
||||
{(id) => (
|
||||
<Input
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { convertZone } from '../../../api/zones'
|
||||
import { Alert } from '../../../ui/Alert'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import type { Notice } from '../types'
|
||||
import styles from '../Zones.module.css'
|
||||
@@ -111,18 +112,16 @@ export function ConvertZone({
|
||||
|
||||
<div className={styles.fields}>
|
||||
<GroupRow modal label="Convert To">
|
||||
{(['Primary', 'Forwarder', 'Catalog'] as ConversionTarget[]).map((d) => (
|
||||
<label key={d} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="convertTo"
|
||||
disabled={!table.enabled2.includes(d)}
|
||||
checked={target === d}
|
||||
onChange={() => setTarget(d)}
|
||||
/>
|
||||
{LABELS[d]}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="convertTo"
|
||||
value={target ?? ''}
|
||||
onChange={(v) => setTarget(v as ConversionTarget)}
|
||||
options={(['Primary', 'Forwarder', 'Catalog'] as ConversionTarget[]).map((d) => ({
|
||||
value: d,
|
||||
label: LABELS[d],
|
||||
disabled: !table.enabled2.includes(d),
|
||||
}))}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
<Alert type="info" title="Note!">
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '../../../api/dnssec'
|
||||
import { Alert } from '../../../ui/Alert'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Field, Input, Select, Textarea } from '../../../ui/Field'
|
||||
import { Loading } from '../../../ui/Empty'
|
||||
@@ -535,25 +536,20 @@ export function DnssecProperties({
|
||||
)}
|
||||
|
||||
<div className={frm.mrowCtl}>
|
||||
{GENERATIONS.map((g) => (
|
||||
<label key={g.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="addKeyGeneration"
|
||||
checked={newKey.generation === g.value}
|
||||
onChange={() =>
|
||||
// zone.js:501-526: the rollover follows, and the PEM is cleared.
|
||||
setNewKey((k) => ({
|
||||
...k,
|
||||
generation: g.value,
|
||||
pem: '',
|
||||
rolloverDays: addKeyRollover(k.keyType, g.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{g.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="addKeyGeneration"
|
||||
value={newKey.generation}
|
||||
onChange={(v) =>
|
||||
// zone.js:501-526: the rollover follows, and the PEM is cleared.
|
||||
setNewKey((k) => ({
|
||||
...k,
|
||||
generation: v as typeof k.generation,
|
||||
pem: '',
|
||||
rolloverDays: addKeyRollover(k.keyType, v as typeof k.generation),
|
||||
}))
|
||||
}
|
||||
options={GENERATIONS.map((g) => ({ value: g.value, label: g.label }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newKey.algorithm === 'RSA' && newKey.generation === 'Automatic' && (
|
||||
@@ -635,20 +631,12 @@ MII...
|
||||
|
||||
<div className={styles.group}>
|
||||
<div className={styles.groupTitle}>Proof of Non-Existence</div>
|
||||
{NX_PROOFS.map((n) => (
|
||||
<div key={n.value}>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="propsNxProof"
|
||||
checked={nxProof === n.value}
|
||||
onChange={() => setNxProof(n.value as NxProof)}
|
||||
/>
|
||||
{n.label}
|
||||
</label>
|
||||
<div className={styles.help}>{n.help}</div>
|
||||
</div>
|
||||
))}
|
||||
<Radios
|
||||
name="propsNxProof"
|
||||
value={nxProof}
|
||||
onChange={(v) => setNxProof(v as NxProof)}
|
||||
options={NX_PROOFS.map((n) => ({ value: n.value, label: n.label, help: n.help }))}
|
||||
/>
|
||||
|
||||
{nxProof === 'NSEC3' && (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { exportZoneText, saveZoneFile } from '../../../api/zones'
|
||||
import { Alert } from '../../../ui/Alert'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Check } from '../../../ui/Check'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Loading } from '../../../ui/Empty'
|
||||
import { LabeledTextarea } from '../../../ui/Field'
|
||||
@@ -132,18 +133,12 @@ export function EditZoneFile({
|
||||
</div>
|
||||
|
||||
<div className={frm.mrowCtl}>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
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 SOA record serial
|
||||
specified in the zone file editor.
|
||||
</div>
|
||||
<Check
|
||||
label="Overwrite SOA Serial"
|
||||
checked={overwriteSoaSerial}
|
||||
onChange={setOverwriteSoaSerial}
|
||||
help="Enable this option to overwrite existing SOA record serial with the SOA record serial specified in the zone file editor."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Alert type="info" title="Note!">
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { importZone } from '../../../api/zones'
|
||||
import { Alert } from '../../../ui/Alert'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Check } from '../../../ui/Check'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { LabeledTextarea } from '../../../ui/Field'
|
||||
import type { Notice } from '../types'
|
||||
@@ -107,52 +109,36 @@ export function ImportZone({
|
||||
|
||||
<div className={styles.fields}>
|
||||
<GroupRow modal label="Import Options">
|
||||
<label className={styles.chk}>
|
||||
<input type="checkbox" checked={overwrite} onChange={(e) => setOverwrite(e.target.checked)} />
|
||||
Overwrite Existing Records
|
||||
</label>
|
||||
<div className={styles.help}>
|
||||
Enable this option to overwrite existing records for the record types being imported.
|
||||
</div>
|
||||
<label className={styles.chk}>
|
||||
<input type="checkbox" checked={overwriteZone} onChange={(e) => setOverwriteZone(e.target.checked)} />
|
||||
Overwrite Zone
|
||||
</label>
|
||||
<div className={styles.help}>
|
||||
Enable this option to delete all existing records from the zone before importing new records.
|
||||
</div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
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>
|
||||
<Check
|
||||
label="Overwrite Existing Records"
|
||||
checked={overwrite}
|
||||
onChange={setOverwrite}
|
||||
help="Enable this option to overwrite existing records for the record types being imported."
|
||||
/>
|
||||
<Check
|
||||
label="Overwrite Zone"
|
||||
checked={overwriteZone}
|
||||
onChange={setOverwriteZone}
|
||||
help="Enable this option to delete all existing records from the zone before importing new records."
|
||||
/>
|
||||
<Check
|
||||
label="Overwrite SOA Serial"
|
||||
checked={overwriteSoaSerial}
|
||||
onChange={setOverwriteSoaSerial}
|
||||
help="Enable this option to overwrite existing SOA record serial with the imported SOA record serial."
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
<GroupRow modal label="Import Type">
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="importType"
|
||||
checked={mode === 'File'}
|
||||
onChange={() => setMode('File')}
|
||||
/>
|
||||
Zone File
|
||||
</label>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="importType"
|
||||
checked={mode === 'Text'}
|
||||
onChange={() => setMode('Text')}
|
||||
/>
|
||||
Text Editor
|
||||
</label>
|
||||
<Radios
|
||||
name="importType"
|
||||
value={mode}
|
||||
onChange={(v) => setMode(v as typeof mode)}
|
||||
options={[
|
||||
{ value: 'File', label: 'Zone File' },
|
||||
{ value: 'Text', label: 'Text Editor' },
|
||||
]}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
{/* Both stay mounted and the other one is hidden, as upstream does
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { signZone, type Algorithm, type NxProof } from '../../../api/dnssec'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Field, Input, Select, Textarea } from '../../../ui/Field'
|
||||
import {
|
||||
@@ -155,17 +156,12 @@ export function SignZone({
|
||||
|
||||
<div className={styles.fields}>
|
||||
<GroupRow modal label="DNSKEY Algorithm">
|
||||
{ALGORITHMS.map((a) => (
|
||||
<label key={a.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="signAlgorithm"
|
||||
checked={f.algorithm === a.value}
|
||||
onChange={() => changeAlgorithm(a.value as Algorithm)}
|
||||
/>
|
||||
{a.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="signAlgorithm"
|
||||
value={f.algorithm}
|
||||
onChange={(v) => changeAlgorithm(v as Algorithm)}
|
||||
options={ALGORITHMS.map((a) => ({ value: a.value, label: a.label }))}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
{isRsa ? (
|
||||
@@ -237,20 +233,12 @@ export function SignZone({
|
||||
/>
|
||||
|
||||
<GroupRow modal label="Proof of Non-Existence">
|
||||
{NX_PROOFS.map((n) => (
|
||||
<div key={n.value}>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
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>
|
||||
))}
|
||||
<Radios
|
||||
name="signNxProof"
|
||||
value={f.nxProof}
|
||||
onChange={(v) => set('nxProof', v as NxProof)}
|
||||
options={NX_PROOFS.map((n) => ({ value: n.value, label: n.label, help: n.help }))}
|
||||
/>
|
||||
</GroupRow>
|
||||
|
||||
{f.nxProof === 'NSEC3' && (
|
||||
@@ -378,17 +366,12 @@ function SigningKey({
|
||||
return (
|
||||
<div className={styles.group}>
|
||||
<div className={styles.groupTitle}>{title}</div>
|
||||
{GENERATIONS.map((g) => (
|
||||
<label key={g.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
checked={generation === g.value}
|
||||
onChange={() => onGeneration(g.value)}
|
||||
/>
|
||||
{g.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name={name}
|
||||
value={generation}
|
||||
onChange={onGeneration}
|
||||
options={GENERATIONS.map((g) => ({ value: g.value, label: g.label }))}
|
||||
/>
|
||||
|
||||
{/* The size only applies to RSA and only with automatic generation. */}
|
||||
{isRsa && generation === 'Automatic' && (
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { getZoneOptions, setZoneOptions, type ZoneOptions as Response } from '../../../api/zones'
|
||||
import { Alert } from '../../../ui/Alert'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Check } from '../../../ui/Check'
|
||||
import { Radios } from '../../../ui/PanelForm'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Field, Input, Select, Textarea } from '../../../ui/Field'
|
||||
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>
|
||||
|
||||
{e.overrideQueryAccess && (
|
||||
<div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={e.overrideLocked}
|
||||
checked={f.overrideCatalogQueryAccess}
|
||||
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>
|
||||
<Check
|
||||
label="Override Query Access Option"
|
||||
disabled={e.overrideLocked}
|
||||
checked={f.overrideCatalogQueryAccess}
|
||||
onChange={(v) => set('overrideCatalogQueryAccess', v)}
|
||||
help="Enable to override Query Access option in the Catalog zone."
|
||||
/>
|
||||
)}
|
||||
{e.overrideZoneTransfer && (
|
||||
<div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={e.overrideLocked}
|
||||
checked={f.overrideCatalogZoneTransfer}
|
||||
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>
|
||||
<Check
|
||||
label="Override Zone Transfer Option"
|
||||
disabled={e.overrideLocked}
|
||||
checked={f.overrideCatalogZoneTransfer}
|
||||
onChange={(v) => set('overrideCatalogZoneTransfer', v)}
|
||||
help="Enable to override Zone Transfer option in the Catalog zone."
|
||||
/>
|
||||
)}
|
||||
{e.overrideNotify && (
|
||||
<div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={e.overrideLocked}
|
||||
checked={f.overrideCatalogNotify}
|
||||
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>
|
||||
<Check
|
||||
label="Override Notify Option"
|
||||
disabled={e.overrideLocked}
|
||||
checked={f.overrideCatalogNotify}
|
||||
onChange={(v) => set('overrideCatalogNotify', v)}
|
||||
help="Enable to override Notify option in the Catalog zone."
|
||||
/>
|
||||
)}
|
||||
<Alert type="info" title="Note!">
|
||||
When a zone becomes a member of a Catalog zone, all of the Catalog zone's Options are
|
||||
@@ -264,18 +251,13 @@ export function ZoneOptions({
|
||||
|
||||
{e.xfrProtocol && (
|
||||
<GroupRow modal label="Zone Transfer Protocol">
|
||||
{XFR_PROTOCOLS.map((x) => (
|
||||
<label key={x.value} className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name="zoneOptionsXfr"
|
||||
disabled={e.primaryServerLocked}
|
||||
checked={f.primaryZoneTransferProtocol === x.value}
|
||||
onChange={() => set('primaryZoneTransferProtocol', x.value)}
|
||||
/>
|
||||
{x.label}
|
||||
</label>
|
||||
))}
|
||||
<Radios
|
||||
name="zoneOptionsXfr"
|
||||
disabled={e.primaryServerLocked}
|
||||
value={f.primaryZoneTransferProtocol}
|
||||
onChange={(v) => set('primaryZoneTransferProtocol', v as typeof f.primaryZoneTransferProtocol)}
|
||||
options={XFR_PROTOCOLS.map((x) => ({ value: x.value, label: x.label }))}
|
||||
/>
|
||||
</GroupRow>
|
||||
)}
|
||||
|
||||
@@ -300,23 +282,19 @@ export function ZoneOptions({
|
||||
)}
|
||||
|
||||
{e.validateZone && (
|
||||
<div>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={e.primaryServerLocked}
|
||||
checked={f.validateZone}
|
||||
onChange={(ev) => set('validateZone', ev.target.checked)}
|
||||
/>
|
||||
Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone
|
||||
</label>
|
||||
{/* index.html:5402 in v15.5.1. */}
|
||||
<div className={styles.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.
|
||||
</div>
|
||||
</div>
|
||||
<Check
|
||||
label="Use ZONEMD to Validate Zone"
|
||||
text={
|
||||
<>
|
||||
Use <External href={RFC_ZONEMD}>ZONEMD</External> to Validate Zone
|
||||
</>
|
||||
}
|
||||
disabled={e.primaryServerLocked}
|
||||
checked={f.validateZone}
|
||||
onChange={(v) => set('validateZone', v)}
|
||||
/* index.html:5402 in v15.5.1. */
|
||||
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."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -616,21 +594,13 @@ function Criterion({
|
||||
return (
|
||||
<>
|
||||
<div className={frm.mrowCtl}>
|
||||
{options.map((o) => (
|
||||
<div key={o.value}>
|
||||
<label className={styles.chk}>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
disabled={locked}
|
||||
checked={value === o.value}
|
||||
onChange={() => onChanged2(o.value)}
|
||||
/>
|
||||
{o.label}
|
||||
</label>
|
||||
<div className={styles.help}>{o.help}</div>
|
||||
</div>
|
||||
))}
|
||||
<Radios
|
||||
name={name}
|
||||
disabled={locked}
|
||||
value={value}
|
||||
onChange={onChanged2}
|
||||
options={options.map((o) => ({ value: o.value, label: o.label, help: o.help }))}
|
||||
/>
|
||||
</div>
|
||||
<Field label={listLabel}>
|
||||
{(id) => (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getZonePermissions, serializePermissions, setZonePermissions } from '../../../api/zones'
|
||||
import { Button } from '../../../ui/Button'
|
||||
import { Tick } from '../../../ui/Tick'
|
||||
import { Dialog } from '../../../ui/Dialog'
|
||||
import { Field, Select } from '../../../ui/Field'
|
||||
import { Loading } from '../../../ui/Empty'
|
||||
@@ -191,13 +192,9 @@ function PermissionsTable({
|
||||
th: { style: { width: 70 } },
|
||||
header: ['View', 'Modify', 'Delete'][n],
|
||||
cell: (f: (typeof rows)[number], i: number) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.chkPerm}
|
||||
aria-label={`${key} for ${f.name}`}
|
||||
checked={f[key]}
|
||||
onChange={(e) => change(i, key, e.target.checked)}
|
||||
/>
|
||||
<span className={styles.chkPerm}>
|
||||
<Tick label={`${key} for ${f.name}`} checked={f[key]} onChange={(v) => change(i, key, v)} />
|
||||
</span>
|
||||
),
|
||||
})),
|
||||
{
|
||||
|
||||
+13
-2
@@ -25,8 +25,12 @@ export function Check({
|
||||
help,
|
||||
disabled,
|
||||
toggle = false,
|
||||
text,
|
||||
}: {
|
||||
/** The control's accessible name, and the visible text unless `text` replaces it. */
|
||||
label: string
|
||||
/** Visible text with markup in it (a link), when `label` alone would lose it. */
|
||||
text?: ReactNode
|
||||
checked: boolean
|
||||
onChange: (v: boolean) => void
|
||||
help?: ReactNode
|
||||
@@ -52,8 +56,15 @@ export function Check({
|
||||
) : (
|
||||
<Checkbox nativeButton render={<button type="button" />} {...common} />
|
||||
)}
|
||||
<span className={styles.text} onClick={() => !disabled && onChange(!checked)}>
|
||||
{label}
|
||||
<span
|
||||
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>
|
||||
</div>
|
||||
{help && <div className={toggle ? `${styles.help} ${styles.helpSwitch}` : styles.help}>{help}</div>}
|
||||
|
||||
+40
-15
@@ -1,4 +1,5 @@
|
||||
import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Alert } from './Alert'
|
||||
import { Button } from './Button'
|
||||
import { Empty } from './Empty'
|
||||
@@ -242,14 +243,21 @@ export function Trailer({ children }: { children: ReactNode }) {
|
||||
|
||||
export interface RadioOption {
|
||||
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
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
The radio group. It shares its row and its help with `ui/Check`: they are the
|
||||
same control with different cardinality, and when each screen wrote its own, a
|
||||
radio's help and a checkbox's help did not land on the same left edge.
|
||||
The radio group, ReUI's `RadioGroup`. It shares its row and its help with `ui/Check`:
|
||||
they are the same control with different cardinality, and when each screen wrote
|
||||
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({
|
||||
name,
|
||||
@@ -257,32 +265,49 @@ export function Radios({
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
label,
|
||||
}: {
|
||||
name: string
|
||||
value: string
|
||||
options: RadioOption[]
|
||||
onChange: (v: string) => void
|
||||
disabled?: boolean
|
||||
/** The group's accessible name, when its heading is not a `GroupRow` label. */
|
||||
label?: string
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<RadioGroup
|
||||
name={name}
|
||||
aria-label={label}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onValueChange={(v) => onChange(String(v))}
|
||||
className="gap-0"
|
||||
>
|
||||
{options.map((o) => (
|
||||
<div key={o.value}>
|
||||
<label className={check.check}>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
<div className={check.check}>
|
||||
<RadioGroupItem
|
||||
value={o.value}
|
||||
checked={value === o.value}
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(o.value)}
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
disabled={o.disabled}
|
||||
aria-label={o.name ?? (typeof o.label === 'string' ? o.label : o.value)}
|
||||
/>
|
||||
<span className={check.text}>{o.label}</span>
|
||||
</label>
|
||||
<span
|
||||
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>}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user