Files
sshkeyvault/src/app/(app)/hostgruppen/[id]/page.tsx
T
florian.wildeandClaude Opus 5.5 90a41dc55d feat(hosts): Seiten fuer Hosts und Hostgruppen (#008)
Ersetzt die Platzhalter: Hostliste mit Suche und Gruppenfilter per GET
(teilbare Links), Formulare fuer Hosts und Gruppen, Hostdetail als Geruest
und Gruppendetail mit Mitgliederauswahl. Bearbeiten-Knoepfe entscheidet der
Server per darf(); jede Seite prueft die Sitzung selbst.

Die Detailseite rendert HostKeyAktionen (#009) und BootstrapAktionen (#010)
als feste Platzhalter mit fertigen Props, damit die Folge-Issues nur ihre
eigene Datei ersetzen. Formulare senden per onSubmit, weil React ein
<form action> nach jeder Antwort zuruecksetzt und Eingaben bei Feldfehlern
sonst verloren gingen. Unter md zeigt die Hostliste Karten statt Tabelle,
damit Status und Host-Key auf 390 px ohne Wischen sichtbar bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:50:34 +02:00

96 lines
3.8 KiB
TypeScript

import { Server } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { adresseMitPort } from "@/components/hosts/Anzeige";
import { GruppenFormular } from "@/components/hosts/GruppenFormular";
import { Breadcrumb } from "@/components/ui/Breadcrumb";
import { Karte } from "@/components/ui/Karte";
import { Leerzustand } from "@/components/ui/Leerzustand";
import { Seitenkopf } from "@/components/ui/Seitenkopf";
import { Statuspunkt } from "@/components/ui/Statuspunkt";
import { Tabelle, TabellenZeile, TabellenZelle } from "@/components/ui/Tabelle";
import { datum } from "@/lib/format";
import { darf } from "@/server/auth/rechte";
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
import { gruppeDetail, hostAuswahl } from "@/server/queries/hosts";
export const metadata: Metadata = { title: "Hostgruppe" };
/** Gruppendetail mit Mitgliedern; wer Hosts verwalten darf, bearbeitet hier auch (#008). */
export default async function HostgruppeSeite({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const { benutzer } = await sitzungErforderlich("lesen", `/hostgruppen/${id}`);
const gruppe = await gruppeDetail(id);
if (!gruppe) notFound();
const verwalten = darf(benutzer.rolle, "hostsVerwalten");
const hosts = verwalten ? await hostAuswahl() : [];
const anzahl = gruppe.mitglieder.length;
return (
<div className="flex flex-col gap-6 sm:gap-7">
<Seitenkopf
oben={<Breadcrumb pfad={[{ titel: "Hostgruppen", href: "/hostgruppen" }, { titel: gruppe.name }]} />}
titel={gruppe.name}
untertitel={
<>
{gruppe.beschreibung && <span className="mb-1 block">{gruppe.beschreibung}</span>}
{anzahl === 1 ? "1 Mitglied" : `${anzahl} Mitglieder`} · angelegt am {datum(gruppe.angelegtAm)}
</>
}
/>
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">
<Karte titel="Mitglieder">
{anzahl === 0 ? (
<Leerzustand
icon={<Server className="size-5" />}
titel="Keine Mitglieder"
text={
verwalten
? "Hosts lassen sich unter „Bearbeiten“ auswählen."
: "Dieser Gruppe ist noch kein Host zugeordnet."
}
/>
) : (
<Tabelle beschriftung="Mitglieder" spalten={[{ titel: "Host" }, { titel: "Status" }]}>
{gruppe.mitglieder.map((h) => (
<TabellenZeile key={h.id}>
<TabellenZelle className="min-w-0">
<Link
href={`/hosts/${h.id}`}
className="text-text hover:text-accent -my-2 inline-flex min-h-11 items-center font-mono text-[13.5px] font-medium break-all transition-colors"
>
{h.name}
</Link>
<p className="text-muted font-mono text-[12.5px] break-all">
{adresseMitPort(h.adresse, h.port)}
</p>
</TabellenZelle>
<TabellenZelle className="whitespace-nowrap">
<Statuspunkt status={h.status} />
</TabellenZelle>
</TabellenZeile>
))}
</Tabelle>
)}
</Karte>
{verwalten && (
<Karte titel="Bearbeiten" untertitel="Name, Beschreibung und Mitglieder">
<GruppenFormular
vorgabe={{
id: gruppe.id,
name: gruppe.name,
beschreibung: gruppe.beschreibung ?? "",
hostIds: gruppe.mitglieder.map((m) => m.id),
}}
hosts={hosts}
/>
</Karte>
)}
</div>
</div>
);
}