diff --git a/apps/console/src/components/assets/AssetsTable.tsx b/apps/console/src/components/assets/AssetsTable.tsx new file mode 100644 index 000000000..a906ac69d --- /dev/null +++ b/apps/console/src/components/assets/AssetsTable.tsx @@ -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; + +export function AssetsTable(props: Props) { + const { connectionId, pagination, assets, hasAnyAction } = props; + + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + const deleteAsset = useDeleteAsset(connectionId); + + return ( + + hasAnyAction ? ( + + + + + {__("Edit")} + + + deleteAsset(item)} + variant="danger" + icon={IconTrashCan} + > + {__("Delete")} + + + ) : null + } + row={({ item }) => ( + <> + + ( + + {item === "PHYSICAL" ? __("Physical") : __("Virtual")} + + )} + defaultValue={item?.assetType ?? defaultValue.assetType} + /> + + + + edge.node) ?? [] + } + /> + + )} + /> + ); +} + +const useDeleteAsset = (connectionId: string) => { + const [mutate] = useMutation(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, + ), + }, + ); + }; +}; \ No newline at end of file diff --git a/apps/console/src/components/assets/SnapshotAssetsTable.tsx b/apps/console/src/components/assets/SnapshotAssetsTable.tsx new file mode 100644 index 000000000..5e8ba0fb7 --- /dev/null +++ b/apps/console/src/components/assets/SnapshotAssetsTable.tsx @@ -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; + +type Props = { + pagination: usePaginationFragmentHookType< + OperationType, + AssetsPageFragment$key, + AssetsPageFragment$data + >; + assets: AssetEntry[]; +}; + +export function SnapshotAssetsTable(props: Props) { + const { pagination, assets } = props; + const { __ } = useTranslate(); + + return ( + + + + {__("Name")} + {__("Type")} + {__("Amount")} + {__("Owner")} + {__("Vendors")} + + + + {assets.map((entry) => ( + + ))} + + + ); +} + +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 ( + + {entry.name} + + + {entry.assetType === "PHYSICAL" ? __("Physical") : __("Virtual")} + + + {entry.amount} + {entry.owner?.fullName ?? __("Unassigned")} + + {vendors.length > 0 ? ( +
+ {vendors.slice(0, 3).map((vendor) => ( + + + {vendor.name} + + ))} + {vendors.length > 3 && ( + + +{vendors.length - 3} + + )} +
+ ) : ( + {__("None")} + )} + + + ); +} \ No newline at end of file diff --git a/apps/console/src/pages/organizations/assets/AssetsPage.tsx b/apps/console/src/pages/organizations/assets/AssetsPage.tsx index 5b4f3c07c..710c06fa2 100644 --- a/apps/console/src/pages/organizations/assets/AssetsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetsPage.tsx @@ -1,47 +1,28 @@ import { - ActionDropdown, - Badge, Button, - DropdownItem, - IconPencil, IconPlusLarge, - IconTrashCan, PageHeader, - SelectCell, - TextCell, - useConfirm, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { usePageTitle } from "@probo/hooks"; import { graphql, type PreloadedQuery, - useMutation, usePaginationFragment, usePreloadedQuery, } from "react-relay"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import { Link, useParams } from "react-router"; +import { useParams } from "react-router"; import { assetsQuery, - createAssetMutation, - deleteAssetMutation, - updateAssetMutation, } from "/hooks/graph/AssetGraph"; import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql"; -import { - getAssetTypeVariant, - promisifyMutation, - sprintf, -} from "@probo/helpers"; import type { AssetsPageFragment$key } from "./__generated__/AssetsPageFragment.graphql"; import { SnapshotBanner } from "/components/SnapshotBanner"; -import z from "zod"; -import { EditableTable } from "/components/table/EditableTable.tsx"; -import { PeopleCell } from "/components/table/PeopleCell.tsx"; -import { VendorsCell } from "/components/table/VendorsCell.tsx"; import { CreateAssetDialog } from "./dialogs/CreateAssetDialog"; import { Authorized, isAuthorized } from "/permissions"; +import { AssetsTable } from "../../../components/assets/AssetsTable"; +import { SnapshotAssetsTable } from "/components/assets/SnapshotAssetsTable"; const paginatedAssetsFragment = graphql` fragment AssetsPageFragment on Organization @@ -95,45 +76,19 @@ type Props = { queryRef: PreloadedQuery; }; -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; - export default function AssetsPage(props: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); const { snapshotId } = useParams<{ snapshotId?: string }>(); const isSnapshotMode = Boolean(snapshotId); - const assetUrl = (entry: { id: string }) => - isSnapshotMode && snapshotId - ? `/organizations/${organizationId}/snapshots/${snapshotId}/assets/${entry.id}` - : `/organizations/${organizationId}/assets/${entry.id}`; - - const data = usePreloadedQuery(assetsQuery, props.queryRef); + const data = usePreloadedQuery(assetsQuery, props.queryRef); const pagination = usePaginationFragment( paginatedAssetsFragment, data.node as AssetsPageFragment$key, ); const assets = pagination.data.assets?.edges.map((edge) => edge.node); const connectionId = pagination.data.assets.__id; - const deleteAsset = useDeleteAsset(connectionId); const hasAnyAction = !isSnapshotMode && @@ -161,116 +116,16 @@ export default function AssetsPage(props: Props) { )} - - hasAnyAction ? ( - - - - - {__("Edit")} - - - deleteAsset(item)} - variant="danger" - icon={IconTrashCan} - > - {__("Delete")} - - - ) : null - } - row={({ item }) => ( - <> - - ( - - {item === "PHYSICAL" ? __("Physical") : __("Virtual")} - - )} - defaultValue={item?.assetType ?? defaultValue.assetType} - /> - - - - edge.node) ?? [] - } - /> - - )} - /> + {isSnapshotMode ? + + : + + } ); } - -const useDeleteAsset = (connectionId: string) => { - const [mutate] = useMutation(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, - ), - }, - ); - }; -};