Files
sshkeyvault/src/components/zuweisungen/ZellenDialog.tsx
T
florian.wildeandClaude Opus 5.5 a47cae4082 feat(zuweisungen): Matrix Schluessel x Host mit CSV-Export (#025)
Bisher liess sich nur je Schluessel oder je Host nachsehen, wo ein Key
verwaltet ist. Die Matrix zeigt das auf einen Blick, loest
Hostgruppen-Zuweisungen je Mitglied auf (P vs. P·G) und macht nicht
synchrone Zustaende ohne Farbe erkennbar.

Die Matrix plant nur: Zuweisen und Entfernen laufen ueber die
vorhandenen Aktionen, ausgerollt wird weiter per Dry-Run auf der
Schluesselseite. Der Aufbau ist eine reine Funktion in src/lib, damit
Seite und CSV-Export garantiert dasselbe zeigen und die
Gruppenaufloesung ohne Datenbank testbar ist. Der CSV-Export entschaerft
Formelbeginne, weil Schluesselnamen frei waehlbar sind und Excel sie
sonst ausfuehren wuerde.

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

190 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { ArrowRight, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { EintragAktionen } from "@/components/ausrollen/EintragAktionen";
import { PublicZuweisenFormular } from "@/components/ausrollen/PublicZuweisenFormular";
import { VerwendungsZustand } from "@/components/ausrollen/VerwendungsZustand";
import { Button } from "@/components/ui/Button";
import { InnereBox } from "@/components/ui/Karte";
import { KENNUNG_TEXT, type MatrixHost, type MatrixSchluessel, type Zelle } from "@/lib/zuweisungsmatrix";
import type { ZielAuswahl } from "@/server/queries/ziele";
export type ZellenAuswahl = { readonly schluesselId: string; readonly hostId: string | null };
/**
* Detail einer Matrixzelle (#025): was dort verwaltet ist, Entfernen und Zuweisen mit den
* vorhandenen Aktionen. Ausgerollt wird weiter nur ueber den Dry-Run der Schluesselseite –
* die Matrix plant, sie schreibt nichts auf Hosts.
*
* Die Zelle kommt aus den Props: nach `router.refresh()` zeigt der offene Dialog sofort den
* neuen Stand, ohne eigenen Zustand nachzufuehren.
*/
export function ZellenDialog({
schluessel,
host,
zelle,
darfZuweisen,
darfPrivatEntfernen,
auswahl,
schliessen,
}: {
schluessel: MatrixSchluessel;
host: MatrixHost | undefined;
zelle: Zelle | undefined;
darfZuweisen: boolean;
darfPrivatEntfernen: boolean;
auswahl?: ZielAuswahl;
schliessen: () => void;
}) {
const dialog = useRef<HTMLDialogElement>(null);
const router = useRouter();
// Nach erfolgreichem Zuweisen ein frisches Formular – sonst schickt ein zweiter Klick
// dieselbe Zuweisung noch einmal und erntet „bereits zugewiesen“.
const [formularNr, setFormularNr] = useState(0);
useEffect(() => {
const element = dialog.current;
if (element && !element.open) element.showModal();
}, []);
const titel = host ? `${schluessel.name} auf ${host.name}` : schluessel.name;
const vorauswahl = host && auswahl?.hosts.some((h) => h.id === host.id) ? `host:${host.id}` : undefined;
const eintraege = zelle?.eintraege ?? [];
return (
<dialog
ref={dialog}
aria-label={titel}
onClose={schliessen}
className="border-border-strong bg-surface text-text rounded-karte m-auto max-h-[calc(100dvh-32px)] w-[calc(100%-32px)] overflow-y-auto overscroll-contain max-w-xl border p-5 backdrop:bg-black/70 sm:p-6"
>
<div className="mb-4 flex items-start justify-between gap-3">
<div className="min-w-0">
<h2 className="font-display text-[18px] font-semibold break-words">{schluessel.name}</h2>
<p className="text-muted mt-0.5 text-[13.5px]">
{host ? (
<>
auf <span className="font-mono">{host.name}</span>
</>
) : (
"Einem Host oder einer Hostgruppe zuweisen"
)}
</p>
</div>
<button
type="button"
onClick={schliessen}
aria-label="Schließen"
className="text-muted hover:text-text hover:bg-raised rounded-feld -mt-2 -mr-2 flex size-11 shrink-0 items-center justify-center"
>
<X className="size-5" aria-hidden />
</button>
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[13.5px]">
<Link
href={`/schluessel/${schluessel.id}`}
className="text-accent inline-flex min-h-11 items-center hover:underline"
>
Zum Schlüssel
</Link>
{host && (
<Link
href={`/hosts/${host.id}`}
className="text-accent inline-flex min-h-11 items-center hover:underline"
>
Zum Host
</Link>
)}
</div>
{host && (
<section className="mt-3 flex flex-col gap-3" aria-label="Verwendungen in dieser Zelle">
{eintraege.length === 0 ? (
<p className="text-muted border-border-strong rounded-box border border-dashed px-4 py-4 text-center text-[14px]">
Auf diesem Host ist der Schlüssel nicht zugewiesen.
</p>
) : (
eintraege.map((e) => (
<InnereBox
key={`${e.art}-${e.verwendungId}`}
className="flex items-start justify-between gap-3"
>
<div className="flex min-w-0 flex-col gap-1">
<p className="text-[14px] font-medium">
{KENNUNG_TEXT[e.kennung]}
{e.gruppe && <> „{e.gruppe.name}“</>}
</p>
<p className="text-muted text-[13px]">
Zielbenutzer <span className="text-text font-mono">{e.zielbenutzer}</span>
</p>
{e.art === "public" ? (
<p className="text-muted text-[13px] break-all">
Optionen <span className="text-text font-mono text-[12.5px]">{e.optionen ?? "–"}</span>
</p>
) : (
<p className="text-muted text-[13px] break-all">
Pfad <span className="text-text font-mono text-[12.5px]">{e.pfad}</span>
</p>
)}
<VerwendungsZustand zustand={e.zustand} />
{e.gruppe && darfZuweisen && (
<p className="text-muted text-[12.5px]">
Entfernen gilt für alle Hosts der Gruppe „{e.gruppe.name}“.
</p>
)}
</div>
<EintragAktionen
art={e.art}
verwendungId={e.verwendungId}
schluesselId={schluessel.id}
darfEntfernen={
e.zustand !== "ENTFERNEN" && (e.art === "public" ? darfZuweisen : darfPrivatEntfernen)
}
testbar={false}
/>
</InnereBox>
))
)}
</section>
)}
{darfZuweisen && auswahl && (
<section className="border-border mt-5 flex flex-col gap-3 border-t pt-5">
<h3 className="text-[15px] font-semibold">Public Key zuweisen</h3>
<PublicZuweisenFormular
key={formularNr}
schluesselId={schluessel.id}
auswahl={auswahl}
{...(vorauswahl ? { vorauswahl } : {})}
beiErfolg={() => {
setFormularNr((n) => n + 1);
router.refresh();
}}
/>
</section>
)}
{darfZuweisen && (
<div className="border-border mt-5 flex flex-col gap-2 border-t pt-4">
<p className="text-muted text-[13px]">
Zuweisen und Entfernen ändern auf dem Host noch nichts – erst der Dry-Run auf der Schlüsselseite
zeigt und schreibt die Änderung.
</p>
<Button
href={`/schluessel/${schluessel.id}`}
klein
className="self-start"
icon={<ArrowRight className="size-4" />}
>
Zum Dry-Run
</Button>
</div>
)}
</dialog>
);
}