From faf1ce0eb9a543ae32d8b23de0fefe47c645be1f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Fri, 23 May 2025 21:56:42 +0200 Subject: [PATCH] Add alias for imports Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- apps/console2/package.json | 3 +- apps/console2/src/assets/react.svg | 1 - .../console2/src/components/SortableTable.tsx | 4 +- .../components/risks/MeasureLinkDialog.tsx | 76 +++-- ...MeasureLinkDialogCreateMutation.graphql.ts | 177 +++++++--- ...MeasureLinkDialogDetachMutation.graphql.ts | 133 ++++++++ apps/console2/src/hooks/graph/RiskGraph.ts | 2 - .../__generated__/RiskGraphQuery.graphql.ts | 313 ------------------ .../organizations/risks/FormRiskDialog.tsx | 16 +- .../organizations/risks/RiskDetailPage.tsx | 6 +- .../pages/organizations/risks/RisksPage.tsx | 15 +- .../risks/tabs/RiskMeasuresTab.tsx | 104 ++++-- .../vendors/CreateVendorDialog.tsx | 4 +- .../organizations/vendors/VendorsPage.tsx | 14 +- apps/console2/src/routes.tsx | 18 +- apps/console2/src/routes/riskRoutes.ts | 20 +- apps/console2/tsconfig.app.json | 4 + apps/console2/vite.config.ts | 12 +- packages/helpers/src/array.ts | 2 +- packages/helpers/src/risk.ts | 2 +- packages/ui/src/Atoms/Select/Select.tsx | 5 +- packages/ui/src/Atoms/Table/Table.tsx | 13 +- packages/ui/src/Molecules/Badge/RiskBadge.tsx | 10 +- 23 files changed, 482 insertions(+), 472 deletions(-) delete mode 100644 apps/console2/src/assets/react.svg create mode 100644 apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts delete mode 100644 apps/console2/src/hooks/graph/__generated__/RiskGraphQuery.graphql.ts diff --git a/apps/console2/package.json b/apps/console2/package.json index 7b85cb746..a1ad45b18 100644 --- a/apps/console2/package.json +++ b/apps/console2/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "relay": "npx relay-compiler", - "build": "vite build", + "build": "tsc -b && vite build", "lint": "eslint .", "check": "tsc --noEmit -p tsconfig.app.json", "preview": "vite preview" @@ -36,6 +36,7 @@ "devDependencies": { "@eslint/js": "^9.25.0", "@tailwindcss/vite": "^4.1.7", + "@types/node": "^22.15.21", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@types/react-relay": "^18.2.1", diff --git a/apps/console2/src/assets/react.svg b/apps/console2/src/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/apps/console2/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/console2/src/components/SortableTable.tsx b/apps/console2/src/components/SortableTable.tsx index b1eebc4f8..2dd0d4140 100644 --- a/apps/console2/src/components/SortableTable.tsx +++ b/apps/console2/src/components/SortableTable.tsx @@ -18,7 +18,7 @@ const SortableContext = createContext({ direction: "DESC", field: "CREATED_AT", }, - onOrderChange: (order: any) => {}, + onOrderChange: (() => {}) as (order: Order) => void, }); const defaultOrder = { @@ -26,7 +26,7 @@ const defaultOrder = { field: "CREATED_AT", } as Order; -export function SortableTable({ +export function SortableTable({ refetch, ...props }: ComponentProps & { diff --git a/apps/console2/src/components/risks/MeasureLinkDialog.tsx b/apps/console2/src/components/risks/MeasureLinkDialog.tsx index 75567bff5..412f11eed 100644 --- a/apps/console2/src/components/risks/MeasureLinkDialog.tsx +++ b/apps/console2/src/components/risks/MeasureLinkDialog.tsx @@ -8,19 +8,19 @@ import { IconPlusLarge, IconTrashCan, Input, + Option, + Select, Spinner, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { Suspense, useMemo, useState, type ReactNode } from "react"; import { graphql } from "relay-runtime"; -import { useLazyLoadQuery } from "react-relay"; +import { useLazyLoadQuery, useMutation } from "react-relay"; import type { MeasureLinkDialogQuery, MeasureLinkDialogQuery$data, } from "./__generated__/MeasureLinkDialogQuery.graphql"; -import type { NodeOf } from "../../types"; -import { useMutationWithToasts } from "../../hooks/useMutationWithToasts"; -import { useToggle } from "@probo/hooks"; +import type { NodeOf } from "/types"; const measuresQuery = graphql` query MeasureLinkDialogQuery($organizationId: ID!) { @@ -46,9 +46,29 @@ const measuresQuery = graphql` const attachMeasureMutation = graphql` mutation MeasureLinkDialogCreateMutation( $input: CreateRiskMeasureMappingInput! + $connections: [ID!]! ) { createRiskMeasureMapping(input: $input) { - success + measureEdge @prependEdge(connections: $connections) { + node { + id + name + description + category + state + } + } + } + } +`; + +export const detachMeasureMutation = graphql` + mutation MeasureLinkDialogDetachMutation( + $input: DeleteRiskMeasureMappingInput! + $connections: [ID!]! + ) { + deleteRiskMeasureMapping(input: $input) { + deletedMeasureId @deleteEdge(connections: $connections) } } `; @@ -58,6 +78,7 @@ type Props = { organizationId: string; connectionId: string; riskId: string; + linkedIds?: Set; }; export function MeasureLinkDialog({ trigger, ...props }: Props) { @@ -84,16 +105,23 @@ function MeasureLinkDialogContent(props: Omit) { }); const { __ } = useTranslate(); const [search, setSearch] = useState(""); + const [category, setCategory] = useState(null); const measures = data.organization?.measures?.edges?.map((edge) => edge.node) ?? []; const filteredMeasures = useMemo(() => { return measures.filter( (measure) => - measure.name.toLowerCase().includes(search.toLowerCase()) || - measure.description?.toLowerCase().includes(search.toLowerCase()) + (category === null || measure.category === category) && + (measure.name.toLowerCase().includes(search.toLowerCase()) || + measure.description?.toLowerCase().includes(search.toLowerCase())) ); - }, [measures, search]); + }, [measures, search, category]); + + const categories = useMemo( + () => Array.from(new Set(measures.map((m) => m.category))), + [measures] + ); return ( <> @@ -103,6 +131,18 @@ function MeasureLinkDialogContent(props: Omit) { placeholder={__("Search measures...")} onValueChange={setSearch} /> +
{filteredMeasures.map((measure) => ( @@ -120,26 +160,22 @@ type RowProps = { } & Omit; function MeasureRow(props: RowProps) { - const [isLinked, toggleLinked] = useToggle(false); + const isLinked = props.linkedIds?.has(props.measure.id) ?? false; const { __ } = useTranslate(); - const [attachMeasure, isFetching] = useMutationWithToasts( - attachMeasureMutation, - { - successMessage: __("Measure linked successfully"), - errorMessage: __("Failed to link measure"), - } - ); + const [attachMeasure, isFetchingAttach] = useMutation(attachMeasureMutation); + const [detachMeasure, isFetchingDetach] = useMutation(detachMeasureMutation); + + const isFetching = isFetchingAttach || isFetchingDetach; const onClick = () => { - attachMeasure({ + const action = isLinked ? detachMeasure : attachMeasure; + action({ variables: { input: { riskId: props.riskId, measureId: props.measure.id, }, - }, - onSuccess() { - toggleLinked(); + connections: [props.connectionId], }, }); }; diff --git a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts b/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts index 855bf49c9..23d412e47 100644 --- a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts +++ b/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<391c4db090caa19c60582c18d426764a>> * @lightSyntaxTransform * @nogrep */ @@ -9,16 +9,26 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; +export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED" | "%future added value"; export type CreateRiskMeasureMappingInput = { measureId: string; riskId: string; }; export type MeasureLinkDialogCreateMutation$variables = { + connections: ReadonlyArray; input: CreateRiskMeasureMappingInput; }; export type MeasureLinkDialogCreateMutation$data = { readonly createRiskMeasureMapping: { - readonly success: boolean; + readonly measureEdge: { + readonly node: { + readonly category: string; + readonly description: string; + readonly id: string; + readonly name: string; + readonly state: MeasureState; + }; + }; }; }; export type MeasureLinkDialogCreateMutation = { @@ -27,67 +37,156 @@ export type MeasureLinkDialogCreateMutation = { }; const node: ConcreteRequest = (function(){ -var v0 = [ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" + "kind": "Variable", + "name": "input", + "variableName": "input" } ], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "CreateRiskMeasureMappingPayload", - "kind": "LinkedField", - "name": "createRiskMeasureMapping", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "success", - "storageKey": null - } - ], - "storageKey": null - } -]; +v3 = { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "measureEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; return { "fragment": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], "kind": "Fragment", "metadata": null, "name": "MeasureLinkDialogCreateMutation", - "selections": (v1/*: any*/), + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateRiskMeasureMappingPayload", + "kind": "LinkedField", + "name": "createRiskMeasureMapping", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], "type": "Mutation", "abstractKey": null }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], "kind": "Operation", "name": "MeasureLinkDialogCreateMutation", - "selections": (v1/*: any*/) + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateRiskMeasureMappingPayload", + "kind": "LinkedField", + "name": "createRiskMeasureMapping", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "measureEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] }, "params": { - "cacheID": "f7117ee68c2d7b6c2ceca56c054a4892", + "cacheID": "309f3c90d91577e2f1c95ea69848ce5a", "id": null, "metadata": {}, "name": "MeasureLinkDialogCreateMutation", "operationKind": "mutation", - "text": "mutation MeasureLinkDialogCreateMutation(\n $input: CreateRiskMeasureMappingInput!\n) {\n createRiskMeasureMapping(input: $input) {\n success\n }\n}\n" + "text": "mutation MeasureLinkDialogCreateMutation(\n $input: CreateRiskMeasureMappingInput!\n) {\n createRiskMeasureMapping(input: $input) {\n measureEdge {\n node {\n id\n name\n description\n category\n state\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "b54abd7d4d4f88ff7d54bfa35121a015"; +(node as any).hash = "bd012f06e39d5a04f4c1a2d2026f497e"; export default node; diff --git a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts b/apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts new file mode 100644 index 000000000..3759cbd47 --- /dev/null +++ b/apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<<327cdb825c0029c21d8812b782ecf14f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteRiskMeasureMappingInput = { + measureId: string; + riskId: string; +}; +export type MeasureLinkDialogDetachMutation$variables = { + connections: ReadonlyArray; + input: DeleteRiskMeasureMappingInput; +}; +export type MeasureLinkDialogDetachMutation$data = { + readonly deleteRiskMeasureMapping: { + readonly deletedMeasureId: string; + }; +}; +export type MeasureLinkDialogDetachMutation = { + response: MeasureLinkDialogDetachMutation$data; + variables: MeasureLinkDialogDetachMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedMeasureId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "MeasureLinkDialogDetachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteRiskMeasureMappingPayload", + "kind": "LinkedField", + "name": "deleteRiskMeasureMapping", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "MeasureLinkDialogDetachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteRiskMeasureMappingPayload", + "kind": "LinkedField", + "name": "deleteRiskMeasureMapping", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedMeasureId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8a54f580a892f60f24c65ec927c7340c", + "id": null, + "metadata": {}, + "name": "MeasureLinkDialogDetachMutation", + "operationKind": "mutation", + "text": "mutation MeasureLinkDialogDetachMutation(\n $input: DeleteRiskMeasureMappingInput!\n) {\n deleteRiskMeasureMapping(input: $input) {\n deletedMeasureId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "e02a17e00e2c07881dcae17fef9513eb"; + +export default node; diff --git a/apps/console2/src/hooks/graph/RiskGraph.ts b/apps/console2/src/hooks/graph/RiskGraph.ts index c0f4dbbb3..fa7c48937 100644 --- a/apps/console2/src/hooks/graph/RiskGraph.ts +++ b/apps/console2/src/hooks/graph/RiskGraph.ts @@ -3,12 +3,10 @@ import { useTranslate } from "@probo/i18n"; import { useMutationWithToasts } from "../useMutationWithToasts.ts"; import type { RiskGraphDeleteMutation } from "./__generated__/RiskGraphDeleteMutation.graphql.ts"; import { - useLazyLoadQuery, usePreloadedQuery, useRefetchableFragment, type PreloadedQuery, } from "react-relay"; -import { useOrganizationId } from "../useOrganizationId.ts"; import type { RiskGraphListQuery } from "./__generated__/RiskGraphListQuery.graphql.ts"; import type { RiskGraphFragment$key } from "./__generated__/RiskGraphFragment.graphql.ts"; diff --git a/apps/console2/src/hooks/graph/__generated__/RiskGraphQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/RiskGraphQuery.graphql.ts deleted file mode 100644 index 5b254fc93..000000000 --- a/apps/console2/src/hooks/graph/__generated__/RiskGraphQuery.graphql.ts +++ /dev/null @@ -1,313 +0,0 @@ -/** - * @generated SignedSource<<25323fd3bf9201703c2f644bf654425d>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; -export type RiskGraphQuery$variables = { - organizationId: string; -}; -export type RiskGraphQuery$data = { - readonly organization: { - readonly id: string; - readonly " $fragmentSpreads": FragmentRefs<"RiskGraphFragment">; - }; -}; -export type RiskGraphQuery = { - response: RiskGraphQuery$data; - variables: RiskGraphQuery$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "organizationId" - } -], -v1 = [ - { - "kind": "Variable", - "name": "id", - "variableName": "organizationId" - } -], -v2 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}, -v3 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null -}, -v4 = [ - { - "kind": "Literal", - "name": "first", - "value": 50 - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "RiskGraphQuery", - "selections": [ - { - "alias": "organization", - "args": (v1/*: any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "args": null, - "kind": "FragmentSpread", - "name": "RiskGraphFragment" - } - ], - "storageKey": null - } - ], - "type": "Query", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "RiskGraphQuery", - "selections": [ - { - "alias": "organization", - "args": (v1/*: any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v3/*: any*/), - (v2/*: any*/), - { - "kind": "InlineFragment", - "selections": [ - { - "alias": null, - "args": (v4/*: any*/), - "concreteType": "RiskConnection", - "kind": "LinkedField", - "name": "risks", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "RiskEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Risk", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "category", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "treatment", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "inherentLikelihood", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "inherentImpact", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "residualLikelihood", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "residualImpact", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "inherentRiskScore", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "residualRiskScore", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "description", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "note", - "storageKey": null - }, - { - "alias": null, - "args": null, - "concreteType": "People", - "kind": "LinkedField", - "name": "owner", - "plural": false, - "selections": [ - (v2/*: any*/) - ], - "storageKey": null - }, - (v3/*: any*/) - ], - "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 - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasPreviousPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "startCursor", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "risks(first:50)" - }, - { - "alias": null, - "args": (v4/*: any*/), - "filters": [ - "orderBy" - ], - "handle": "connection", - "key": "RisksListQuery_risks", - "kind": "LinkedHandle", - "name": "risks" - } - ], - "type": "Organization", - "abstractKey": null - } - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "f522d32a3f55657d67c408ac7b1fe5dd", - "id": null, - "metadata": {}, - "name": "RiskGraphQuery", - "operationKind": "query", - "text": "query RiskGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...RiskGraphFragment\n }\n}\n\nfragment RiskGraphFragment on Organization {\n risks(first: 50) {\n edges {\n node {\n id\n name\n category\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n ...useRiskFormFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" - } -}; -})(); - -(node as any).hash = "eae4d0ae12b88751b50d61bb70bd55a4"; - -export default node; diff --git a/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx b/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx index 1911612e9..ff1c90358 100644 --- a/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx +++ b/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx @@ -15,22 +15,22 @@ import { Textarea, } from "@probo/ui"; import { useMemo, useState, type ReactNode } from "react"; -import { useFetchQuery } from "../../../hooks/useFetchQuery"; -import { ConnectionHandler, graphql } from "relay-runtime"; -import { PeopleSelect } from "../../../components/form/PeopleSelect"; -import { useOrganizationId } from "../../../hooks/useOrganizationId"; +import { useFetchQuery } from "/hooks/useFetchQuery"; +import { graphql } from "relay-runtime"; +import { PeopleSelect } from "/components/form/PeopleSelect"; +import { useOrganizationId } from "/hooks/useOrganizationId"; import { useToggle } from "@probo/hooks"; import { ControlledField, ControlledSelect, -} from "../../../components/form/ControlledField"; +} from "/components/form/ControlledField"; import { useRiskForm, type RiskForm, type RiskKey, -} from "../../../hooks/forms/useRiskForm"; +} from "/hooks/forms/useRiskForm"; import type { FieldErrors } from "react-hook-form"; -import { useMutationWithToasts } from "../../../hooks/useMutationWithToasts"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import type { FormRiskDialogMutation } from "./__generated__/FormRiskDialogMutation.graphql"; import type { FormRiskDialogUpdateRiskMutation } from "./__generated__/FormRiskDialogUpdateRiskMutation.graphql"; import { getRiskImpacts, getRiskLikelihoods } from "@probo/helpers"; @@ -125,7 +125,7 @@ export default function FormRiskDialog({ ...data, organizationId, }, - connections: [connection], + connections: [connection!], }, successMessage: __("Risk created successfully."), errorMessage: __("Failed to create risk. Please try again."), diff --git a/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx b/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx index 93c779160..68a6b9c32 100644 --- a/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx +++ b/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx @@ -21,13 +21,13 @@ import { ConnectionHandler } from "relay-runtime"; import { usePreloadedQuery, type PreloadedQuery } from "react-relay"; import FormRiskDialog from "./FormRiskDialog"; import { usePageTitle } from "@probo/hooks"; -import { useOrganizationId } from "../../../hooks/useOrganizationId"; +import { useOrganizationId } from "/hooks/useOrganizationId"; import { riskNodeQuery, RisksConnectionKey, useDeleteRiskMutation, -} from "../../../hooks/graph/RiskGraph"; -import type { RiskGraphNodeQuery } from "../../../hooks/graph/__generated__/RiskGraphNodeQuery.graphql"; +} from "/hooks/graph/RiskGraph"; +import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql"; type Props = { queryRef: PreloadedQuery; diff --git a/apps/console2/src/pages/organizations/risks/RisksPage.tsx b/apps/console2/src/pages/organizations/risks/RisksPage.tsx index 6cb646908..505287733 100644 --- a/apps/console2/src/pages/organizations/risks/RisksPage.tsx +++ b/apps/console2/src/pages/organizations/risks/RisksPage.tsx @@ -17,21 +17,18 @@ import { } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import FormRiskDialog from "./FormRiskDialog"; -import { useOrganizationId } from "../../../hooks/useOrganizationId"; +import { useOrganizationId } from "/hooks/useOrganizationId"; import { useState } from "react"; import { usePageTitle } from "@probo/hooks"; import { getTreatment, sprintf } from "@probo/helpers"; -import type { ItemOf } from "../../../types"; -import { - useDeleteRiskMutation, - useRisksQuery, -} from "../../../hooks/graph/RiskGraph"; -import { SortableTable, SortableTh } from "../../../components/SortableTable"; +import type { ItemOf } from "/types"; +import { useDeleteRiskMutation, useRisksQuery } from "/hooks/graph/RiskGraph"; +import { SortableTable, SortableTh } from "/components/SortableTable"; import type { PreloadedQuery } from "react-relay"; -import type { RiskGraphQuery } from "../../../hooks/graph/__generated__/RiskGraphQuery.graphql"; +import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphListQuery.graphql"; type Props = { - queryRef: PreloadedQuery; + queryRef: PreloadedQuery; }; export default function RisksPage(props: Props) { diff --git a/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx b/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx index 63fb06a6f..f425821e3 100644 --- a/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx +++ b/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx @@ -1,4 +1,4 @@ -import { graphql, useFragment } from "react-relay"; +import { graphql, useFragment, useMutation } from "react-relay"; import type { RiskMeasuresTabFragment$data, RiskMeasuresTabFragment$key, @@ -13,11 +13,16 @@ import { Td, Th, Tr, + IconTrashCan, } from "@probo/ui"; -import { MeasureLinkDialog } from "../../../../components/risks/MeasureLinkDialog"; -import { useOrganizationId } from "../../../../hooks/useOrganizationId"; +import { + detachMeasureMutation, + MeasureLinkDialog, +} from "/components/risks/MeasureLinkDialog"; +import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOutletContext } from "react-router"; -import type { NodeOf } from "../../../../types"; +import type { NodeOf } from "/types"; +import { useMemo } from "react"; const measuresFragment = graphql` fragment RiskMeasuresTabFragment on Risk { @@ -47,12 +52,17 @@ export default function RiskMeasuresTab() { const { __ } = useTranslate(); const connectionId = data.measures.__id; const measures = data.measures?.edges?.map((edge) => edge.node) ?? []; + const linkedIds = useMemo( + () => new Set(measures.map((measure) => measure.id)), + [measures] + ); if (measures.length === 0) { return (
{__("No measures associated with this risk.")} - - - {__("Measure")} - {__("Frameworks")} - {__("Risks")} - {__("Lead")} - {__("Status")} - - - - - {measures.map((measure) => ( - - ))} - - +
+ + {__("Link measure")} + + } + /> + + + + + + + + + {measures.map((measure) => ( + + ))} + +
{__("Measure")}
+
); } function MeasureRow({ measure, + riskId, + connectionId, }: { measure: NodeOf; + riskId: string; + connectionId: string; }) { + const { __ } = useTranslate(); + + const [detachMeasure, isFetching] = useMutation(detachMeasureMutation); + + const onClick = () => { + detachMeasure({ + variables: { + input: { + riskId: riskId, + measureId: measure.id, + }, + connections: [connectionId], + }, + }); + }; return ( {measure.name} - {measure.category} - {measure.createdAt} - {measure.state} - + + + ); } diff --git a/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx b/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx index 80080db42..aff3c53ee 100644 --- a/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx +++ b/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx @@ -8,10 +8,10 @@ import { Avatar, IconPlusLarge, } from "@probo/ui"; -import { useVendorSearch } from "../../../hooks/useVendorSearch"; +import { useVendorSearch } from "/hooks/useVendorSearch"; import { faviconUrl } from "@probo/helpers"; import type { Vendor } from "@probo/vendors"; -import { useCreateVendorMutation } from "../../../hooks/graph/VendorGraph"; +import { useCreateVendorMutation } from "/hooks/graph/VendorGraph"; import { type ReactNode, useState } from "react"; type Props = { diff --git a/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx b/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx index 48a16b9fe..203e9e1e1 100644 --- a/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx +++ b/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx @@ -20,15 +20,15 @@ import { useTranslate } from "@probo/i18n"; import { usePageTitle } from "@probo/hooks"; import { ConnectionHandler, graphql } from "relay-runtime"; import { useLazyLoadQuery } from "react-relay"; -import { useOrganizationId } from "../../../hooks/useOrganizationId"; +import { useOrganizationId } from "/hooks/useOrganizationId"; import type { VendorsPageQuery, VendorsPageQuery$data, } from "./__generated__/VendorsPageQuery.graphql"; import { faviconUrl, sprintf } from "@probo/helpers"; -import type { NodeOf } from "../../../types"; +import type { NodeOf } from "/types"; import { CreateVendorDialog } from "./CreateVendorDialog"; -import { useDeleteVendorMutation } from "../../../hooks/graph/VendorGraph"; +import { useDeleteVendorMutation } from "/hooks/graph/VendorGraph"; const vendorsQuery = graphql` query VendorsPageQuery($organizationId: ID!) { @@ -84,7 +84,7 @@ export default function VendorsPage() { onDelete(vendor.id)} > diff --git a/apps/console2/src/routes.tsx b/apps/console2/src/routes.tsx index 999756c3a..6f082b6dc 100644 --- a/apps/console2/src/routes.tsx +++ b/apps/console2/src/routes.tsx @@ -1,27 +1,15 @@ import { createBrowserRouter, - redirect, useLoaderData, useRouteError, type RouteObject, } from "react-router"; import { MainLayout } from "./layouts/MainLayout"; import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui"; -import { - Fragment, - lazy, - Suspense, - type FC, - type LazyExoticComponent, -} from "react"; -import { - relayEnvironment, - UnAuthenticatedError, -} from "./providers/RelayProviders"; -import { RisksPageSkeleton } from "./components/skeletons/RisksPageSkeleton.tsx"; +import { lazy, Suspense, type FC, type LazyExoticComponent } from "react"; +import { UnAuthenticatedError } from "./providers/RelayProviders"; import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx"; -import { loadQuery, type PreloadedQuery } from "react-relay"; -import { riskNodeQuery, risksQuery } from "./hooks/graph/RiskGraph.ts"; +import { type PreloadedQuery } from "react-relay"; import { useCleanup } from "./hooks/useDelayedEffect.ts"; import { riskRoutes } from "./routes/riskRoutes.ts"; diff --git a/apps/console2/src/routes/riskRoutes.ts b/apps/console2/src/routes/riskRoutes.ts index 740378cc6..fc7f7d428 100644 --- a/apps/console2/src/routes/riskRoutes.ts +++ b/apps/console2/src/routes/riskRoutes.ts @@ -1,10 +1,10 @@ import { Fragment, lazy } from "react"; import { loadQuery } from "react-relay"; -import { RisksPageSkeleton } from "../components/skeletons/RisksPageSkeleton.tsx"; -import type { AppRoute } from "../routes.tsx"; -import { relayEnvironment } from "../providers/RelayProviders"; -import { riskNodeQuery, risksQuery } from "../hooks/graph/RiskGraph"; -import { PageSkeleton } from "../components/skeletons/PageSkeleton.tsx"; +import { RisksPageSkeleton } from "/components/skeletons/RisksPageSkeleton.tsx"; +import type { AppRoute } from "/routes.tsx"; +import { relayEnvironment } from "/providers/RelayProviders"; +import { riskNodeQuery, risksQuery } from "/hooks/graph/RiskGraph"; +import { PageSkeleton } from "/components/skeletons/PageSkeleton.tsx"; import { redirect } from "react-router"; export const riskRoutes = [ @@ -13,16 +13,14 @@ export const riskRoutes = [ fallback: RisksPageSkeleton, queryLoader: ({ organizationId }) => loadQuery(relayEnvironment, risksQuery, { organizationId }), - Component: lazy(() => import("../pages/organizations/risks/RisksPage")), + Component: lazy(() => import("/pages/organizations/risks/RisksPage")), }, { path: "risks/:riskId", fallback: PageSkeleton, queryLoader: ({ riskId }) => loadQuery(relayEnvironment, riskNodeQuery, { riskId }), - Component: lazy( - () => import("../pages/organizations/risks/RiskDetailPage") - ), + Component: lazy(() => import("/pages/organizations/risks/RiskDetailPage")), children: [ { path: "", @@ -34,13 +32,13 @@ export const riskRoutes = [ { path: "overview", Component: lazy( - () => import("../pages/organizations/risks/tabs/RiskOverviewTab.tsx") + () => import("/pages/organizations/risks/tabs/RiskOverviewTab.tsx") ), }, { path: "measures", Component: lazy( - () => import("../pages/organizations/risks/tabs/RiskMeasuresTab.tsx") + () => import("/pages/organizations/risks/tabs/RiskMeasuresTab.tsx") ), }, ], diff --git a/apps/console2/tsconfig.app.json b/apps/console2/tsconfig.app.json index c9ccbd4c5..6b1da7159 100644 --- a/apps/console2/tsconfig.app.json +++ b/apps/console2/tsconfig.app.json @@ -14,6 +14,10 @@ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "/*": ["src/*"] + }, /* Linting */ "strict": true, diff --git a/apps/console2/vite.config.ts b/apps/console2/vite.config.ts index 282dbb459..42b9be8ea 100644 --- a/apps/console2/vite.config.ts +++ b/apps/console2/vite.config.ts @@ -1,14 +1,22 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; -import { resolve } from "node:path"; +import { fileURLToPath, URL } from "node:url"; // https://vite.dev/config/ export default defineConfig({ plugins: [react({ babel: { plugins: ["relay"] } }), tailwindcss()], resolve: { alias: { - "@": resolve(__dirname, "./src"), + "/type": fileURLToPath(new URL("./src/type.ts", import.meta.url)), + "/components": fileURLToPath( + new URL("./src/components", import.meta.url) + ), + "/hooks": fileURLToPath(new URL("./src/hooks", import.meta.url)), + "/layouts": fileURLToPath(new URL("./src/layouts", import.meta.url)), + "/pages": fileURLToPath(new URL("./src/pages", import.meta.url)), + "/routes": fileURLToPath(new URL("./src/routes", import.meta.url)), + "/providers": fileURLToPath(new URL("./src/providers", import.meta.url)), }, }, }); diff --git a/packages/helpers/src/array.ts b/packages/helpers/src/array.ts index eb8c4db43..2a27a9d2f 100644 --- a/packages/helpers/src/array.ts +++ b/packages/helpers/src/array.ts @@ -22,7 +22,7 @@ export function groupBy( /** * Check that a value is empty (null, undefined, empty string, empty array, empty object) */ -export function isEmpty(v: unknown) { +export function isEmpty(v: unknown): boolean { if (Array.isArray(v)) { return v.find((v) => !isEmpty(v)) === undefined; } diff --git a/packages/helpers/src/risk.ts b/packages/helpers/src/risk.ts index 5eec1cc2c..8e4821ac5 100644 --- a/packages/helpers/src/risk.ts +++ b/packages/helpers/src/risk.ts @@ -25,7 +25,7 @@ export function getRiskImpacts(__: Translator) { ]; } -export function getTreatment(__: Translator, treatment: string): string { +export function getTreatment(__: Translator, treatment?: string): string { switch (treatment) { case "MITIGATED": return __("Mitigate"); diff --git a/packages/ui/src/Atoms/Select/Select.tsx b/packages/ui/src/Atoms/Select/Select.tsx index 160f3a964..9818c8ec6 100644 --- a/packages/ui/src/Atoms/Select/Select.tsx +++ b/packages/ui/src/Atoms/Select/Select.tsx @@ -29,6 +29,7 @@ type Props = PropsWithChildren< variant?: "default" | "editor" | "dashed"; invalid?: boolean; disabled?: boolean; + className?: string; } & Omit, "onChange"> >; @@ -112,11 +113,11 @@ export function Select({ return ( - +
{valueNode ? ( - + {valueNode} ) : null} diff --git a/packages/ui/src/Atoms/Table/Table.tsx b/packages/ui/src/Atoms/Table/Table.tsx index 5d25a55d1..f90c454ba 100644 --- a/packages/ui/src/Atoms/Table/Table.tsx +++ b/packages/ui/src/Atoms/Table/Table.tsx @@ -19,8 +19,15 @@ export function Thead({ children }: PropsWithChildren) { ); } -export function Th({ children }: PropsWithChildren) { - return {children}; +export function Th({ + children, + className, +}: PropsWithChildren<{ className?: string }>) { + return ( + + {children} + + ); } const TrContext = createContext({} as { to?: string }); @@ -54,7 +61,7 @@ export function Td({ }: PropsWithChildren<{ noLink?: boolean }>) { const { to } = useContext(TrContext); if (!to || noLink) { - return {children}; + return {children}; } return ( diff --git a/packages/ui/src/Molecules/Badge/RiskBadge.tsx b/packages/ui/src/Molecules/Badge/RiskBadge.tsx index 1b20b601b..3478d5d05 100644 --- a/packages/ui/src/Molecules/Badge/RiskBadge.tsx +++ b/packages/ui/src/Molecules/Badge/RiskBadge.tsx @@ -8,12 +8,12 @@ type Props = { const badgeVariant = (level: string | number) => { if (typeof level === "number") { if (level >= 15) { - return "CRITICAL"; + level = "CRITICAL"; + } else if (level >= 8) { + level = "HIGH"; + } else { + level = "LOW"; } - if (level >= 8) { - return "HIGH"; - } - return "LOW"; } switch (level) { case "CRITICAL":