Add risk tier, criticality on vendors

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-18 11:14:20 +01:00
parent 53bceb5479
commit 6da223ed36
10 changed files with 940 additions and 236 deletions

View File

@@ -1,29 +1,32 @@
"use client";
import { useState } from "react";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
import { HelpCircle } from "lucide-react";
import { HelpCircle, ArrowUpRight } from "lucide-react";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { Suspense, useEffect } from "react";
import { Suspense, useEffect, useState } from "react";
import type { VendorOverviewPageQuery as VendorOverviewPageQueryType } from "./__generated__/VendorOverviewPageQuery.graphql";
import { useParams } from "react-router";
import { Helmet } from "react-helmet-async";
import { useBreadcrumb } from "@/contexts/BreadcrumbContext";
import { cn } from "@/lib/utils";
const VendorOverviewPageQuery = graphql`
const vendorOverviewPageQuery = graphql`
query VendorOverviewPageQuery($vendorId: ID!) {
node(id: $vendorId) {
... on Vendor {
id
name
serviceStartDate
serviceTerminationDate
serviceCriticality
riskTier
statusPageUrl
createdAt
updatedAt
}
@@ -36,9 +39,10 @@ function VendorOverviewPageContent({
}: {
queryRef: PreloadedQuery<VendorOverviewPageQueryType>;
}) {
const data = usePreloadedQuery(VendorOverviewPageQuery, queryRef);
const [riskLevel, setRiskLevel] = useState("medium");
const data = usePreloadedQuery(vendorOverviewPageQuery, queryRef);
const { setBreadcrumbSegment } = useBreadcrumb();
const [selectedCriticality, setSelectedCriticality] = useState(data.node.serviceCriticality);
const [selectedRiskTier, setSelectedRiskTier] = useState(data.node.riskTier);
useEffect(() => {
if (data.node?.name) {
@@ -50,57 +54,18 @@ function VendorOverviewPageContent({
<div className="space-y-6 p-4 md:p-6 lg:p-8">
<div className="mx-auto max-w-4xl space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-2">
<img
src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-G4dMt0eis6LFLrCaYPeMr7Nkt2iPoj.png"
alt="Google Cloud"
className="h-8 w-8"
/>
<h1 className="text-xl font-semibold text-gray-900">
{data.node?.name}
</h1>
</div>
<p className="text-gray-600">
Use this form to assess the risk of using Google Workspace as a
vendor.
</p>
<h1 className="text-xl font-semibold text-gray-900">
{data.node?.name}
</h1>
<p className="text-gray-600">View and manage vendor details</p>
</div>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">Asset Storage Type</h2>
<h2 className="text-lg font-medium">Service Information</h2>
<p className="text-sm text-gray-500">
Questions related to the type of assets stored by the vendor.
</p>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Does this vendor store physical or digital assets?
</Label>
</div>
<div className="flex gap-2">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
Digital
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Physical
</button>
</div>
</div>
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">Data Storage and Handling</h2>
<p className="text-sm text-gray-500">
Questions related to the vendor's data storage and handling
practices.
Basic information about the vendor service
</p>
</div>
@@ -108,178 +73,142 @@ function VendorOverviewPageContent({
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Does this vendor store production data?
</Label>
</div>
<div className="flex gap-2">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
No
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Yes
</button>
<Label className="text-sm">Service Start Date</Label>
</div>
<p className="text-sm text-gray-600">
{new Date(data.node?.serviceStartDate).toLocaleDateString()}
</p>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Is the production data encrypted at rest?
</Label>
</div>
<div className="flex gap-2">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
No
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Yes
</button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Who has access to the production data?
</Label>
</div>
{data.node?.serviceTerminationDate && (
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox id="vendor-staff" defaultChecked />
<label
htmlFor="vendor-staff"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Vendor Staff
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="third-parties" />
<label
htmlFor="third-parties"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Third Parties
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="automated-systems" />
<label
htmlFor="automated-systems"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Automated Systems
</label>
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Service Termination Date</Label>
</div>
<p className="text-sm text-gray-600">
{new Date(data.node?.serviceTerminationDate).toLocaleDateString()}
</p>
</div>
</div>
</div>
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">
Vendor Reliability and Continuity
</h2>
<p className="text-sm text-gray-500">
Evaluating the vendor's reliability and plans for business
continuity.
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Does the vendor have an uptime commitment?
</Label>
</div>
<RadioGroup defaultValue="greater" className="space-y-2">
<div className="flex items-center space-x-2">
<RadioGroupItem value="greater" id="greater" />
<Label htmlFor="greater">Greater than 99.9% uptime</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="between" id="between" />
<Label htmlFor="between">
Between 99% and 99.9% uptime
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="less" id="less" />
<Label htmlFor="less">Less than 99% uptime</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="na" id="na" />
<Label htmlFor="na">Not applicable</Label>
</div>
</RadioGroup>
</div>
)}
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Does the vendor have a disaster recovery plan?
</Label>
<Label className="text-sm">Service Criticality</Label>
</div>
<div className="flex gap-2">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
No
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
Yes
</button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">
Impact of a Data Breach or Outage
</Label>
</div>
<div className="flex gap-2">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
<button
onClick={() => setSelectedCriticality('LOW')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
selectedCriticality === 'LOW'
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Low
</button>
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
<button
onClick={() => setSelectedCriticality('MEDIUM')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
selectedCriticality === 'MEDIUM'
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Medium
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
<button
onClick={() => setSelectedCriticality('HIGH')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
selectedCriticality === 'HIGH'
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
High
</button>
</div>
<p className="text-sm text-gray-500">
{selectedCriticality === 'HIGH' &&
"Critical service - downtime severely impacts end-users"}
{selectedCriticality === 'MEDIUM' &&
"Important service - downtime moderately affects end-users"}
{selectedCriticality === 'LOW' &&
"Non-critical service - minimal end-user impact if down"}
</p>
</div>
</div>
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-lg font-medium">Risk level</h2>
<p className="text-sm text-gray-500">
Based on the responses, the suggested risk level is medium.
</p>
</div>
<div className="flex gap-2">
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
Low
</button>
<button className="rounded-full bg-gray-100 px-4 py-1 text-sm text-gray-900">
Medium
</button>
<button className="rounded-full bg-green-100 px-4 py-1 text-sm text-green-900">
High
</button>
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Risk Tier</Label>
</div>
<div className="flex gap-2">
<button
onClick={() => setSelectedRiskTier('CRITICAL')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
selectedRiskTier === 'CRITICAL'
? "bg-red-100 text-red-900 ring-2 ring-red-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Critical
</button>
<button
onClick={() => setSelectedRiskTier('SIGNIFICANT')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
selectedRiskTier === 'SIGNIFICANT'
? "bg-yellow-100 text-yellow-900 ring-2 ring-yellow-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
Significant
</button>
<button
onClick={() => setSelectedRiskTier('GENERAL')}
className={cn(
"rounded-full px-4 py-1 text-sm transition-colors",
selectedRiskTier === 'GENERAL'
? "bg-green-100 text-green-900 ring-2 ring-green-600 ring-offset-2"
: "bg-gray-100 text-gray-900 hover:bg-gray-200"
)}
>
General
</button>
</div>
<p className="text-sm text-gray-500">
{selectedRiskTier === 'CRITICAL' &&
"Handles sensitive data, critical for platform operation"}
{selectedRiskTier === 'SIGNIFICANT' &&
"No user data access, but important for platform management"}
{selectedRiskTier === 'GENERAL' &&
"General vendor with minimal risk"}
</p>
</div>
{data.node?.statusPageUrl && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<HelpCircle className="h-4 w-4 text-gray-400" />
<Label className="text-sm">Status Page</Label>
</div>
<a
href={data.node.statusPageUrl}
className="text-primary hover:underline inline-flex items-center gap-1"
target="_blank"
rel="noopener noreferrer"
>
View Status Page
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
)}
</div>
</div>
</Card>
@@ -296,7 +225,7 @@ function VendorOverviewPageFallback() {
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
</div>
<div className="space-y-2">
{[1, 2, 3].map((i) => (
{[1, 2].map((i) => (
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
))}
</div>
@@ -307,7 +236,7 @@ function VendorOverviewPageFallback() {
export default function VendorOverviewPage() {
const { vendorId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<VendorOverviewPageQueryType>(
VendorOverviewPageQuery,
vendorOverviewPageQuery,
);
useEffect(() => {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<a9ec0002895dd62d2845ae4fd6df3a53>>
* @generated SignedSource<<939b83dd396154844197ee10514be613>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,6 +9,8 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RiskTier = "CRITICAL" | "GENERAL" | "SIGNIFICANT";
export type ServiceCriticality = "HIGH" | "LOW" | "MEDIUM";
export type VendorOverviewPageQuery$variables = {
vendorId: string;
};
@@ -17,6 +19,11 @@ export type VendorOverviewPageQuery$data = {
readonly createdAt?: any;
readonly id?: string;
readonly name?: string;
readonly riskTier?: RiskTier;
readonly serviceCriticality?: ServiceCriticality;
readonly serviceStartDate?: any;
readonly serviceTerminationDate?: any | null | undefined;
readonly statusPageUrl?: string | null | undefined;
readonly updatedAt?: any;
};
};
@@ -58,10 +65,45 @@ v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"name": "serviceStartDate",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceTerminationDate",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceCriticality",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "riskTier",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "statusPageUrl",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -89,7 +131,12 @@ return {
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/)
],
"type": "Vendor",
"abstractKey": null
@@ -128,7 +175,12 @@ return {
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/)
],
"type": "Vendor",
"abstractKey": null
@@ -139,16 +191,16 @@ return {
]
},
"params": {
"cacheID": "93086ca61d518fce9c6ed49fa9bd841e",
"cacheID": "c9d7bade02d1802838f6ac3b02ef7c9a",
"id": null,
"metadata": {},
"name": "VendorOverviewPageQuery",
"operationKind": "query",
"text": "query VendorOverviewPageQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n createdAt\n updatedAt\n }\n id\n }\n}\n"
"text": "query VendorOverviewPageQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n serviceStartDate\n serviceTerminationDate\n serviceCriticality\n riskTier\n statusPageUrl\n createdAt\n updatedAt\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "7959720cf1a6376d406ac5f478f6d9c3";
(node as any).hash = "92a640f674261d28028d752f027ec141";
export default node;