@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<StrictMode>
|
||||
@@ -41,6 +41,7 @@ function App() {
|
||||
<Route path="/peoples" element={<PeoplesPage />} />
|
||||
<Route path="/vendors" element={<VendorsPage />} />
|
||||
<Route path="/frameworks" element={<FrameworksPage />} />
|
||||
<Route path="/frameworks/:frameworkId" element={<FrameworkPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
|
||||
26
apps/console/src/components/ui/progress.tsx
Normal file
26
apps/console/src/components/ui/progress.tsx
Normal file
@@ -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<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
206
apps/console/src/pages/FrameworkPage.tsx
Normal file
206
apps/console/src/pages/FrameworkPage.tsx
Normal file
@@ -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<FrameworkPageQueryType>;
|
||||
}) {
|
||||
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: <Building2 className="w-4 h-4" />,
|
||||
title: "Secure your offices and internet access",
|
||||
controls: controlsByCategory.office,
|
||||
completed: controlsByCategory.office.filter(c => c?.state === 'IMPLEMENTED').length,
|
||||
total: controlsByCategory.office.length,
|
||||
},
|
||||
{
|
||||
icon: <Computer className="w-4 h-4" />,
|
||||
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 (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-4 mb-8">
|
||||
<h1 className="text-2xl font-semibold">{framework.name}</h1>
|
||||
<p className="text-muted-foreground max-w-3xl">
|
||||
{framework.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="mb-8 space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="bg-primary/10 text-primary px-3 py-1 rounded-md flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
Frame 153
|
||||
</div>
|
||||
<div className="bg-warning/10 text-warning px-3 py-1 rounded-md flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
12 hours left
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card p-4 rounded-lg space-y-4">
|
||||
<h3 className="font-medium">Preparation phase</h3>
|
||||
<Progress value={30} className="h-2" />
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>Preparation</span>
|
||||
<span>Observation period: 3 month</span>
|
||||
<span>Audit: 6-9 days</span>
|
||||
<span>Report: 10-14 days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold">Controls</h2>
|
||||
<div className="text-primary">{totalImplemented} out of {controls.length} validated</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{controlCards.map((card, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
{card.icon}
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarFallback>U</AvatarFallback>
|
||||
</Avatar>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-1 my-2">
|
||||
{Array(card.total)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-2 w-2 rounded-sm ${
|
||||
i < card.completed ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{card.completed}/{card.total} Controls validated
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FrameworkPageFallback() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<div className="mb-8">
|
||||
<div className="h-8 w-48 bg-muted animate-pulse rounded" />
|
||||
<div className="h-4 w-96 bg-muted animate-pulse rounded mt-2" />
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-6">
|
||||
<div className="relative mb-6">
|
||||
<div className="bg-muted w-24 h-24 rounded-full animate-pulse mb-4" />
|
||||
<div className="h-6 w-48 bg-muted animate-pulse rounded mb-2" />
|
||||
<div className="h-20 w-full bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
<div className="h-4 w-32 bg-muted animate-pulse rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FrameworkPage() {
|
||||
const { frameworkId } = useParams();
|
||||
const [queryRef, loadQuery] = useQueryLoader<FrameworkPageQueryType>(FrameworkPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ frameworkId });
|
||||
}, [loadQuery, frameworkId]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FrameworkPageFallback />}>
|
||||
{queryRef && <FrameworkPageContent queryRef={queryRef} />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{frameworks.map((framework) => (
|
||||
<Card key={framework?.id} className="bg-card/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="relative mb-6">
|
||||
<Badge className="absolute right-0 top-0 bg-green-500/10 text-green-500 hover:bg-green-500/20">
|
||||
Active
|
||||
</Badge>
|
||||
<div className="bg-green-500/10 w-24 h-24 rounded-full flex items-center justify-center mb-4">
|
||||
<Globe2 className="w-12 h-12 text-green-500" />
|
||||
<div className="absolute text-xs font-medium text-green-500">{framework?.name}</div>
|
||||
<Link key={framework?.id} to={`/frameworks/${framework.id}`}>
|
||||
<Card className="bg-card/50 hover:bg-card/70 transition-colors">
|
||||
<CardContent className="p-6">
|
||||
<div className="relative mb-6">
|
||||
<Badge className="absolute right-0 top-0 bg-green-500/10 text-green-500 hover:bg-green-500/20">
|
||||
Active
|
||||
</Badge>
|
||||
<div className="bg-green-500/10 w-24 h-24 rounded-full flex items-center justify-center mb-4">
|
||||
<Globe2 className="w-12 h-12 text-green-500" />
|
||||
<div className="absolute text-xs font-medium text-green-500">{framework?.name}</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">{framework?.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{framework?.description}
|
||||
</p>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">{framework?.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{framework?.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center text-muted-foreground text-sm">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{framework?.controls?.edges.length} Controls
|
||||
<div className="flex items-center text-muted-foreground text-sm">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
{framework?.controls?.edges.length} Controls
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
191
apps/console/src/pages/__generated__/FrameworkPageQuery.graphql.ts
generated
Normal file
191
apps/console/src/pages/__generated__/FrameworkPageQuery.graphql.ts
generated
Normal file
@@ -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;
|
||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user