metis-desk/modules/resources/src/web/resource-detail.tsx

126 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-10-05 11:13:51 +00:00
import { useTranslation } from "react-i18next";
2026-10-05 15:01:32 +00:00
import type { ResourceDTO, ResourceInput, ResourceReferenceDTO } from "../common/inventory";
2026-10-05 11:13:51 +00:00
2026-10-05 15:01:32 +00:00
export function ResourceDetail({
resource,
references = [],
}: {
resource: ResourceDTO;
references?: readonly ResourceReferenceDTO[];
}) {
2026-10-05 11:13:51 +00:00
const { t, i18n } = useTranslation("resources");
const display = (value: string | number | boolean | undefined) =>
value === undefined
? t("noValue")
: typeof value === "boolean"
? t(value ? "yes" : "no")
: String(value);
return (
<div className="space-y-5">
<p className="text-sm text-muted-foreground">{t("snapshotNotice")}</p>
<dl className="grid gap-4 sm:grid-cols-2">
<div>
<dt className="text-sm text-muted-foreground">{t("template")}</dt>
<dd>{resource.specification.templateName}</dd>
</div>
<div>
<dt className="text-sm text-muted-foreground">{t("status")}</dt>
<dd>{t(resource.values.active ? "active" : "inactive")}</dd>
</div>
{resource.specification.fields.map((field) => (
<div key={field.fieldId}>
<dt className="text-sm text-muted-foreground">
{field.name}
{field.unit ? ` (${field.unit})` : ""}
</dt>
<dd className="whitespace-pre-wrap break-words">
{display(resource.values.characteristics[field.fieldId])}
</dd>
</div>
))}
</dl>
2026-10-05 15:01:32 +00:00
<IntegrationSummary integration={resource.values.integration} references={references} />
2026-10-05 11:13:51 +00:00
{resource.values.notes && (
<p className="whitespace-pre-wrap break-words">{resource.values.notes}</p>
)}
<h3 className="font-medium">
{t("history")} ({resource.history.length})
</h3>
<ol className="space-y-4">
{[...resource.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3 text-sm" key={revision.id}>
<p className="font-medium">
{new Intl.DateTimeFormat(i18n.language, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(revision.recordedAt))}{" "}
· {t("recordedBy", { name: revision.actorName })}
</p>
<p>
{revision.values.name} · {t(revision.values.active ? "active" : "inactive")}
</p>
<dl className="grid gap-2 sm:grid-cols-2">
{resource.specification.fields.map((field) => (
<div key={field.fieldId}>
<dt className="text-muted-foreground">{field.name}</dt>
<dd className="whitespace-pre-wrap break-words">
{display(revision.values.characteristics[field.fieldId])}
</dd>
</div>
))}
</dl>
2026-10-05 15:01:32 +00:00
<IntegrationSummary integration={revision.values.integration} references={references} />
2026-10-05 11:13:51 +00:00
{revision.values.notes && (
<p className="whitespace-pre-wrap break-words">{revision.values.notes}</p>
)}
</li>
))}
</ol>
</div>
);
}
2026-10-05 15:01:32 +00:00
function IntegrationSummary({
integration,
references,
}: {
integration: ResourceInput["integration"];
references: readonly ResourceReferenceDTO[];
}) {
const { t } = useTranslation("resources");
if (!integration) {
return null;
}
const entries =
integration.kind === "mailbox"
? [
{ label: t("mailboxKind"), value: t(integration.mailboxKind) },
{
label: t("relatedAccount"),
value: referenceName(integration.accountId, references, t("noValue")),
},
]
: (["resourceTypeId", "systemId", "environmentId"] as const).map((key) => ({
label: t(key),
value: referenceName(integration[key], references, t("noValue")),
}));
return (
<dl className="grid gap-2 sm:grid-cols-2">
{entries.map((entry) => (
<div key={entry.label}>
<dt className="text-sm text-muted-foreground">{entry.label}</dt>
<dd>{entry.value}</dd>
</div>
))}
</dl>
);
}
function referenceName(
id: string | null,
references: readonly ResourceReferenceDTO[],
missing: string
) {
return id ? (references.find((item) => item.id === id)?.name ?? id) : missing;
}