- Implement folder account options helper for filtering access items based on person ID and date. - Create FolderAccessAccountField component for selecting accounts with profile integration. - Develop AssetLocalIdentities component for managing local identities associated with assets. - Introduce LocalIdentityOwner component to display and manage local identity ownership. - Add LocalIdentityEditor for creating, associating, and retiring local identities. - Implement local identity API for fetching and executing local identity commands. - Create tests for local identity UI components to ensure functionality and correctness. - Add NetworkShareOptions class for structured and paginated SMB selection. - Introduce NetworkShareAddress component for displaying and copying network share addresses. - Create RemoteOptionList and RemoteOptionPicker components for remote option selection with pagination and search. - Add tests for RemoteOptionList to validate rendering and behavior under various states.
147 lines
5.2 KiB
TypeScript
147 lines
5.2 KiB
TypeScript
import { BrowserLink } from "@metis/core/client";
|
|
import { CursorPagination } from "@repo/rdx-ui/components/cursor-pagination";
|
|
import { DetailPanel } from "@repo/rdx-ui/components/detail-panel";
|
|
import { EntityStatus } from "@repo/rdx-ui/components/entity-list";
|
|
import { KeyRoundIcon } from "@repo/rdx-ui/lib/icons";
|
|
import { Button } from "@repo/shadcn-ui/components/button";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import { getAccess } from "../../../access/list/api";
|
|
import { accessKeys } from "../../../access/shared/queries";
|
|
import type { LocalIdentityOperation } from "../../manage/ui/local-identity-editor";
|
|
import { LocalIdentityEditor } from "../../manage/ui/local-identity-editor";
|
|
import { localIdentityApi, localIdentityKeys } from "../../shared/api/local-identity-api";
|
|
|
|
export function AssetLocalIdentities({
|
|
companyId,
|
|
asset,
|
|
}: {
|
|
companyId: string;
|
|
asset: { id: string; name: string };
|
|
}) {
|
|
const { t } = useTranslation("companies");
|
|
const [cursors, setCursors] = useState<string[]>([]);
|
|
const cursor = cursors.at(-1);
|
|
const [operation, setOperation] = useState<LocalIdentityOperation>();
|
|
const query = useQuery({
|
|
queryKey: localIdentityKeys.page(companyId, asset.id, cursor),
|
|
queryFn: () => localIdentityApi.get(companyId, asset.id, cursor),
|
|
});
|
|
const access = useQuery({
|
|
queryKey: accessKeys.scope(companyId),
|
|
queryFn: () => getAccess(companyId),
|
|
});
|
|
|
|
const label = (id: string) => {
|
|
const value = access.data?.items.find((item) => item.id === id)?.values;
|
|
|
|
return value && "name" in value ? value.name : "—";
|
|
};
|
|
|
|
return (
|
|
<DetailPanel
|
|
actions={
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button onClick={() => setOperation({ action: "associate", asset })} variant="outline">
|
|
{t("localIdentity.associate")}
|
|
</Button>
|
|
<Button onClick={() => setOperation({ action: "create", asset })}>
|
|
{t("localIdentity.create")}
|
|
</Button>
|
|
</div>
|
|
}
|
|
icon={<KeyRoundIcon />}
|
|
title={t("localIdentity.title")}
|
|
>
|
|
<p className="text-sm text-muted-foreground">{t("localIdentity.description")}</p>
|
|
{(query.isPending || access.isPending) && <p role="status">{t("loading")}</p>}
|
|
{(query.isError || access.isError) && (
|
|
<div role="alert">
|
|
<p>{t("localIdentity.error")}</p>
|
|
<Button
|
|
onClick={async () => {
|
|
await Promise.all([query.refetch(), access.refetch()]);
|
|
}}
|
|
variant="outline"
|
|
>
|
|
{t("retry")}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{!(query.isError || access.isError) && query.data && (
|
|
<>
|
|
{query.data.environments.length ? (
|
|
<ul className="flex flex-col gap-3">
|
|
{query.data.environments.map((item) => (
|
|
<li
|
|
className="flex flex-wrap items-start justify-between gap-3 rounded-lg border p-3 text-sm"
|
|
key={item.id}
|
|
>
|
|
<div className="min-w-0 flex flex-col gap-1">
|
|
<BrowserLink
|
|
href={`/companies/${companyId}/access?kind=account&environmentId=${item.environmentId}`}
|
|
>
|
|
{label(item.environmentId)}
|
|
</BrowserLink>
|
|
<p>{label(item.systemId)}</p>
|
|
<EntityStatus active={!item.endedAt}>
|
|
{t(item.endedAt ? "localIdentity.ended" : "localIdentity.current")}
|
|
</EntityStatus>
|
|
<p>
|
|
<time dateTime={item.startedAt}>{item.startedAt}</time> ·{" "}
|
|
{item.endedAt ?? "—"}
|
|
</p>
|
|
</div>
|
|
{!item.endedAt && (
|
|
<Button
|
|
onClick={() =>
|
|
setOperation({ action: "retire", environmentId: item.environmentId })
|
|
}
|
|
variant="outline"
|
|
>
|
|
{t("localIdentity.retire")}
|
|
</Button>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground" role="status">
|
|
{t("localIdentity.empty")}
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
<CursorPagination
|
|
canNext={!!query.data?.nextAfterId && !query.isError}
|
|
canPrevious={cursors.length > 0}
|
|
nextLabel={t("localIdentity.next")}
|
|
onNext={() => {
|
|
const next = query.data?.nextAfterId;
|
|
|
|
if (next) {
|
|
setCursors((values) => [...values, next]);
|
|
}
|
|
}}
|
|
onPrevious={() => setCursors((values) => values.slice(0, -1))}
|
|
onRestart={() => {
|
|
setCursors([]);
|
|
query.refetch().catch(() => undefined);
|
|
}}
|
|
pending={query.isFetching}
|
|
previousLabel={t("localIdentity.previous")}
|
|
restartLabel={t("retry")}
|
|
/>
|
|
{operation && (
|
|
<LocalIdentityEditor
|
|
companyId={companyId}
|
|
onClose={() => setOperation(undefined)}
|
|
operation={operation}
|
|
/>
|
|
)}
|
|
</DetailPanel>
|
|
);
|
|
}
|