metis-desk/modules/assets/src/web/infrastructure/shared/ui/asset-infrastructure-panel.tsx
2026-10-17 17:01:16 +02:00

133 lines
4.6 KiB
TypeScript

import { useListQuery } from "@metis/core/client";
import { Button } from "@repo/shadcn-ui/components/button";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { catalogApi, catalogKeys } from "../../../catalog/shared/api";
import { InventoryFeedback } from "../../../shared";
import { infrastructureApi, infrastructureKeys } from "../api/infrastructure-api";
import { infrastructureRoles } from "../helpers/infrastructure-roles";
import { InfrastructureEditorDialog } from "./infrastructure-editor-dialog";
export function AssetInfrastructurePanel({
companyId,
assetId,
assetName,
templateId,
}: {
companyId: string;
assetId: string;
assetName: string;
templateId: string;
}) {
const { t, i18n } = useTranslation("assets");
const [editing, setEditing] = useState(false);
const query = useListQuery({
queryKey: infrastructureKeys.company(companyId),
queryFn: () => infrastructureApi.get(companyId),
});
const catalog = useListQuery({
queryKey: catalogKeys.scope(companyId),
queryFn: () => catalogApi.get(companyId),
});
const current = query.data?.items.find((item) => item.assetId === assetId);
return (
<div className="space-y-3">
{query.isPending && <p role="status">{t("loading")}</p>}
<InventoryFeedback error={query.error ?? catalog.error} />
{(query.isError || catalog.isError) && (
<Button
onClick={() => {
Promise.all([query.refetch(), catalog.refetch()]).catch(() => undefined);
}}
size="sm"
variant="outline"
>
{t("retry")}
</Button>
)}
{current?.values.roles.length ? (
<p className="text-sm">
{current.values.roles.map((role) => t(`infrastructure.roles.${role}`)).join(" · ")}
</p>
) : null}
{query.isSuccess && !current?.values.interfaces.length && (
<p className="text-sm text-muted-foreground">{t("infrastructure.emptyInterfaces")}</p>
)}
<ul className="space-y-3">
{current?.values.interfaces.map((entry) => (
<li className="rounded-md border p-3 text-sm" key={entry.id}>
<p className="font-medium">
{entry.name} · {t(`infrastructure.kinds.${entry.kind}`)}
</p>
<p>
{entry.addresses.length
? entry.addresses.map((address) => address.address).join(" · ")
: "—"}
</p>
<p className="text-muted-foreground">
{t("infrastructure.mac")}: {entry.mac || "—"}
</p>
{entry.notes && <p className="whitespace-pre-wrap">{entry.notes}</p>}
</li>
))}
</ul>
<Button
disabled={!(query.isSuccess && catalog.isSuccess)}
onClick={() => setEditing(true)}
size="sm"
variant="outline"
>
{t("infrastructure.edit")}
</Button>
{current && (
<details>
<summary className="cursor-pointer text-sm font-medium">
{t("history")} ({current.history.length})
</summary>
<ol className="mt-3 space-y-3">
{[...current.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3 text-sm" key={revision.id}>
<p>
{new Intl.DateTimeFormat(i18n.language, {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(revision.recordedAt))}{" "}
· {revision.actorName}
</p>
<p>
{revision.values.roles
.map((role) => t(`infrastructure.roles.${role}`))
.join(" · ") || "—"}
</p>
<ul>
{revision.values.interfaces.map((entry) => (
<li key={entry.id}>
{entry.name} · {t(`infrastructure.kinds.${entry.kind}`)} ·{" "}
{entry.addresses.map((address) => address.address).join(" · ") || "—"} ·{" "}
{entry.mac || "—"}
{entry.notes && <p className="whitespace-pre-wrap">{entry.notes}</p>}
</li>
))}
</ul>
</li>
))}
</ol>
</details>
)}
{editing && (
<InfrastructureEditorDialog
assetId={assetId}
assetName={assetName}
companyId={companyId}
current={current}
initialRoles={infrastructureRoles(templateId, catalog.data?.items ?? [])}
onClose={() => setEditing(false)}
/>
)}
</div>
);
}