Fix data pages permissions handling

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-31 16:31:33 +01:00
committed by Bryan Frimin
parent 5b0627b05a
commit f3e23850ff
12 changed files with 333 additions and 60 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<2000fd2ccfcf4b832c51ff71b0326fef>> * @generated SignedSource<<bfe5e85608cbcd0fd0db44ad909ac493>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -322,6 +322,32 @@ return {
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:delete\")"
},
(v13/*: any*/) (v13/*: any*/)
], ],
"storageKey": null "storageKey": null
@@ -411,16 +437,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "dbca2ff147e8b1e367a266fcf016b184", "cacheID": "c022cb17d3ede74899755b606ce95ba9",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "DataListQuery", "name": "DataListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query DataListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 10\n $last: Int = null\n $order: DatumOrder = null\n $snapshotId: ID = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...DataPageFragment_25MC8O\n id\n }\n}\n\nfragment DataPageFragment_25MC8O on Organization {\n data(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {snapshotId: $snapshotId}) {\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 hasPreviousPage\n startCursor\n }\n }\n id\n}\n" "text": "query DataListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 10\n $last: Int = null\n $order: DatumOrder = null\n $snapshotId: ID = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...DataPageFragment_25MC8O\n id\n }\n}\n\nfragment DataPageFragment_25MC8O on Organization {\n data(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {snapshotId: $snapshotId}) {\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 canUpdate: permission(action: \"core:datum:update\")\n canDelete: permission(action: \"core:datum:delete\")\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
} }
}; };
})(); })();
(node as any).hash = "5d3069a0d856613ccb854033b5b1f14e"; (node as any).hash = "9a4453365a733d298ab9f5237fc4153b";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<9619ee8f74aea67be931f8c40a8815c8>> * @generated SignedSource<<213dd4c7bd0e87ba0e64fa6ce8d2c37b>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,6 +16,8 @@ export type DataPageFragment$data = {
readonly __id: string; readonly __id: string;
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: any; readonly createdAt: any;
readonly dataClassification: DataClassification; readonly dataClassification: DataClassification;
readonly id: string; readonly id: string;
@@ -248,6 +250,32 @@ return {
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:delete\")"
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -329,6 +357,6 @@ return {
}; };
})(); })();
(node as any).hash = "5d3069a0d856613ccb854033b5b1f14e"; (node as any).hash = "9a4453365a733d298ab9f5237fc4153b";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<43b67fe6155860e492f2a36543727d49>> * @generated SignedSource<<8106bb4a10940caebf1f523fb6136350>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -25,6 +25,8 @@ export type DatumGraphCreateMutation$data = {
readonly createDatum: { readonly createDatum: {
readonly datumEdge: { readonly datumEdge: {
readonly node: { readonly node: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: any; readonly createdAt: any;
readonly dataClassification: DataClassification; readonly dataClassification: DataClassification;
readonly id: string; readonly id: string;
@@ -181,6 +183,32 @@ v5 = {
"kind": "ScalarField", "kind": "ScalarField",
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
},
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:delete\")"
} }
], ],
"storageKey": null "storageKey": null
@@ -254,16 +282,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "23ff315b0cc090bcaca7739d945d8d1b", "cacheID": "37d345acfe37d5edf653d021f8aad43c",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "DatumGraphCreateMutation", "name": "DatumGraphCreateMutation",
"operationKind": "mutation", "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: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n }\n }\n }\n}\n" "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: 50) {\n edges {\n node {\n id\n name\n websiteUrl\n }\n }\n }\n createdAt\n canUpdate: permission(action: \"core:datum:update\")\n canDelete: permission(action: \"core:datum:delete\")\n }\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "b148ef55d18d3a45c04b5072581f2285"; (node as any).hash = "db4781176e1b58224619f8237af92716";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<7e67f2a3d41fbea46b7b8447595302a5>> * @generated SignedSource<<034a68d54259fb22cf2d563699ad4257>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,6 +16,7 @@ export type DatumGraphListQuery$variables = {
}; };
export type DatumGraphListQuery$data = { export type DatumGraphListQuery$data = {
readonly node: { readonly node: {
readonly canCreateDatum?: boolean;
readonly " $fragmentSpreads": FragmentRefs<"DataPageFragment">; readonly " $fragmentSpreads": FragmentRefs<"DataPageFragment">;
}; };
}; };
@@ -44,30 +45,43 @@ v1 = [
"variableName": "organizationId" "variableName": "organizationId"
} }
], ],
v2 = [ v2 = {
"alias": "canCreateDatum",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:create"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:create\")"
},
v3 = [
{ {
"kind": "Variable", "kind": "Variable",
"name": "snapshotId", "name": "snapshotId",
"variableName": "snapshotId" "variableName": "snapshotId"
} }
], ],
v3 = { v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v4 = { v5 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "id",
"storageKey": null "storageKey": null
}, },
v5 = [ v6 = [
{ {
"fields": (v2/*: any*/), "fields": (v3/*: any*/),
"kind": "ObjectValue", "kind": "ObjectValue",
"name": "filter" "name": "filter"
}, },
@@ -77,7 +91,7 @@ v5 = [
"value": 10 "value": 10
} }
], ],
v6 = { v7 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
@@ -102,8 +116,9 @@ return {
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v2/*: any*/),
{ {
"args": (v2/*: any*/), "args": (v3/*: any*/),
"kind": "FragmentSpread", "kind": "FragmentSpread",
"name": "DataPageFragment" "name": "DataPageFragment"
} }
@@ -132,14 +147,15 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v5/*: any*/), "args": (v6/*: any*/),
"concreteType": "DatumConnection", "concreteType": "DatumConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "data", "name": "data",
@@ -161,8 +177,8 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v4/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -185,7 +201,7 @@ return {
"name": "fullName", "name": "fullName",
"storageKey": null "storageKey": null
}, },
(v4/*: any*/) (v5/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -219,8 +235,8 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v4/*: any*/), (v5/*: any*/),
(v6/*: any*/), (v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -244,7 +260,33 @@ return {
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
(v3/*: any*/) {
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:delete\")"
},
(v4/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -314,7 +356,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v5/*: any*/), "args": (v6/*: any*/),
"filters": [ "filters": [
"filter" "filter"
], ],
@@ -333,16 +375,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "7200b7cf37859346d740de3f96f1d443", "cacheID": "203f6661d76c730336cffaa96b631ab8",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "DatumGraphListQuery", "name": "DatumGraphListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query DatumGraphListQuery(\n $organizationId: ID!\n $snapshotId: ID = null\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...DataPageFragment_3iomuz\n }\n id\n }\n}\n\nfragment DataPageFragment_3iomuz on Organization {\n data(first: 10, filter: {snapshotId: $snapshotId}) {\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 hasPreviousPage\n startCursor\n }\n }\n id\n}\n" "text": "query DatumGraphListQuery(\n $organizationId: ID!\n $snapshotId: ID = null\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n canCreateDatum: permission(action: \"core:datum:create\")\n ...DataPageFragment_3iomuz\n }\n id\n }\n}\n\nfragment DataPageFragment_3iomuz on Organization {\n data(first: 10, filter: {snapshotId: $snapshotId}) {\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 canUpdate: permission(action: \"core:datum:update\")\n canDelete: permission(action: \"core:datum:delete\")\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
} }
}; };
})(); })();
(node as any).hash = "e0782332e7e82bd1a5e7f5e40ada2dae"; (node as any).hash = "e071e19eda0d4c105668ffd9671399bd";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<5cfbdbc980894977800a18064306f2c1>> * @generated SignedSource<<e2ff7cc39a32a043a4d4c2ad4dac8411>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,6 +16,8 @@ export type DatumGraphNodeQuery$variables = {
}; };
export type DatumGraphNodeQuery$data = { export type DatumGraphNodeQuery$data = {
readonly node: { readonly node: {
readonly canDelete?: boolean;
readonly canUpdate?: boolean;
readonly createdAt?: any; readonly createdAt?: any;
readonly dataClassification?: DataClassification; readonly dataClassification?: DataClassification;
readonly id?: string; readonly id?: string;
@@ -188,6 +190,32 @@ v10 = {
"kind": "ScalarField", "kind": "ScalarField",
"name": "updatedAt", "name": "updatedAt",
"storageKey": null "storageKey": null
},
v11 = {
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:update\")"
},
v12 = {
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:datum:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:datum:delete\")"
}; };
return { return {
"fragment": { "fragment": {
@@ -215,7 +243,9 @@ return {
(v7/*: any*/), (v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/) (v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/)
], ],
"type": "Datum", "type": "Datum",
"abstractKey": null "abstractKey": null
@@ -259,7 +289,9 @@ return {
(v7/*: any*/), (v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/) (v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/)
], ],
"type": "Datum", "type": "Datum",
"abstractKey": null "abstractKey": null
@@ -270,16 +302,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "6ed49a3413c0214dc95da9a422e484b4", "cacheID": "036588bb9b3daffb20c9d01a708976ac",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "DatumGraphNodeQuery", "name": "DatumGraphNodeQuery",
"operationKind": "query", "operationKind": "query",
"text": "query DatumGraphNodeQuery(\n $dataId: ID!\n) {\n node(id: $dataId) {\n __typename\n ... on Datum {\n id\n snapshotId\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" "text": "query DatumGraphNodeQuery(\n $dataId: ID!\n) {\n node(id: $dataId) {\n __typename\n ... on Datum {\n id\n snapshotId\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 canUpdate: permission(action: \"core:datum:update\")\n canDelete: permission(action: \"core:datum:delete\")\n }\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "2f4df20b67f8b21eda5f510f250abda1"; (node as any).hash = "2b2e220e0bdd98bbee7ea72a00db7a61";
export default node; export default node;

View File

@@ -8,6 +8,7 @@ export const dataQuery = graphql`
query DatumGraphListQuery($organizationId: ID!, $snapshotId: ID = null) { query DatumGraphListQuery($organizationId: ID!, $snapshotId: ID = null) {
node(id: $organizationId) { node(id: $organizationId) {
... on Organization { ... on Organization {
canCreateDatum: permission(action: "core:datum:create")
...DataPageFragment @arguments(snapshotId: $snapshotId) ...DataPageFragment @arguments(snapshotId: $snapshotId)
} }
} }
@@ -41,6 +42,8 @@ export const datumNodeQuery = graphql`
} }
createdAt createdAt
updatedAt updatedAt
canUpdate: permission(action: "core:datum:update")
canDelete: permission(action: "core:datum:delete")
} }
} }
} }
@@ -71,6 +74,8 @@ export const createDatumMutation = graphql`
} }
} }
createdAt createdAt
canUpdate: permission(action: "core:datum:update")
canDelete: permission(action: "core:datum:delete")
} }
} }
} }
@@ -116,7 +121,7 @@ export const deleteDatumMutation = graphql`
export const useDeleteDatum = ( export const useDeleteDatum = (
datum: { id?: string; name?: string }, datum: { id?: string; name?: string },
connectionId: string connectionId: string,
) => { ) => {
const [mutate] = useMutation(deleteDatumMutation); const [mutate] = useMutation(deleteDatumMutation);
const confirm = useConfirm(); const confirm = useConfirm();
@@ -139,11 +144,11 @@ export const useDeleteDatum = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete "%s". This action cannot be undone.' 'This will permanently delete "%s". This action cannot be undone.',
), ),
datum.name datum.name,
), ),
} },
); );
}; };
}; };

View File

@@ -35,8 +35,6 @@ import type {
import type { DataListQuery } from "/__generated__/core/DataListQuery.graphql"; import type { DataListQuery } from "/__generated__/core/DataListQuery.graphql";
import { SortableTable } from "/components/SortableTable"; import { SortableTable } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedDataFragment = graphql` const paginatedDataFragment = graphql`
fragment DataPageFragment on Organization fragment DataPageFragment on Organization
@@ -76,6 +74,8 @@ const paginatedDataFragment = graphql`
} }
} }
createdAt createdAt
canUpdate: permission(action: "core:datum:update")
canDelete: permission(action: "core:datum:delete")
} }
} }
} }
@@ -93,15 +93,12 @@ export default function DataPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const queryData = usePreloadedQuery<DatumGraphListQuery>( const { node: data } = usePreloadedQuery<DatumGraphListQuery>(
dataQuery, dataQuery,
props.queryRef, props.queryRef,
); );
const data = queryData.node;
const pagination = usePaginationFragment<DataListQuery, DataPageFragment$key>( const pagination = usePaginationFragment<DataListQuery, DataPageFragment$key>(
paginatedDataFragment, paginatedDataFragment,
data, data,
@@ -128,8 +125,7 @@ export default function DataPage(props: Props) {
const hasAnyAction = const hasAnyAction =
!isSnapshotMode && !isSnapshotMode &&
(isAuthorized("Datum", "updateDatum") || dataEntries.some(({ canDelete, canUpdate }) => canUpdate || canDelete);
isAuthorized("Datum", "deleteDatum"));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -142,7 +138,7 @@ export default function DataPage(props: Props) {
"Manage your organization's data assets and their classifications.", "Manage your organization's data assets and their classifications.",
)} )}
> >
{!snapshotId && isAuthorized("Organization", "createDatum") && ( {!snapshotId && data.canCreateDatum && (
<CreateDatumDialog <CreateDatumDialog
connection={connectionId} connection={connectionId}
organizationId={organizationId} organizationId={organizationId}
@@ -193,7 +189,6 @@ function DataRow({
const { __ } = useTranslate(); const { __ } = useTranslate();
const deleteDatum = useDeleteDatum(entry, connectionId); const deleteDatum = useDeleteDatum(entry, connectionId);
const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? []; const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const detailUrl = snapshotId const detailUrl = snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/data/${entry.id}` ? `/organizations/${organizationId}/snapshots/${snapshotId}/data/${entry.id}`
: `/organizations/${organizationId}/data/${entry.id}`; : `/organizations/${organizationId}/data/${entry.id}`;
@@ -235,7 +230,7 @@ function DataRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
{isAuthorized("Datum", "deleteDatum") && ( {entry.canDelete && (
<DropdownItem <DropdownItem
onClick={deleteDatum} onClick={deleteDatum}
variant="danger" variant="danger"

View File

@@ -29,8 +29,6 @@ import z from "zod";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { validateSnapshotConsistency } from "@probo/helpers"; import { validateSnapshotConsistency } from "@probo/helpers";
import type { DatumGraphNodeQuery } from "/__generated__/core/DatumGraphNodeQuery.graphql"; import type { DatumGraphNodeQuery } from "/__generated__/core/DatumGraphNodeQuery.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateDatumSchema = z.object({ const updateDatumSchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
@@ -58,7 +56,6 @@ export default function DatumDetailsPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const deleteDatum = useDeleteDatum( const deleteDatum = useDeleteDatum(
datumEntry, datumEntry,
@@ -112,6 +109,8 @@ export default function DatumDetailsPage(props: Props) {
}, },
]; ];
const disabled = !isSnapshotMode && datumEntry.canUpdate;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{isSnapshotMode && snapshotId && ( {isSnapshotMode && snapshotId && (
@@ -124,7 +123,7 @@ export default function DatumDetailsPage(props: Props) {
<div className="text-2xl">{datumEntry?.name}</div> <div className="text-2xl">{datumEntry?.name}</div>
<Badge variant="info">{datumEntry?.dataClassification}</Badge> <Badge variant="info">{datumEntry?.dataClassification}</Badge>
</div> </div>
{!isSnapshotMode && isAuthorized("Datum", "deleteDatum") && ( {!isSnapshotMode && datumEntry.canDelete && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -142,7 +141,7 @@ export default function DatumDetailsPage(props: Props) {
label={__("Name")} label={__("Name")}
{...register("name")} {...register("name")}
type="text" type="text"
disabled={isSnapshotMode} disabled={!disabled}
/> />
<ControlledField <ControlledField
@@ -150,7 +149,7 @@ export default function DatumDetailsPage(props: Props) {
name="dataClassification" name="dataClassification"
type="select" type="select"
label={__("Classification")} label={__("Classification")}
disabled={isSnapshotMode} disabled={!disabled}
> >
<Option value="PUBLIC">{__("Public")}</Option> <Option value="PUBLIC">{__("Public")}</Option>
<Option value="INTERNAL">{__("Internal")}</Option> <Option value="INTERNAL">{__("Internal")}</Option>
@@ -163,7 +162,7 @@ export default function DatumDetailsPage(props: Props) {
control={control} control={control}
name="ownerId" name="ownerId"
label={__("Owner")} label={__("Owner")}
disabled={isSnapshotMode} disabled={!disabled}
/> />
<VendorsMultiSelectField <VendorsMultiSelectField
@@ -171,13 +170,13 @@ export default function DatumDetailsPage(props: Props) {
control={control} control={control}
name="vendorIds" name="vendorIds"
label={__("Vendors")} label={__("Vendors")}
disabled={isSnapshotMode} disabled={!disabled}
selectedVendors={vendors} selectedVendors={vendors}
/> />
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && isAuthorized("Datum", "updateDatum") && ( {formState.isDirty && datumEntry.canUpdate && (
<Button type="submit" disabled={formState.isSubmitting}> <Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")} {formState.isSubmitting ? __("Updating...") : __("Update")}
</Button> </Button>

View File

@@ -4972,6 +4972,8 @@ type Datum implements Node
organization: Organization! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true)
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
} }
input DatumOrder input DatumOrder

View File

@@ -494,6 +494,7 @@ type ComplexityRoot struct {
Name func(childComplexity int) int Name func(childComplexity int) int
Organization func(childComplexity int) int Organization func(childComplexity int) int
Owner func(childComplexity int) int Owner func(childComplexity int) int
Permission func(childComplexity int, action string) int
SnapshotID func(childComplexity int) int SnapshotID func(childComplexity int) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) int Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) int
@@ -2005,6 +2006,8 @@ type DatumResolver interface {
Owner(ctx context.Context, obj *types.Datum) (*types.People, error) Owner(ctx context.Context, obj *types.Datum) (*types.People, error)
Vendors(ctx context.Context, obj *types.Datum, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error) Vendors(ctx context.Context, obj *types.Datum, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error)
Organization(ctx context.Context, obj *types.Datum) (*types.Organization, error) Organization(ctx context.Context, obj *types.Datum) (*types.Organization, error)
Permission(ctx context.Context, obj *types.Datum, action string) (bool, error)
} }
type DatumConnectionResolver interface { type DatumConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.DatumConnection) (int, error) TotalCount(ctx context.Context, obj *types.DatumConnection) (int, error)
@@ -3709,6 +3712,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.Datum.Owner(childComplexity), true return e.complexity.Datum.Owner(childComplexity), true
case "Datum.permission":
if e.complexity.Datum.Permission == nil {
break
}
args, err := ec.field_Datum_permission_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Datum.Permission(childComplexity, args["action"].(string)), true
case "Datum.snapshotId": case "Datum.snapshotId":
if e.complexity.Datum.SnapshotID == nil { if e.complexity.Datum.SnapshotID == nil {
break break
@@ -15314,6 +15328,8 @@ type Datum implements Node
organization: Organization! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true)
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
} }
input DatumOrder input DatumOrder
@@ -15876,6 +15892,17 @@ func (ec *executionContext) field_DataProtectionImpactAssessment_permission_args
return args, nil return args, nil
} }
func (ec *executionContext) field_Datum_permission_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "action", ec.unmarshalNString2string)
if err != nil {
return nil, err
}
args["action"] = arg0
return args, nil
}
func (ec *executionContext) field_Datum_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Datum_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -26455,6 +26482,47 @@ func (ec *executionContext) fieldContext_Datum_updatedAt(_ context.Context, fiel
return fc, nil return fc, nil
} }
func (ec *executionContext) _Datum_permission(ctx context.Context, field graphql.CollectedField, obj *types.Datum) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Datum_permission,
func(ctx context.Context) (any, error) {
fc := graphql.GetFieldContext(ctx)
return ec.resolvers.Datum().Permission(ctx, obj, fc.Args["action"].(string))
},
nil,
ec.marshalNBoolean2bool,
true,
true,
)
}
func (ec *executionContext) fieldContext_Datum_permission(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Datum",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Datum_permission_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _DatumConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.DatumConnection) (ret graphql.Marshaler) { func (ec *executionContext) _DatumConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.DatumConnection) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -26629,6 +26697,8 @@ func (ec *executionContext) fieldContext_DatumEdge_node(_ context.Context, field
return ec.fieldContext_Datum_createdAt(ctx, field) return ec.fieldContext_Datum_createdAt(ctx, field)
case "updatedAt": case "updatedAt":
return ec.fieldContext_Datum_updatedAt(ctx, field) return ec.fieldContext_Datum_updatedAt(ctx, field)
case "permission":
return ec.fieldContext_Datum_permission(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Datum", field.Name) return nil, fmt.Errorf("no field named %q was found under type Datum", field.Name)
}, },
@@ -53394,6 +53464,8 @@ func (ec *executionContext) fieldContext_UpdateDatumPayload_datum(_ context.Cont
return ec.fieldContext_Datum_createdAt(ctx, field) return ec.fieldContext_Datum_createdAt(ctx, field)
case "updatedAt": case "updatedAt":
return ec.fieldContext_Datum_updatedAt(ctx, field) return ec.fieldContext_Datum_updatedAt(ctx, field)
case "permission":
return ec.fieldContext_Datum_permission(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type Datum", field.Name) return nil, fmt.Errorf("no field named %q was found under type Datum", field.Name)
}, },
@@ -73399,6 +73471,42 @@ func (ec *executionContext) _Datum(ctx context.Context, sel ast.SelectionSet, ob
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) atomic.AddUint32(&out.Invalids, 1)
} }
case "permission":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Datum_permission(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
return res
}
if field.Deferrable != nil {
dfs, ok := deferred[field.Deferrable.Label]
di := 0
if ok {
dfs.AddField(field)
di = len(dfs.Values) - 1
} else {
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
deferred[field.Deferrable.Label] = dfs
}
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
return innerFunc(ctx, dfs)
})
// don't run the out.Concurrently() call below
out.Values[i] = graphql.Null
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }

View File

@@ -79,6 +79,9 @@ func NewDatum(d *coredata.Datum) *Datum {
Organization: &Organization{ Organization: &Organization{
ID: d.OrganizationID, ID: d.OrganizationID,
}, },
Owner: &People{
ID: d.OwnerID,
},
OrganizationID: d.OrganizationID, OrganizationID: d.OrganizationID,
Name: d.Name, Name: d.Name,
SnapshotID: d.SnapshotID, SnapshotID: d.SnapshotID,

View File

@@ -702,6 +702,11 @@ func (r *datumResolver) Organization(ctx context.Context, obj *types.Datum) (*ty
return types.NewOrganization(org), nil return types.NewOrganization(org), nil
} }
// Permission is the resolver for the permission field.
func (r *datumResolver) Permission(ctx context.Context, obj *types.Datum, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field. // TotalCount is the resolver for the totalCount field.
func (r *datumConnectionResolver) TotalCount(ctx context.Context, obj *types.DatumConnection) (int, error) { func (r *datumConnectionResolver) TotalCount(ctx context.Context, obj *types.DatumConnection) (int, error) {
r.MustAuthorize(ctx, obj.ParentID, probo.ActionDatumList) r.MustAuthorize(ctx, obj.ParentID, probo.ActionDatumList)