diff --git a/src/components/serverpuls/AppsTable.jsx b/src/components/serverpuls/AppsTable.jsx index 25f6760..052e0d8 100644 --- a/src/components/serverpuls/AppsTable.jsx +++ b/src/components/serverpuls/AppsTable.jsx @@ -1,22 +1,26 @@ import { useMemo, useState } from 'react' import { formatBytes, formatDelta, formatPct } from '../../lib/serverpulsFormat' +import { isRowActivation } from '../../lib/rowClick' import Sparkline from './Sparkline' -const COLS = [ - { key: 'name', label: 'Projekt', get: (r) => r.name.toLowerCase() }, - { key: 'sizeBytes', label: 'Größe', get: (r) => r.sizeBytes }, - { key: 'delta7d', label: 'Δ 7 T', get: (r) => r.delta7d }, - { key: 'delta30d', label: 'Δ 30 T', get: (r) => r.delta30d }, - { key: 'cpuPct', label: 'CPU', get: (r) => r.cpuPct }, - { key: 'memBytes', label: 'RAM', get: (r) => r.memBytes }, -] - +// Das Vorzeichen steckt in d.text, die Richtung färbt den Text (rot = gestiegen, grün = gesunken) function Delta({ pct }) { const d = formatDelta(pct) - const arrow = d.dir === 'up' ? '▲ ' : d.dir === 'down' ? '▼ ' : '' - return {arrow}{d.text} + return {d.text} } +// num: Zahlenspalte (rechts, tabellarische Ziffern), hide: unter 860 px ausgeblendet (Kernspalten bleiben) +const COLS = [ + { key: 'name', label: 'Projekt', get: (r) => r.name.toLowerCase() }, + { key: 'sizeBytes', label: 'Größe', num: true, get: (r) => r.sizeBytes, cell: (r) => formatBytes(r.sizeBytes) }, + { key: 'delta7d', label: 'Δ 7 T', num: true, get: (r) => r.delta7d, cell: (r) => }, + { key: 'delta30d', label: 'Δ 30 T', num: true, hide: true, get: (r) => r.delta30d, cell: (r) => }, + { key: 'cpuPct', label: 'CPU', num: true, hide: true, get: (r) => r.cpuPct, cell: (r) => (r.cpuPct === null ? '–' : formatPct(r.cpuPct)) }, + { key: 'memBytes', label: 'RAM', num: true, get: (r) => r.memBytes, cell: (r) => formatBytes(r.memBytes) }, +] + +const cellClass = (c) => [c.num && 'right num', c.hide && 'hide-sm'].filter(Boolean).join(' ') || undefined + export default function AppsTable({ rows, selectedId, onSelect }) { const [q, setQ] = useState('') const [sort, setSort] = useState({ key: 'sizeBytes', dir: 'desc' }) @@ -39,52 +43,71 @@ export default function AppsTable({ rows, selectedId, onSelect }) { const toggle = (key) => setSort((s) => (s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: key === 'name' ? 'asc' : 'desc' })) + // Der Knopf in der ersten Zelle trägt die Tastatur, der Zeilenklick die Maus: isRowActivation filtert Klicks auf Knöpfe aus, + // so löst jede Aktivierung onSelect genau einmal aus + const pick = (id) => onSelect(selectedId === id ? null : id) + return ( -
- setQ(e.target.value)} - /> -
- - - - {COLS.map((c) => ( - - ))} - - - - - {list.map((r) => ( +
+
+ setQ(e.target.value)} + /> +
+
toggle(c.key)} className="sp-sortable" data-sort={c.key}> - {c.label}{sort.key === c.key ? (sort.dir === 'asc' ? ' ↑' : ' ↓') : ''} - 30 Tage
+ + + {COLS.map((c) => ( + + ))} + + + + + {list.map((r) => { + const selected = selectedId === r.id + return ( onSelect(selectedId === r.id ? null : r.id)} + onClick={(e) => { + if (isRowActivation(e)) pick(r.id) + }} > - - - - - - - + + {COLS.slice(1).map((c) => ( + + ))} + - ))} - {list.length === 0 && ( - - )} - -
+ + 30 Tage
{r.name}{formatBytes(r.sizeBytes)}{r.cpuPct === null ? '–' : formatPct(r.cpuPct)}{formatBytes(r.memBytes)} + + {c.cell(r)}
Keine Projekte gefunden.
-
+ ) + })} + {list.length === 0 && ( + Keine Projekte gefunden. + )} + +
) } diff --git a/src/styles/serverpuls.css b/src/styles/serverpuls.css index 4cda93c..21d4df0 100644 --- a/src/styles/serverpuls.css +++ b/src/styles/serverpuls.css @@ -1,4 +1,4 @@ -/* Serverpuls (P9): Kopf, Warnungen, Kacheln, Umschalter, Panels. Die Tabellenregeln weiter unten gehoeren Task 5. */ +/* Serverpuls (P9): Kopf, Warnungen, Kacheln, Umschalter, Panels, Projekttabelle */ .sp-page .msg { margin: 0 0 16px; } .sp-head-top { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } @@ -26,16 +26,14 @@ .sp-chart .u-legend { font-size: 13px; color: var(--text-2); } .sp-nodata { padding: 40px 0; text-align: center; color: var(--text-2); } -.sp-search { width: 100%; max-width: 320px; margin-bottom: var(--sp-3); background: var(--surface-2); border: 1px solid var(--border); color: var(--text); border-radius: var(--r-sm); padding: 8px 12px; font: inherit; } -.sp-table-wrap { overflow-x: auto; } -.sp-table { width: 100%; border-collapse: collapse; font-size: 14px; } -.sp-table th { text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; } -.sp-sortable { cursor: pointer; user-select: none; } -.sp-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; } -.sp-row { cursor: pointer; } -.sp-row:hover, .sp-row-active { background: var(--surface-2); } -.sp-name { font-weight: 600; } -.sp-empty { text-align: center; color: var(--text-muted); } +.sp-sort { display: block; width: 100%; text-align: inherit; white-space: nowrap; } +.sp-page .table td.num { white-space: nowrap; } +.sp-name { overflow-wrap: anywhere; } +.sp-name .link { text-align: left; overflow-wrap: anywhere; } +.sp-page .table tr.sp-row-active td { background: var(--bg-subtle); } +.sp-page .table tr.sp-row-active td:first-child { box-shadow: -12px 0 0 var(--bg-subtle); } +.sp-page .table tr.sp-row-active td:last-child { box-shadow: 12px 0 0 var(--bg-subtle); } +.sp-empty { text-align: center; color: var(--text-2); } .sp-delta { font-variant-numeric: tabular-nums; } .sp-detail { margin-top: 28px; }