system monitor panel keyed (544 -> 528)
Pm2View's column metadata is module-level, so it carries labelKey and the header resolves it. '#', '↺' and 'PID' keep plain labels — a glyph and an acronym read the same in every locale, and a key whose two sides can never differ is a cost bought for nothing. PanelTranslationKey is now exported from the locales barrel, which panel-level data tables need in the same way officer-web needed AppTranslationKey.
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Cpu, MemoryStick, HardDrive, Server, Thermometer, CircuitBoard, Network, Zap } from 'lucide-react';
|
||||
import { fmtBytes, fmtDuration } from './shared';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
type Sensor = { name: string; label: string; celsius: number };
|
||||
type Stats = {
|
||||
@@ -67,6 +68,7 @@ const Card = ({
|
||||
);
|
||||
|
||||
export const BtopView = () => {
|
||||
const { t } = useTranslation();
|
||||
const { get } = useClient();
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -133,7 +135,7 @@ export const BtopView = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Memory" icon={MemoryStick}>
|
||||
<Card title={t('systemMonitor.memory')} icon={MemoryStick}>
|
||||
{stats.mem ? (
|
||||
<>
|
||||
<div className="flex items-end justify-between">
|
||||
@@ -154,7 +156,7 @@ export const BtopView = () => {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Disks" icon={HardDrive}>
|
||||
<Card title={t('systemMonitor.disks')} icon={HardDrive}>
|
||||
{stats.disks.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{stats.disks.map((d) => (
|
||||
@@ -195,7 +197,7 @@ export const BtopView = () => {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Temperature" icon={Thermometer}>
|
||||
<Card title={t('systemMonitor.temperature')} icon={Thermometer}>
|
||||
{stats.temp?.cpuC != null ? (
|
||||
<>
|
||||
<div className="flex items-end justify-between">
|
||||
@@ -223,7 +225,7 @@ export const BtopView = () => {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Network" icon={Network}>
|
||||
<Card title={t('systemMonitor.network')} icon={Network}>
|
||||
{stats.net ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between text-lg font-semibold text-foreground">
|
||||
@@ -247,11 +249,11 @@ export const BtopView = () => {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Power" icon={Zap}>
|
||||
<Card title={t('systemMonitor.power')} icon={Zap}>
|
||||
{stats.power ? (
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">CPU package</span>
|
||||
<span className="text-muted-foreground">{t('systemMonitor.cpuPackage')}</span>
|
||||
<span className="font-mono text-foreground">
|
||||
{stats.power.cpuWatts != null ? `${stats.power.cpuWatts} W` : '—'}
|
||||
</span>
|
||||
@@ -269,16 +271,16 @@ export const BtopView = () => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title="Top processes" icon={Server}>
|
||||
<Card title={t('systemMonitor.topProcesses')} icon={Server}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
<th className="pb-2 pr-4 font-medium">PID</th>
|
||||
<th className="pb-2 pr-4 font-medium">User</th>
|
||||
<th className="pb-2 pr-4 text-right font-medium">CPU%</th>
|
||||
<th className="pb-2 pr-4 text-right font-medium">MEM%</th>
|
||||
<th className="pb-2 font-medium">Command</th>
|
||||
<th className="pb-2 pr-4 font-medium">{t('systemMonitor.user')}</th>
|
||||
<th className="pb-2 pr-4 text-right font-medium">{t('systemMonitor.cpuPct')}</th>
|
||||
<th className="pb-2 pr-4 text-right font-medium">{t('systemMonitor.memPct')}</th>
|
||||
<th className="pb-2 font-medium">{t('systemMonitor.command')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { LogStream } from './LogStream';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
type Container = { id: string; name: string; image: string; state: string; status: string; ports: string };
|
||||
|
||||
@@ -8,6 +9,7 @@ const POLL_MS = 3000;
|
||||
const stateColor = (s: string) => (s === 'running' ? 'bg-success' : s === 'exited' ? 'bg-destructive' : 'bg-busy');
|
||||
|
||||
export const DockerView = () => {
|
||||
const { t } = useTranslation();
|
||||
const { get } = useClient();
|
||||
const [containers, setContainers] = useState<Container[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -59,7 +61,7 @@ export const DockerView = () => {
|
||||
<input
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="filter name or image…"
|
||||
placeholder={t('systemMonitor.filterPlaceholder')}
|
||||
className="w-56 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { fmtBytes, fmtDuration } from './shared';
|
||||
import { LogStream } from './LogStream';
|
||||
import type { PanelTranslationKey } from '../../locales';
|
||||
import { useTranslation } from '../../locales';
|
||||
|
||||
type Proc = {
|
||||
id: number;
|
||||
@@ -25,18 +27,24 @@ const statusColor = (s: string) =>
|
||||
? 'bg-destructive'
|
||||
: 'bg-busy';
|
||||
|
||||
const COLS: { key: SortKey; label: string; align: 'left' | 'right' }[] = [
|
||||
/*
|
||||
Column metadata lives outside the component, so it carries catalogue KEYS and the header resolves
|
||||
them. `#`, `↺` and `PID` have no key: a glyph and an acronym are the same in every locale, and a key
|
||||
whose two sides can never differ is a maintenance cost bought for nothing.
|
||||
*/
|
||||
const COLS: { key: SortKey; labelKey?: PanelTranslationKey; label?: string; align: 'left' | 'right' }[] = [
|
||||
{ key: 'id', label: '#', align: 'right' },
|
||||
{ key: 'name', label: 'Name', align: 'left' },
|
||||
{ key: 'status', label: 'Status', align: 'left' },
|
||||
{ key: 'cpuPct', label: 'CPU%', align: 'right' },
|
||||
{ key: 'memBytes', label: 'Mem', align: 'right' },
|
||||
{ key: 'name', labelKey: 'systemMonitor.name', align: 'left' },
|
||||
{ key: 'status', labelKey: 'systemMonitor.status', align: 'left' },
|
||||
{ key: 'cpuPct', labelKey: 'systemMonitor.cpuPct', align: 'right' },
|
||||
{ key: 'memBytes', labelKey: 'systemMonitor.mem', align: 'right' },
|
||||
{ key: 'restarts', label: '↺', align: 'right' },
|
||||
{ key: 'uptimeMs', label: 'Uptime', align: 'left' },
|
||||
{ key: 'uptimeMs', labelKey: 'systemMonitor.uptime', align: 'left' },
|
||||
{ key: 'pid', label: 'PID', align: 'right' },
|
||||
];
|
||||
|
||||
export const Pm2View = () => {
|
||||
const { t } = useTranslation();
|
||||
const { get } = useClient();
|
||||
const [procs, setProcs] = useState<Proc[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -116,7 +124,7 @@ export const Pm2View = () => {
|
||||
onClick={() => toggleSort(c.key)}
|
||||
className={`inline-flex items-center gap-1 hover:text-foreground ${c.align === 'right' ? 'flex-row-reverse' : ''} ${sortKey === c.key ? 'text-foreground' : ''}`}
|
||||
>
|
||||
{c.label}
|
||||
{c.labelKey ? t(c.labelKey) : c.label}
|
||||
<span className="w-2 text-[10px]">
|
||||
{sortKey === c.key ? (sortDir === 'asc' ? '▲' : '▼') : ''}
|
||||
</span>
|
||||
|
||||
@@ -51,5 +51,35 @@
|
||||
"installing": "installing",
|
||||
"failed": "failed",
|
||||
"installedIcon": "installed"
|
||||
},
|
||||
"systemMonitor": {
|
||||
"cpuPackage": "CPU package",
|
||||
"user": "User",
|
||||
"cpuPct": "CPU%",
|
||||
"memPct": "MEM%",
|
||||
"command": "Command",
|
||||
"memory": "Memory",
|
||||
"disks": "Disks",
|
||||
"temperature": "Temperature",
|
||||
"network": "Network",
|
||||
"power": "Power",
|
||||
"topProcesses": "Top processes",
|
||||
"filterPlaceholder": "filter name or image…",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"mem": "Mem",
|
||||
"uptime": "Uptime"
|
||||
},
|
||||
"codeEditor": {
|
||||
"closeFile": "Close {{name}}",
|
||||
"loading": "Loading…",
|
||||
"noFiles": "No files",
|
||||
"explorer": "Explorer",
|
||||
"openAFile": "Open a file from the explorer to start editing",
|
||||
"refresh": "Refresh the tree and reload unmodified open files",
|
||||
"readFailed": "Failed to read file",
|
||||
"saved": "File saved",
|
||||
"saveFailed": "Failed to save file",
|
||||
"title": "Code Editor"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ import ptPT from './pt-PT.json';
|
||||
export const panelCatalogs: TranslationMap = { 'en-GB': enGB, 'pt-PT': ptPT };
|
||||
|
||||
export { useTranslation } from './useTranslation';
|
||||
export type { PanelTranslationKey } from './useTranslation';
|
||||
|
||||
@@ -51,5 +51,35 @@
|
||||
"installing": "a instalar",
|
||||
"failed": "falhou",
|
||||
"installedIcon": "instalada"
|
||||
},
|
||||
"systemMonitor": {
|
||||
"cpuPackage": "Pacote do CPU",
|
||||
"user": "Utilizador",
|
||||
"cpuPct": "CPU%",
|
||||
"memPct": "MEM%",
|
||||
"command": "Comando",
|
||||
"memory": "Memória",
|
||||
"disks": "Discos",
|
||||
"temperature": "Temperatura",
|
||||
"network": "Rede",
|
||||
"power": "Energia",
|
||||
"topProcesses": "Processos principais",
|
||||
"filterPlaceholder": "filtrar por nome ou imagem…",
|
||||
"name": "Nome",
|
||||
"status": "Estado",
|
||||
"mem": "Mem",
|
||||
"uptime": "Tempo ativo"
|
||||
},
|
||||
"codeEditor": {
|
||||
"closeFile": "Fechar {{name}}",
|
||||
"loading": "A carregar…",
|
||||
"noFiles": "Sem ficheiros",
|
||||
"explorer": "Explorador",
|
||||
"openAFile": "Abra um ficheiro no explorador para começar a editar",
|
||||
"refresh": "Atualizar a árvore e recarregar os ficheiros abertos não modificados",
|
||||
"readFailed": "Falha ao ler o ficheiro",
|
||||
"saved": "Ficheiro guardado",
|
||||
"saveFailed": "Falha ao guardar o ficheiro",
|
||||
"title": "Editor de código"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
|
||||
import { useTranslation as useTranslationBase } from 'i18n';
|
||||
import type en from './en-GB.json';
|
||||
|
||||
type PanelTranslationKey = TranslationKeys<typeof en>;
|
||||
export type PanelTranslationKey = TranslationKeys<typeof en>;
|
||||
|
||||
// The `panel.` prefix is namespace wiring for the merged catalog (D6) — callers never spell it, they
|
||||
// pass the same bare key the JSON file uses.
|
||||
|
||||
Reference in New Issue
Block a user