Add frameworks listing and import
Signed-off-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
7cb84d8ce8
commit
1870e050ce
BIN
apps/console2/public/logos/gdpr.png
Normal file
BIN
apps/console2/public/logos/gdpr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
apps/console2/public/logos/iso27001.png
Normal file
BIN
apps/console2/public/logos/iso27001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
apps/console2/public/logos/soc2.png
Normal file
BIN
apps/console2/public/logos/soc2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
apps/console2/public/logos/sox.png
Normal file
BIN
apps/console2/public/logos/sox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
75
apps/console2/src/hooks/graph/FrameworkGraph.ts
Normal file
75
apps/console2/src/hooks/graph/FrameworkGraph.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import { useMutationWithToasts } from "../useMutationWithToasts";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { sprintf } from "@probo/helpers";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { useConfirm } from "@probo/ui";
|
||||||
|
|
||||||
|
export const frameworksQuery = graphql`
|
||||||
|
query FrameworkGraphListQuery($organizationId: ID!) {
|
||||||
|
organization: node(id: $organizationId) {
|
||||||
|
... on Organization {
|
||||||
|
id
|
||||||
|
frameworks(first: 100)
|
||||||
|
@connection(key: "FrameworksListQuery_frameworks") {
|
||||||
|
__id
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
...FrameworksPageCardFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deleteFrameworkMutation = graphql`
|
||||||
|
mutation FrameworkGraphDeleteMutation(
|
||||||
|
$input: DeleteFrameworkInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
deleteFramework(input: $input) {
|
||||||
|
deletedFrameworkId @deleteEdge(connections: $connections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const useDeleteFrameworkMutation = (
|
||||||
|
framework: { id: string; name: string },
|
||||||
|
connectionId: string
|
||||||
|
) => {
|
||||||
|
const [commitDelete, isDeleting] = useMutationWithToasts(
|
||||||
|
deleteFrameworkMutation,
|
||||||
|
{
|
||||||
|
errorMessage: "Failed to delete framework",
|
||||||
|
successMessage: "Framework deleted successfully",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
confirm(
|
||||||
|
() => {
|
||||||
|
return commitDelete({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
frameworkId: framework.id!,
|
||||||
|
},
|
||||||
|
connections: [connectionId],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: sprintf(
|
||||||
|
__(
|
||||||
|
'This will permanently delete framework "%s". This action cannot be undone.'
|
||||||
|
),
|
||||||
|
framework.name
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [framework, connectionId, commitDelete]);
|
||||||
|
};
|
||||||
132
apps/console2/src/hooks/graph/__generated__/FrameworkGraphDeleteMutation.graphql.ts
generated
Normal file
132
apps/console2/src/hooks/graph/__generated__/FrameworkGraphDeleteMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<05d197ce59adad67cfb89dff4c502c58>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DeleteFrameworkInput = {
|
||||||
|
frameworkId: string;
|
||||||
|
};
|
||||||
|
export type FrameworkGraphDeleteMutation$variables = {
|
||||||
|
connections: ReadonlyArray<string>;
|
||||||
|
input: DeleteFrameworkInput;
|
||||||
|
};
|
||||||
|
export type FrameworkGraphDeleteMutation$data = {
|
||||||
|
readonly deleteFramework: {
|
||||||
|
readonly deletedFrameworkId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type FrameworkGraphDeleteMutation = {
|
||||||
|
response: FrameworkGraphDeleteMutation$data;
|
||||||
|
variables: FrameworkGraphDeleteMutation$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": "deletedFrameworkId",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "FrameworkGraphDeleteMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "DeleteFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "FrameworkGraphDeleteMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "DeleteFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "deleteEdge",
|
||||||
|
"key": "",
|
||||||
|
"kind": "ScalarHandle",
|
||||||
|
"name": "deletedFrameworkId",
|
||||||
|
"handleArgs": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "connections",
|
||||||
|
"variableName": "connections"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "761a391f497aa0e2e03d0db958089712",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "FrameworkGraphDeleteMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation FrameworkGraphDeleteMutation(\n $input: DeleteFrameworkInput!\n) {\n deleteFramework(input: $input) {\n deletedFrameworkId\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "6ebefeb003ce3a27bd134651e1b5c94b";
|
||||||
|
|
||||||
|
export default node;
|
||||||
302
apps/console2/src/hooks/graph/__generated__/FrameworkGraphListQuery.graphql.ts
generated
Normal file
302
apps/console2/src/hooks/graph/__generated__/FrameworkGraphListQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<a67eb2a88e39d7eaf96910e0318dd1bc>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type FrameworkGraphListQuery$variables = {
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type FrameworkGraphListQuery$data = {
|
||||||
|
readonly organization: {
|
||||||
|
readonly frameworks?: {
|
||||||
|
readonly __id: string;
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
readonly id?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type FrameworkGraphListQuery = {
|
||||||
|
response: FrameworkGraphListQuery$data;
|
||||||
|
variables: FrameworkGraphListQuery$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 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v5 = {
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
v6 = {
|
||||||
|
"kind": "ClientExtension",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__id",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
v7 = [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "FrameworkGraphListQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "organization",
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"kind": "InlineFragment",
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": "frameworks",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__FrameworksListQuery_frameworks_connection",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "FrameworksPageCardFragment"
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/),
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "FrameworkGraphListQuery",
|
||||||
|
"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": (v7/*: any*/),
|
||||||
|
"concreteType": "FrameworkConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworks",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"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": "description",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v4/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v5/*: any*/),
|
||||||
|
(v6/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": "frameworks(first:100)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v7/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "FrameworksListQuery_frameworks",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "frameworks"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Organization",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "b31afd68395ae800a18885bca3abfa1c",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {
|
||||||
|
"connection": [
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"cursor": null,
|
||||||
|
"direction": "forward",
|
||||||
|
"path": [
|
||||||
|
"organization",
|
||||||
|
"frameworks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "FrameworkGraphListQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query FrameworkGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n frameworks(first: 100) {\n edges {\n node {\n id\n ...FrameworksPageCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "4aaead183328e669aba942992db6f89c";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import { usePageTitle } from "@probo/hooks";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
ActionDropdown,
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
FileButton,
|
||||||
|
FrameworkSelector,
|
||||||
|
IconChevronDown,
|
||||||
|
IconPlusLarge,
|
||||||
|
IconTrashCan,
|
||||||
|
IconUpload,
|
||||||
|
PageHeader,
|
||||||
|
useDialogRef,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
useFragment,
|
||||||
|
usePreloadedQuery,
|
||||||
|
type PreloadedQuery,
|
||||||
|
} from "react-relay";
|
||||||
|
import type { FrameworkGraphListQuery } from "/hooks/graph/__generated__/FrameworkGraphListQuery.graphql";
|
||||||
|
import {
|
||||||
|
frameworksQuery,
|
||||||
|
useDeleteFrameworkMutation,
|
||||||
|
} from "/hooks/graph/FrameworkGraph";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql";
|
||||||
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
|
import { useState, type ChangeEventHandler } from "react";
|
||||||
|
import { availableFrameworks } from "@probo/helpers";
|
||||||
|
import { CreateFrameworkDialog } from "./dialogs/CreateFrameworkDialog";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queryRef: PreloadedQuery<FrameworkGraphListQuery>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importFrameworkMutation = graphql`
|
||||||
|
mutation FrameworksPageImportMutation(
|
||||||
|
$input: ImportFrameworkInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
importFramework(input: $input) {
|
||||||
|
frameworkEdge @prependEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
...FrameworksPageCardFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const availableLogos = new Map(
|
||||||
|
availableFrameworks.map((framework) => [framework.name, framework.logo])
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function FrameworksPage(props: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
usePageTitle(__("Frameworks"));
|
||||||
|
const data = usePreloadedQuery(frameworksQuery, props.queryRef);
|
||||||
|
const connectionId = data.organization.frameworks!.__id;
|
||||||
|
const frameworks =
|
||||||
|
data.organization.frameworks?.edges.map((edge) => edge.node) ?? [];
|
||||||
|
const [commitImport, isImporting] = useMutationWithToasts(
|
||||||
|
importFrameworkMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Framework imported successfully"),
|
||||||
|
errorMessage: __("Failed to import framework"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const [isUploading, setUploading] = useState(false);
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const importNamedFramework = async (name: string) => {
|
||||||
|
// For custom framework, open the form
|
||||||
|
if (name === "custom") {
|
||||||
|
console.log(name, dialogRef);
|
||||||
|
dialogRef.current?.open();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise load the JSON and send the file to the server
|
||||||
|
try {
|
||||||
|
setUploading(true);
|
||||||
|
const fileName = `${name}.json`;
|
||||||
|
const json = await fetch(`/data/frameworks/${fileName}`).then((res) =>
|
||||||
|
res.text()
|
||||||
|
);
|
||||||
|
const file = new File([json], fileName, {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
await importFile(file);
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importFile = (file: File) => {
|
||||||
|
return commitImport({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: data.organization.id!,
|
||||||
|
file: null,
|
||||||
|
},
|
||||||
|
connections: [connectionId],
|
||||||
|
},
|
||||||
|
uploadables: {
|
||||||
|
"input.file": file,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||||
|
const input = event.currentTarget;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importFile(file).finally(() => {
|
||||||
|
input.value = "";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = isUploading || isImporting;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<CreateFrameworkDialog
|
||||||
|
ref={dialogRef}
|
||||||
|
connectionId={connectionId}
|
||||||
|
organizationId={data.organization.id!}
|
||||||
|
/>
|
||||||
|
<PageHeader
|
||||||
|
title={__("Frameworks")}
|
||||||
|
description={__("Manage your compliance frameworks")}
|
||||||
|
>
|
||||||
|
<FileButton
|
||||||
|
variant="secondary"
|
||||||
|
icon={IconUpload}
|
||||||
|
onChange={handleUpload}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{__("Import")}
|
||||||
|
</FileButton>
|
||||||
|
<FrameworkSelector
|
||||||
|
onSelect={importNamedFramework}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{frameworks.map((framework) => (
|
||||||
|
<FrameworkCard
|
||||||
|
organizationId={data.organization.id!}
|
||||||
|
connectionId={connectionId}
|
||||||
|
key={framework.id}
|
||||||
|
framework={framework}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameworkCardFragment = graphql`
|
||||||
|
fragment FrameworksPageCardFragment on Framework {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type FrameworkCardProps = {
|
||||||
|
organizationId: string;
|
||||||
|
connectionId: string;
|
||||||
|
framework: FrameworksPageCardFragment$key;
|
||||||
|
};
|
||||||
|
|
||||||
|
function FrameworkCard(props: FrameworkCardProps) {
|
||||||
|
const framework = useFragment(frameworkCardFragment, props.framework);
|
||||||
|
const deleteFramework = useDeleteFrameworkMutation(
|
||||||
|
framework,
|
||||||
|
props.connectionId
|
||||||
|
);
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const logo = availableLogos.get(framework.name);
|
||||||
|
return (
|
||||||
|
<Card padded className="p-6 bg-white rounded shadow relative">
|
||||||
|
<div className="flex justify-between mb-3">
|
||||||
|
{logo ? (
|
||||||
|
<img src={logo} alt="" className="size-12" />
|
||||||
|
) : (
|
||||||
|
<Avatar name={framework.name} size="l" className="size-12" />
|
||||||
|
)}
|
||||||
|
<ActionDropdown className="z-10 relative">
|
||||||
|
<DropdownItem
|
||||||
|
icon={IconTrashCan}
|
||||||
|
onClick={deleteFramework}
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
{__("Delete")}
|
||||||
|
</DropdownItem>
|
||||||
|
</ActionDropdown>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-medium">
|
||||||
|
<Link
|
||||||
|
className="hover:underline after:absolute after:content-[''] after:inset-0"
|
||||||
|
to={`/organizations/${props.organizationId}/frameworks/${framework.id}`}
|
||||||
|
>
|
||||||
|
{framework.name}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-txt-secondary">{framework.description}</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageCardFragment.graphql.ts
generated
Normal file
58
apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageCardFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<ec6bd0c6fff930b82216d940e83d8ef0>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type FrameworksPageCardFragment$data = {
|
||||||
|
readonly description: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly " $fragmentType": "FrameworksPageCardFragment";
|
||||||
|
};
|
||||||
|
export type FrameworksPageCardFragment$key = {
|
||||||
|
readonly " $data"?: FrameworksPageCardFragment$data;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ReaderFragment = {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "FrameworksPageCardFragment",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Framework",
|
||||||
|
"abstractKey": null
|
||||||
|
};
|
||||||
|
|
||||||
|
(node as any).hash = "4481f380673963e72f88071031e37d14";
|
||||||
|
|
||||||
|
export default node;
|
||||||
202
apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageImportMutation.graphql.ts
generated
Normal file
202
apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageImportMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<9436ef052ea63a9785d82ec810e810f1>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type ImportFrameworkInput = {
|
||||||
|
file: any;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type FrameworksPageImportMutation$variables = {
|
||||||
|
connections: ReadonlyArray<string>;
|
||||||
|
input: ImportFrameworkInput;
|
||||||
|
};
|
||||||
|
export type FrameworksPageImportMutation$data = {
|
||||||
|
readonly importFramework: {
|
||||||
|
readonly frameworkEdge: {
|
||||||
|
readonly node: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type FrameworksPageImportMutation = {
|
||||||
|
response: FrameworksPageImportMutation$data;
|
||||||
|
variables: FrameworksPageImportMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "connections"
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
},
|
||||||
|
v2 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "FrameworksPageImportMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "ImportFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "importFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "FrameworksPageCardFragment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "FrameworksPageImportMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "ImportFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "importFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "prependEdge",
|
||||||
|
"key": "",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"handleArgs": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "connections",
|
||||||
|
"variableName": "connections"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "87a38788cb2e060657b552af69acb707",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "FrameworksPageImportMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation FrameworksPageImportMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "e2ff0f6d6780cf967a021a093542dab9";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
useDialogRef,
|
||||||
|
type DialogRef,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||||
|
|
||||||
|
const createFrameworkMutation = graphql`
|
||||||
|
mutation CreateFrameworkDialogMutation(
|
||||||
|
$input: CreateFrameworkInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createFramework(input: $input) {
|
||||||
|
frameworkEdge @prependEdge(connections: $connections) {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
...FrameworksPageCardFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
connectionId: string;
|
||||||
|
organizationId: string;
|
||||||
|
ref: DialogRef;
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1).max(255),
|
||||||
|
description: z.string().max(255).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function CreateFrameworkDialog(props: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
reset,
|
||||||
|
} = useFormWithSchema(schema, {});
|
||||||
|
const [commitCreate, isCreating] = useMutationWithToasts(
|
||||||
|
createFrameworkMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Framework created successfully"),
|
||||||
|
errorMessage: __("Failed to create framework"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
await commitCreate({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
...data,
|
||||||
|
organizationId: props.organizationId,
|
||||||
|
},
|
||||||
|
connections: [props.connectionId],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
reset();
|
||||||
|
props.ref.current?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
defaultOpen
|
||||||
|
ref={props.ref}
|
||||||
|
title={<Breadcrumb items={[__("Framework"), __("New Framework")]} />}
|
||||||
|
>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<DialogContent padded className="space-y-4">
|
||||||
|
<Input
|
||||||
|
{...register("name")}
|
||||||
|
variant="title"
|
||||||
|
required
|
||||||
|
placeholder={__("Framework title")}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
{...register("description")}
|
||||||
|
variant="ghost"
|
||||||
|
autogrow
|
||||||
|
placeholder={__("Add description")}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={isCreating}>
|
||||||
|
{__("Create framework")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<a91c1ee7ea4934d7d6e524f32462832a>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type CreateFrameworkInput = {
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type CreateFrameworkDialogMutation$variables = {
|
||||||
|
connections: ReadonlyArray<string>;
|
||||||
|
input: CreateFrameworkInput;
|
||||||
|
};
|
||||||
|
export type CreateFrameworkDialogMutation$data = {
|
||||||
|
readonly createFramework: {
|
||||||
|
readonly frameworkEdge: {
|
||||||
|
readonly node: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type CreateFrameworkDialogMutation = {
|
||||||
|
response: CreateFrameworkDialogMutation$data;
|
||||||
|
variables: CreateFrameworkDialogMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "connections"
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
},
|
||||||
|
v2 = [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v3 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "CreateFrameworkDialogMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "CreateFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "FrameworksPageCardFragment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "CreateFrameworkDialogMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "CreateFrameworkPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createFramework",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "FrameworkEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Framework",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "description",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "prependEdge",
|
||||||
|
"key": "",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "frameworkEdge",
|
||||||
|
"handleArgs": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "connections",
|
||||||
|
"variableName": "connections"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "d08bc5a14bbf8d131029da890bdab850",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "CreateFrameworkDialogMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation CreateFrameworkDialogMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "957d00afa55875c9a6879292e8fa7bb1";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
IconPlusLarge,
|
IconPlusLarge,
|
||||||
useConfirm,
|
useConfirm,
|
||||||
|
ActionDropdown,
|
||||||
|
DropdownItem,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import {
|
import {
|
||||||
useFragment,
|
useFragment,
|
||||||
@@ -157,14 +159,11 @@ function PolicyRow({
|
|||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
confirm(
|
confirm(
|
||||||
() =>
|
() =>
|
||||||
new Promise<void>((resolve) => {
|
|
||||||
deletePolicy({
|
deletePolicy({
|
||||||
variables: {
|
variables: {
|
||||||
input: { policyId: policy.id },
|
input: { policyId: policy.id },
|
||||||
connections: [connectionId],
|
connections: [connectionId],
|
||||||
},
|
},
|
||||||
onCompleted: () => resolve(),
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
message: sprintf(
|
message: sprintf(
|
||||||
@@ -178,7 +177,6 @@ function PolicyRow({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
@@ -215,14 +213,16 @@ function PolicyRow({
|
|||||||
{signedCount}/{signatures.length}
|
{signedCount}/{signatures.length}
|
||||||
</Td>
|
</Td>
|
||||||
<Td noLink width={50} className="text-end">
|
<Td noLink width={50} className="text-end">
|
||||||
<Button
|
<ActionDropdown>
|
||||||
icon={IconTrashCan}
|
<DropdownItem
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
icon={IconTrashCan}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={isDeleting}
|
>
|
||||||
/>
|
{__("Delete")}
|
||||||
|
</DropdownItem>
|
||||||
|
</ActionDropdown>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { policiesRoutes } from "./routes/policiesRoutes.ts";
|
|||||||
import { vendorRoutes } from "./routes/vendorRoutes.ts";
|
import { vendorRoutes } from "./routes/vendorRoutes.ts";
|
||||||
import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts";
|
import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts";
|
||||||
import { peopleRoutes } from "./routes/peopleRoutes.ts";
|
import { peopleRoutes } from "./routes/peopleRoutes.ts";
|
||||||
|
import { frameworkRoutes } from "./routes/frameworkRoutes.ts";
|
||||||
|
|
||||||
function ErrorBoundary() {
|
function ErrorBoundary() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
@@ -85,6 +86,7 @@ const routes = [
|
|||||||
...policiesRoutes,
|
...policiesRoutes,
|
||||||
...peopleRoutes,
|
...peopleRoutes,
|
||||||
...vendorRoutes,
|
...vendorRoutes,
|
||||||
|
...frameworkRoutes,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies AppRoute[];
|
] satisfies AppRoute[];
|
||||||
|
|||||||
18
apps/console2/src/routes/frameworkRoutes.ts
Normal file
18
apps/console2/src/routes/frameworkRoutes.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { loadQuery } from "react-relay";
|
||||||
|
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
|
||||||
|
import { relayEnvironment } from "/providers/RelayProviders";
|
||||||
|
import { frameworksQuery } from "/hooks/graph/FrameworkGraph";
|
||||||
|
import type { AppRoute } from "/routes";
|
||||||
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
export const frameworkRoutes = [
|
||||||
|
{
|
||||||
|
path: "frameworks",
|
||||||
|
fallback: PageSkeleton,
|
||||||
|
queryLoader: ({ organizationId }) =>
|
||||||
|
loadQuery(relayEnvironment, frameworksQuery, { organizationId }),
|
||||||
|
Component: lazy(
|
||||||
|
() => import("/pages/organizations/frameworks/FrameworksPage")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
] satisfies AppRoute[];
|
||||||
26
packages/helpers/src/frameworks.ts
Normal file
26
packages/helpers/src/frameworks.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export const availableFrameworks = [
|
||||||
|
{
|
||||||
|
id: "ISO27001-2022",
|
||||||
|
name: "ISO 27001 (2022)",
|
||||||
|
logo: "/logos/iso27001.png",
|
||||||
|
description: "Information security management systems",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SOC2",
|
||||||
|
name: "SOC 2",
|
||||||
|
logo: "/logos/soc2.png",
|
||||||
|
description: "System and Organization Controls 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "GDPR",
|
||||||
|
name: "GDPR",
|
||||||
|
logo: "/logos/gdpr.png",
|
||||||
|
description: "General Data Protection Regulation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SOX",
|
||||||
|
name: "SOX",
|
||||||
|
logo: "/logos/sox.png",
|
||||||
|
description: "Sarbanes-Oxley Act",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -12,3 +12,4 @@ export { randomInt } from "./number";
|
|||||||
export { getMeasureStateLabel, measureStates } from "./measure";
|
export { getMeasureStateLabel, measureStates } from "./measure";
|
||||||
export { getRole, getRoles, peopleRoles } from "./people";
|
export { getRole, getRoles, peopleRoles } from "./people";
|
||||||
export { certificationCategoryLabel, certifications } from "./certifications";
|
export { certificationCategoryLabel, certifications } from "./certifications";
|
||||||
|
export { availableFrameworks } from "./frameworks";
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
import clsx from "clsx";
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string;
|
name: string;
|
||||||
src?: string | null;
|
src?: string | null;
|
||||||
size?: "s" | "m" | "l" | "xl";
|
size?: "s" | "m" | "l" | "xl";
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Avatar({ name, src, size = "m" }: Props) {
|
const avatar = tv({
|
||||||
const className = clsx(
|
base: "bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center flex-none",
|
||||||
"bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center flex-none",
|
variants: {
|
||||||
size === "s" && "size-5 text-xss",
|
size: {
|
||||||
size === "m" && "size-6 text-xxs",
|
s: "size-5 text-xss",
|
||||||
size === "l" && "size-8 text-sm",
|
m: "size-6 text-xxs",
|
||||||
size === "xl" && "size-16 text-3xl",
|
l: "size-8 text-sm",
|
||||||
);
|
xl: "size-16 text-3xl",
|
||||||
if (src) {
|
},
|
||||||
return <img className={className} src={src} alt={name} />;
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "m",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function Avatar(props: Props) {
|
||||||
|
const className = avatar(props);
|
||||||
|
if (props.src) {
|
||||||
|
return <img className={className} src={props.src} alt={props.name} />;
|
||||||
}
|
}
|
||||||
return <div className={className}>{extractInitials(name ?? "")}</div>;
|
return <div className={className}>{extractInitials(props.name ?? "")}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractInitials(name: string) {
|
function extractInitials(name: string) {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function ActionDropdown(
|
|||||||
{...props}
|
{...props}
|
||||||
toggle={
|
toggle={
|
||||||
<Button
|
<Button
|
||||||
className="inline-flex"
|
className="inline-flex isolate z-5"
|
||||||
variant={props.variant ?? "tertiary"}
|
variant={props.variant ?? "tertiary"}
|
||||||
icon={IconDotGrid1x3Horizontal}
|
icon={IconDotGrid1x3Horizontal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,17 +37,22 @@ export const dialog = tv({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type DialogRef = RefObject<{
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
} | null>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
trigger?: ReactNode;
|
trigger?: ReactNode;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
ref?: RefObject<{ open: () => void; close: () => void } | null>;
|
ref?: DialogRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDialogRef = () => {
|
export const useDialogRef = (): DialogRef => {
|
||||||
return useRef<{ open: () => void; close: () => void } | null>(null);
|
return useRef(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FrameworkSelector } from "./FrameworkSelector";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Molecules/FrameworkSelector",
|
||||||
|
component: FrameworkSelector,
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<typeof FrameworkSelector>;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof FrameworkSelector>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Dropdown, DropdownItem } from "../../Atoms/Dropdown/Dropdown";
|
||||||
|
import { IconChevronDown, IconPlusLarge } from "../../Atoms/Icons";
|
||||||
|
import { Button } from "../../Atoms/Button/Button";
|
||||||
|
import { availableFrameworks } from "@probo/helpers";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
|
||||||
|
type Framework = (typeof availableFrameworks)[number];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
disabled?: boolean;
|
||||||
|
onSelect: (frameworkId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FrameworkSelector({ disabled, onSelect }: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
toggle={
|
||||||
|
<Button
|
||||||
|
icon={IconPlusLarge}
|
||||||
|
iconAfter={IconChevronDown}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{__("New framework")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FrameworkItem onClick={() => onSelect("custom")} />
|
||||||
|
{availableFrameworks.map((framework) => (
|
||||||
|
<FrameworkItem
|
||||||
|
key={framework.id}
|
||||||
|
framework={framework}
|
||||||
|
onClick={() => onSelect(framework.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FrameworkItem(props: { framework?: Framework; onClick: () => void }) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
if (!props.framework) {
|
||||||
|
return (
|
||||||
|
<DropdownItem onClick={props.onClick} className="">
|
||||||
|
<div className="rounded-full size-8 bg-highlight text-txt-primary flex items-center justify-center">
|
||||||
|
<IconPlusLarge size={16} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-[2px]">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{__("Custom framework")}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-txt-secondary">
|
||||||
|
{__("Start from scratch")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DropdownItem onClick={props.onClick} className="">
|
||||||
|
<img
|
||||||
|
src={props.framework.logo}
|
||||||
|
alt={props.framework.name}
|
||||||
|
className="size-8"
|
||||||
|
/>
|
||||||
|
<div className="space-y-[2px]">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{props.framework.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-txt-secondary">
|
||||||
|
{props.framework.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ export {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
useDialogRef,
|
useDialogRef,
|
||||||
|
type DialogRef,
|
||||||
} from "./Molecules/Dialog/Dialog";
|
} from "./Molecules/Dialog/Dialog";
|
||||||
export { useConfirm } from "./Molecules/Dialog/ConfirmDialog";
|
export { useConfirm } from "./Molecules/Dialog/ConfirmDialog";
|
||||||
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
|
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
|
||||||
@@ -58,6 +59,7 @@ export { Combobox, ComboboxItem } from "./Molecules/Combobox/Combobox";
|
|||||||
export { FileButton } from "./Molecules/FileButton/FileButton";
|
export { FileButton } from "./Molecules/FileButton/FileButton";
|
||||||
export { MeasureBadge } from "./Molecules/Badge/MeasureBadge";
|
export { MeasureBadge } from "./Molecules/Badge/MeasureBadge";
|
||||||
export { MeasureImplementation } from "./Molecules/MeasureImplementation/MeasureImplementation";
|
export { MeasureImplementation } from "./Molecules/MeasureImplementation/MeasureImplementation";
|
||||||
|
export { FrameworkSelector } from "./Molecules/FrameworkSelector/FrameworkSelector";
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";
|
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";
|
||||||
|
|||||||
Reference in New Issue
Block a user