Move the console onto ReUI PRO

Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move
the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel,
Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame
(DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale.

Design only: behaviour, texts and roles are unchanged. Tests that asserted the old
DOM were updated; the tests now format numbers in English wherever they run.
docs/reui-migration-plan.md records the plan and its decisions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-27 00:06:06 +07:00
commit f3531c64f9
680 files changed
+139836

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
/*
Stale data: the list is still there and you can see it is no longer current.
It is a decision of the Zones design, and the console's design rule: *old data
never looks like new data*.
What was there already kept the zones when a refresh failed, and that is right:
throwing them away would leave the user with nothing over a network error. What
was missing was SAYING SO. Without the mark, a ten-minute-old list reads like a
one-second-old one, and in a DNS console that means believing a state that no
longer exists.
*/
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as client from '../../api/client'
import { Zones } from './Zones'
afterEach(() => vi.restoreAllMocks())
const ZONE = {
name: 'casa.test', type: 'Primary', lastModified: '2026-08-26T10:00:00Z',
disabled: false, soaSerial: 7, catalog: null, dnssecStatus: 'Unsigned',
hasDnssecPrivateKeys: false, notifyFailed: false, notifyFailedFor: [],
}
const LIST = { zones: [ZONE], pageNumber: 1, totalPages: 1, totalZones: 1 }
/** Answers well the first time and badly from the second on. */
function goodThenBroken() {
let n = 0
return vi.spyOn(client, 'apiRequest').mockImplementation(async (route) => {
const base = route.split('?')[0]
if (base !== 'zones/list') return { kind: 'ok', data: { status: 'ok', response: {} } } as never
n += 1
return n === 1
? ({ kind: 'ok', data: { status: 'ok', response: LIST } } as never)
: ({ kind: 'error', message: 'boom' } as never)
})
}
const clickGo = async (user: ReturnType<typeof userEvent.setup>) =>
user.click(screen.getByRole('button', { name: 'Go' }))
describe('stale data', () => {
it('while the refresh goes well, there is no mark', async () => {
vi.spyOn(client, 'apiRequest').mockResolvedValue({
kind: 'ok', data: { status: 'ok', response: LIST },
} as never)
render(<Zones token="t" canModify canDelete />)
expect(await screen.findByRole('button', { name: 'casa.test' })).toBeInTheDocument()
expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument()
})
/* The one that matters: the list STAYS, and it is also said to be stale. */
it('when the refresh fails, the list stays and it is reported', async () => {
const user = userEvent.setup()
goodThenBroken()
render(<Zones token="t" canModify canDelete />)
await screen.findByRole('button', { name: 'casa.test' })
await clickGo(user)
await screen.findByText(/Could not refresh/)
/* The previous one is not thrown away: that would leave the user with nothing over a network error. */
expect(screen.getByRole('button', { name: 'casa.test' })).toBeInTheDocument()
})
it('the strip carries the time of the last good data and a Retry', async () => {
const user = userEvent.setup()
goodThenBroken()
render(<Zones token="t" canModify canDelete />)
await screen.findByRole('button', { name: 'casa.test' })
await clickGo(user)
const banner = (await screen.findByText(/Could not refresh/)).closest('[role=alert]')!
expect(banner).toHaveTextContent(/Last good data:/)
expect(within(banner as HTMLElement).getByRole('button', { name: 'Retry' })).toBeInTheDocument()
})
/* No selection: acting in bulk on a list that no longer reflects the server is
how the wrong zone gets deleted. */
it('with stale data there is no selecting', async () => {
const user = userEvent.setup()
goodThenBroken()
render(<Zones token="t" canModify canDelete />)
await screen.findByRole('button', { name: 'casa.test' })
expect(screen.getByLabelText('Select casa.test')).toBeEnabled()
await clickGo(user)
await screen.findByText(/Could not refresh/)
expect(screen.getByLabelText('Select casa.test')).toBeDisabled()
expect(screen.getByLabelText('Select all zones')).toBeDisabled()
})
/* And with no previous data nothing goes stale: that is a plain error, and the
notice reports it. Marking a list that never existed promises data there is none of. */
it('when the FIRST load fails, it does not say anything is stale', async () => {
vi.spyOn(client, 'apiRequest').mockResolvedValue({ kind: 'error', message: 'boom' } as never)
render(<Zones token="t" canModify canDelete />)
await waitFor(() => expect(screen.queryByRole('button', { name: 'casa.test' })).not.toBeInTheDocument())
expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument()
})
it('and when it goes well again, the mark goes away', async () => {
const user = userEvent.setup()
let n = 0
vi.spyOn(client, 'apiRequest').mockImplementation(async (route) => {
const base = route.split('?')[0]
if (base !== 'zones/list') return { kind: 'ok', data: { status: 'ok', response: {} } } as never
n += 1
return n === 2
? ({ kind: 'error', message: 'boom' } as never)
: ({ kind: 'ok', data: { status: 'ok', response: LIST } } as never)
})
render(<Zones token="t" canModify canDelete />)
await screen.findByRole('button', { name: 'casa.test' })
await clickGo(user)
await screen.findByText(/Could not refresh/)
await clickGo(user)
await waitFor(() => expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument())
expect(screen.getByLabelText('Select casa.test')).toBeEnabled()
})
})
/*
A failure is reported ONCE.
With previous data the strip reports it (and it also says since when and offers a
retry), and the notice at the top keeps quiet. If both fired, the same failure
would appear twice and you would have to decide which to read. With no previous
data it is the other way round: there is nothing to go stale, so the notice
speaks, carrying the server's message.
*/
describe('who reports the failure', () => {
it('with previous data the strip speaks, and only the strip', async () => {
const user = userEvent.setup()
goodThenBroken()
render(<Zones token="t" canModify canDelete />)
await screen.findByRole('button', { name: 'casa.test' })
await clickGo(user)
await screen.findByText(/Could not refresh/)
/* A single `role=alert` on the screen: the strip. The server notice does not
duplicate the same event. */
expect(screen.getAllByRole('alert')).toHaveLength(1)
expect(screen.queryByText(/boom/)).not.toBeInTheDocument()
})
it('with no previous data the notice speaks, with the server message', async () => {
vi.spyOn(client, 'apiRequest').mockResolvedValue({ kind: 'error', message: 'boom' } as never)
render(<Zones token="t" canModify canDelete />)
expect(await screen.findByText(/boom/)).toBeInTheDocument()
expect(screen.queryByText(/Could not refresh/)).not.toBeInTheDocument()
})
})