fix(web): wrap long record values in diff and zone view (no horizontal overflow)

RecordRow now splits into a top line (badge/name/read-only, unaffected
by value length) and a plain block-level values line below it, so a
~400-char unbreakable DKIM key wraps via break-all instead of stretching
the flex row and forcing page-wide horizontal scroll. Zone records table
gets break-all on the values cell too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
2026-07-05 14:27:33 +07:00
parent 1b367c4bda
commit 784e7bd822
3 changed files with 63 additions and 24 deletions
+24
View File
@@ -27,6 +27,30 @@ test("marks read-only records", () => {
expect(screen.getByText(/NS/)).toBeInTheDocument() expect(screen.getByText(/NS/)).toBeInTheDocument()
}) })
test("renders a very long unbreakable value (DKIM key) without crashing", () => {
// Real DKIM records ship a ~400-char unbroken p= blob. This must not
// throw and the value must land in the DOM (wrapping itself is a CSS
// concern verified manually, not via jsdom layout).
const longValue = "v=DKIM1; k=rsa; p=" + "A".repeat(400)
const csWithDkim: ChangesetResponse = {
updates: [
{
kind: "update",
type: "TXT",
name: "default._domainkey.example.com.",
desired: [longValue],
actual: [],
readOnly: false,
},
],
prunes: [],
readOnly: [],
inSyncCount: 0,
}
render(<DiffView changeset={csWithDkim} />)
expect(screen.getByText(new RegExp(longValue))).toBeInTheDocument()
})
test("does not crash when changeset fields are null", () => { test("does not crash when changeset fields are null", () => {
// An empty changeset from an older/edge backend can arrive with null slices // An empty changeset from an older/edge backend can arrive with null slices
// instead of []. DiffView must normalise them, not blow up on .length/.map. // instead of []. DiffView must normalise them, not blow up on .length/.map.
+27 -14
View File
@@ -47,11 +47,14 @@ function RecordRow({ record, tone }: { record: RecordView; tone: Tone }) {
return ( return (
<div <div
className={cn( className={cn(
"group/row flex items-center gap-3 border-l-2 px-3 py-2.5 transition-colors", "group/row flex flex-col gap-1.5 border-l-2 px-3 py-2.5 transition-colors",
"hover:bg-foreground/[0.025]", "hover:bg-foreground/[0.025]",
)} )}
style={{ borderLeftColor: meta.dot }} style={{ borderLeftColor: meta.dot }}
> >
{/* Top line: type badge, name, read-only flag — always single-line,
never affected by how long the record values are. */}
<div className="flex items-center gap-3">
<Badge <Badge
variant="outline" variant="outline"
className="font-dns w-11 shrink-0 justify-center border-border text-[10px] tracking-wide text-muted-foreground" className="font-dns w-11 shrink-0 justify-center border-border text-[10px] tracking-wide text-muted-foreground"
@@ -63,28 +66,38 @@ function RecordRow({ record, tone }: { record: RecordView; tone: Tone }) {
{record.name} {record.name}
</span> </span>
<span className="font-dns hidden shrink-0 items-center gap-1.5 text-xs text-muted-foreground sm:flex">
<Values values={record.actual} />
{showArrow && (
<>
<ArrowRight className="size-3 text-muted-foreground/50" strokeWidth={1.75} />
<span style={{ color: meta.dot }}>
<Values values={record.desired} />
</span>
</>
)}
</span>
{record.readOnly && ( {record.readOnly && (
<Badge <Badge
variant="secondary" variant="secondary"
className="ml-1 shrink-0 gap-1 bg-muted text-[10px] text-muted-foreground" className="shrink-0 gap-1 bg-muted text-[10px] text-muted-foreground"
> >
<Lock className="size-2.5" strokeWidth={2} /> <Lock className="size-2.5" strokeWidth={2} />
read-only read-only
</Badge> </Badge>
)} )}
</div> </div>
{/* Values line: plain block-level text (not flex) so a long
unbreakable value like a DKIM key wraps within the row's own
width instead of stretching it — a flex item's content can
otherwise refuse to shrink below its intrinsic width. Indented
to align under the name (badge width + gap). */}
<div className="font-dns hidden pl-14 text-xs leading-relaxed break-all text-muted-foreground sm:block">
<Values values={record.actual} />
{showArrow && (
<>
{" "}
<ArrowRight
className="mx-1 inline-block size-3 align-[-2px] text-muted-foreground/50"
strokeWidth={1.75}
/>{" "}
<span style={{ color: meta.dot }}>
<Values values={record.desired} />
</span>
</>
)}
</div>
</div>
) )
} }
+3 -1
View File
@@ -167,7 +167,9 @@ export function DomainDiffPage() {
<TableCell className="font-dns">{r.type}</TableCell> <TableCell className="font-dns">{r.type}</TableCell>
<TableCell className="font-dns">{r.name}</TableCell> <TableCell className="font-dns">{r.name}</TableCell>
<TableCell className="font-dns">{r.ttl}</TableCell> <TableCell className="font-dns">{r.ttl}</TableCell>
<TableCell className="font-dns whitespace-pre-line">{r.values.join("\n")}</TableCell> <TableCell className="font-dns whitespace-pre-line break-all">
{r.values.join("\n")}
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>