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,
|
||||
IconPlusLarge,
|
||||
useConfirm,
|
||||
ActionDropdown,
|
||||
DropdownItem,
|
||||
} from "@probo/ui";
|
||||
import {
|
||||
useFragment,
|
||||
@@ -157,14 +159,11 @@ function PolicyRow({
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
deletePolicy({
|
||||
variables: {
|
||||
input: { policyId: policy.id },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted: () => resolve(),
|
||||
});
|
||||
deletePolicy({
|
||||
variables: {
|
||||
input: { policyId: policy.id },
|
||||
connections: [connectionId],
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: sprintf(
|
||||
@@ -178,51 +177,52 @@ function PolicyRow({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
||||
<Td>
|
||||
<div className="flex gap-4 items-center">
|
||||
<img
|
||||
src="/policy.png"
|
||||
alt=""
|
||||
width={28}
|
||||
height={36}
|
||||
className="border-4 border-highlight rounded box-content"
|
||||
/>
|
||||
{policy.title}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={isDraft ? "neutral" : "success"}>
|
||||
{isDraft ? __("Draft") : __("Published")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar name={policy.owner.fullName} />
|
||||
{policy.owner.fullName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{dateFormat(policy.updatedAt, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
{signedCount}/{signatures.length}
|
||||
</Td>
|
||||
<Td noLink width={50} className="text-end">
|
||||
<Button
|
||||
icon={IconTrashCan}
|
||||
variant="danger"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
||||
<Td>
|
||||
<div className="flex gap-4 items-center">
|
||||
<img
|
||||
src="/policy.png"
|
||||
alt=""
|
||||
width={28}
|
||||
height={36}
|
||||
className="border-4 border-highlight rounded box-content"
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
{policy.title}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={isDraft ? "neutral" : "success"}>
|
||||
{isDraft ? __("Draft") : __("Published")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar name={policy.owner.fullName} />
|
||||
{policy.owner.fullName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{dateFormat(policy.updatedAt, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
{signedCount}/{signatures.length}
|
||||
</Td>
|
||||
<Td noLink width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { policiesRoutes } from "./routes/policiesRoutes.ts";
|
||||
import { vendorRoutes } from "./routes/vendorRoutes.ts";
|
||||
import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts";
|
||||
import { peopleRoutes } from "./routes/peopleRoutes.ts";
|
||||
import { frameworkRoutes } from "./routes/frameworkRoutes.ts";
|
||||
|
||||
function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
@@ -85,6 +86,7 @@ const routes = [
|
||||
...policiesRoutes,
|
||||
...peopleRoutes,
|
||||
...vendorRoutes,
|
||||
...frameworkRoutes,
|
||||
],
|
||||
},
|
||||
] 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 { getRole, getRoles, peopleRoles } from "./people";
|
||||
export { certificationCategoryLabel, certifications } from "./certifications";
|
||||
export { availableFrameworks } from "./frameworks";
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
import clsx from "clsx";
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
src?: string | null;
|
||||
size?: "s" | "m" | "l" | "xl";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Avatar({ name, src, size = "m" }: Props) {
|
||||
const className = clsx(
|
||||
"bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center flex-none",
|
||||
size === "s" && "size-5 text-xss",
|
||||
size === "m" && "size-6 text-xxs",
|
||||
size === "l" && "size-8 text-sm",
|
||||
size === "xl" && "size-16 text-3xl",
|
||||
);
|
||||
if (src) {
|
||||
return <img className={className} src={src} alt={name} />;
|
||||
const avatar = tv({
|
||||
base: "bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center flex-none",
|
||||
variants: {
|
||||
size: {
|
||||
s: "size-5 text-xss",
|
||||
m: "size-6 text-xxs",
|
||||
l: "size-8 text-sm",
|
||||
xl: "size-16 text-3xl",
|
||||
},
|
||||
},
|
||||
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) {
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ActionDropdown(
|
||||
{...props}
|
||||
toggle={
|
||||
<Button
|
||||
className="inline-flex"
|
||||
className="inline-flex isolate z-5"
|
||||
variant={props.variant ?? "tertiary"}
|
||||
icon={IconDotGrid1x3Horizontal}
|
||||
/>
|
||||
|
||||
@@ -37,17 +37,22 @@ export const dialog = tv({
|
||||
},
|
||||
});
|
||||
|
||||
export type DialogRef = RefObject<{
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
} | null>;
|
||||
|
||||
type Props = {
|
||||
trigger?: ReactNode;
|
||||
title?: ReactNode;
|
||||
children?: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
ref?: RefObject<{ open: () => void; close: () => void } | null>;
|
||||
ref?: DialogRef;
|
||||
};
|
||||
|
||||
export const useDialogRef = () => {
|
||||
return useRef<{ open: () => void; close: () => void } | null>(null);
|
||||
export const useDialogRef = (): DialogRef => {
|
||||
return useRef(null);
|
||||
};
|
||||
|
||||
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,
|
||||
DialogTitle,
|
||||
useDialogRef,
|
||||
type DialogRef,
|
||||
} from "./Molecules/Dialog/Dialog";
|
||||
export { useConfirm } from "./Molecules/Dialog/ConfirmDialog";
|
||||
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
|
||||
@@ -58,6 +59,7 @@ export { Combobox, ComboboxItem } from "./Molecules/Combobox/Combobox";
|
||||
export { FileButton } from "./Molecules/FileButton/FileButton";
|
||||
export { MeasureBadge } from "./Molecules/Badge/MeasureBadge";
|
||||
export { MeasureImplementation } from "./Molecules/MeasureImplementation/MeasureImplementation";
|
||||
export { FrameworkSelector } from "./Molecules/FrameworkSelector/FrameworkSelector";
|
||||
|
||||
// Hooks
|
||||
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";
|
||||
|
||||
Reference in New Issue
Block a user