a906c6705d
- 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>
117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
'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>
|
|
);
|
|
}
|