Files
certvia/src/components/risk-modals.tsx
T
MartinandClaude Opus 4.8 5fb4b3cbf4 Risiko-Bearbeiten: Maßnahmen in voller Breite, Brutto→Rest visualisiert
- Maßnahmen-Bereich unterhalb der Stammdaten über die komplette Breite
  (Tabelle mit ID, Titel, editierbarer Minderung je Dimension)
- Zwei Aktionen als aufklappbare Buttons: "Maßnahme hinzufügen"
  (bestehende auswählen) und "Neue Maßnahme erstellen" (legt an und
  verknüpft direkt mit dem Risiko, Rest-Risiko wird neu berechnet)
- Risiko-Detail: Brutto→Rest als zwei farbig abgesetzte Karten (rot/
  orange) mit Pfeil, großer Score-Zahl und Level-Chip; darunter
  Farbskala (grün→rot) mit Markern für Brutto und Rest (Referenz:
  docs/ISMS-GUI-Verbesserungen-GEFIM.html)

Verifiziert: neue Maßnahme -0,75 → Rest 1,75 × 4 = 7; Skala/Karten
korrekt gerendert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:46:37 +02:00

796 lines
27 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.
import Link from "next/link";
import { getFormatter, getTranslations } from "next-intl/server";
import { ArrowRight, Check, Pencil, Plus, Trash2, X } from "lucide-react";
import type { Prisma } from "@prisma/client";
import {
addRiskAsset,
createRisk,
deleteRisk,
removeRiskAsset,
updateRisk,
} from "@/server/actions/risks";
import {
createMeasureForRisk,
linkMeasureToRisk,
unlinkMeasureFromRisk,
} from "@/server/actions/measures";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Modal } from "@/components/modal";
import { CiaBadge, Pill, Tag } from "@/components/mockup-ui";
import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk";
import { measureRef, MEASURE_STATUS_TONE } from "@/lib/measure";
export type RiskWithDetail = Prisma.RiskGetPayload<{
include: {
owner: { select: { id: true; name: true } };
process: { select: { id: true; name: true } };
riskAssets: {
include: {
asset: {
select: {
id: true;
name: true;
type: true;
confidentiality: true;
integrity: true;
availability: true;
};
};
};
};
riskMeasures: {
include: {
measure: { select: { id: true; refNo: true; title: true; status: true } };
};
};
};
}>;
const SCALE = [1, 2, 3, 4, 5] as const;
const TREATMENTS = ["AVOID", "MITIGATE", "TRANSFER", "ACCEPT"] as const;
const STATUSES = ["OPEN", "IN_TREATMENT", "ACCEPTED", "CLOSED"] as const;
const STATUS_TONE = {
OPEN: "risk",
IN_TREATMENT: "warn",
ACCEPTED: "info",
CLOSED: "mut",
} as const;
async function ScorePill({ score }: { score: number }) {
const tLevel = await getTranslations("riskLevel");
const format = await getFormatter();
const level = riskLevel(score);
return (
<Pill tone={RISK_PILL_TONE[level]}>
{format.number(score, { maximumFractionDigits: 2 })} · {tLevel(level)}
</Pill>
);
}
/** Bewertungs-Karte: große Score-Zahl, darunter Wahrscheinlichkeit × Schaden + Level-Chip. */
async function RatingCard({
title,
likelihood,
impact,
score,
accent,
}: {
title: string;
likelihood: number | null;
impact: number | null;
score: number | null;
accent: "gross" | "residual";
}) {
const t = await getTranslations("risks");
const format = await getFormatter();
const num = (v: number) => format.number(v, { maximumFractionDigits: 2 });
const border = accent === "gross" ? "border-l-[#d64c4c]" : "border-l-[#e0982e]";
return (
<div className={`rounded-xl border border-l-[3px] bg-white p-4 ${border}`}>
<div className="text-[11px] font-bold tracking-wide text-muted-foreground uppercase">
{title}
</div>
{score != null && likelihood != null && impact != null ? (
<>
<div className="mt-1 flex items-baseline gap-2">
<span className="font-heading text-3xl font-bold leading-none text-foreground">
{num(score)}
</span>
<ScorePill score={score} />
</div>
<div className="mt-1.5 text-[12px] text-muted-foreground">
{t("likelihoodShort")} {num(likelihood)} × {t("damageShort")} {num(impact)}
</div>
</>
) : (
<p className="mt-1.5 text-[12px] text-muted-foreground">{t("noMeasures")}</p>
)}
{accent === "residual" && (
<p className="mt-2 text-[11.5px] text-muted-foreground">{t("residualAuto")}</p>
)}
</div>
);
}
/** Farbskala grün→rot mit Markern für Brutto (dunkel) und Rest (orange). SPEC §4.2. */
function RiskScoreScale({ gross, residual }: { gross: number; residual: number | null }) {
// 5×5 → Score 1..25; Position linear auf der Skala
const pos = (v: number) => `${((Math.min(25, Math.max(1, v)) - 1) / 24) * 100}%`;
return (
<div className="mt-4">
<div
className="relative h-3 rounded-lg"
style={{
background:
"linear-gradient(90deg,#dff3e5,#fdf0cf,#fbdcc0,#f6c3c1,#ef9a98)",
}}
>
<span
className="absolute -top-1 h-5 w-[3px] -translate-x-1/2 rounded-sm bg-[#2b2646]"
style={{ left: pos(gross) }}
title={`Brutto ${gross}`}
/>
{residual != null && (
<span
className="absolute -top-1 h-5 w-[3px] -translate-x-1/2 rounded-sm bg-[#e0982e]"
style={{ left: pos(residual) }}
title={`Rest ${residual}`}
/>
)}
</div>
<div className="mt-1.5 flex items-center gap-4 text-[11px] text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<i className="inline-block h-3 w-[3px] rounded-sm bg-[#2b2646]" /> Brutto
</span>
<span className="inline-flex items-center gap-1.5">
<i className="inline-block h-3 w-[3px] rounded-sm bg-[#e0982e]" /> Rest
</span>
</div>
</div>
);
}
/** Read-only-Risiko-Detail als Popup (SPEC §4.2 Risiko-Detailansicht). */
export async function RiskDetailModal({
risk,
canWrite,
}: {
risk: RiskWithDetail;
canWrite: boolean;
}) {
const t = await getTranslations("risks");
const tType = await getTranslations("assetType");
const tTreat = await getTranslations("riskTreatment");
const tStatus = await getTranslations("riskStatus");
const tMStatus = await getTranslations("measureStatus");
const tc = await getTranslations("common");
return (
<Modal
title={t("detailHeading", { ref: riskRef(risk.refNo) })}
sub={t("detailSub")}
headerExtra={
<span className="flex items-center gap-2">
<Pill tone={STATUS_TONE[risk.status]}>{tStatus(risk.status)}</Pill>
<ScorePill score={risk.score} />
</span>
}
closeHref="/risks"
closeLabel={t("close")}
footer={
<>
{canWrite && (
<Button
variant="outline"
nativeButton={false}
render={<Link href={`/risks?edit=${risk.id}`} />}
>
<Pencil className="size-4" /> {tc("edit")}
</Button>
)}
<Button nativeButton={false} render={<Link href="/risks" />}>
{t("close")}
</Button>
</>
}
>
<div className="grid gap-5 p-5 md:grid-cols-2">
<div>
<div className="rounded-xl border border-l-[3px] border-l-[#5d52a3] bg-[#faf9fd] p-4">
<b>{risk.title}</b>
{risk.description && (
<p className="mt-1.5 text-[12.5px] leading-relaxed">{risk.description}</p>
)}
<dl className="mt-3 grid grid-cols-[7.5rem_1fr] gap-1.5 text-[12.5px]">
<dt className="text-muted-foreground">{t("threat")}</dt>
<dd>{risk.threat ?? tc("none")}</dd>
<dt className="text-muted-foreground">{t("vulnerability")}</dt>
<dd>{risk.vulnerability ?? tc("none")}</dd>
<dt className="text-muted-foreground">{t("owner")}</dt>
<dd>{risk.owner?.name ?? tc("none")}</dd>
<dt className="text-muted-foreground">{t("treatment")}</dt>
<dd>
<Tag>{tTreat(risk.treatment)}</Tag>
</dd>
<dt className="text-muted-foreground">{t("process")}</dt>
<dd>
{risk.process ? (
<Link href={`/processes?detail=${risk.process.id}`} className="hover:underline">
{risk.process.name}
</Link>
) : (
tc("none")
)}
</dd>
</dl>
</div>
</div>
<div>
<div className="mb-2.5 flex items-center gap-2.5">
<Pill tone="info">{t("affectedAssets")}</Pill>
<span className="text-[12.5px] text-muted-foreground">{t("affectedNote")}</span>
</div>
{risk.riskAssets.length === 0 && (
<p className="text-sm text-muted-foreground">{t("noAssets")}</p>
)}
<table className="w-full text-sm">
<tbody>
{risk.riskAssets.map((ra) => (
<tr key={ra.id} className="border-b last:border-0">
<td className="py-2.5 pr-2 font-bold">
<Link href={`/assets?detail=${ra.asset.id}`} className="hover:underline">
{ra.asset.name}
</Link>
</td>
<td className="py-2.5 pr-2">
<Tag>{tType(ra.asset.type)}</Tag>
</td>
<td className="py-2.5 text-right">
<CiaBadge
c={ra.asset.confidentiality}
i={ra.asset.integrity}
a={ra.asset.availability}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Brutto → Rest-Risiko: zwei Karten mit Pfeil + Farbskala */}
<div className="mx-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4">
<div className="grid items-center gap-3 sm:grid-cols-[1fr_auto_1fr]">
<RatingCard
title={t("gross")}
likelihood={risk.likelihood}
impact={risk.impact}
score={risk.score}
accent="gross"
/>
<ArrowRight className="mx-auto size-5 rotate-90 text-muted-foreground sm:rotate-0" />
<RatingCard
title={t("residual")}
likelihood={risk.residualLikelihood}
impact={risk.residualImpact}
score={risk.residualScore}
accent="residual"
/>
</div>
<RiskScoreScale gross={risk.score} residual={risk.residualScore} />
</div>
{/* Notwendige Maßnahmen — echt verknüpft */}
<div className="mx-5 mb-5 mt-4">
<p className="text-sm font-semibold">{t("measures")}</p>
{risk.riskMeasures.length === 0 && (
<p className="mt-1 text-sm text-muted-foreground">{t("noMeasures")}</p>
)}
<ul className="mt-1.5 space-y-1.5 text-sm">
{risk.riskMeasures.map((rm) => (
<li key={rm.id} className="flex flex-wrap items-center gap-2">
<Link href={`/measures?detail=${rm.measure.id}`} className="font-bold hover:underline">
{measureRef(rm.measure.refNo)}
</Link>
<Link href={`/measures?detail=${rm.measure.id}`} className="hover:underline">
{rm.measure.title}
</Link>
<Pill tone={MEASURE_STATUS_TONE[rm.measure.status]}>
{tMStatus(rm.measure.status)}
</Pill>
{(rm.reductionLikelihood > 0 || rm.reductionImpact > 0) && (
<ReductionLabel l={rm.reductionLikelihood} i={rm.reductionImpact} />
)}
</li>
))}
</ul>
</div>
</Modal>
);
}
/** Bearbeiten als Popup: Bewertung, betroffene Assets, Maßnahmen, Löschen. */
export async function RiskEditModal({
risk,
users,
processes,
availableAssets,
availableMeasures,
threats,
vulnerabilities,
}: {
risk: RiskWithDetail;
users: { id: string; name: string }[];
processes: { id: string; name: string }[];
availableAssets: { id: string; name: string }[];
availableMeasures: { id: string; refNo: number; title: string }[];
threats: string[];
vulnerabilities: string[];
}) {
const t = await getTranslations("risks");
const tc = await getTranslations("common");
const selectClass = "h-9 rounded-md border border-input bg-transparent px-3 text-sm";
return (
<Modal
title={t("editTitle")}
sub={`${riskRef(risk.refNo)} · ${risk.title}`}
closeHref={`/risks?detail=${risk.id}`}
closeLabel={t("close")}
>
<div className="grid gap-6 p-5 md:grid-cols-2">
<form
key={risk.updatedAt.toISOString()}
action={updateRisk.bind(null, risk.id)}
className="space-y-4"
>
<RiskFields
risk={risk}
users={users}
processes={processes}
threats={threats}
vulnerabilities={vulnerabilities}
selectClass={selectClass}
/>
<Button type="submit">{tc("save")}</Button>
</form>
<div className="space-y-5 text-sm">
{/* Betroffene Assets */}
<div>
<p className="font-medium">{t("affectedAssets")}</p>
<p className="mb-1 text-xs text-muted-foreground">{t("affectedNote")}</p>
{risk.riskAssets.length === 0 && <p>{tc("none")}</p>}
<ul className="space-y-1.5">
{risk.riskAssets.map((ra) => (
<li key={ra.id} className="flex items-center gap-2">
{ra.asset.name}
<form action={removeRiskAsset.bind(null, risk.id, ra.id)}>
<button
type="submit"
title={tc("remove")}
className="text-muted-foreground hover:text-destructive"
>
<X className="size-3.5" />
</button>
</form>
</li>
))}
</ul>
{availableAssets.length > 0 && (
<form action={addRiskAsset.bind(null, risk.id)} className="mt-2 flex gap-2">
<select name="assetId" required className={`${selectClass} flex-1`}>
{availableAssets.map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</select>
<Button type="submit" variant="secondary" size="sm">
{t("addAsset")}
</Button>
</form>
)}
</div>
</div>
</div>
{/* Maßnahmen → bestimmen das Rest-Risiko (volle Breite unter den Stammdaten) */}
<div className="border-t p-5 text-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="font-heading text-[15px] font-semibold">{t("measures")}</p>
<p className="text-xs text-muted-foreground">{t("residualAuto")}</p>
</div>
<div className="flex gap-2">
{/* Aufklappbare Formulare per <details> — serverseitig, kein Client-State */}
{availableMeasures.length > 0 && (
<details className="relative">
<summary className="bg-grad-soft inline-flex h-8 cursor-pointer list-none items-center gap-1.5 rounded-lg px-2.5 font-heading text-sm font-semibold text-white select-none hover:opacity-90 [&::-webkit-details-marker]:hidden">
<Plus className="size-4" /> {t("addExistingBtn")}
</summary>
<form
action={linkMeasureToRisk.bind(null, risk.id)}
className="shadow-card absolute right-0 z-10 mt-2 w-80 space-y-2 rounded-xl border bg-card p-3"
>
<p className="text-xs font-semibold">{t("linkMeasure")}</p>
<select name="measureId" required className={`${selectClass} w-full`}>
{availableMeasures.map((m) => (
<option key={m.id} value={m.id}>
{measureRef(m.refNo)} · {m.title}
</option>
))}
</select>
<ReductionInputs />
<Button type="submit" variant="secondary" size="sm">
{t("linkMeasure")}
</Button>
</form>
</details>
)}
<details className="relative">
<summary className="inline-flex h-8 cursor-pointer list-none items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 font-heading text-sm font-semibold select-none hover:bg-muted [&::-webkit-details-marker]:hidden">
<Plus className="size-4" /> {t("createNewBtn")}
</summary>
<form
action={createMeasureForRisk.bind(null, risk.id)}
className="shadow-card absolute right-0 z-10 mt-2 w-80 space-y-2 rounded-xl border bg-card p-3"
>
<p className="text-xs font-semibold">{t("newMeasure")}</p>
<Input name="title" required placeholder={t("measureTitle")} />
<ReductionInputs />
<Button type="submit" variant="secondary" size="sm">
{tc("add")}
</Button>
</form>
</details>
</div>
</div>
{risk.riskMeasures.length === 0 && (
<p className="mt-3 text-muted-foreground">{tc("none")}</p>
)}
{risk.riskMeasures.length > 0 && (
<div className="mt-3 overflow-hidden rounded-xl border">
<div className="grid grid-cols-[7rem_1fr_11rem_11rem_auto_auto] items-center gap-x-3 border-b bg-muted/60 px-3 py-2 text-[11px] font-bold tracking-[.04em] text-muted-foreground uppercase max-md:hidden">
<span>{t("id")}</span>
<span>{t("measureCol")}</span>
<span>{t("reductionL")}</span>
<span>{t("reductionI")}</span>
<span />
<span />
</div>
{risk.riskMeasures.map((rm) => (
<form
key={rm.id + "-" + rm.reductionLikelihood + "-" + rm.reductionImpact}
action={linkMeasureToRisk.bind(null, risk.id)}
className="grid grid-cols-1 items-center gap-2 border-b px-3 py-2.5 last:border-0 md:grid-cols-[7rem_1fr_11rem_11rem_auto_auto] md:gap-x-3"
>
<input type="hidden" name="measureId" value={rm.measureId} />
<b>{measureRef(rm.measure.refNo)}</b>
<span className="min-w-0 truncate" title={rm.measure.title}>
{rm.measure.title}
</span>
<Input
name="reductionLikelihood"
type="number"
min={0}
max={4}
step={0.05}
defaultValue={rm.reductionLikelihood}
aria-label={t("reductionL")}
className="h-8"
/>
<Input
name="reductionImpact"
type="number"
min={0}
max={4}
step={0.05}
defaultValue={rm.reductionImpact}
aria-label={t("reductionI")}
className="h-8"
/>
<Button type="submit" variant="secondary" size="icon-sm" title={tc("save")}>
<Check className="size-3.5" />
</Button>
<Button
type="submit"
variant="ghost"
size="icon-sm"
title={tc("remove")}
formAction={unlinkMeasureFromRisk.bind(null, risk.id, rm.id)}
className="text-muted-foreground hover:text-destructive"
>
<X className="size-3.5" />
</Button>
</form>
))}
</div>
)}
</div>
<div className="border-t p-5">
<form action={deleteRisk.bind(null, risk.id)}>
<Button type="submit" variant="destructive" size="sm">
<Trash2 className="size-4" /> {tc("delete")}
</Button>
</form>
</div>
</Modal>
);
}
/** Anzeige einer Minderung mit lokalisierten Dezimalzahlen. */
async function ReductionLabel({ l, i }: { l: number; i: number }) {
const t = await getTranslations("risks");
const format = await getFormatter();
const num = (v: number) => format.number(v, { maximumFractionDigits: 2 });
return (
<span className="text-xs text-muted-foreground">
{t("reduction")}: {num(l)} {t("likelihoodShort")} / {num(i)} {t("damageShort")}
</span>
);
}
/** Minderungs-Eingaben (dezimal, 0,004,00) für Maßnahmen-Verknüpfungen. */
async function ReductionInputs({
defaultL = 0,
defaultI = 0,
}: {
defaultL?: number;
defaultI?: number;
}) {
const t = await getTranslations("risks");
return (
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-[11px] text-muted-foreground">{t("reductionL")}</label>
<Input
name="reductionLikelihood"
type="number"
min={0}
max={4}
step={0.05}
defaultValue={defaultL}
className="h-9"
/>
</div>
<div>
<label className="text-[11px] text-muted-foreground">{t("reductionI")}</label>
<Input
name="reductionImpact"
type="number"
min={0}
max={4}
step={0.05}
defaultValue={defaultI}
className="h-9"
/>
</div>
</div>
);
}
/** Formularfelder (Anlegen + Bearbeiten teilen sich das Markup). */
async function RiskFields({
risk,
users,
processes,
threats,
vulnerabilities,
selectClass,
}: {
risk?: RiskWithDetail;
users: { id: string; name: string }[];
processes: { id: string; name: string }[];
threats: string[];
vulnerabilities: string[];
selectClass: string;
}) {
const t = await getTranslations("risks");
const tTreat = await getTranslations("riskTreatment");
const tStatus = await getTranslations("riskStatus");
const tc = await getTranslations("common");
return (
<>
<div>
<Label htmlFor="title">{t("titleField")}</Label>
<Input id="title" name="title" required defaultValue={risk?.title} className="mt-1" />
</div>
<div>
<Label htmlFor="description">{t("description")}</Label>
<Textarea
id="description"
name="description"
rows={2}
defaultValue={risk?.description ?? ""}
className="mt-1"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="threat">{t("threat")}</Label>
<Input
id="threat"
name="threat"
list="threat-catalog"
placeholder={t("catalogHint")}
defaultValue={risk?.threat ?? ""}
className="mt-1"
/>
<datalist id="threat-catalog">
{threats.map((name) => (
<option key={name} value={name} />
))}
</datalist>
</div>
<div>
<Label htmlFor="vulnerability">{t("vulnerability")}</Label>
<Input
id="vulnerability"
name="vulnerability"
list="vulnerability-catalog"
placeholder={t("catalogHint")}
defaultValue={risk?.vulnerability ?? ""}
className="mt-1"
/>
<datalist id="vulnerability-catalog">
{vulnerabilities.map((name) => (
<option key={name} value={name} />
))}
</datalist>
</div>
<div>
<Label htmlFor="likelihood">{t("likelihood")}</Label>
<select
id="likelihood"
name="likelihood"
defaultValue={risk?.likelihood ?? 3}
className={`${selectClass} mt-1 w-full`}
>
{SCALE.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="impact">{t("impact")}</Label>
<select
id="impact"
name="impact"
defaultValue={risk?.impact ?? 3}
className={`${selectClass} mt-1 w-full`}
>
{SCALE.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="treatment">{t("treatment")}</Label>
<select
id="treatment"
name="treatment"
defaultValue={risk?.treatment ?? "MITIGATE"}
className={`${selectClass} mt-1 w-full`}
>
{TREATMENTS.map((v) => (
<option key={v} value={v}>
{tTreat(v)}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="status">{t("status")}</Label>
<select
id="status"
name="status"
defaultValue={risk?.status ?? "OPEN"}
className={`${selectClass} mt-1 w-full`}
>
{STATUSES.map((v) => (
<option key={v} value={v}>
{tStatus(v)}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="ownerId">{t("owner")}</Label>
<select
id="ownerId"
name="ownerId"
defaultValue={risk?.ownerId ?? ""}
className={`${selectClass} mt-1 w-full`}
>
<option value="">{tc("none")}</option>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.name}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="processId">{t("process")}</Label>
<select
id="processId"
name="processId"
defaultValue={risk?.processId ?? ""}
className={`${selectClass} mt-1 w-full`}
>
<option value="">{tc("none")}</option>
{processes.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
</div>
</>
);
}
/** Anlegen als Popup — optional mit vorverknüpftem Asset (aus dem Asset-Popup). */
export async function RiskCreateModal({
users,
processes,
preselectedAsset,
threats,
vulnerabilities,
}: {
users: { id: string; name: string }[];
processes: { id: string; name: string }[];
preselectedAsset?: { id: string; name: string } | null;
threats: string[];
vulnerabilities: string[];
}) {
const t = await getTranslations("risks");
const tc = await getTranslations("common");
const selectClass = "h-9 rounded-md border border-input bg-transparent px-3 text-sm";
return (
<Modal
title={t("createTitle")}
sub={preselectedAsset ? `${t("affectedAssets")}: ${preselectedAsset.name}` : undefined}
closeHref="/risks"
closeLabel={t("close")}
>
<form action={createRisk} className="space-y-4 p-5">
{preselectedAsset && <input type="hidden" name="assetId" value={preselectedAsset.id} />}
<RiskFields
users={users}
processes={processes}
threats={threats}
vulnerabilities={vulnerabilities}
selectClass={selectClass}
/>
<div className="flex gap-2 pt-1">
<Button type="submit">{tc("save")}</Button>
<Button variant="outline" nativeButton={false} render={<Link href="/risks" />}>
{tc("cancel")}
</Button>
</div>
</form>
</Modal>
);
}