admin: title-only information becomes visible text

the client address follows its name as visible muted text in the query tables, the config lock indicator prints its reason beside the tag except in table rows where a page-level note explains the lock instead, and the locked delete buttons describe themselves through that one visible note. the chart legend tooltip is deleted because a named client is deliberately not addressed in the chart, and the dead series address field went with it. titles that merely repeat visible copyable text stay.
This commit is contained in:
2026-08-29 13:03:30 +02:00
parent 207252acee
commit c65d92d8f8
9 changed files with 154 additions and 51 deletions
+30 -6
View File
@@ -10,7 +10,7 @@ import NetworkAssignments from "./NetworkAssignments";
import { ClientDisplayName } from "./clientIdentity";
import InlineError from "@/lib/InlineError";
import ConfirmDialog from "@/ui/ConfirmDialog";
import ConfigLockIndicator from "@/features/configuration/ConfigLockIndicator";
import ConfigLockIndicator, { lockReason } from "@/features/configuration/ConfigLockIndicator";
import { useAuthority, useReadOnlyConfig, type Authority } from "@/features/configuration/authority";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
@@ -25,6 +25,9 @@ import { colors } from "@/ui/tokens.stylex";
* alone cannot say which operator surface set the row — so the sentence names
* the doubt rather than asserting a declaration, the way `provenanceOf` does.
*/
/** The one note every locked Delete on this page describes itself with. */
const DELETE_LOCK_NOTE_ID = "clients-delete-locked-note";
function declaredDeleteNote(authority: Authority): string {
if (authority.state === "resolved") {
return "This client is declared in the configuration file; remove it there and restart.";
@@ -52,6 +55,15 @@ const styles = stylex.create({
marginTop: "1rem",
color: colors.textMuted,
},
lockNote: {
marginTop: "1rem",
display: "flex",
flexDirection: "column",
gap: "0.25rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
filterBar: {
marginTop: "1rem",
display: "flex",
@@ -127,6 +139,11 @@ export default function ClientsPage() {
// question: the operator is told why, and only they close the dialog.
const deleteLocked = pendingDelete !== null && readOnly && pendingDelete.hand_edited;
// The reason a locked Delete will not answer, printed once above the table.
// Per row it would repeat down the whole page; on the button it was a `title`
// that a keyboard and a touch screen never reached.
const deletesLocked = readOnly && rows.some((client) => client.hand_edited);
return (
<section>
<h1 {...stylex.props(styles.heading)}>Clients</h1>
@@ -142,6 +159,15 @@ export default function ClientsPage() {
</Link>
</div>
)}
{/* The whole explanation for this page's locks, printed once. Per row it
would repeat down the table; on the controls it was a `title` that a
keyboard and a touch screen never reached. */}
{readOnly && (
<div {...stylex.props(styles.lockNote)}>
<p>{lockReason(authority)}.</p>
{deletesLocked && <p id={DELETE_LOCK_NOTE_ID}>{declaredDeleteNote(authority)}</p>}
</div>
)}
{clients.length === 0 ? (
<p {...stylex.props(styles.empty)}>
No clients yet. Rows appear automatically as devices on the network make DNS queries there is
@@ -187,7 +213,7 @@ export default function ClientsPage() {
{/* Naming a client writes configuration, so the affordance is
absent — not disabled — wherever the write cannot land. */}
{readOnly ? (
<ConfigLockIndicator />
<ConfigLockIndicator compact />
) : (
<button
type="button"
@@ -201,10 +227,8 @@ export default function ClientsPage() {
type="button"
onClick={() => setPendingDelete(client)}
disabled={readOnly && client.hand_edited}
title={
readOnly && client.hand_edited
? declaredDeleteNote(authority)
: undefined
aria-describedby={
readOnly && client.hand_edited ? DELETE_LOCK_NOTE_ID : undefined
}
{...stylex.props(
shared.smallButton,