From f77676c88c0a76ea6a9dc9c6e1301dd55b54a08b Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 10 Dec 2025 14:41:54 +0100 Subject: [PATCH] Add auditor role Signed-off-by: Sacha Al Himdani --- .../components/audits/LinkedAuditsCard.tsx | 51 +-- .../controls/LinkedControlsCard.tsx | 47 ++- .../documents/LinkedDocumentsCard.tsx | 53 +-- .../measures/LinkedMeasuresCard.tsx | 54 +-- .../obligations/LinkedObligationsCard.tsx | 54 +-- .../organizations/InviteUserDialog.tsx | 6 +- .../InviteUserDialogMutation.graphql.ts | 4 +- .../src/components/risks/LinkedRisksCard.tsx | 48 ++- .../snapshots/LinkedSnapshotsCard.tsx | 56 +-- apps/console/src/hooks/graph/MeasureGraph.ts | 4 - .../MeasureGraphNodeQuery.graphql.ts | 208 +++------- .../documents/DocumentDetailPage.tsx | 41 +- .../frameworks/FrameworkControlPage.tsx | 20 + .../measures/MeasureDetailPage.tsx | 54 ++- ...easureDetailPageTasksCountQuery.graphql.ts | 143 +++++++ .../measures/tabs/MeasureControlsTab.tsx | 8 + .../measures/tabs/MeasureEvidencesTab.tsx | 31 +- .../measures/tabs/MeasureRisksTab.tsx | 8 + .../measures/tabs/MeasureTasksTab.tsx | 51 ++- .../MeasureTasksTabFragment.graphql.ts | 201 ---------- .../MeasureTasksTabQuery.graphql.ts | 370 ++++++++++++++++++ .../people/tabs/PeopleRoleTab.tsx | 22 +- .../risks/tabs/RiskDocumentsTab.tsx | 8 + .../risks/tabs/RiskMeasuresTab.tsx | 9 + .../risks/tabs/RiskObligationsTab.tsx | 9 + .../settings/MembersSettingsTab.tsx | 4 + ...sSettingsTabInvitationsFragment.graphql.ts | 4 +- ...sSettingsTabMembershipsFragment.graphql.ts | 4 +- ...ngsTab_UpdateMembershipMutation.graphql.ts | 4 +- .../vendors/VendorDetailPage.tsx | 12 +- .../vendors/tabs/VendorCertificationsTab.tsx | 67 ++-- .../vendors/tabs/VendorComplianceTab.tsx | 13 +- .../vendors/tabs/VendorContactsTab.tsx | 63 +-- .../vendors/tabs/VendorOverviewTab.tsx | 115 +++--- .../vendors/tabs/VendorRiskAssessmentTab.tsx | 9 +- .../vendors/tabs/VendorServicesTab.tsx | 59 +-- apps/console/src/routes.tsx | 10 +- packages/helpers/src/roles.ts | 5 +- pkg/authz/permissions.go | 99 ++--- pkg/coredata/entity_type_reg.go | 2 +- pkg/coredata/member_role.go | 3 + pkg/coredata/migrations/20251210T133440Z.sql | 1 + pkg/server/api/console/v1/schema.graphql | 2 + pkg/server/api/console/v1/schema/schema.go | 4 + pkg/server/api/console/v1/types/types.go | 12 +- 45 files changed, 1256 insertions(+), 796 deletions(-) create mode 100644 apps/console/src/pages/organizations/measures/__generated__/MeasureDetailPageTasksCountQuery.graphql.ts delete mode 100644 apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabFragment.graphql.ts create mode 100644 apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabQuery.graphql.ts create mode 100644 pkg/coredata/migrations/20251210T133440Z.sql diff --git a/apps/console/src/components/audits/LinkedAuditsCard.tsx b/apps/console/src/components/audits/LinkedAuditsCard.tsx index 8818cea8c..e7bcfd585 100644 --- a/apps/console/src/components/audits/LinkedAuditsCard.tsx +++ b/apps/console/src/components/audits/LinkedAuditsCard.tsx @@ -55,6 +55,7 @@ type Props = { onAttach: Mutation; onDetach: Mutation; variant?: "card" | "table"; + readOnly?: boolean; }; export function LinkedAuditsCard(props: Props) { @@ -97,16 +98,18 @@ export function LinkedAuditsCard(props: Props) { {variant === "card" && (
{__("Audits")}
- - - + {!props.readOnly && ( + + + + )}
)} @@ -114,13 +117,13 @@ export function LinkedAuditsCard(props: Props) { - + {!props.readOnly && } {audits.length === 0 && ( - @@ -130,9 +133,10 @@ export function LinkedAuditsCard(props: Props) { key={audit.id} audit={audit} onClick={onDetach} + readOnly={props.readOnly} /> ))} - {variant === "table" && ( + {variant === "table" && !props.readOnly && ( (props: Props) { function AuditRow(props: { audit: LinkedAuditsCardFragment$key & { id: string }; onClick: (auditId: string) => void; + readOnly?: boolean; }) { const audit = useFragment(linkedAuditFragment, props.audit); const organizationId = useOrganizationId(); @@ -187,15 +192,17 @@ function AuditRow(props: { {audit.state.replace(/_/g, " ")} - + {!props.readOnly && ( + + )} ); } diff --git a/apps/console/src/components/controls/LinkedControlsCard.tsx b/apps/console/src/components/controls/LinkedControlsCard.tsx index b92ff1308..0c6384aca 100644 --- a/apps/console/src/components/controls/LinkedControlsCard.tsx +++ b/apps/console/src/components/controls/LinkedControlsCard.tsx @@ -54,6 +54,7 @@ type Props = { onAttach?: Mutation; // Allow sorting in the table refetch: ComponentProps["refetch"]; + readOnly?: boolean; }; /** @@ -96,13 +97,13 @@ export function LinkedControlsCard(props: Props) { {__("Reference")} - + {!props.readOnly && } {controls.length === 0 && ( - @@ -113,17 +114,20 @@ export function LinkedControlsCard(props: Props) { control={control} onClick={onDetach} onAttach={onAttach} + readOnly={props.readOnly} /> ))} - - {__("Link control")} - + {!props.readOnly && ( + + {__("Link control")} + + )} ); @@ -133,6 +137,7 @@ function ControlRow(props: { control: LinkedControlsCardFragment$key & { id: string }; onClick: (controlId: string) => void; onAttach?: (controlId: string) => void; + readOnly?: boolean; }) { const control = useFragment(linkedControlFragment, props.control); const organizationId = useOrganizationId(); @@ -149,15 +154,17 @@ function ControlRow(props: { - + {!props.readOnly && ( + + )} ); } diff --git a/apps/console/src/components/documents/LinkedDocumentsCard.tsx b/apps/console/src/components/documents/LinkedDocumentsCard.tsx index d09632117..ffb14c25b 100644 --- a/apps/console/src/components/documents/LinkedDocumentsCard.tsx +++ b/apps/console/src/components/documents/LinkedDocumentsCard.tsx @@ -64,6 +64,7 @@ type Props = { // Mutation to detach a document (will receive {documentId, ...params}) onDetach: Mutation; variant?: "card" | "table"; + readOnly?: boolean; }; /** @@ -109,17 +110,19 @@ export function LinkedDocumentsCard(props: Props) { {variant === "card" && (
{__("Documents")}
- - - + {!props.readOnly && ( + + + + )}
)}
{__("Name")} {__("State")}
+ {__("No audits linked")}
- - + +
{__("Name")}
+ {__("No controls linked")}
{control.name} - - + +
@@ -128,13 +131,13 @@ export function LinkedDocumentsCard(props: Props) { - + {!props.readOnly && } {documents.length === 0 && ( - @@ -144,9 +147,10 @@ export function LinkedDocumentsCard(props: Props) { key={document.id} document={document} onClick={onDetach} + readOnly={props.readOnly} /> ))} - {variant === "table" && ( + {variant === "table" && !props.readOnly && ( (props: Props) { function DocumentRow(props: { document: LinkedDocumentsCardFragment$key & { id: string }; onClick: (documentId: string) => void; + readOnly?: boolean; }) { const document = useFragment(linkedDocumentFragment, props.document); const organizationId = useOrganizationId(); @@ -203,15 +208,17 @@ function DocumentRow(props: { - + {!props.readOnly && ( + + )} ); } diff --git a/apps/console/src/components/measures/LinkedMeasuresCard.tsx b/apps/console/src/components/measures/LinkedMeasuresCard.tsx index 25e939ea4..d9c74dddb 100644 --- a/apps/console/src/components/measures/LinkedMeasuresCard.tsx +++ b/apps/console/src/components/measures/LinkedMeasuresCard.tsx @@ -55,6 +55,7 @@ type Props = { // Mutation to detach a measure (will receive {measureId, ...params}) onDetach: Mutation; variant?: "card" | "table"; + readOnly?: boolean; }; /** @@ -102,17 +103,19 @@ export function LinkedMeasuresCard(props: Props) { {variant === "card" && (
{__("Measures")}
- - - + {!props.readOnly && ( + + + + )}
)}
{__("Name")} {__("Type")} {__("State")}
+ {__("No documents linked")}
- - + +
@@ -120,21 +123,21 @@ export function LinkedMeasuresCard(props: Props) { - + {!props.readOnly && } {measures.length === 0 && ( - )} {measures.map((measure) => ( - + ))} - {variant === "table" && ( + {variant === "table" && !props.readOnly && ( (props: Props) { function MeasureRow(props: { measure: LinkedMeasuresCardFragment$key & { id: string }; onClick: (measureId: string) => void; + readOnly?: boolean; }) { const measure = useFragment(linkedMeasureFragment, props.measure); const organizationId = useOrganizationId(); @@ -177,15 +181,17 @@ function MeasureRow(props: { - + {!props.readOnly && ( + + )} ); } diff --git a/apps/console/src/components/obligations/LinkedObligationsCard.tsx b/apps/console/src/components/obligations/LinkedObligationsCard.tsx index 6992cc514..fc0da525f 100644 --- a/apps/console/src/components/obligations/LinkedObligationsCard.tsx +++ b/apps/console/src/components/obligations/LinkedObligationsCard.tsx @@ -51,6 +51,7 @@ type Props = { connectionId: string; disabled?: boolean; variant?: "card" | "table"; + readOnly?: boolean; params: Params; @@ -102,17 +103,19 @@ export function LinkedObligationsCard(props: Props) { {variant === "card" && (
{__("Obligations")}
- - - + {!props.readOnly && ( + + + + )}
)}
{__("Name")} {__("State")}
+ {__("No measures linked")}
- - + +
@@ -122,21 +125,21 @@ export function LinkedObligationsCard(props: Props) { - + {!props.readOnly && } {obligations.length === 0 && ( - )} {obligations.map((obligation) => ( - + ))} - {variant === "table" && ( + {variant === "table" && !props.readOnly && ( (props: Props) { function ObligationRow(props: { obligation: LinkedObligationsCardFragment$key & { id: string }; onClick: (obligationId: string) => void; + readOnly?: boolean; }) { const { __ } = useTranslate(); const obligation = useFragment(linkedObligationFragment, props.obligation); @@ -199,15 +203,17 @@ function ObligationRow(props: { - + {!props.readOnly && ( + + )} ); } diff --git a/apps/console/src/components/organizations/InviteUserDialog.tsx b/apps/console/src/components/organizations/InviteUserDialog.tsx index ec7c3e16b..de8e24f7b 100644 --- a/apps/console/src/components/organizations/InviteUserDialog.tsx +++ b/apps/console/src/components/organizations/InviteUserDialog.tsx @@ -45,7 +45,7 @@ const inviteMutation = graphql` const schema = z.object({ email: z.string().email(), fullName: z.string(), - role: z.enum(["OWNER", "ADMIN", "FULL", "VIEWER", "EMPLOYEE"]).default("VIEWER"), + role: z.enum(["OWNER", "ADMIN", "FULL", "VIEWER", "AUDITOR", "EMPLOYEE"]).default("VIEWER"), createPeople: z.boolean().default(false), }); @@ -126,6 +126,7 @@ function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) { {assignableRoles.includes("OWNER") && } {assignableRoles.includes("ADMIN") && } {assignableRoles.includes("VIEWER") && } + {assignableRoles.includes("AUDITOR") && } {assignableRoles.includes("EMPLOYEE") && }
@@ -138,6 +139,9 @@ function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) { {field.value === "VIEWER" && (

{__("Read-only access")}

)} + {field.value === "AUDITOR" && ( +

{__("Read-only access without settings, tasks and meetings")}

+ )} {field.value === "EMPLOYEE" && (

{__("Access to employee page")}

)} diff --git a/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts b/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts index 4a444ae50..9f747f5ee 100644 --- a/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts +++ b/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<4dda94f89f726842762d68c28d8180b6>> + * @generated SignedSource<<31b0b8ac2c59ccf3addd4508d56bd166>> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type InviteUserInput = { createPeople: boolean; email: string; diff --git a/apps/console/src/components/risks/LinkedRisksCard.tsx b/apps/console/src/components/risks/LinkedRisksCard.tsx index bf27b34bd..d2c3a76a5 100644 --- a/apps/console/src/components/risks/LinkedRisksCard.tsx +++ b/apps/console/src/components/risks/LinkedRisksCard.tsx @@ -48,6 +48,7 @@ type Props = { onAttach: Mutation; // Mutation to detach a risk (will receive {riskId, ...params}) onDetach: Mutation; + readOnly?: boolean; }; /** @@ -88,29 +89,31 @@ export function LinkedRisksCard(props: Props) {
- + {!props.readOnly && } {props.risks.length === 0 && ( - )} {props.risks.map((risk) => ( - + ))} - - {__("Link risk")} - + {!props.readOnly && ( + + {__("Link risk")} + + )}
{__("Source")} {__("Status")} {__("Owner")}
+ {__("No obligations linked")}
{obligation.owner?.fullName || __("Unassigned")} - - + +
{__("Name")} {__("Inherent Risk")} {__("Residual Risk")}
+ {__("No risks linked")}
@@ -120,6 +123,7 @@ export function LinkedRisksCard(props: Props) { function RiskRow(props: { risk: LinkedRisksCardFragment$key & { id: string }; onClick: (riskId: string) => void; + readOnly?: boolean; }) { const risk = useFragment(linkedRiskFragment, props.risk); const organizationId = useOrganizationId(); @@ -134,15 +138,17 @@ function RiskRow(props: { - - - + {!props.readOnly && ( + + + + )} ); } diff --git a/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx b/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx index 0f3802ed6..55f7f0b57 100644 --- a/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx +++ b/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx @@ -50,6 +50,7 @@ type Props = { onAttach: Mutation; onDetach: Mutation; variant?: "card" | "table"; + readOnly?: boolean; }; export function LinkedSnapshotsCard(props: Props) { @@ -87,21 +88,26 @@ export function LinkedSnapshotsCard(props: Props) { const Wrapper = variant === "card" ? Card : "div"; + const colSpanTable = props.readOnly ? 4 : 5; + const colSpanCard = props.readOnly ? 3 : 4; + return ( {variant === "card" && (
{__("Snapshots")}
- - - + {!props.readOnly && ( + + + + )}
)} @@ -111,13 +117,13 @@ export function LinkedSnapshotsCard(props: Props) { {variant === "table" && } - + {!props.readOnly && } {snapshots.length === 0 && ( - @@ -128,16 +134,17 @@ export function LinkedSnapshotsCard(props: Props) { snapshot={snapshot} onClick={onDetach} variant={variant} + readOnly={props.readOnly} /> ))} - {variant === "table" && ( + {variant === "table" && !props.readOnly && ( - + {__("Link snapshot")} @@ -162,6 +169,7 @@ function SnapshotRow(props: { snapshot: LinkedSnapshotsCardFragment$key & { id: string }; onClick: (snapshotId: string) => void; variant: "card" | "table"; + readOnly?: boolean; }) { const snapshot = useFragment(linkedSnapshotFragment, props.snapshot); const organizationId = useOrganizationId(); @@ -186,15 +194,17 @@ function SnapshotRow(props: { - + {!props.readOnly && ( + + )} ); } diff --git a/apps/console/src/hooks/graph/MeasureGraph.ts b/apps/console/src/hooks/graph/MeasureGraph.ts index cf4277b27..c6195c1ec 100644 --- a/apps/console/src/hooks/graph/MeasureGraph.ts +++ b/apps/console/src/hooks/graph/MeasureGraph.ts @@ -52,14 +52,10 @@ export const measureNodeQuery = graphql` risksInfos: risks(first: 0) { totalCount } - tasksInfos: tasks(first: 0) { - totalCount - } controlsInfos: controls(first: 0) { totalCount } ...MeasureRisksTabFragment - ...MeasureTasksTabFragment ...MeasureControlsTabFragment ...MeasureFormDialogMeasureFragment ...MeasureEvidencesTabFragment diff --git a/apps/console/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts index 2b9aa6d9e..9e5afc130 100644 --- a/apps/console/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<02c0b1d637edafea060ddb9ee3b3f130>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -30,10 +30,7 @@ export type MeasureGraphNodeQuery$data = { readonly totalCount: number; }; readonly state?: MeasureState; - readonly tasksInfos?: { - readonly totalCount: number; - }; - readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment" | "MeasureTasksTabFragment">; + readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment">; }; }; export type MeasureGraphNodeQuery = { @@ -128,16 +125,6 @@ v10 = { "storageKey": "risks(first:0)" }, v11 = { - "alias": "tasksInfos", - "args": (v7/*: any*/), - "concreteType": "TaskConnection", - "kind": "LinkedField", - "name": "tasks", - "plural": false, - "selections": (v8/*: any*/), - "storageKey": "tasks(first:0)" -}, -v12 = { "alias": "controlsInfos", "args": (v7/*: any*/), "concreteType": "ControlConnection", @@ -147,55 +134,42 @@ v12 = { "selections": (v8/*: any*/), "storageKey": "controls(first:0)" }, -v13 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v14 = [ +v13 = [ { "kind": "Literal", "name": "first", "value": 100 } ], -v15 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v16 = { +v15 = { "alias": null, "args": null, "kind": "ScalarField", "name": "endCursor", "storageKey": null }, -v17 = { +v16 = { "alias": null, "args": null, "kind": "ScalarField", "name": "hasNextPage", "storageKey": null }, -v18 = { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - (v16/*: any*/), - (v17/*: any*/) - ], - "storageKey": null -}, -v19 = { +v17 = { "kind": "ClientExtension", "selections": [ { @@ -207,14 +181,14 @@ v19 = { } ] }, -v20 = [ +v18 = [ { "kind": "Literal", "name": "first", "value": 20 } ], -v21 = { +v19 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -222,8 +196,8 @@ v21 = { "name": "pageInfo", "plural": false, "selections": [ + (v15/*: any*/), (v16/*: any*/), - (v17/*: any*/), { "alias": null, "args": null, @@ -241,7 +215,7 @@ v21 = { ], "storageKey": null }, -v22 = [ +v20 = [ { "kind": "Literal", "name": "first", @@ -274,17 +248,11 @@ return { (v9/*: any*/), (v10/*: any*/), (v11/*: any*/), - (v12/*: any*/), { "args": null, "kind": "FragmentSpread", "name": "MeasureRisksTabFragment" }, - { - "args": null, - "kind": "FragmentSpread", - "name": "MeasureTasksTabFragment" - }, { "args": null, "kind": "FragmentSpread", @@ -325,7 +293,7 @@ return { "name": "node", "plural": false, "selections": [ - (v13/*: any*/), + (v12/*: any*/), (v2/*: any*/), { "kind": "InlineFragment", @@ -337,10 +305,9 @@ return { (v9/*: any*/), (v10/*: any*/), (v11/*: any*/), - (v12/*: any*/), { "alias": null, - "args": (v14/*: any*/), + "args": (v13/*: any*/), "concreteType": "RiskConnection", "kind": "LinkedField", "name": "risks", @@ -378,22 +345,34 @@ return { "name": "residualRiskScore", "storageKey": null }, - (v13/*: any*/) + (v12/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v18/*: any*/), - (v19/*: any*/) + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + (v15/*: any*/), + (v16/*: any*/) + ], + "storageKey": null + }, + (v17/*: any*/) ], "storageKey": "risks(first:100)" }, { "alias": null, - "args": (v14/*: any*/), + "args": (v13/*: any*/), "filters": null, "handle": "connection", "key": "Measure__risks", @@ -402,102 +381,7 @@ return { }, { "alias": null, - "args": (v14/*: any*/), - "concreteType": "TaskConnection", - "kind": "LinkedField", - "name": "tasks", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "TaskEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Task", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - (v3/*: any*/), - (v5/*: any*/), - (v4/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "timeEstimate", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "deadline", - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "People", - "kind": "LinkedField", - "name": "assignedTo", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "Measure", - "kind": "LinkedField", - "name": "measure", - "plural": false, - "selections": [ - (v2/*: any*/) - ], - "storageKey": null - }, - (v13/*: any*/) - ], - "storageKey": null - }, - (v15/*: any*/) - ], - "storageKey": null - }, - (v18/*: any*/), - (v19/*: any*/) - ], - "storageKey": "tasks(first:100)" - }, - { - "alias": null, - "args": (v14/*: any*/), - "filters": null, - "handle": "connection", - "key": "Measure__tasks", - "kind": "LinkedHandle", - "name": "tasks" - }, - { - "alias": null, - "args": (v20/*: any*/), + "args": (v18/*: any*/), "concreteType": "ControlConnection", "kind": "LinkedField", "name": "controls", @@ -541,22 +425,22 @@ return { ], "storageKey": null }, - (v13/*: any*/) + (v12/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v21/*: any*/), - (v19/*: any*/) + (v19/*: any*/), + (v17/*: any*/) ], "storageKey": "controls(first:20)" }, { "alias": null, - "args": (v20/*: any*/), + "args": (v18/*: any*/), "filters": [ "orderBy", "filter" @@ -568,7 +452,7 @@ return { }, { "alias": null, - "args": (v22/*: any*/), + "args": (v20/*: any*/), "concreteType": "EvidenceConnection", "kind": "LinkedField", "name": "evidences", @@ -638,22 +522,22 @@ return { "name": "createdAt", "storageKey": null }, - (v13/*: any*/) + (v12/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v21/*: any*/), - (v19/*: any*/) + (v19/*: any*/), + (v17/*: any*/) ], "storageKey": "evidences(first:50)" }, { "alias": null, - "args": (v22/*: any*/), + "args": (v20/*: any*/), "filters": [ "orderBy" ], @@ -672,16 +556,16 @@ return { ] }, "params": { - "cacheID": "d20815cee9ca8dec2e09fcefdd54a68a", + "cacheID": "fb510405c5934dbaf802c61c3ee8a547", "id": null, "metadata": {}, "name": "MeasureGraphNodeQuery", "operationKind": "query", - "text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n tasksInfos: tasks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureTasksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureTasksTabFragment on Measure {\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n" + "text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" } }; })(); -(node as any).hash = "34931b0c662961228537121bf18bab4e"; +(node as any).hash = "fea0fb9de2bebb764995341906e0de58"; export default node; diff --git a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx index 1e2673a19..27c2f5c9e 100644 --- a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -461,7 +461,7 @@ export default function DocumentDetailPage(props: Props) { ]} />
- {isDraft && ( + {isDraft && isAuthorized("Document", "publishDocumentVersion") && (
) } @@ -616,7 +616,7 @@ export default function DocumentDetailPage(props: Props) { /> ) : ( - setIsEditingOwner(true)}> + setIsEditingOwner(true)} canEdit={isAuthorized("Document", "updateDocument")}> {currentVersion.owner?.fullName} @@ -643,7 +643,7 @@ export default function DocumentDetailPage(props: Props) { ) : ( - setIsEditingType(true)}> + setIsEditingType(true)} canEdit={isAuthorized("Document", "updateDocument")}>
{getDocumentTypeLabel(__, document.documentType)}
@@ -671,6 +671,7 @@ export default function DocumentDetailPage(props: Props) { ) : ( setIsEditingClassification(true)} + canEdit={isAuthorized("Document", "updateDocument")} >
{getDocumentClassificationLabel(__, currentVersion.classification)} @@ -739,14 +740,16 @@ function EditablePropertyContent({ function ReadOnlyPropertyContent({ children, onEdit, + canEdit = true, }: { children: React.ReactNode; onEdit: () => void; + canEdit?: boolean; }) { return (
{children} -
); } diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx index d44d81c5c..f59fe7cbd 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx @@ -178,6 +178,22 @@ export default function FrameworkControlPage({ queryRef }: Props) { const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation); const [deleteControl] = useMutation(deleteControlMutation); + const canLinkMeasure = isAuthorized("Control", "createControlMeasureMapping"); + const canUnlinkMeasure = isAuthorized("Control", "deleteControlMeasureMapping"); + const measuresReadOnly = !canLinkMeasure && !canUnlinkMeasure; + + const canLinkDocument = isAuthorized("Control", "createControlDocumentMapping"); + const canUnlinkDocument = isAuthorized("Control", "deleteControlDocumentMapping"); + const documentsReadOnly = !canLinkDocument && !canUnlinkDocument; + + const canLinkAudit = isAuthorized("Control", "createControlAuditMapping"); + const canUnlinkAudit = isAuthorized("Control", "deleteControlAuditMapping"); + const auditsReadOnly = !canLinkAudit && !canUnlinkAudit; + + const canLinkSnapshot = isAuthorized("Control", "createControlSnapshotMapping"); + const canUnlinkSnapshot = isAuthorized("Control", "deleteControlSnapshotMapping"); + const snapshotsReadOnly = !canLinkSnapshot && !canUnlinkSnapshot; + const withErrorHandling = ( mutationFn: (config: UseMutationConfig) => void, errorMessage: string @@ -285,6 +301,7 @@ export default function FrameworkControlPage({ queryRef }: Props) { onAttach={withErrorHandling(attachMeasure, __("Failed to link measure"))} onDetach={withErrorHandling(detachMeasure, __("Failed to unlink measure"))} disabled={isAttachingMeasure || isDetachingMeasure} + readOnly={measuresReadOnly} />
@@ -296,6 +313,7 @@ export default function FrameworkControlPage({ queryRef }: Props) { onAttach={withErrorHandling(attachDocument, __("Failed to link document"))} onDetach={withErrorHandling(detachDocument, __("Failed to unlink document"))} disabled={isAttachingDocument || isDetachingDocument} + readOnly={documentsReadOnly} />
@@ -307,6 +325,7 @@ export default function FrameworkControlPage({ queryRef }: Props) { onAttach={withErrorHandling(attachAudit, __("Failed to link audit"))} onDetach={withErrorHandling(detachAudit, __("Failed to unlink audit"))} disabled={isAttachingAudit || isDetachingAudit} + readOnly={auditsReadOnly} />
@@ -318,6 +337,7 @@ export default function FrameworkControlPage({ queryRef }: Props) { onAttach={withErrorHandling(attachSnapshot, __("Failed to link snapshot"))} onDetach={withErrorHandling(detachSnapshot, __("Failed to unlink snapshot"))} disabled={isAttachingSnapshot || isDetachingSnapshot} + readOnly={snapshotsReadOnly} />
diff --git a/apps/console/src/pages/organizations/measures/MeasureDetailPage.tsx b/apps/console/src/pages/organizations/measures/MeasureDetailPage.tsx index 43ca233df..e58270ee6 100644 --- a/apps/console/src/pages/organizations/measures/MeasureDetailPage.tsx +++ b/apps/console/src/pages/organizations/measures/MeasureDetailPage.tsx @@ -26,10 +26,13 @@ import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge"; import { useTranslate } from "@probo/i18n"; import { ConnectionHandler, + graphql, type PreloadedQuery, + useLazyLoadQuery, usePreloadedQuery, } from "react-relay"; import type { MeasureGraphNodeQuery } from "/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql"; +import type { MeasureDetailPageTasksCountQuery } from "./__generated__/MeasureDetailPageTasksCountQuery.graphql"; import { MeasureConnectionKey, measureNodeQuery, @@ -43,9 +46,30 @@ import { sprintf, } from "@probo/helpers"; import MeasureFormDialog from "./dialog/MeasureFormDialog"; -import { use } from "react"; +import { Suspense, use } from "react"; import { PermissionsContext } from "/providers/PermissionsContext"; +const tasksCountQuery = graphql` + query MeasureDetailPageTasksCountQuery($measureId: ID!) { + node(id: $measureId) { + ... on Measure { + tasks(first: 0) { + totalCount + } + } + } + } +`; + +function TasksCountBadge({ measureId }: { measureId: string }) { + const data = useLazyLoadQuery( + tasksCountQuery, + { measureId } + ); + const count = data.node?.tasks?.totalCount ?? 0; + return {count}; +} + type Props = { queryRef: PreloadedQuery; }; @@ -67,7 +91,7 @@ export default function MeasureDetailPage(props: Props) { ); } - const tasksCount = measure.tasksInfos?.totalCount ?? 0; + const canViewTasks = isAuthorized("Measure", "listTasks"); const evidencesCount = measure.evidencesInfos?.totalCount ?? 0; const controlsCount = measure.controlsInfos?.totalCount ?? 0; const risksCount = measure.risksInfos?.totalCount ?? 0; @@ -160,13 +184,13 @@ export default function MeasureDetailPage(props: Props) { )} - - {isAuthorized("Measure", "deleteMeasure") && ( + {isAuthorized("Measure", "deleteMeasure") && ( + {__("Delete")} - )} - + + )} @@ -177,13 +201,17 @@ export default function MeasureDetailPage(props: Props) { {__("Evidences")} {evidencesCount} - - - {__("Tasks")} - {tasksCount} - + {canViewTasks && ( + + + {__("Tasks")} + -}> + + + + )} diff --git a/apps/console/src/pages/organizations/measures/__generated__/MeasureDetailPageTasksCountQuery.graphql.ts b/apps/console/src/pages/organizations/measures/__generated__/MeasureDetailPageTasksCountQuery.graphql.ts new file mode 100644 index 000000000..51e021349 --- /dev/null +++ b/apps/console/src/pages/organizations/measures/__generated__/MeasureDetailPageTasksCountQuery.graphql.ts @@ -0,0 +1,143 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type MeasureDetailPageTasksCountQuery$variables = { + measureId: string; +}; +export type MeasureDetailPageTasksCountQuery$data = { + readonly node: { + readonly tasks?: { + readonly totalCount: number; + }; + }; +}; +export type MeasureDetailPageTasksCountQuery = { + response: MeasureDetailPageTasksCountQuery$data; + variables: MeasureDetailPageTasksCountQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "measureId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "measureId" + } +], +v2 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 0 + } + ], + "concreteType": "TaskConnection", + "kind": "LinkedField", + "name": "tasks", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + } + ], + "storageKey": "tasks(first:0)" + } + ], + "type": "Measure", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MeasureDetailPageTasksCountQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MeasureDetailPageTasksCountQuery", + "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*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "36a1d5cf32d07ed5d76e5e64e7f19005", + "id": null, + "metadata": {}, + "name": "MeasureDetailPageTasksCountQuery", + "operationKind": "query", + "text": "query MeasureDetailPageTasksCountQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n tasks(first: 0) {\n totalCount\n }\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f0a8dde53529b525f3fe9aa35dee9e8a"; + +export default node; diff --git a/apps/console/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx b/apps/console/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx index a480691a4..ecab24d7d 100644 --- a/apps/console/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx +++ b/apps/console/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx @@ -3,6 +3,8 @@ import { useOutletContext } from "react-router"; import { LinkedControlsCard } from "/components/controls/LinkedControlsCard"; import type { MeasureControlsTabFragment$key } from "./__generated__/MeasureControlsTabFragment.graphql"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const controlsFragment = graphql` fragment MeasureControlsTabFragment on Measure @@ -69,6 +71,11 @@ export default function MeasureControlsTab() { const [data, refetch] = useRefetchableFragment(controlsFragment, measure); const connectionId = data.controls.__id; const controls = data.controls?.edges?.map((edge) => edge.node) ?? []; + const { isAuthorized } = use(PermissionsContext); + + const canLinkControl = isAuthorized("Control", "createControlMeasureMapping"); + const canUnlinkControl = isAuthorized("Control", "deleteControlMeasureMapping"); + const readOnly = !canLinkControl && !canUnlinkControl; const incrementOptions = { id: data.id, @@ -99,6 +106,7 @@ export default function MeasureControlsTab() { params={{ measureId: data.id }} connectionId={connectionId} refetch={refetch} + readOnly={readOnly} /> ); } diff --git a/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx b/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx index ac6ba9689..8001803af 100644 --- a/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx +++ b/apps/console/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx @@ -29,10 +29,11 @@ import { fileSize, fileType, sprintf, formatDate } from "@probo/helpers"; import { EvidencePreviewDialog } from "../dialog/EvidencePreviewDialog"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { CreateEvidenceDialog } from "../dialog/CreateEvidenceDialog"; -import { useState } from "react"; +import { use, useState } from "react"; import { EvidenceDownloadDialog } from "../dialog/EvidenceDownloadDialog"; import { updateStoreCounter } from "/hooks/useMutationWithIncrement"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const evidencesFragment = graphql` fragment MeasureEvidencesTabFragment on Measure @@ -107,6 +108,10 @@ export default function MeasureEvidencesTab() { const organizationId = useOrganizationId(); const dialogRef = useDialogRef(); const isSnapshotMode = Boolean(snapshotId); + const { isAuthorized } = use(PermissionsContext); + + const canAddEvidence = isAuthorized("Measure", "uploadMeasureEvidence"); + const canDeleteEvidence = isAuthorized("Evidence", "deleteEvidence"); usePageTitle(measure.name + " - " + __("Evidences")); @@ -131,10 +136,11 @@ export default function MeasureEvidencesTab() { organizationId={organizationId} connectionId={connectionId} hideActions={isSnapshotMode} + canDelete={canDeleteEvidence} snapshotId={snapshotId} /> ))} - {!isSnapshotMode && ( + {!isSnapshotMode && canAddEvidence && ( dialogRef.current?.open()} @@ -158,7 +164,7 @@ export default function MeasureEvidencesTab() { filename={evidence.file?.fileName || ""} /> )} - {!isSnapshotMode && ( + {!isSnapshotMode && canAddEvidence && ( {__("Download")} - - {__("Delete")} - + {props.canDelete && ( + + {__("Delete")} + + )} )} diff --git a/apps/console/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx b/apps/console/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx index ee8c1e63f..66215c846 100644 --- a/apps/console/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx +++ b/apps/console/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx @@ -3,6 +3,8 @@ import type { MeasureRisksTabFragment$key } from "./__generated__/MeasureRisksTa import { useOutletContext } from "react-router"; import { LinkedRisksCard } from "/components/risks/LinkedRisksCard"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const risksFragment = graphql` fragment MeasureRisksTabFragment on Measure { @@ -53,6 +55,11 @@ export default function MeasureRisksTab() { const data = useFragment(risksFragment, measure); const connectionId = data.risks.__id; const risks = data.risks?.edges?.map((edge) => edge.node) ?? []; + const { isAuthorized } = use(PermissionsContext); + + const canLinkRisk = isAuthorized("Risk", "createRiskMeasureMapping"); + const canUnlinkRisk = isAuthorized("Risk", "deleteRiskMeasureMapping"); + const readOnly = !canLinkRisk && !canUnlinkRisk; const incrementOptions = { id: data.id, @@ -82,6 +89,7 @@ export default function MeasureRisksTab() { onDetach={detachRisk} params={{ measureId: data.id }} connectionId={connectionId} + readOnly={readOnly} /> ); } diff --git a/apps/console/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx b/apps/console/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx index b44d0e4e0..480cab491 100644 --- a/apps/console/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx +++ b/apps/console/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx @@ -1,26 +1,31 @@ import { graphql } from "relay-runtime"; -import type { MeasureTasksTabFragment$key } from "./__generated__/MeasureTasksTabFragment.graphql"; +import type { MeasureTasksTabQuery } from "./__generated__/MeasureTasksTabQuery.graphql"; import { useOutletContext } from "react-router"; -import { useFragment } from "react-relay"; +import { useLazyLoadQuery } from "react-relay"; import TasksCard from "/components/tasks/TasksCard"; import { Button, IconPlusLarge } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import TaskFormDialog from "/components/tasks/TaskFormDialog"; -export const tasksFragment = graphql` - fragment MeasureTasksTabFragment on Measure { - tasks(first: 100) @connection(key: "Measure__tasks") { - __id - edges { - node { - id - name - state - description - ...TaskFormDialogFragment - assignedTo { - id - fullName +const tasksQuery = graphql` + query MeasureTasksTabQuery($measureId: ID!) { + node(id: $measureId) { + ... on Measure { + id + tasks(first: 100) @connection(key: "Measure__tasks") { + __id + edges { + node { + id + name + state + description + ...TaskFormDialogFragment + assignedTo { + id + fullName + } + } } } } @@ -31,11 +36,17 @@ export const tasksFragment = graphql` export default function MeasureTasksTab() { const { __ } = useTranslate(); const { measure } = useOutletContext<{ - measure: MeasureTasksTabFragment$key & { id: string }; + measure: { id: string }; }>(); - const data = useFragment(tasksFragment, measure); - const connectionId = data.tasks.__id; - const tasks = data.tasks?.edges?.map((edge) => edge.node) ?? []; + const data = useLazyLoadQuery(tasksQuery, { + measureId: measure.id, + }); + const node = data.node; + if (!node || !node.tasks) { + return null; + } + const connectionId = node.tasks.__id; + const tasks = node.tasks.edges?.map((edge) => edge.node) ?? []; return (
diff --git a/apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabFragment.graphql.ts b/apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabFragment.graphql.ts deleted file mode 100644 index c2dd2582e..000000000 --- a/apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabFragment.graphql.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ReaderFragment } from 'relay-runtime'; -export type TaskState = "DONE" | "TODO"; -import { FragmentRefs } from "relay-runtime"; -export type MeasureTasksTabFragment$data = { - readonly tasks: { - readonly __id: string; - readonly edges: ReadonlyArray<{ - readonly node: { - readonly assignedTo: { - readonly fullName: string; - readonly id: string; - } | null | undefined; - readonly description: string | null | undefined; - readonly id: string; - readonly name: string; - readonly state: TaskState; - readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">; - }; - }>; - }; - readonly " $fragmentType": "MeasureTasksTabFragment"; -}; -export type MeasureTasksTabFragment$key = { - readonly " $data"?: MeasureTasksTabFragment$data; - readonly " $fragmentSpreads": FragmentRefs<"MeasureTasksTabFragment">; -}; - -const node: ReaderFragment = (function(){ -var v0 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}; -return { - "argumentDefinitions": [], - "kind": "Fragment", - "metadata": { - "connection": [ - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "tasks" - ] - } - ] - }, - "name": "MeasureTasksTabFragment", - "selections": [ - { - "alias": "tasks", - "args": null, - "concreteType": "TaskConnection", - "kind": "LinkedField", - "name": "__Measure__tasks_connection", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "TaskEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Task", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "state", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "description", - "storageKey": null - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "TaskFormDialogFragment" - }, - { - "alias": null, - "args": null, - "concreteType": "People", - "kind": "LinkedField", - "name": "assignedTo", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "fullName", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - } - ], - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "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 - }, - { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] - } - ], - "storageKey": null - } - ], - "type": "Measure", - "abstractKey": null -}; -})(); - -(node as any).hash = "40d3211a52860f68da7c89ba9123af61"; - -export default node; diff --git a/apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabQuery.graphql.ts b/apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabQuery.graphql.ts new file mode 100644 index 000000000..222a269dd --- /dev/null +++ b/apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabQuery.graphql.ts @@ -0,0 +1,370 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type TaskState = "DONE" | "TODO"; +export type MeasureTasksTabQuery$variables = { + measureId: string; +}; +export type MeasureTasksTabQuery$data = { + readonly node: { + readonly id?: string; + readonly tasks?: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly assignedTo: { + readonly fullName: string; + readonly id: string; + } | null | undefined; + readonly description: string | null | undefined; + readonly id: string; + readonly name: string; + readonly state: TaskState; + readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">; + }; + }>; + }; + }; +}; +export type MeasureTasksTabQuery = { + response: MeasureTasksTabQuery$data; + variables: MeasureTasksTabQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "measureId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "measureId" + } +], +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": "state", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "assignedTo", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v9 = { + "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 +}, +v10 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v11 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MeasureTasksTabQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + { + "alias": "tasks", + "args": null, + "concreteType": "TaskConnection", + "kind": "LinkedField", + "name": "__Measure__tasks_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TaskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Task", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "TaskFormDialogFragment" + }, + (v6/*: any*/), + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/), + (v10/*: any*/) + ], + "storageKey": null + } + ], + "type": "Measure", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MeasureTasksTabQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v7/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v11/*: any*/), + "concreteType": "TaskConnection", + "kind": "LinkedField", + "name": "tasks", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TaskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Task", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "timeEstimate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deadline", + "storageKey": null + }, + (v6/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "measure", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/), + (v10/*: any*/) + ], + "storageKey": "tasks(first:100)" + }, + { + "alias": null, + "args": (v11/*: any*/), + "filters": null, + "handle": "connection", + "key": "Measure__tasks", + "kind": "LinkedHandle", + "name": "tasks" + } + ], + "type": "Measure", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "bc71d01128d96026a1e630b96e649409", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "node", + "tasks" + ] + } + ] + }, + "name": "MeasureTasksTabQuery", + "operationKind": "query", + "text": "query MeasureTasksTabQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "12445eb032af3bc00fea9edce6f427e2"; + +export default node; diff --git a/apps/console/src/pages/organizations/people/tabs/PeopleRoleTab.tsx b/apps/console/src/pages/organizations/people/tabs/PeopleRoleTab.tsx index cde1c7cf9..cda2f53af 100644 --- a/apps/console/src/pages/organizations/people/tabs/PeopleRoleTab.tsx +++ b/apps/console/src/pages/organizations/people/tabs/PeopleRoleTab.tsx @@ -1,6 +1,6 @@ import { useTranslate } from "@probo/i18n"; import { Button, Card, IconCheckmark1 } from "@probo/ui"; -import type { PropsWithChildren } from "react"; +import { use, type PropsWithChildren } from "react"; import z from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { ControlledField } from "/components/form/ControlledField"; @@ -11,6 +11,7 @@ import { useOutletContext } from "react-router"; import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql"; +import { PermissionsContext } from "/providers/PermissionsContext"; const schema = z.object({ kind: z.enum(peopleRoles), @@ -21,6 +22,8 @@ export default function PeopleRoleTab() { people: PeopleGraphNodeQuery$data["node"]; }>(); const { __ } = useTranslate(); + const { isAuthorized } = use(PermissionsContext); + const canUpdatePeople = isAuthorized("People", "updatePeople"); const { control, formState, handleSubmit, reset } = useFormWithSchema( schema, { @@ -65,6 +68,7 @@ export default function PeopleRoleTab() { name="kind" type="select" label={__("Role")} + disabled={!canUpdatePeople} > {getRoles(__).map((role) => (
-
- {formState.isDirty && ( - - )} -
+ {canUpdatePeople && ( +
+ {formState.isDirty && ( + + )} +
+ )} ); } diff --git a/apps/console/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx b/apps/console/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx index fd6be1863..1579f3593 100644 --- a/apps/console/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx +++ b/apps/console/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx @@ -3,6 +3,8 @@ import { useOutletContext } from "react-router"; import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard"; import type { RiskDocumentsTabFragment$key } from "./__generated__/RiskDocumentsTabFragment.graphql"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const documentsFragment = graphql` fragment RiskDocumentsTabFragment on Risk { @@ -53,6 +55,11 @@ export default function RiskDocumentsTab() { const data = useFragment(documentsFragment, risk); const connectionId = data.documents.__id; const documents = data.documents?.edges?.map((edge) => edge.node) ?? []; + const { isAuthorized } = use(PermissionsContext); + + const canLinkDocument = isAuthorized("Risk", "createRiskDocumentMapping"); + const canUnlinkDocument = isAuthorized("Risk", "deleteRiskDocumentMapping"); + const readOnly = !canLinkDocument && !canUnlinkDocument; const incrementOptions = { id: data.id, @@ -82,6 +89,7 @@ export default function RiskDocumentsTab() { onDetach={detachDocument} params={{ riskId: data.id }} connectionId={connectionId} + readOnly={readOnly} /> ); } diff --git a/apps/console/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx b/apps/console/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx index 0c576f6aa..c8d59d08e 100644 --- a/apps/console/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx +++ b/apps/console/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx @@ -3,6 +3,8 @@ import type { RiskMeasuresTabFragment$key } from "./__generated__/RiskMeasuresTa import { useOutletContext } from "react-router"; import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const measuresFragment = graphql` fragment RiskMeasuresTabFragment on Risk { @@ -53,6 +55,12 @@ export default function RiskMeasuresTab() { const data = useFragment(measuresFragment, risk); const connectionId = data.measures.__id; const measures = data.measures?.edges?.map((edge) => edge.node) ?? []; + const { isAuthorized } = use(PermissionsContext); + + const canLinkMeasure = isAuthorized("Risk", "createRiskMeasureMapping"); + const canUnlinkMeasure = isAuthorized("Risk", "deleteRiskMeasureMapping"); + const readOnly = !canLinkMeasure && !canUnlinkMeasure; + const incrementOptions = { id: data.id, node: "measures(first:0)", @@ -81,6 +89,7 @@ export default function RiskMeasuresTab() { onDetach={detachMeasure} params={{ riskId: data.id }} connectionId={connectionId} + readOnly={readOnly} /> ); } diff --git a/apps/console/src/pages/organizations/risks/tabs/RiskObligationsTab.tsx b/apps/console/src/pages/organizations/risks/tabs/RiskObligationsTab.tsx index 243792547..5164ae377 100644 --- a/apps/console/src/pages/organizations/risks/tabs/RiskObligationsTab.tsx +++ b/apps/console/src/pages/organizations/risks/tabs/RiskObligationsTab.tsx @@ -3,6 +3,8 @@ import type { RiskObligationsTabFragment$key } from "./__generated__/RiskObligat import { useOutletContext } from "react-router"; import { LinkedObligationsCard } from "/components/obligations/LinkedObligationsCard"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const obligationsFragment = graphql` fragment RiskObligationsTabFragment on Risk { @@ -53,6 +55,12 @@ export default function RiskObligationsTab() { const data = useFragment(obligationsFragment, risk); const connectionId = data.obligations.__id; const obligations = data.obligations?.edges?.map((edge) => edge.node) ?? []; + const { isAuthorized } = use(PermissionsContext); + + const canLinkObligation = isAuthorized("Risk", "createRiskObligationMapping"); + const canUnlinkObligation = isAuthorized("Risk", "deleteRiskObligationMapping"); + const readOnly = !canLinkObligation && !canUnlinkObligation; + const incrementOptions = { id: data.id, node: "obligations(first:0)", @@ -82,6 +90,7 @@ export default function RiskObligationsTab() { params={{ riskId: data.id }} connectionId={connectionId} variant="table" + readOnly={readOnly} /> ); } diff --git a/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx b/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx index be5f6e810..e0d79e9c4 100644 --- a/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx +++ b/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx @@ -535,6 +535,7 @@ function MembershipRowContent(props: { {availableRoles.includes("OWNER") && } {availableRoles.includes("ADMIN") && } {availableRoles.includes("VIEWER") && } + {availableRoles.includes("AUDITOR") && } {availableRoles.includes("EMPLOYEE") && } @@ -549,6 +550,9 @@ function MembershipRowContent(props: { {selectedRole === "VIEWER" && (

{__("Read-only access")}

)} + {selectedRole === "AUDITOR" && ( +

{__("Read-only access without settings, tasks and meetings")}

+ )} {selectedRole === "EMPLOYEE" && (

{__("Access to employee page")}

)} diff --git a/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts index 268773ae8..f672acfdb 100644 --- a/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts +++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<0bd95d20e79294c530610625c86e88d7>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -10,7 +10,7 @@ import { ReaderFragment } from 'relay-runtime'; export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING"; -export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; import { FragmentRefs } from "relay-runtime"; export type MembersSettingsTabInvitationsFragment$data = { readonly id: string; diff --git a/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts index 011bdb503..301af0ce3 100644 --- a/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts +++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8fcd99714c4bf7dba138fcb0de398a3a>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type UserAuthMethod = "PASSWORD" | "SAML"; import { FragmentRefs } from "relay-runtime"; export type MembersSettingsTabMembershipsFragment$data = { diff --git a/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_UpdateMembershipMutation.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_UpdateMembershipMutation.graphql.ts index 988ed6bbf..5cc2e4072 100644 --- a/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_UpdateMembershipMutation.graphql.ts +++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_UpdateMembershipMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<9aaf763355340403cfd0c9666b61be19>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; +export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type UpdateMembershipInput = { memberId: string; organizationId: string; diff --git a/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx b/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx index 7d34de1c7..3ee7a7305 100644 --- a/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx +++ b/apps/console/src/pages/organizations/vendors/VendorDetailPage.tsx @@ -91,11 +91,13 @@ export default function VendorDetailPage(props: Props) { {!isSnapshotMode && (
- - - + {isAuthorized("Vendor", "assessVendor") && ( + + + + )} {isAuthorized("Vendor", "deleteVendor") && ( (); const { __ } = useTranslate(); const { control, handleSubmit } = useVendorForm(vendor); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); + const { isAuthorized } = use(PermissionsContext); + const canUpdateVendor = isAuthorized("Vendor", "updateVendor"); return ( -
+ )} /> -
- -
+ {!isSnapshotMode && canUpdateVendor && ( +
+ +
+ )} ); } @@ -54,6 +62,7 @@ export default function VendorCertificationsTab() { type CertificationsProps = { value: string[]; onValueChange: (value: string[]) => void; + readOnly?: boolean; }; /** @@ -94,31 +103,37 @@ function Certifications(props: CertificationsProps) {
{certifications.map((certification) => ( - + {props.readOnly ? ( + {certification} + ) : ( + + )} ))}
))} - !props.value.includes(c) - )} - onAdd={addCertificate} - /> + {!props.readOnly && ( + !props.value.includes(c) + )} + onAdd={addCertificate} + /> + )} ); } diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx index b03e569fa..a8ac014ef 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx @@ -20,6 +20,8 @@ import type { VendorComplianceTabFragment_report$key } from "./__generated__/Ven import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { sprintf, fileSize, formatDate } from "@probo/helpers"; import { SortableTable, SortableTh } from "/components/SortableTable"; +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; export const complianceReportsFragment = graphql` fragment VendorComplianceTabFragment on Vendor @@ -103,6 +105,9 @@ export default function VendorComplianceTab() { const { __ } = useTranslate(); const { snapshotId } = useParams<{ snapshotId?: string }>(); const isSnapshotMode = Boolean(snapshotId); + const { isAuthorized } = use(PermissionsContext); + const canUploadReport = isAuthorized("Vendor", "uploadVendorComplianceReport"); + const canDeleteReport = isAuthorized("VendorComplianceReport", "deleteVendorComplianceReport"); usePageTitle(vendor.name + " - " + __("Compliance reports")); @@ -130,7 +135,7 @@ export default function VendorComplianceTab() { return (
- {!isSnapshotMode && ( + {!isSnapshotMode && canUploadReport && ( {__("Report date")}
- {!isSnapshotMode && } + {!isSnapshotMode && canDeleteReport && } @@ -158,6 +163,7 @@ export default function VendorComplianceTab() { reportKey={report} connectionId={connectionId} isSnapshotMode={isSnapshotMode} + canDelete={canDeleteReport} /> ))} @@ -170,6 +176,7 @@ type ReportRowProps = { reportKey: VendorComplianceTabFragment_report$key; connectionId: string; isSnapshotMode: boolean; + canDelete?: boolean; }; function ReportRow(props: ReportRowProps) { @@ -212,7 +219,7 @@ function ReportRow(props: ReportRowProps) { - {!props.isSnapshotMode && ( + {!props.isSnapshotMode && props.canDelete && ( - {!isSnapshotMode && } + {!isSnapshotMode && hasAnyAction && } @@ -135,12 +141,14 @@ export default function VendorContactsTab() { connectionId={connectionId} onEdit={setEditingContact} isSnapshotMode={isSnapshotMode} + canUpdate={canUpdateContact} + canDelete={canDeleteContact} /> ))} - {editingContact && !isSnapshotMode && ( + {editingContact && !isSnapshotMode && canUpdateContact && ( void; isSnapshotMode: boolean; + canUpdate?: boolean; + canDelete?: boolean; }; function ContactRow(props: ContactRowProps) { @@ -175,6 +185,7 @@ function ContactRow(props: ContactRowProps) { successMessage: __("Contact deleted successfully"), errorMessage: __("Failed to delete contact"), }); + const hasAnyAction = props.canUpdate || props.canDelete; const handleDelete = () => { confirm( @@ -226,28 +237,32 @@ function ContactRow(props: ContactRowProps) { )} - {!props.isSnapshotMode && ( + {!props.isSnapshotMode && hasAnyAction && ( )} diff --git a/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx b/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx index 7463443b8..b09bfca7a 100644 --- a/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx +++ b/apps/console/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx @@ -59,6 +59,13 @@ export default function VendorOverviewTab() { const { __ } = useTranslate(); const { isAuthorized } = use(PermissionsContext); + const canUpdateVendor = isAuthorized("Vendor", "updateVendor"); + const canUploadBAA = isAuthorized("Vendor", "uploadVendorBusinessAssociateAgreement"); + const canUpdateBAA = isAuthorized("Vendor", "updateVendorBusinessAssociateAgreement"); + const canDeleteBAA = isAuthorized("Vendor", "deleteVendorBusinessAssociateAgreement"); + const canUploadDPA = isAuthorized("Vendor", "uploadVendorDataPrivacyAgreement"); + const canUpdateDPA = isAuthorized("Vendor", "updateVendorDataPrivacyAgreement"); + const canDeleteDPA = isAuthorized("Vendor", "deleteVendorDataPrivacyAgreement"); const vendorCategories: { value: VendorCategory; label: string }[] = [ { value: "ANALYTICS", label: __("Analytics") }, { value: "CLOUD_MONITORING", label: __("Cloud Monitoring") }, @@ -128,8 +135,10 @@ export default function VendorOverviewTab() { usePageTitle(vendor.name + " - " + __("Overview")); + const isFormDisabled = isSubmitting || isSnapshotMode || !canUpdateVendor; + return ( - + {/* Vendor Details */}

{__("Vendor details")}

@@ -139,14 +148,14 @@ export default function VendorOverviewTab() { label={__("Name")} type="text" error={errors.name?.message} - disabled={isSubmitting || isSnapshotMode} + disabled={isFormDisabled} /> {vendorCategories.map((category) => (
@@ -193,7 +202,7 @@ export default function VendorOverviewTab() { @@ -208,7 +217,7 @@ export default function VendorOverviewTab() { name="businessOwnerId" label={__("Business owner")} error={errors.businessOwnerId?.message} - disabled={isSubmitting || isSnapshotMode} + disabled={isFormDisabled} optional={true} /> @@ -246,7 +255,7 @@ export default function VendorOverviewTab() { type="text" placeholder="https://..." variant="ghost" - disabled={isSubmitting || isSnapshotMode} + disabled={isFormDisabled} /> ))} @@ -287,30 +296,30 @@ export default function VendorOverviewTab() { > {__("Download PDF")} - {!isSnapshotMode && ( - <> - window.location.reload()} - > - - {!isSnapshotMode && ( - <> - window.location.reload()} - > - - {!isSnapshotMode && ( + {!isSnapshotMode && canCreateRiskAssessment && ( (null); + const { isAuthorized } = use(PermissionsContext); + const canCreateService = isAuthorized("Vendor", "createVendorService"); + const canUpdateService = isAuthorized("VendorService", "updateVendorService"); + const canDeleteService = isAuthorized("VendorService", "deleteVendorService"); + const hasAnyAction = canUpdateService || canDeleteService; usePageTitle(vendor.name + " - " + __("Services")); @@ -103,7 +109,7 @@ export default function VendorServicesTab() { title={__("Services")} description={__("Manage services provided by this vendor.")} > - {!isSnapshotMode && ( + {!isSnapshotMode && canCreateService && ( {__("Name")} - {!isSnapshotMode && } + {!isSnapshotMode && hasAnyAction && } @@ -129,12 +135,14 @@ export default function VendorServicesTab() { connectionId={connectionId} onEdit={setEditingService} isSnapshotMode={isSnapshotMode} + canUpdate={canUpdateService} + canDelete={canDeleteService} /> ))} - {editingService && !isSnapshotMode && ( + {editingService && !isSnapshotMode && canUpdateService && ( void; isSnapshotMode: boolean; + canUpdate?: boolean; + canDelete?: boolean; }; function ServiceRow(props: ServiceRowProps) { @@ -167,6 +177,7 @@ function ServiceRow(props: ServiceRowProps) { successMessage: __("Service deleted successfully"), errorMessage: __("Failed to delete service"), }); + const hasAnyAction = props.canUpdate || props.canDelete; const handleDelete = () => { confirm( @@ -194,26 +205,30 @@ function ServiceRow(props: ServiceRowProps) { - {!props.isSnapshotMode && ( + {!props.isSnapshotMode && hasAnyAction && ( )} diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index b38d4fb4f..630cb7a12 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -165,10 +165,14 @@ const routes = [ path: "", Component: () => { const { role } = use(PermissionsContext); - if (role === Role.EMPLOYEE) { - return ; + switch (role) { + case Role.EMPLOYEE: + return ; + case Role.AUDITOR: + return ; + default: + return ; } - return ; }, }, { diff --git a/packages/helpers/src/roles.ts b/packages/helpers/src/roles.ts index c17e01fb8..c2bfdeb36 100644 --- a/packages/helpers/src/roles.ts +++ b/packages/helpers/src/roles.ts @@ -2,6 +2,7 @@ export const Role = { OWNER: "OWNER", ADMIN: "ADMIN", VIEWER: "VIEWER", + AUDITOR: "AUDITOR", EMPLOYEE: "EMPLOYEE", } as const @@ -9,11 +10,11 @@ export type Role = (typeof Role)[keyof typeof Role]; export function getAssignableRoles(currentRole: Role): Role[] { if (currentRole === Role.OWNER) { - return [Role.OWNER, Role.ADMIN, Role.VIEWER, Role.EMPLOYEE]; + return [Role.OWNER, Role.ADMIN, Role.VIEWER, Role.AUDITOR, Role.EMPLOYEE]; } if (currentRole === Role.ADMIN) { - return [Role.ADMIN, Role.VIEWER, Role.EMPLOYEE]; + return [Role.ADMIN, Role.VIEWER, Role.AUDITOR, Role.EMPLOYEE]; } return []; diff --git a/pkg/authz/permissions.go b/pkg/authz/permissions.go index d23beba90..8419a2199 100644 --- a/pkg/authz/permissions.go +++ b/pkg/authz/permissions.go @@ -31,6 +31,7 @@ const ( RoleAdmin Role = "ADMIN" RoleEmployee Role = "EMPLOYEE" RoleViewer Role = "VIEWER" + RoleAuditor Role = "AUDITOR" RoleFull Role = "FULL" ) @@ -257,33 +258,32 @@ const ( ) var ( - AllRoles = []Role{RoleOwner, RoleAdmin, RoleEmployee, RoleViewer, RoleFull} - NonEmployeeRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull} - EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull} + AllRoles = []Role{RoleOwner, RoleAdmin, RoleEmployee, RoleViewer, RoleAuditor, RoleFull} + + EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull} + + CoreRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull} + NonEmployeeRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, RoleFull} + InternalRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleEmployee, RoleFull} ) var Permissions = map[uint16]map[Action][]Role{ coredata.OrganizationEntityType: { - ActionGet: AllRoles, - ActionListSignableDocuments: AllRoles, - ActionGetLogoUrl: AllRoles, + ActionGet: AllRoles, + ActionGetLogoUrl: AllRoles, + + ActionListSignableDocuments: InternalRoles, ActionListDocuments: NonEmployeeRoles, ActionGetHorizontalLogoUrl: NonEmployeeRoles, - ActionMemberships: NonEmployeeRoles, ActionPeoples: NonEmployeeRoles, ActionTotalCount: NonEmployeeRoles, - ActionListMembers: NonEmployeeRoles, - ActionListInvitations: NonEmployeeRoles, - ActionListSlackConnections: NonEmployeeRoles, ActionListFrameworks: NonEmployeeRoles, ActionListControls: NonEmployeeRoles, ActionListVendors: NonEmployeeRoles, ActionListPeople: NonEmployeeRoles, - ActionListMeetings: NonEmployeeRoles, ActionListMeasures: NonEmployeeRoles, ActionListRisks: NonEmployeeRoles, - ActionListTasks: NonEmployeeRoles, ActionListAssets: NonEmployeeRoles, ActionListData: NonEmployeeRoles, ActionListAudits: NonEmployeeRoles, @@ -292,13 +292,20 @@ var Permissions = map[uint16]map[Action][]Role{ ActionListContinualImprovements: NonEmployeeRoles, ActionListProcessingActivities: NonEmployeeRoles, ActionListSnapshots: NonEmployeeRoles, - ActionListTrustCenterFiles: NonEmployeeRoles, - ActionGetTrustCenter: NonEmployeeRoles, - ActionGetCustomDomain: NonEmployeeRoles, - ActionListSAMLConfigurations: NonEmployeeRoles, ActionConfirmEmail: NonEmployeeRoles, ActionAcceptInvitation: NonEmployeeRoles, + ActionListTrustCenterFiles: CoreRoles, + ActionGetTrustCenter: CoreRoles, + ActionMemberships: CoreRoles, + ActionListMembers: CoreRoles, + ActionListInvitations: CoreRoles, + ActionListSlackConnections: CoreRoles, + ActionGetCustomDomain: CoreRoles, + ActionListSAMLConfigurations: CoreRoles, + ActionListMeetings: CoreRoles, + ActionListTasks: CoreRoles, + ActionUpdateOrganization: EditRoles, ActionDeleteOrganizationHorizontalLogo: EditRoles, ActionCreateTrustCenter: EditRoles, @@ -336,11 +343,11 @@ var Permissions = map[uint16]map[Action][]Role{ ActionDeleteOrganization: {RoleOwner}, }, coredata.TrustCenterEntityType: { - ActionGet: NonEmployeeRoles, - ActionGetNdaFileUrl: NonEmployeeRoles, - ActionGetOrganization: NonEmployeeRoles, - ActionListAccesses: NonEmployeeRoles, - ActionListReferences: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionGetNdaFileUrl: CoreRoles, + ActionGetOrganization: CoreRoles, + ActionListAccesses: CoreRoles, + ActionListReferences: CoreRoles, ActionUpdateTrustCenter: EditRoles, ActionUploadTrustCenterNDA: EditRoles, @@ -349,26 +356,26 @@ var Permissions = map[uint16]map[Action][]Role{ ActionCreateTrustCenterReference: EditRoles, }, coredata.TrustCenterAccessEntityType: { - ActionGet: NonEmployeeRoles, - ActionActiveCount: NonEmployeeRoles, - ActionPendingRequestCount: NonEmployeeRoles, - ActionAvailableDocumentAccesses: NonEmployeeRoles, - ActionGetTrustCenterFile: NonEmployeeRoles, - ActionGetReport: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionActiveCount: CoreRoles, + ActionPendingRequestCount: CoreRoles, + ActionAvailableDocumentAccesses: CoreRoles, + ActionGetTrustCenterFile: CoreRoles, + ActionGetReport: CoreRoles, ActionUpdateTrustCenterAccess: EditRoles, ActionDeleteTrustCenterAccess: EditRoles, }, coredata.TrustCenterReferenceEntityType: { - ActionGet: NonEmployeeRoles, - ActionGetLogoUrl: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionGetLogoUrl: CoreRoles, ActionUpdateTrustCenterReference: EditRoles, ActionDeleteTrustCenterReference: EditRoles, }, coredata.TrustCenterFileEntityType: { - ActionGet: NonEmployeeRoles, - ActionGetFileUrl: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionGetFileUrl: CoreRoles, ActionUpdateTrustCenterFile: EditRoles, ActionGetTrustCenterFile: EditRoles, @@ -487,9 +494,10 @@ var Permissions = map[uint16]map[Action][]Role{ ActionDeleteControlSnapshotMapping: EditRoles, }, coredata.MeasureEntityType: { + ActionListTasks: CoreRoles, + ActionGet: NonEmployeeRoles, ActionListEvidences: NonEmployeeRoles, - ActionListTasks: NonEmployeeRoles, ActionListRisks: NonEmployeeRoles, ActionListControls: NonEmployeeRoles, ActionTotalCount: NonEmployeeRoles, @@ -499,11 +507,11 @@ var Permissions = map[uint16]map[Action][]Role{ ActionUploadMeasureEvidence: EditRoles, }, coredata.TaskEntityType: { - ActionGet: NonEmployeeRoles, - ActionGetAssignedTo: NonEmployeeRoles, - ActionGetOrganization: NonEmployeeRoles, - ActionGetMeasure: NonEmployeeRoles, - ActionListEvidences: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionGetAssignedTo: CoreRoles, + ActionGetOrganization: CoreRoles, + ActionGetMeasure: CoreRoles, + ActionListEvidences: CoreRoles, ActionUpdateTask: EditRoles, ActionDeleteTask: EditRoles, @@ -513,7 +521,7 @@ var Permissions = map[uint16]map[Action][]Role{ coredata.EvidenceEntityType: { ActionGet: NonEmployeeRoles, ActionGetFile: NonEmployeeRoles, - ActionGetTask: NonEmployeeRoles, + ActionGetTask: CoreRoles, ActionGetMeasure: NonEmployeeRoles, ActionDeleteEvidence: EditRoles, @@ -521,7 +529,8 @@ var Permissions = map[uint16]map[Action][]Role{ coredata.DocumentEntityType: { ActionListSignableDocumentVersion: AllRoles, ActionGetSigned: AllRoles, - ActionGetSignableDocument: AllRoles, + + ActionGetSignableDocument: InternalRoles, ActionGet: NonEmployeeRoles, ActionGetOwner: NonEmployeeRoles, @@ -689,14 +698,14 @@ var Permissions = map[uint16]map[Action][]Role{ ActionDownloadUrl: NonEmployeeRoles, }, coredata.TrustCenterDocumentAccessEntityType: { - ActionGet: NonEmployeeRoles, - ActionReport: NonEmployeeRoles, - ActionTrustCenterFile: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionReport: CoreRoles, + ActionTrustCenterFile: CoreRoles, }, coredata.MeetingEntityType: { - ActionGet: NonEmployeeRoles, - ActionGetOrganization: NonEmployeeRoles, - ActionTotalCount: NonEmployeeRoles, + ActionGet: CoreRoles, + ActionGetOrganization: CoreRoles, + ActionTotalCount: CoreRoles, ActionUpdateMeeting: EditRoles, ActionDeleteMeeting: EditRoles, diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 53862c35c..ee5c725c3 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -220,7 +220,7 @@ var entityRegistry = map[uint16]EntityInfo{ Table: "trust_center_references", }, TrustCenterDocumentAccessEntityType: { - Model: "", + Model: "TrustCenterDocumentAccess", Table: "trust_center_document_accesses", }, CustomDomainEntityType: { diff --git a/pkg/coredata/member_role.go b/pkg/coredata/member_role.go index a03cb5bd1..b96cd966e 100644 --- a/pkg/coredata/member_role.go +++ b/pkg/coredata/member_role.go @@ -26,6 +26,7 @@ const ( MembershipRoleAdmin MembershipRole = "ADMIN" MembershipRoleEmployee MembershipRole = "EMPLOYEE" MembershipRoleViewer MembershipRole = "VIEWER" + MembershipRoleAuditor MembershipRole = "AUDITOR" ) func (r MembershipRole) String() string { @@ -52,6 +53,8 @@ func (r *MembershipRole) Scan(value any) error { *r = MembershipRoleEmployee case "VIEWER": *r = MembershipRoleViewer + case "AUDITOR": + *r = MembershipRoleAuditor default: return fmt.Errorf("invalid MembershipRole value: %q", s) } diff --git a/pkg/coredata/migrations/20251210T133440Z.sql b/pkg/coredata/migrations/20251210T133440Z.sql new file mode 100644 index 000000000..c52b6afd1 --- /dev/null +++ b/pkg/coredata/migrations/20251210T133440Z.sql @@ -0,0 +1 @@ +ALTER TYPE authz_role ADD VALUE 'AUDITOR'; diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index efb15c25c..cc480fdd9 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -38,6 +38,7 @@ enum Role { OWNER ADMIN VIEWER + AUDITOR FULL } @@ -98,6 +99,7 @@ enum MembershipRole EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") + AUDITOR @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor") } enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") { diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 7f937f92f..7b0e2fa13 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -9592,6 +9592,7 @@ enum Role { OWNER ADMIN VIEWER + AUDITOR FULL } @@ -9652,6 +9653,7 @@ enum MembershipRole EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") + AUDITOR @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor") } enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") { @@ -87915,12 +87917,14 @@ var ( "ADMIN": coredata.MembershipRoleAdmin, "EMPLOYEE": coredata.MembershipRoleEmployee, "VIEWER": coredata.MembershipRoleViewer, + "AUDITOR": coredata.MembershipRoleAuditor, } marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole = map[coredata.MembershipRole]string{ coredata.MembershipRoleOwner: "OWNER", coredata.MembershipRoleAdmin: "ADMIN", coredata.MembershipRoleEmployee: "EMPLOYEE", coredata.MembershipRoleViewer: "VIEWER", + coredata.MembershipRoleAuditor: "AUDITOR", } ) diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 18feb593c..d02972d94 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -2537,22 +2537,24 @@ type Viewer struct { type Role string const ( - RoleOwner Role = "OWNER" - RoleAdmin Role = "ADMIN" - RoleViewer Role = "VIEWER" - RoleFull Role = "FULL" + RoleOwner Role = "OWNER" + RoleAdmin Role = "ADMIN" + RoleViewer Role = "VIEWER" + RoleAuditor Role = "AUDITOR" + RoleFull Role = "FULL" ) var AllRole = []Role{ RoleOwner, RoleAdmin, RoleViewer, + RoleAuditor, RoleFull, } func (e Role) IsValid() bool { switch e { - case RoleOwner, RoleAdmin, RoleViewer, RoleFull: + case RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, RoleFull: return true } return false
{__("Type")}{__("Description")}{__("Created")}
+ {__("No snapshots linked")}
{formatDate(snapshot.createdAt)} - - + +
{__("Valid until")} {__("File size")}{__("Actions")}{__("Actions")}
{formatDate(report.reportDate)} {formatDate(report.validUntil)} {fileSize(__, report.file?.size)} (null); + const { isAuthorized } = use(PermissionsContext); + const canCreateContact = isAuthorized("Vendor", "createVendorContact"); + const canUpdateContact = isAuthorized("VendorContact", "updateVendorContact"); + const canDeleteContact = isAuthorized("VendorContact", "deleteVendorContact"); + const hasAnyAction = canUpdateContact || canDeleteContact; usePageTitle(vendor.name + " - " + __("Contacts")); @@ -107,7 +113,7 @@ export default function VendorContactsTab() { title={__("Contacts")} description={__("Manage vendor contacts and their information.")} > - {!isSnapshotMode && ( + {!isSnapshotMode && canCreateContact && ( {__("Email")} {__("Phone")} {__("Role")}{__("Actions")}{__("Actions")}
{contact.role || __("—")} - props.onEdit({ - id: contact.id, - fullName: contact.fullName, - email: contact.email, - phone: contact.phone, - role: contact.role, - })} - > - {__("Edit")} - - - {__("Delete")} - + {props.canUpdate && ( + props.onEdit({ + id: contact.id, + fullName: contact.fullName, + email: contact.email, + phone: contact.phone, + role: contact.role, + })} + > + {__("Edit")} + + )} + {props.canDelete && ( + + {__("Delete")} + + )}
{__("Description")}{__("Actions")}{__("Actions")}
{service.name} {service.description || __("—")} - props.onEdit({ - id: service.id, - name: service.name, - description: service.description, - })} - > - {__("Edit")} - - - {__("Delete")} - + {props.canUpdate && ( + props.onEdit({ + id: service.id, + name: service.name, + description: service.description, + })} + > + {__("Edit")} + + )} + {props.canDelete && ( + + {__("Delete")} + + )}