Fix snapshot mode should not be editable
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
167
apps/console/src/components/assets/AssetsTable.tsx
Normal file
167
apps/console/src/components/assets/AssetsTable.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { getAssetTypeVariant, promisifyMutation, sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { ActionDropdown, Badge, DropdownItem, IconPencil, IconTrashCan, SelectCell, TextCell, useConfirm } from "@probo/ui";
|
||||
import { useMutation } from "react-relay";
|
||||
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
||||
import { Link } from "react-router";
|
||||
import { z } from "zod";
|
||||
import { EditableTable } from "../table/EditableTable";
|
||||
import { PeopleCell } from "../table/PeopleCell";
|
||||
import { VendorsCell } from "../table/VendorsCell";
|
||||
import { createAssetMutation, deleteAssetMutation, updateAssetMutation } from "/hooks/graph/AssetGraph";
|
||||
import type { AssetGraphDeleteMutation } from "/hooks/graph/__generated__/AssetGraphDeleteMutation.graphql";
|
||||
import type { AssetsPageFragment$data, AssetsPageFragment$key } from "/pages/organizations/assets/__generated__/AssetsPageFragment.graphql";
|
||||
import type { OperationType } from "relay-runtime";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
|
||||
type Props = {
|
||||
connectionId: string;
|
||||
pagination: usePaginationFragmentHookType<
|
||||
OperationType,
|
||||
AssetsPageFragment$key,
|
||||
AssetsPageFragment$data
|
||||
>;
|
||||
assets: AssetsPageFragment$data["assets"]["edges"][0]["node"][];
|
||||
hasAnyAction: boolean;
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().trim().min(1, "Name is required"),
|
||||
amount: z.coerce.number().min(1, "Amount is required"),
|
||||
assetType: z.enum(["PHYSICAL", "VIRTUAL"]),
|
||||
ownerId: z.string().trim().min(1, "Owner is required"),
|
||||
vendorIds: z.array(z.string()).optional(),
|
||||
dataTypesStored: z.string().trim().min(1, "Data types stored is required"),
|
||||
organizationId: z.string().trim().min(1, "Organization is required"),
|
||||
});
|
||||
|
||||
const defaultValue = {
|
||||
name: "",
|
||||
amount: 0,
|
||||
assetType: "VIRTUAL",
|
||||
ownerId: "",
|
||||
vendorIds: [],
|
||||
dataTypesStored: "",
|
||||
organizationId: "",
|
||||
} satisfies z.infer<typeof schema>;
|
||||
|
||||
export function AssetsTable(props: Props) {
|
||||
const { connectionId, pagination, assets, hasAnyAction } = props;
|
||||
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const deleteAsset = useDeleteAsset(connectionId);
|
||||
|
||||
return (
|
||||
<EditableTable
|
||||
connectionId={connectionId}
|
||||
pagination={pagination}
|
||||
items={assets}
|
||||
columns={[
|
||||
__("Name"),
|
||||
__("Type"),
|
||||
__("Data Types stored"),
|
||||
__("Amount"),
|
||||
__("Owner"),
|
||||
__("Vendors"),
|
||||
]}
|
||||
schema={schema}
|
||||
updateMutation={updateAssetMutation}
|
||||
createMutation={createAssetMutation}
|
||||
addLabel={__("Add a new asset")}
|
||||
defaultValue={{
|
||||
...defaultValue,
|
||||
organizationId,
|
||||
}}
|
||||
action={({ item }) =>
|
||||
hasAnyAction ? (
|
||||
<ActionDropdown>
|
||||
<DropdownItem asChild>
|
||||
<Link to={`/organizations/${organizationId}/assets/${item.id}`}>
|
||||
<IconPencil size={16} />
|
||||
{__("Edit")}
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={() => deleteAsset(item)}
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
) : null
|
||||
}
|
||||
row={({ item }) => (
|
||||
<>
|
||||
<TextCell name="name" defaultValue={item?.name ?? ""} required />
|
||||
<SelectCell
|
||||
name="assetType"
|
||||
items={["VIRTUAL", "PHYSICAL"]}
|
||||
itemRenderer={({ item }) => (
|
||||
<Badge variant={getAssetTypeVariant(item ?? "VIRTUAL")}>
|
||||
{item === "PHYSICAL" ? __("Physical") : __("Virtual")}
|
||||
</Badge>
|
||||
)}
|
||||
defaultValue={item?.assetType ?? defaultValue.assetType}
|
||||
/>
|
||||
<TextCell
|
||||
name="dataTypesStored"
|
||||
defaultValue={
|
||||
item?.dataTypesStored ?? defaultValue.dataTypesStored
|
||||
}
|
||||
required
|
||||
/>
|
||||
<TextCell
|
||||
name="amount"
|
||||
defaultValue={(item?.amount ?? defaultValue.amount).toString()}
|
||||
required
|
||||
/>
|
||||
<PeopleCell
|
||||
name="ownerId"
|
||||
defaultValue={item?.owner}
|
||||
organizationId={organizationId}
|
||||
/>
|
||||
<VendorsCell
|
||||
name="vendorIds"
|
||||
organizationId={organizationId}
|
||||
defaultValue={
|
||||
item?.vendors?.edges?.map((edge) => edge.node) ?? []
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const useDeleteAsset = (connectionId: string) => {
|
||||
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
||||
const confirm = useConfirm();
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (asset: { id: string; name: string }) => {
|
||||
if (!asset.id || !asset.name) {
|
||||
return alert(__("Failed to delete asset: missing id or name"));
|
||||
}
|
||||
confirm(
|
||||
() =>
|
||||
promisifyMutation(mutate)({
|
||||
variables: {
|
||||
input: {
|
||||
assetId: asset.id!,
|
||||
},
|
||||
connections: [connectionId],
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
__(
|
||||
'This will permanently delete "%s". This action cannot be undone.',
|
||||
),
|
||||
asset.name,
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
99
apps/console/src/components/assets/SnapshotAssetsTable.tsx
Normal file
99
apps/console/src/components/assets/SnapshotAssetsTable.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Avatar, Badge, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
||||
import { SortableTable } from "../SortableTable";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
||||
import type { AssetsPageFragment$data, AssetsPageFragment$key } from "/pages/organizations/assets/__generated__/AssetsPageFragment.graphql";
|
||||
import type { OperationType } from "relay-runtime";
|
||||
import type { NodeOf } from "/types";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { useParams } from "react-router";
|
||||
import { faviconUrl, getAssetTypeVariant } from "@probo/helpers";
|
||||
|
||||
type AssetEntry = NodeOf<AssetsPageFragment$data["assets"]>;
|
||||
|
||||
type Props = {
|
||||
pagination: usePaginationFragmentHookType<
|
||||
OperationType,
|
||||
AssetsPageFragment$key,
|
||||
AssetsPageFragment$data
|
||||
>;
|
||||
assets: AssetEntry[];
|
||||
};
|
||||
|
||||
export function SnapshotAssetsTable(props: Props) {
|
||||
const { pagination, assets } = props;
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (
|
||||
<SortableTable {...pagination}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Type")}</Th>
|
||||
<Th>{__("Amount")}</Th>
|
||||
<Th>{__("Owner")}</Th>
|
||||
<Th>{__("Vendors")}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{assets.map((entry) => (
|
||||
<AssetRow
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
);
|
||||
}
|
||||
|
||||
function AssetRow({
|
||||
entry,
|
||||
}: {
|
||||
entry: AssetEntry;
|
||||
}) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? [];
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/snapshots/${snapshotId}/assets/${entry.id}`}>
|
||||
<Td>{entry.name}</Td>
|
||||
<Td>
|
||||
<Badge variant={getAssetTypeVariant(entry.assetType)}>
|
||||
{entry.assetType === "PHYSICAL" ? __("Physical") : __("Virtual")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>{entry.amount}</Td>
|
||||
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
|
||||
<Td>
|
||||
{vendors.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{vendors.slice(0, 3).map((vendor) => (
|
||||
<Badge
|
||||
key={vendor.id}
|
||||
variant="neutral"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Avatar
|
||||
name={vendor.name}
|
||||
src={faviconUrl(vendor.websiteUrl)}
|
||||
size="s"
|
||||
/>
|
||||
<span className="text-xs">{vendor.name}</span>
|
||||
</Badge>
|
||||
))}
|
||||
{vendors.length > 3 && (
|
||||
<Badge variant="neutral" className="text-xs">
|
||||
+{vendors.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-txt-secondary text-sm">{__("None")}</span>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user