From bf266cfe562352e09784faea4e077ecc9bb70799 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 12 Jun 2025 12:42:55 -0700 Subject: [PATCH] Add data inventory to the new ui Signed-off-by: Sacha Al Himdani --- .../form/VendorsMultiSelectField.tsx | 133 ++++++ apps/console2/src/hooks/graph/DatumGraph.ts | 250 ++++++++++ apps/console2/src/hooks/graph/VendorGraph.ts | 31 +- .../DatumGraphCreateMutation.graphql.ts | 269 +++++++++++ .../DatumGraphDeleteMutation.graphql.ts | 132 +++++ .../DatumGraphListQuery.graphql.ts | 449 ++++++++++++++++++ .../DatumGraphNodeQuery.graphql.ts | 275 +++++++++++ .../DatumGraphUpdateMutation.graphql.ts | 216 +++++++++ .../VendorGraphSelectQuery.graphql.ts | 188 ++++++++ apps/console2/src/layouts/MainLayout.tsx | 6 + .../src/pages/organizations/data/DataPage.tsx | 132 +++++ .../organizations/data/DatumDetailsPage.tsx | 149 ++++++ .../data/dialogs/CreateDatumDialog.tsx | 102 ++++ apps/console2/src/routes.tsx | 2 + apps/console2/src/routes/dataRoutes.ts | 26 + 15 files changed, 2359 insertions(+), 1 deletion(-) create mode 100644 apps/console2/src/components/form/VendorsMultiSelectField.tsx create mode 100644 apps/console2/src/hooks/graph/DatumGraph.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/DatumGraphCreateMutation.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/DatumGraphDeleteMutation.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/DatumGraphListQuery.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/DatumGraphNodeQuery.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/DatumGraphUpdateMutation.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/VendorGraphSelectQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/data/DataPage.tsx create mode 100644 apps/console2/src/pages/organizations/data/DatumDetailsPage.tsx create mode 100644 apps/console2/src/pages/organizations/data/dialogs/CreateDatumDialog.tsx create mode 100644 apps/console2/src/routes/dataRoutes.ts diff --git a/apps/console2/src/components/form/VendorsMultiSelectField.tsx b/apps/console2/src/components/form/VendorsMultiSelectField.tsx new file mode 100644 index 000000000..bf35f9def --- /dev/null +++ b/apps/console2/src/components/form/VendorsMultiSelectField.tsx @@ -0,0 +1,133 @@ +import { Avatar, Field, Option, Select, Badge, Button, IconCrossLargeX } from "@probo/ui"; +import { Suspense, useState, type ComponentProps } from "react"; +import { useTranslate } from "@probo/i18n"; +import { type Control, Controller } from "react-hook-form"; +import { useVendors } from "/hooks/graph/VendorGraph.ts"; +import { faviconUrl } from "@probo/helpers"; + +type Props = { + organizationId: string; + control: Control; + name: string; + label?: string; + error?: string; +} & ComponentProps; + +export function VendorsMultiSelectField({ + organizationId, + control, + ...props +}: Props) { + return ( + + } + > + + + + ); +} + +function VendorsMultiSelectWithQuery( + props: Pick +) { + const { __ } = useTranslate(); + const { name, organizationId, control } = props; + const vendors = useVendors(organizationId); + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + { + const selectedVendorIds = Array.isArray(field.value) ? field.value : []; + const selectedVendors = vendors.filter(v => selectedVendorIds.includes(v.id)); + const availableVendors = vendors.filter(v => !selectedVendorIds.includes(v.id)); + + const handleAddVendor = (vendorId: string) => { + const newValue = [...selectedVendorIds, vendorId]; + field.onChange(newValue); + setIsOpen(false); + }; + + const handleRemoveVendor = (vendorId: string) => { + const newValue = selectedVendorIds.filter((id: string) => id !== vendorId); + field.onChange(newValue); + }; + + return ( +
+ {availableVendors.length > 0 && ( + + )} + + {selectedVendors.length > 0 && ( +
+ {selectedVendors.map((vendor) => ( + + + {vendor.name} +
+ )} + + {selectedVendors.length === 0 && availableVendors.length === 0 && ( +
+ {__("No vendors available")} +
+ )} +
+ ); + }} + /> + + ); +} diff --git a/apps/console2/src/hooks/graph/DatumGraph.ts b/apps/console2/src/hooks/graph/DatumGraph.ts new file mode 100644 index 000000000..8c4e07603 --- /dev/null +++ b/apps/console2/src/hooks/graph/DatumGraph.ts @@ -0,0 +1,250 @@ +import { graphql } from "relay-runtime"; +import { useMutation, usePreloadedQuery, type PreloadedQuery } from "react-relay"; +import { useConfirm } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { promisifyMutation, sprintf } from "@probo/helpers"; +import { useMemo } from "react"; +import type { DatumGraphListQuery } from "./__generated__/DatumGraphListQuery.graphql"; + +export const dataQuery = graphql` + query DatumGraphListQuery($organizationId: ID!) { + node(id: $organizationId) { + ... on Organization { + data(first: 100) @connection(key: "DataPage_data") { + __id + edges { + node { + id + name + dataClassification + owner { + fullName + } + vendors(first: 50) { + edges { + node { + id + name + websiteUrl + } + } + } + createdAt + } + } + } + peoples(first: 100) { + edges { + node { + id + fullName + } + } + } + } + } + } +`; + +export const datumNodeQuery = graphql` + query DatumGraphNodeQuery($dataId: ID!) { + node(id: $dataId) { + ... on Datum { + id + name + dataClassification + owner { + id + fullName + } + vendors(first: 50) { + edges { + node { + id + name + websiteUrl + category + } + } + } + organization { + id + } + createdAt + updatedAt + } + } + } +`; + +export const createDatumMutation = graphql` + mutation DatumGraphCreateMutation( + $input: CreateDatumInput! + $connections: [ID!]! + ) { + createDatum(input: $input) { + datumEdge @prependEdge(connections: $connections) { + node { + id + name + dataClassification + owner { + id + fullName + } + vendors(first: 10) { + edges { + node { + id + name + websiteUrl + } + } + } + createdAt + } + } + } + } +`; + +export const updateDatumMutation = graphql` + mutation DatumGraphUpdateMutation($input: UpdateDatumInput!) { + updateDatum(input: $input) { + datum { + id + name + dataClassification + owner { + id + fullName + } + vendors(first: 50) { + edges { + node { + id + name + websiteUrl + } + } + } + updatedAt + } + } + } +`; + +export const deleteDatumMutation = graphql` + mutation DatumGraphDeleteMutation( + $input: DeleteDatumInput! + $connections: [ID!]! + ) { + deleteDatum(input: $input) { + deletedDatumId @deleteEdge(connections: $connections) + } + } +`; + +export const useDeleteDatum = ( + datum: { id?: string; name?: string }, + connectionId: string +) => { + const [mutate] = useMutation(deleteDatumMutation); + const confirm = useConfirm(); + const { __ } = useTranslate(); + + return () => { + if (!datum.id || !datum.name) { + return alert(__("Failed to delete data: missing id or name")); + } + confirm( + () => + promisifyMutation(mutate)({ + variables: { + input: { + datumId: datum.id!, + }, + connections: [connectionId], + }, + }), + { + message: sprintf( + __( + 'This will permanently delete "%s". This action cannot be undone.' + ), + datum.name + ), + } + ); + }; +}; + +export const useCreateDatum = (connectionId: string) => { + const [mutate] = useMutation(createDatumMutation); + const { __ } = useTranslate(); + + return (input: { + name: string; + dataClassification: string; + ownerId: string; + organizationId: string; + vendorIds?: string[]; + }) => { + if (!input.name?.trim()) { + return alert(__("Failed to create data: name is required")); + } + if (!input.ownerId) { + return alert(__("Failed to create data: owner is required")); + } + if (!input.organizationId) { + return alert(__("Failed to create data: organization is required")); + } + + return promisifyMutation(mutate)({ + variables: { + input, + connections: [connectionId], + }, + }); + }; +}; + +export const useUpdateDatum = () => { + const [mutate] = useMutation(updateDatumMutation); + const { __ } = useTranslate(); + + return (input: { + id: string; + name?: string; + dataClassification?: string; + ownerId?: string; + vendorIds?: string[]; + }) => { + if (!input.id) { + return alert(__("Failed to update data: missing id")); + } + + return promisifyMutation(mutate)({ + variables: { + input, + }, + }); + }; +}; + +export const useData = (queryRef: PreloadedQuery) => { + const data = usePreloadedQuery(dataQuery, queryRef); + + return useMemo(() => { + const dataEntries = data.node?.data?.edges.map((edge) => edge.node) ?? []; + const connectionId = data.node?.data?.__id ?? ""; + const peoples = data.node?.peoples?.edges.map((edge) => edge.node) ?? []; + + return { + dataEntries, + connectionId, + peoples, + rawData: data, + }; + }, [data]); +}; diff --git a/apps/console2/src/hooks/graph/VendorGraph.ts b/apps/console2/src/hooks/graph/VendorGraph.ts index 88d256a1c..97eb50f48 100644 --- a/apps/console2/src/hooks/graph/VendorGraph.ts +++ b/apps/console2/src/hooks/graph/VendorGraph.ts @@ -3,9 +3,11 @@ import { graphql } from "relay-runtime"; import { useMutationWithToasts } from "../useMutationWithToasts.ts"; import type { VendorGraphCreateMutation } from "./__generated__/VendorGraphCreateMutation.graphql.ts"; import type { VendorGraphDeleteMutation } from "./__generated__/VendorGraphDeleteMutation.graphql.ts"; -import { useMutation } from "react-relay"; +import type { VendorGraphSelectQuery } from "./__generated__/VendorGraphSelectQuery.graphql.ts"; +import { useMutation, useLazyLoadQuery } from "react-relay"; import { useConfirm } from "@probo/ui"; import { promisifyMutation, sprintf } from "@probo/helpers"; +import { useMemo } from "react"; const createVendorMutation = graphql` mutation VendorGraphCreateMutation( @@ -141,3 +143,30 @@ export const vendorNodeQuery = graphql` } } `; + +const vendorsSelectQuery = graphql` + query VendorGraphSelectQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + ... on Organization { + vendors(first: 100, orderBy: { direction: ASC, field: NAME }) { + edges { + node { + id + name + websiteUrl + } + } + } + } + } + } +`; + +export function useVendors(organizationId: string) { + const data = useLazyLoadQuery(vendorsSelectQuery, { + organizationId: organizationId, + }); + return useMemo(() => { + return data.organization?.vendors?.edges.map((edge) => edge.node) ?? []; + }, [data]); +} diff --git a/apps/console2/src/hooks/graph/__generated__/DatumGraphCreateMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DatumGraphCreateMutation.graphql.ts new file mode 100644 index 000000000..912493963 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/DatumGraphCreateMutation.graphql.ts @@ -0,0 +1,269 @@ +/** + * @generated SignedSource<<36898ed088182687925ddd9d0b08b66a>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DataClassification = "CONFIDENTIAL" | "INTERNAL" | "PUBLIC" | "SECRET"; +export type CreateDatumInput = { + dataClassification: DataClassification; + name: string; + organizationId: string; + ownerId: string; + vendorIds?: ReadonlyArray | null | undefined; +}; +export type DatumGraphCreateMutation$variables = { + connections: ReadonlyArray; + input: CreateDatumInput; +}; +export type DatumGraphCreateMutation$data = { + readonly createDatum: { + readonly datumEdge: { + readonly node: { + readonly createdAt: any; + readonly dataClassification: DataClassification; + readonly id: string; + readonly name: string; + readonly owner: { + readonly fullName: string; + readonly id: string; + }; + readonly vendors: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; + }; + }; + }; +}; +export type DatumGraphCreateMutation = { + response: DatumGraphCreateMutation$data; + variables: DatumGraphCreateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "concreteType": "DatumEdge", + "kind": "LinkedField", + "name": "datumEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Datum", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataClassification", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 10 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:10)" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "DatumGraphCreateMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateDatumPayload", + "kind": "LinkedField", + "name": "createDatum", + "plural": false, + "selections": [ + (v5/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "DatumGraphCreateMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateDatumPayload", + "kind": "LinkedField", + "name": "createDatum", + "plural": false, + "selections": [ + (v5/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "datumEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "31d614c716382bedf677502633969fac", + "id": null, + "metadata": {}, + "name": "DatumGraphCreateMutation", + "operationKind": "mutation", + "text": "mutation DatumGraphCreateMutation(\n $input: CreateDatumInput!\n) {\n createDatum(input: $input) {\n datumEdge {\n node {\n id\n name\n dataClassification\n owner {\n id\n fullName\n }\n vendors(first: 10) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "b99929d8e7d004f77b06510051ee2e04"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/DatumGraphDeleteMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DatumGraphDeleteMutation.graphql.ts new file mode 100644 index 000000000..f288f8b8a --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/DatumGraphDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<50c6ee9ebfd56ffeac153d8613118345>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteDatumInput = { + datumId: string; +}; +export type DatumGraphDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeleteDatumInput; +}; +export type DatumGraphDeleteMutation$data = { + readonly deleteDatum: { + readonly deletedDatumId: string; + }; +}; +export type DatumGraphDeleteMutation = { + response: DatumGraphDeleteMutation$data; + variables: DatumGraphDeleteMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedDatumId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "DatumGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteDatumPayload", + "kind": "LinkedField", + "name": "deleteDatum", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "DatumGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteDatumPayload", + "kind": "LinkedField", + "name": "deleteDatum", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedDatumId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "c8ac1dfe4d44edfa33008bfc92dee99d", + "id": null, + "metadata": {}, + "name": "DatumGraphDeleteMutation", + "operationKind": "mutation", + "text": "mutation DatumGraphDeleteMutation(\n $input: DeleteDatumInput!\n) {\n deleteDatum(input: $input) {\n deletedDatumId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "1d6ffd466ce547e7377669b6ba567b9d"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/DatumGraphListQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DatumGraphListQuery.graphql.ts new file mode 100644 index 000000000..2d0d574d2 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/DatumGraphListQuery.graphql.ts @@ -0,0 +1,449 @@ +/** + * @generated SignedSource<<9e4a2879379ca7f3999937db340c18f3>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DataClassification = "CONFIDENTIAL" | "INTERNAL" | "PUBLIC" | "SECRET"; +export type DatumGraphListQuery$variables = { + organizationId: string; +}; +export type DatumGraphListQuery$data = { + readonly node: { + readonly data?: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: any; + readonly dataClassification: DataClassification; + readonly id: string; + readonly name: string; + readonly owner: { + readonly fullName: string; + }; + readonly vendors: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; + }; + }>; + }; + readonly peoples?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly fullName: string; + readonly id: string; + }; + }>; + }; + }; +}; +export type DatumGraphListQuery = { + response: DatumGraphListQuery$data; + variables: DatumGraphListQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataClassification", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v6 = { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:50)" +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v10 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v11 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v12 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } +], +v13 = { + "alias": null, + "args": (v12/*: any*/), + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v5/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "peoples(first:100)" +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "DatumGraphListQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + { + "alias": "data", + "args": null, + "concreteType": "DatumConnection", + "kind": "LinkedField", + "name": "__DataPage_data_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DatumEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Datum", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v5/*: any*/) + ], + "storageKey": null + }, + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v10/*: any*/), + (v11/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/) + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "DatumGraphListQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v8/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v12/*: any*/), + "concreteType": "DatumConnection", + "kind": "LinkedField", + "name": "data", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DatumEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Datum", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v5/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + }, + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v10/*: any*/), + (v11/*: any*/) + ], + "storageKey": "data(first:100)" + }, + { + "alias": null, + "args": (v12/*: any*/), + "filters": null, + "handle": "connection", + "key": "DataPage_data", + "kind": "LinkedHandle", + "name": "data" + }, + (v13/*: any*/) + ], + "type": "Organization", + "abstractKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "d4e47a72e0f7585fe7d74ffac75a0f06", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "node", + "data" + ] + } + ] + }, + "name": "DatumGraphListQuery", + "operationKind": "query", + "text": "query DatumGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n data(first: 100) {\n edges {\n node {\n id\n name\n dataClassification\n owner {\n fullName\n id\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n peoples(first: 100) {\n edges {\n node {\n id\n fullName\n }\n }\n }\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "2104705ef9537afe6c45469cf35f4f2c"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/DatumGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DatumGraphNodeQuery.graphql.ts new file mode 100644 index 000000000..2e51a6138 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/DatumGraphNodeQuery.graphql.ts @@ -0,0 +1,275 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DataClassification = "CONFIDENTIAL" | "INTERNAL" | "PUBLIC" | "SECRET"; +export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL"; +export type DatumGraphNodeQuery$variables = { + dataId: string; +}; +export type DatumGraphNodeQuery$data = { + readonly node: { + readonly createdAt?: any; + readonly dataClassification?: DataClassification; + readonly id?: string; + readonly name?: string; + readonly organization?: { + readonly id: string; + }; + readonly owner?: { + readonly fullName: string; + readonly id: string; + }; + readonly updatedAt?: any; + readonly vendors?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly category: VendorCategory; + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; + }; +}; +export type DatumGraphNodeQuery = { + response: DatumGraphNodeQuery$data; + variables: DatumGraphNodeQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "dataId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "dataId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataClassification", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null +}, +v6 = { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:50)" +}, +v7 = { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null +}, +v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "DatumGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/) + ], + "type": "Datum", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "DatumGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/) + ], + "type": "Datum", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "7dcd0ba0b57e917592a333f2368f65c4", + "id": null, + "metadata": {}, + "name": "DatumGraphNodeQuery", + "operationKind": "query", + "text": "query DatumGraphNodeQuery(\n $dataId: ID!\n) {\n node(id: $dataId) {\n __typename\n ... on Datum {\n id\n name\n dataClassification\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n category\n }\n }\n }\n organization {\n id\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "c8a8fc6a37cce32b83f53379317d8f67"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/DatumGraphUpdateMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DatumGraphUpdateMutation.graphql.ts new file mode 100644 index 000000000..eed59c93e --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/DatumGraphUpdateMutation.graphql.ts @@ -0,0 +1,216 @@ +/** + * @generated SignedSource<<2fd708bf07a7c93afe66bd9d3e8031cc>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DataClassification = "CONFIDENTIAL" | "INTERNAL" | "PUBLIC" | "SECRET"; +export type UpdateDatumInput = { + dataClassification?: DataClassification | null | undefined; + id: string; + name?: string | null | undefined; + ownerId?: string | null | undefined; + vendorIds?: ReadonlyArray | null | undefined; +}; +export type DatumGraphUpdateMutation$variables = { + input: UpdateDatumInput; +}; +export type DatumGraphUpdateMutation$data = { + readonly updateDatum: { + readonly datum: { + readonly dataClassification: DataClassification; + readonly id: string; + readonly name: string; + readonly owner: { + readonly fullName: string; + readonly id: string; + }; + readonly updatedAt: any; + readonly vendors: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; + }; + }; +}; +export type DatumGraphUpdateMutation = { + response: DatumGraphUpdateMutation$data; + variables: DatumGraphUpdateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v3 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateDatumPayload", + "kind": "LinkedField", + "name": "updateDatum", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Datum", + "kind": "LinkedField", + "name": "datum", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataClassification", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:50)" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "DatumGraphUpdateMutation", + "selections": (v3/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "DatumGraphUpdateMutation", + "selections": (v3/*: any*/) + }, + "params": { + "cacheID": "0dbe0c32132e627730510b4bdddf5e84", + "id": null, + "metadata": {}, + "name": "DatumGraphUpdateMutation", + "operationKind": "mutation", + "text": "mutation DatumGraphUpdateMutation(\n $input: UpdateDatumInput!\n) {\n updateDatum(input: $input) {\n datum {\n id\n name\n dataClassification\n owner {\n id\n fullName\n }\n vendors(first: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "682e71be938af8024fed4dcfb65e2ff3"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/VendorGraphSelectQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/VendorGraphSelectQuery.graphql.ts new file mode 100644 index 000000000..0b1a9369b --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/VendorGraphSelectQuery.graphql.ts @@ -0,0 +1,188 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type VendorGraphSelectQuery$variables = { + organizationId: string; +}; +export type VendorGraphSelectQuery$data = { + readonly organization: { + readonly vendors?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly websiteUrl: string | null | undefined; + }; + }>; + }; + }; +}; +export type VendorGraphSelectQuery = { + response: VendorGraphSelectQuery$data; + variables: VendorGraphSelectQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 100 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "NAME" + } + } + ], + "concreteType": "VendorConnection", + "kind": "LinkedField", + "name": "vendors", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "vendors(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})" + } + ], + "type": "Organization", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "VendorGraphSelectQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "VendorGraphSelectQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v3/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8127e6998fabfabfcc50a290c7ae3ed4", + "id": null, + "metadata": {}, + "name": "VendorGraphSelectQuery", + "operationKind": "query", + "text": "query VendorGraphSelectQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n vendors(first: 100, orderBy: {direction: ASC, field: NAME}) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "cd161c358ec53aacea5be6d1e7698307"; + +export default node; diff --git a/apps/console2/src/layouts/MainLayout.tsx b/apps/console2/src/layouts/MainLayout.tsx index 1aa7a2100..4be4c828c 100644 --- a/apps/console2/src/layouts/MainLayout.tsx +++ b/apps/console2/src/layouts/MainLayout.tsx @@ -11,6 +11,7 @@ import { IconSettingsGear2, IconStore, IconTodo, + IconListStack, Layout, SidebarItem, UserDropdown as UserDropdownRoot, @@ -117,6 +118,11 @@ export function MainLayout() { icon={IconPageTextLine} to={`${prefix}/documents`} /> + ["data"]>>; + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function DataPage(props: Props) { + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + + const { dataEntries, connectionId } = useData(props.queryRef); + + usePageTitle(__("Data")); + + return ( +
+ + + + + + + + + + + + + + + + + {dataEntries.map((entry) => ( + + ))} + +
{__("Name")}{__("Classification")}{__("Owner")}{__("Vendors")}
+
+ ); +} + +function DataRow({ + entry, + connectionId, +}: { + entry: DataEntry; + connectionId: string; +}) { + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + const deleteDatum = useDeleteDatum(entry, connectionId); + const vendors = entry.vendors?.edges.map(edge => edge.node) ?? []; + + return ( + + {entry.name} + + {entry.dataClassification} + + {entry.owner?.fullName ?? __("Unassigned")} + + {vendors.length > 0 ? ( +
+ {vendors.slice(0, 3).map((vendor) => ( + + + {vendor.name} + + ))} + {vendors.length > 3 && ( + + +{vendors.length - 3} + + )} +
+ ) : ( + {__("None")} + )} + + + + + {__("Delete")} + + + + + ); +} diff --git a/apps/console2/src/pages/organizations/data/DatumDetailsPage.tsx b/apps/console2/src/pages/organizations/data/DatumDetailsPage.tsx new file mode 100644 index 000000000..fe048c1ec --- /dev/null +++ b/apps/console2/src/pages/organizations/data/DatumDetailsPage.tsx @@ -0,0 +1,149 @@ +import { + ConnectionHandler, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import { + datumNodeQuery, + useDeleteDatum, + useUpdateDatum, +} from "../../../hooks/graph/DatumGraph"; +import { + ActionDropdown, + Badge, + Breadcrumb, + Button, + DropdownItem, + Field, + IconTrashCan, + Option, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { ControlledField } from "/components/form/ControlledField"; +import { PeopleSelectField } from "/components/form/PeopleSelectField"; +import { VendorsMultiSelectField } from "/components/form/VendorsMultiSelectField"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import z from "zod"; + +const updateDatumSchema = z.object({ + name: z.string().min(1, "Name is required"), + dataClassification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]), + ownerId: z.string().min(1, "Owner is required"), + vendorIds: z.array(z.string()).optional(), +}); + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function DatumDetailsPage(props: Props) { + const datum = usePreloadedQuery(datumNodeQuery, props.queryRef); + const datumEntry = datum.node; + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + const deleteDatum = useDeleteDatum( + datumEntry, + ConnectionHandler.getConnectionID(organizationId, "DataPage_data") + ); + + const vendors = datumEntry?.vendors?.edges.map((edge: any) => edge.node) ?? []; + const vendorIds = vendors.map((vendor: any) => vendor.id); + + const { control, formState, handleSubmit, register, reset } = useFormWithSchema(updateDatumSchema, { + defaultValues: { + name: datumEntry?.name || "", + dataClassification: datumEntry?.dataClassification || "PUBLIC", + ownerId: datumEntry?.owner?.id || "", + vendorIds: vendorIds, + }, + }); + + const updateDatum = useUpdateDatum(); + + const onSubmit = handleSubmit(async (formData) => { + try { + await updateDatum({ + id: datumEntry?.id, + ...formData, + }); + reset(formData); + } catch (error) { + console.error("Failed to update datum:", error); + } + }); + + return ( +
+ + +
+
+
{datumEntry?.name}
+ {datumEntry?.dataClassification} +
+ + + {__("Delete")} + + +
+ +
+ + + + + + + + + + + + + +
+ {formState.isDirty && ( + + )} +
+ +
+ ); +} diff --git a/apps/console2/src/pages/organizations/data/dialogs/CreateDatumDialog.tsx b/apps/console2/src/pages/organizations/data/dialogs/CreateDatumDialog.tsx new file mode 100644 index 000000000..82fdb7841 --- /dev/null +++ b/apps/console2/src/pages/organizations/data/dialogs/CreateDatumDialog.tsx @@ -0,0 +1,102 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Field, + Option, + useDialogRef, + Breadcrumb, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import z from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { ControlledField } from "/components/form/ControlledField"; +import { PeopleSelectField } from "/components/form/PeopleSelectField"; +import { VendorsMultiSelectField } from "/components/form/VendorsMultiSelectField"; +import { useCreateDatum } from "../../../../hooks/graph/DatumGraph"; + +const schema = z.object({ + name: z.string().min(1, "Name is required"), + dataClassification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]), + ownerId: z.string().min(1, "Owner is required"), + vendorIds: z.array(z.string()).optional(), +}); + +type Props = { + children: React.ReactNode; + connection: string; + organizationId: string; +}; + +export function CreateDatumDialog({ children, connection, organizationId }: Props) { + const { __ } = useTranslate(); + const { control, handleSubmit, register, formState } = useFormWithSchema(schema, { + defaultValues: { + name: "", + dataClassification: "PUBLIC", + ownerId: "", + vendorIds: [], + }, + }); + const ref = useDialogRef(); + const createDatum = useCreateDatum(connection); + + const onSubmit = handleSubmit(async (data) => { + try { + await createDatum({ + ...data, + organizationId, + }); + ref.current?.close(); + } catch (error) { + console.error("Failed to create datum:", error); + } + }); + + return ( + } + > +
+ + + + + + + + + + + + + + +
+
+ ); +} diff --git a/apps/console2/src/routes.tsx b/apps/console2/src/routes.tsx index c4505521d..2aa3554a9 100644 --- a/apps/console2/src/routes.tsx +++ b/apps/console2/src/routes.tsx @@ -25,6 +25,7 @@ import { peopleRoutes } from "./routes/peopleRoutes.ts"; import { frameworkRoutes } from "./routes/frameworkRoutes.ts"; import { PageError } from "./components/PageError.tsx"; import { taskRoutes } from "./routes/taskRoutes.ts"; +import { dataRoutes } from "./routes/dataRoutes.ts"; import { lazy } from "@probo/react-lazy"; function ErrorBoundary() { @@ -93,6 +94,7 @@ const routes = [ }), Component: lazy(() => import("./pages/organizations/SettingsPage")), }, + ...dataRoutes, ...riskRoutes, ...measureRoutes, ...documentsRoutes, diff --git a/apps/console2/src/routes/dataRoutes.ts b/apps/console2/src/routes/dataRoutes.ts new file mode 100644 index 000000000..aec0edbfd --- /dev/null +++ b/apps/console2/src/routes/dataRoutes.ts @@ -0,0 +1,26 @@ +import { loadQuery } from "react-relay"; +import { relayEnvironment } from "/providers/RelayProviders"; +import { PageSkeleton } from "/components/skeletons/PageSkeleton"; +import { lazy } from "@probo/react-lazy"; +import { dataQuery, datumNodeQuery } from "../hooks/graph/DatumGraph"; + +export const dataRoutes = [ + { + path: "data", + fallback: PageSkeleton, + queryLoader: (params: Record) => + loadQuery(relayEnvironment, dataQuery, { organizationId: params.organizationId }), + Component: lazy( + () => import("/pages/organizations/data/DataPage") + ), + }, + { + path: "data/:dataId", + fallback: PageSkeleton, + queryLoader: (params: Record) => + loadQuery(relayEnvironment, datumNodeQuery, { dataId: params.dataId }), + Component: lazy( + () => import("../pages/organizations/data/DatumDetailsPage") + ), + }, +]