From e401cbaf9811380cb40df584981122e399731efe Mon Sep 17 00:00:00 2001 From: Juan Denis Date: Tue, 3 Nov 2026 17:59:02 -0500 Subject: [PATCH] refactor(frontend): declutter ServerDetail header Title is just the server name (placeholder names show 'Pending pairing'), status is a small dot, and endpoint + ID (with a copy button) sit behind an (i) InfoTip. The favorite star is subtler, and on narrow screens the title and actions share one row. --- frontend/src/components/ds/StatusDot.jsx | 9 +++++ frontend/src/components/ds/index.js | 1 + .../serverdetail/ServerHeaderMeta.jsx | 33 ++++++++++++++++++ .../serverdetail/serverDetailData.js | 5 +++ .../serverdetail/serverDetailData.test.mjs | 14 ++++++++ frontend/src/i18n/locales/en.json | 5 +++ frontend/src/pages/ServerDetail.jsx | 17 +++------- .../src/styles/components/_design-system.scss | 16 +++++++++ frontend/src/styles/pages/_servers.scss | 34 +++++++++++++++++++ 9 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/ds/StatusDot.jsx create mode 100644 frontend/src/components/serverdetail/ServerHeaderMeta.jsx create mode 100644 frontend/src/components/serverdetail/serverDetailData.test.mjs diff --git a/frontend/src/components/ds/StatusDot.jsx b/frontend/src/components/ds/StatusDot.jsx new file mode 100644 index 00000000..8924ff40 --- /dev/null +++ b/frontend/src/components/ds/StatusDot.jsx @@ -0,0 +1,9 @@ +import { cn } from '@/lib/utils'; + +// A Pill's dot without the label; the status text rides along as the accessible +// name and tooltip. kind: same vocabulary as Pill. +export function StatusDot({ kind = 'gray', label, className }) { + return ; +} + +export default StatusDot; diff --git a/frontend/src/components/ds/index.js b/frontend/src/components/ds/index.js index 5ae38562..a66deb05 100644 --- a/frontend/src/components/ds/index.js +++ b/frontend/src/components/ds/index.js @@ -2,6 +2,7 @@ // Shared, reusable building blocks for the new "infra console" look. Styles // live in styles/components/_design-system.scss (sk-* namespace). export { Pill } from './Pill'; +export { StatusDot } from './StatusDot'; export { statusKind, statusVariant, statusDotClass, statusLabel, alertStatusKind, processStateVariant, envActionKind, diff --git a/frontend/src/components/serverdetail/ServerHeaderMeta.jsx b/frontend/src/components/serverdetail/ServerHeaderMeta.jsx new file mode 100644 index 00000000..1ed9c32d --- /dev/null +++ b/frontend/src/components/serverdetail/ServerHeaderMeta.jsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next'; +import FavoriteStar from '@/components/FavoriteStar'; +import InfoTip from '@/components/InfoTip'; +import CopyButton from '@/components/CopyButton'; +import { StatusDot } from '../ds'; +import { serverStatusKind } from './serverDetailData'; + +// Header meta: [status dot] [(i) with endpoint + ID] [star]. Everything the +// header used to spell out lives behind the (i). +export default function ServerHeaderMeta({ server }) { + const { t } = useTranslation(); + const status = server.status || 'pending'; + const endpoint = server.hostname || server.ip_address || t('app.serverDetail.noEndpointConfigured', 'No endpoint configured'); + return ( + <> + + +
+
{t('common.labels.status', 'Status')}
+
{status}
+
{t('app.serverDetail.endpoint', 'Endpoint')}
+
{endpoint}
+
{t('app.serverDetail.serverId', 'ID')}
+
+ {server.id} + +
+
+
+ + + ); +} diff --git a/frontend/src/components/serverdetail/serverDetailData.js b/frontend/src/components/serverdetail/serverDetailData.js index 258dc345..e9a8c8fa 100644 --- a/frontend/src/components/serverdetail/serverDetailData.js +++ b/frontend/src/components/serverdetail/serverDetailData.js @@ -5,6 +5,11 @@ import { statusKind } from '../ds/status.js'; // Server connection states ride the shared vocabulary. export const serverStatusKind = (status) => statusKind(status); +// The backend names an unpaired server "Pending pairing ()" +// (api/servers.py _placeholder_name); the id suffix is noise in a heading. +const PLACEHOLDER_NAME = /^Pending pairing \(.*\)$/; +export const isPlaceholderServerName = (name) => PLACEHOLDER_NAME.test(name ?? ''); + export const PRESET_LABELS = { '* * * * *': 'Every minute', '*/5 * * * *': 'Every 5 minutes', diff --git a/frontend/src/components/serverdetail/serverDetailData.test.mjs b/frontend/src/components/serverdetail/serverDetailData.test.mjs new file mode 100644 index 00000000..68dc32b6 --- /dev/null +++ b/frontend/src/components/serverdetail/serverDetailData.test.mjs @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { isPlaceholderServerName } from './serverDetailData.js'; + +test('backend placeholder names are detected', () => { + assert.equal(isPlaceholderServerName('Pending pairing (342ade51)'), true); +}); + +test('real names are left alone', () => { + assert.equal(isPlaceholderServerName('web-01'), false); + assert.equal(isPlaceholderServerName('Pending pairing'), false); + assert.equal(isPlaceholderServerName(undefined), false); +}); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index c9298e20..0d65f546 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -5387,7 +5387,10 @@ "alerts": "Alerts", "areYouSureYouWantTo": "Remove this server? This can't be undone.", "backToServers": "Back to servers", + "copyId": "Copy ID", "cron": "Cron", + "details": "Server details", + "endpoint": "Endpoint", "errorLoadingServer": "Couldn't load the server.", "failedToAcknowledgeAlert": "Couldn't acknowledge the alert.", "failedToPingServer": "Couldn't ping the server.", @@ -5398,6 +5401,7 @@ "metrics": "Metrics", "noEndpointConfigured": "No endpoint configured", "packages": "Packages", + "pendingPairing": "Pending pairing", "ping": "Ping", "proxy": "Proxy", "remoteAccess": "Remote access", @@ -5405,6 +5409,7 @@ "restorePoints": "Restore points", "sections": "Server sections", "serverDidNotRespond": "Server did not respond", + "serverId": "ID", "serverNotFound": "Server not found", "serverRemovedSuccessfully": "Server removed", "serverRespondedInMs": "Server responded in {{latency}}ms", diff --git a/frontend/src/pages/ServerDetail.jsx b/frontend/src/pages/ServerDetail.jsx index 8d9debbc..4b8f252a 100644 --- a/frontend/src/pages/ServerDetail.jsx +++ b/frontend/src/pages/ServerDetail.jsx @@ -1,13 +1,12 @@ -import { serverStatusKind } from '../components/serverdetail/serverDetailData'; +import { isPlaceholderServerName } from '../components/serverdetail/serverDetailData'; +import ServerHeaderMeta from '../components/serverdetail/ServerHeaderMeta'; import { useState, useEffect, useCallback } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import api from '../services/api'; import { useToast } from '../contexts/useToast.js'; import { useConfirm } from '../hooks/useConfirm'; import { useRecordVisit } from '@/hooks/useRecordVisit'; -import FavoriteStar from '@/components/FavoriteStar'; import { Button } from '@/components/ui/button'; -import { Pill } from '../components/ds'; import { Tabs, TabsContent } from '@/components/ui/tabs'; import { Tip } from '@/components/ui/tooltip'; import { Server, ArrowLeft } from 'lucide-react'; @@ -276,17 +275,9 @@ const ServerDetail = () => { } - title={server.name} + title={isPlaceholderServerName(server.name) ? t('app.serverDetail.pendingPairing', 'Pending pairing') : server.name} navLabel={t('app.serverDetail.sections', 'Server sections')} - meta={( - <> - - {server.status || 'pending'} - - {server.hostname || server.ip_address || t('app.serverDetail.noEndpointConfigured', 'No endpoint configured')} - - - )} + meta={} tabs={tabs.map((item) => ({ id: item.id, label: item.badge ? <>{item.label}{item.badge} : item.label, diff --git a/frontend/src/styles/components/_design-system.scss b/frontend/src/styles/components/_design-system.scss index 2a836513..357b4eae 100644 --- a/frontend/src/styles/components/_design-system.scss +++ b/frontend/src/styles/components/_design-system.scss @@ -14,6 +14,22 @@ $sk-tones: ('accent', 'green', 'cyan', 'amber', 'red', 'violet'); // ------------------------------------------------------------ // Pill — status badge (dot + label) // ------------------------------------------------------------ +// StatusDot: the Pill's dot on its own, for headers that want colour without a label. +.sk-statusdot { + display: inline-block; + flex: none; + width: 8px; // geometry: status dot + height: 8px; // geometry: status dot + border-radius: 50%; + + &--green { background: var(--green); } + &--amber { background: var(--amber); } + &--red { background: var(--red); } + &--cyan { background: var(--cyan); } + &--violet { background: var(--violet); } + &--gray { background: var(--text-faint); } +} + .sk-pill { // Badge spec: 20px tall, 4px radius, 12px/500, tinted fill, flat dot. display: inline-flex; diff --git a/frontend/src/styles/pages/_servers.scss b/frontend/src/styles/pages/_servers.scss index 68ffeac9..a6d83bdf 100644 --- a/frontend/src/styles/pages/_servers.scss +++ b/frontend/src/styles/pages/_servers.scss @@ -2554,3 +2554,37 @@ flex-direction: column; gap: $space-3; } + +// Header: dot / (i) / star share one centered row beside the title; on narrow +// screens the title block and actions stay on ONE row (title truncates), tabs below. +.server-detail-page .sk-topbar { + &__titles { align-items: center; } + &__meta { display: inline-flex; align-items: center; gap: $space-2; } + + .fav-star { + width: 24px; // geometry: matches the InfoTip md footprint + height: 24px; // geometry: matches the InfoTip md footprint + svg { width: 14px; height: 14px; } // geometry: inline icon + opacity: 0.6; + &:hover, &.is-on { opacity: 1; } + } + + @media (max-width: 768px) { + &__titles { flex: 1 1 0; min-width: 0; flex-wrap: nowrap; } + &__title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; } + &__spacer { display: none; } + &__actions { order: 0; width: auto; margin-left: auto; justify-content: flex-end; overflow: visible; flex-wrap: nowrap; } + } +} + +.server-detail-info { + display: grid; + grid-template-columns: auto 1fr; + gap: $space-1 $space-3; + margin: 0; + + dt { color: var(--text-faint); } + dd { margin: 0; min-width: 0; overflow-wrap: anywhere; } + + &__id { display: flex; align-items: center; gap: $space-1; code { font-family: $font-mono; } } +}