From 8ac4929f66ade94b11e63b4dc3c08b8af1953490 Mon Sep 17 00:00:00 2001 From: gearnode Date: Sun, 9 Feb 2025 21:35:13 -0800 Subject: [PATCH] Add framewor overview page Signed-off-by: gearnode --- apps/console/package.json | 1 + apps/console/src/App.tsx | 3 +- apps/console/src/components/ui/progress.tsx | 26 +++ apps/console/src/pages/FrameworkPage.tsx | 206 ++++++++++++++++++ apps/console/src/pages/FrameworksPage.tsx | 45 ++-- .../FrameworkPageQuery.graphql.ts | 191 ++++++++++++++++ package-lock.json | 25 +++ 7 files changed, 475 insertions(+), 22 deletions(-) create mode 100644 apps/console/src/components/ui/progress.tsx create mode 100644 apps/console/src/pages/FrameworkPage.tsx create mode 100644 apps/console/src/pages/__generated__/FrameworkPageQuery.graphql.ts diff --git a/apps/console/package.json b/apps/console/package.json index f2b97729a..6be2483e4 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index bbe8b57fe..4658285cb 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -25,7 +25,7 @@ const NotFoundPage = lazy(() => import("./pages/NotFoundPage")); const PeoplesPage = lazy(() => import("./pages/PeoplesPage")); const VendorsPage = lazy(() => import("./pages/VendorsPage")); const FrameworksPage = lazy(() => import("./pages/FrameworksPage")); - +const FrameworkPage = lazy(() => import("./pages/FrameworkPage")); function App() { return ( @@ -41,6 +41,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/apps/console/src/components/ui/progress.tsx b/apps/console/src/components/ui/progress.tsx new file mode 100644 index 000000000..3fd47adad --- /dev/null +++ b/apps/console/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/apps/console/src/pages/FrameworkPage.tsx b/apps/console/src/pages/FrameworkPage.tsx new file mode 100644 index 000000000..ebe4542c9 --- /dev/null +++ b/apps/console/src/pages/FrameworkPage.tsx @@ -0,0 +1,206 @@ +import { Suspense, useEffect } from "react"; +import { useParams } from "react-router"; +import { + graphql, + PreloadedQuery, + usePreloadedQuery, + useQueryLoader, +} from "react-relay"; +import { + Building2, + Computer, + Mail, + Shield, + Network, + Database, + FileText, + Share2, + AlertTriangle, + Users, + Clock, +} from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import type { FrameworkPageQuery as FrameworkPageQueryType } from "./__generated__/FrameworkPageQuery.graphql"; + +const FrameworkPageQuery = graphql` + query FrameworkPageQuery($frameworkId: ID!) { + node(id: $frameworkId) { + id + ... on Framework { + name + description + controls { + edges { + node { + id + name + description + state + } + } + } + } + } + } +`; + +function FrameworkPageContent({ + queryRef, +}: { + queryRef: PreloadedQuery; +}) { + const data = usePreloadedQuery(FrameworkPageQuery, queryRef); + const framework = data.node; + const controls = framework.controls?.edges.map(edge => edge?.node) ?? []; + + const controlsByCategory = { + office: controls.filter(c => c?.name.toLowerCase().includes('office')), + computer: controls.filter(c => c?.name.toLowerCase().includes('computer')), + employees: controls.filter(c => c?.name.toLowerCase().includes('employee')), + email: controls.filter(c => c?.name.toLowerCase().includes('email')), + code: controls.filter(c => c?.name.toLowerCase().includes('code')), + infrastructure: controls.filter(c => c?.name.toLowerCase().includes('infrastructure')), + network: controls.filter(c => c?.name.toLowerCase().includes('network')), + data: controls.filter(c => c?.name.toLowerCase().includes('data')), + logging: controls.filter(c => c?.name.toLowerCase().includes('log')), + incidents: controls.filter(c => c?.name.toLowerCase().includes('incident')), + vendors: controls.filter(c => c?.name.toLowerCase().includes('vendor')), + sharing: controls.filter(c => c?.name.toLowerCase().includes('share')) + }; + + const controlCards = [ + { + icon: , + title: "Secure your offices and internet access", + controls: controlsByCategory.office, + completed: controlsByCategory.office.filter(c => c?.state === 'IMPLEMENTED').length, + total: controlsByCategory.office.length, + }, + { + icon: , + title: "Manage your computers", + controls: controlsByCategory.computer, + completed: controlsByCategory.computer.filter(c => c?.state === 'IMPLEMENTED').length, + total: controlsByCategory.computer.length, + }, + ]; + + const totalImplemented = controls.filter(c => c?.state === 'IMPLEMENTED').length; + + return ( +
+ {/* Header */} +
+

{framework.name}

+

+ {framework.description} +

+
+ + {/* Timeline */} +
+
+
+ + Frame 153 +
+
+ + 12 hours left +
+
+
+

Preparation phase

+ +
+ Preparation + Observation period: 3 month + Audit: 6-9 days + Report: 10-14 days +
+
+
+ +
+

Controls

+
{totalImplemented} out of {controls.length} validated
+
+ +
+ {controlCards.map((card, index) => ( + + + + {card.icon} + {card.title} + + + U + + + +
+ {Array(card.total) + .fill(0) + .map((_, i) => ( +
+ ))} +
+

+ {card.completed}/{card.total} Controls validated +

+ + + ))} +
+
+ ); +} + +function FrameworkPageFallback() { + return ( +
+
+
+
+
+
+ {[1, 2, 3].map((i) => ( + + +
+
+
+
+
+
+ + + ))} +
+
+ ); +} + +export default function FrameworkPage() { + const { frameworkId } = useParams(); + const [queryRef, loadQuery] = useQueryLoader(FrameworkPageQuery); + + useEffect(() => { + loadQuery({ frameworkId }); + }, [loadQuery, frameworkId]); + + return ( + }> + {queryRef && } + + ); +} + diff --git a/apps/console/src/pages/FrameworksPage.tsx b/apps/console/src/pages/FrameworksPage.tsx index 96130bb35..f508422b0 100644 --- a/apps/console/src/pages/FrameworksPage.tsx +++ b/apps/console/src/pages/FrameworksPage.tsx @@ -8,6 +8,7 @@ import { import { Globe2, CheckCircle } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Link } from "react-router"; import type { FrameworksPageQuery as FrameworksPageQueryType } from "./__generated__/FrameworksPageQuery.graphql"; const FrameworksPageQuery = graphql` @@ -60,29 +61,31 @@ function FrameworksPageContent({
{frameworks.map((framework) => ( - - -
- - Active - -
- -
{framework?.name}
+ + + +
+ + Active + +
+ +
{framework?.name}
+
+

{framework?.name}

+

+ {framework?.description} +

-

{framework?.name}

-

- {framework?.description} -

-
-
-
- - {framework?.controls?.edges.length} Controls +
+
+ + {framework?.controls?.edges.length} Controls +
-
- - + + + ))}
diff --git a/apps/console/src/pages/__generated__/FrameworkPageQuery.graphql.ts b/apps/console/src/pages/__generated__/FrameworkPageQuery.graphql.ts new file mode 100644 index 000000000..93cb75aa6 --- /dev/null +++ b/apps/console/src/pages/__generated__/FrameworkPageQuery.graphql.ts @@ -0,0 +1,191 @@ +/** + * @generated SignedSource<<51618c4ff52c6da6eb39cc0756d037e4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; +export type FrameworkPageQuery$variables = { + frameworkId: string; +}; +export type FrameworkPageQuery$data = { + readonly node: { + readonly controls?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly description: string; + readonly id: string; + readonly name: string; + readonly state: ControlState; + }; + }>; + }; + readonly description?: string; + readonly id: string; + readonly name?: string; + }; +}; +export type FrameworkPageQuery = { + response: FrameworkPageQuery$data; + variables: FrameworkPageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "frameworkId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "frameworkId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null +}, +v5 = { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "controls", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Framework", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FrameworkPageQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v5/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FrameworkPageQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + (v5/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "20c37c26ec0c229a526ba1f84a0bb22e", + "id": null, + "metadata": {}, + "name": "FrameworkPageQuery", + "operationKind": "query", + "text": "query FrameworkPageQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n id\n ... on Framework {\n name\n description\n controls {\n edges {\n node {\n id\n name\n description\n state\n }\n }\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "c018807d1942d694eceae254a3078c4d"; + +export default node; diff --git a/package-lock.json b/package-lock.json index 69cf31f17..9c55df78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", @@ -2699,6 +2700,30 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz", + "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",