feat(260924-i8v): Proxmox-Kachel als Durchstich mit Balken und Zusammenfassung
- Typ proxmox am Ende von WIDGET_TYPES, WIDGET_MODULE_SLUGS = { proxmox: 'proxmox' }
- gemeinsame Statusteile (proxmox-status, HealthBar, status-styles) nach components/proxmox/
- HealthBar variant compact: 6 px, ohne Legende, aria-hidden
- Registry 3/4/8/8, Katalog-/Registry-/API-Tests auf die erste Modul-Kachel umgestellt
- ProxmoxWidget liest nur listServers, zeigt Lade-, Leer-, Fehlerzustand
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { WidgetProps } from '@/components/dashboard/widget-registry';
|
||||
import { HealthBar } from '@/components/proxmox/HealthBar';
|
||||
import { summarizeHealth } from '@/components/proxmox/proxmox-status';
|
||||
import { HEALTH_STYLE } from '@/components/proxmox/status-styles';
|
||||
// T-I8V-02: aus dem Proxmox-Klienten NUR die Leseabfrage des Zwischenlagers.
|
||||
// Die manuelle Abfrage (POST .../poll) gehoert der Modulseite und darf hier
|
||||
// nie auftauchen — sonst loeste jede Kachel je Benutzer Live-Abfragen aus.
|
||||
import { listServers, type ProxmoxServer } from '@/lib/proxmox-api';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
import { healthSummary } from './proxmox-widget-model';
|
||||
|
||||
/**
|
||||
* ProxmoxWidget (quick-260924-i8v) — die erste Modul-Kachel. Zeigt den
|
||||
* Zustand der Proxmox-Server in der Statussprache der Modulseite: kompakter
|
||||
* Gesundheitsbalken und darunter die Zusammenfassung in Worten.
|
||||
*
|
||||
* Liest ausschliesslich das Zwischenlager (`GET /modules/proxmox/servers`),
|
||||
* das `@UseModule('proxmox')` serverseitig schuetzt.
|
||||
*/
|
||||
export function ProxmoxWidget({ isEditMode }: WidgetProps) {
|
||||
const t = useTranslations('widgets');
|
||||
const tp = useTranslations('proxmox');
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
|
||||
|
||||
const [servers, setServers] = useState<ProxmoxServer[] | null>(null);
|
||||
// Fehler als Flag, nicht als Text: `t` gehoert nicht in die Effekt-
|
||||
// Abhaengigkeiten (Befund 14 aus favorites-widget).
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
listServers()
|
||||
.then((list) => {
|
||||
if (cancelled) return;
|
||||
setServers(list);
|
||||
setLoadFailed(false);
|
||||
setNow(Date.now());
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setLoadFailed(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const counts = useMemo(() => summarizeHealth(servers ?? [], now), [servers, now]);
|
||||
const summary = useMemo(() => healthSummary(counts), [counts]);
|
||||
|
||||
if (servers === null && loadFailed) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-2.5">
|
||||
<p className="text-center text-sm text-muted-foreground">{tp('loadError')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (servers === null) {
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden p-2.5" aria-busy="true">
|
||||
<div className="h-1.5 w-full shrink-0 animate-pulse rounded-full bg-muted motion-reduce:animate-none" />
|
||||
<span className="sr-only">{tp('loading')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (servers.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-1.5 p-2.5 text-center">
|
||||
<p className="text-sm text-muted-foreground">{t('proxmox.empty')}</p>
|
||||
{isAdmin &&
|
||||
(isEditMode ? (
|
||||
<span className="text-sm font-medium text-foreground">{tp('card.settingsLink')}</span>
|
||||
) : (
|
||||
<Link
|
||||
href="/modules/proxmox/settings"
|
||||
className="rounded-sm text-sm font-medium text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{tp('card.settingsLink')}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const summaryText = summary.allOk
|
||||
? t('proxmox.allOk')
|
||||
: summary.entries
|
||||
.map((e) => `${e.count} ${tp(`legend.${e.health}`, { count: e.count })}`)
|
||||
.join(', ');
|
||||
const summaryColor = summary.allOk
|
||||
? HEALTH_STYLE.ok.text
|
||||
: summary.worst
|
||||
? HEALTH_STYLE[summary.worst].text
|
||||
: 'text-muted-foreground';
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-2 overflow-hidden p-2.5">
|
||||
<HealthBar variant="compact" counts={counts} />
|
||||
<p
|
||||
className={`truncate text-sm font-medium ${summaryColor}`}
|
||||
title={summaryText}
|
||||
data-testid="proxmox-summary"
|
||||
>
|
||||
{summaryText}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user