Merge branch 'phase/025-zuweisungsmatrix'
This commit is contained in:
@@ -1,18 +1,164 @@
|
||||
import { Download, Network, Search } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { Platzhalterseite } from "@/components/huelle/Platzhalterseite";
|
||||
import { Zuweisungsmatrix } from "@/components/zuweisungen/Zuweisungsmatrix";
|
||||
import { Button, buttonKlassen } from "@/components/ui/Button";
|
||||
import { Ankreuzfeld, Auswahlliste, Feld } from "@/components/ui/Feld";
|
||||
import { Kennzahl } from "@/components/ui/Kennzahl";
|
||||
import { Leerzustand } from "@/components/ui/Leerzustand";
|
||||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||||
import { KATEGORIE_TEXT } from "@/lib/schluessel";
|
||||
import { matrixFilterAus, matrixFilterQuery } from "@/lib/zuweisungsmatrix";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { zielAuswahl } from "@/server/queries/ziele";
|
||||
import { zuweisungsMatrix } from "@/server/queries/zuweisungen";
|
||||
|
||||
export const metadata: Metadata = { title: "Zuweisungen" };
|
||||
|
||||
export default async function Zuweisungen() {
|
||||
type Suchparameter = Promise<Record<string, string | string[] | undefined>>;
|
||||
|
||||
/**
|
||||
* Zuweisungsmatrix Schluessel × Host (#025). Filter per GET, damit Links und der CSV-Export
|
||||
* denselben Ausschnitt zeigen. Die Matrix plant nur – ausgerollt wird ueber den Dry-Run.
|
||||
*/
|
||||
export default async function Zuweisungen({ searchParams }: { searchParams: Suchparameter }) {
|
||||
// Jede Seite prueft selbst; die Huelle ist nur das zweite Netz (CLAUDE.md).
|
||||
await sitzungErforderlich();
|
||||
const { benutzer } = await sitzungErforderlich("lesen", "/zuweisungen");
|
||||
const filter = matrixFilterAus(await searchParams);
|
||||
const darfZuweisen = darf(benutzer.rolle, "publicKeyAusrollen");
|
||||
const [{ matrix, gruppen, personen }, auswahl] = await Promise.all([
|
||||
zuweisungsMatrix(filter),
|
||||
darfZuweisen ? zielAuswahl() : Promise.resolve(undefined),
|
||||
]);
|
||||
const gefiltert = Boolean(
|
||||
filter.gruppeId || filter.kategorie || filter.personId || filter.q || filter.nurBelegt,
|
||||
);
|
||||
const { zaehler } = matrix;
|
||||
const leer = matrix.zeilen.length === 0 || matrix.spalten.length === 0;
|
||||
|
||||
return (
|
||||
<Platzhalterseite
|
||||
titel="Zuweisungen"
|
||||
untertitel="Welcher Schlüssel auf welchem Host-Konto gilt"
|
||||
phase={2}
|
||||
issue="#016"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-col gap-6 sm:gap-7">
|
||||
<Seitenkopf
|
||||
titel="Zuweisungen"
|
||||
untertitel="Welcher Schlüssel auf welchem Host verwaltet ist"
|
||||
aktionen={
|
||||
<a
|
||||
href={`/api/zuweisungen.csv${matrixFilterQuery(filter)}`}
|
||||
download
|
||||
className={buttonKlassen("sekundaer")}
|
||||
>
|
||||
<Download className="size-4" aria-hidden />
|
||||
CSV exportieren
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-5 xl:grid-cols-4">
|
||||
<Kennzahl titel="Schlüssel" wert={zaehler.schluessel} hinweis="aktiv, in der Ansicht" />
|
||||
<Kennzahl titel="Hosts" wert={zaehler.hosts} hinweis="nicht archiviert" />
|
||||
<Kennzahl titel="Zuweisungen" wert={zaehler.zuweisungen} hinweis="Gruppen je Host gezählt" />
|
||||
<Kennzahl
|
||||
titel="Nicht synchron"
|
||||
wert={zaehler.nichtSynchron}
|
||||
variante={zaehler.nichtSynchron > 0 ? "messing" : "neutral"}
|
||||
hinweis="geplant, Drift, entfernen, Fehler"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form
|
||||
method="get"
|
||||
role="search"
|
||||
className="grid items-end gap-3 sm:grid-cols-2 lg:grid-cols-[minmax(0,1fr)_11rem_10rem_11rem]"
|
||||
>
|
||||
<Feld
|
||||
beschriftung="Suche"
|
||||
name="q"
|
||||
type="search"
|
||||
defaultValue={filter.q}
|
||||
placeholder="Schlüssel- oder Hostname"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="sm:col-span-2 lg:col-span-1"
|
||||
/>
|
||||
<Auswahlliste beschriftung="Hostgruppe" name="gruppe" defaultValue={filter.gruppeId}>
|
||||
<option value="">Alle Hosts</option>
|
||||
{gruppen.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
</option>
|
||||
))}
|
||||
</Auswahlliste>
|
||||
<Auswahlliste beschriftung="Kategorie" name="kategorie" defaultValue={filter.kategorie}>
|
||||
<option value="">Alle</option>
|
||||
{Object.entries(KATEGORIE_TEXT).map(([wert, text]) => (
|
||||
<option key={wert} value={wert}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</Auswahlliste>
|
||||
<Auswahlliste beschriftung="Person" name="person" defaultValue={filter.personId}>
|
||||
<option value="">Alle Personen</option>
|
||||
{personen.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</Auswahlliste>
|
||||
<div className="flex flex-col gap-3 sm:col-span-2 sm:flex-row sm:items-center sm:justify-between lg:col-span-4">
|
||||
<Ankreuzfeld
|
||||
name="belegt"
|
||||
value="1"
|
||||
defaultChecked={filter.nurBelegt}
|
||||
beschriftung="Nur Zeilen und Spalten mit Zuweisungen"
|
||||
className="-mx-2"
|
||||
/>
|
||||
<div className="flex gap-2.5">
|
||||
<Button type="submit" icon={<Search className="size-4" />} className="flex-1 sm:flex-none">
|
||||
Filtern
|
||||
</Button>
|
||||
{gefiltert && (
|
||||
<Button href="/zuweisungen" className="flex-1 sm:flex-none">
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{leer ? (
|
||||
gefiltert ? (
|
||||
<Leerzustand
|
||||
icon={<Search className="size-5" />}
|
||||
titel="Keine Treffer"
|
||||
text="Kein Schlüssel und kein Host passen zu Suche und Filter."
|
||||
aktion={<Button href="/zuweisungen">Filter zurücksetzen</Button>}
|
||||
/>
|
||||
) : (
|
||||
<Leerzustand
|
||||
icon={<Network className="size-5" />}
|
||||
titel="Noch nichts zuzuordnen"
|
||||
text="Die Matrix braucht mindestens einen aktiven Schlüssel und einen Host."
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<section className="flex min-w-0 flex-col gap-3" aria-label="Matrix">
|
||||
<p className="text-muted text-[13px]">
|
||||
<span className="text-text font-medium">P</span> Public Key direkt ·{" "}
|
||||
<span className="text-text font-medium">P·G</span> über Hostgruppe ·{" "}
|
||||
<span className="text-text font-medium">K</span> Private Key · <span className="text-ok">●</span>{" "}
|
||||
synchron · <span className="text-accent">○</span> geplant oder wird entfernt ·{" "}
|
||||
<span className="text-danger">●</span> Drift oder Fehler
|
||||
</p>
|
||||
<Zuweisungsmatrix
|
||||
zeilen={matrix.zeilen}
|
||||
spalten={matrix.spalten}
|
||||
zellen={Object.fromEntries(matrix.zellen)}
|
||||
darfZuweisen={darfZuweisen}
|
||||
darfPrivatEntfernen={darf(benutzer.rolle, "privateKeyAusrollen")}
|
||||
{...(auswahl ? { auswahl } : {})}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { matrixCsv, matrixFilterAus } from "@/lib/zuweisungsmatrix";
|
||||
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { zuweisungsMatrix } from "@/server/queries/zuweisungen";
|
||||
|
||||
/**
|
||||
* Zuweisungsmatrix als CSV (#025) mit denselben Filtern wie die Seite. Jede Rolle darf lesen;
|
||||
* der Handler prueft das selbst, der Proxy laesst ohne Cookie ohnehin niemanden durch.
|
||||
*/
|
||||
export async function GET(anfrage: Request) {
|
||||
try {
|
||||
await rechtErforderlich("lesen");
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof RechteFehler) return NextResponse.json({ fehler: fehler.message }, { status: 403 });
|
||||
throw fehler;
|
||||
}
|
||||
const filter = matrixFilterAus(Object.fromEntries(new URL(anfrage.url).searchParams));
|
||||
const { matrix } = await zuweisungsMatrix(filter);
|
||||
return new NextResponse(matrixCsv(matrix), {
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": 'attachment; filename="sshkv-zuweisungen.csv"',
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user