diff --git a/apps/console/src/components/ui/switch.tsx b/apps/console/src/components/ui/switch.tsx index 3a5434ff0..b0363e3f8 100644 --- a/apps/console/src/components/ui/switch.tsx +++ b/apps/console/src/components/ui/switch.tsx @@ -1,29 +1,29 @@ -"use client"; +import * as React from "react" +import * as SwitchPrimitive from "@radix-ui/react-switch" -import * as React from "react"; -import * as SwitchPrimitives from "@radix-ui/react-switch"; +import { cn } from "@/lib/utils" -import { cn } from "@/lib/utils"; - -const Switch = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - ) { + return ( + - -)); -Switch.displayName = SwitchPrimitives.Root.displayName; + {...props} + > + + + ) +} -export { Switch }; +export { Switch } diff --git a/apps/console/src/pages/organizations/risks/EditRiskView.tsx b/apps/console/src/pages/organizations/risks/EditRiskView.tsx index 7370e99a2..876f27427 100644 --- a/apps/console/src/pages/organizations/risks/EditRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/EditRiskView.tsx @@ -105,11 +105,11 @@ function EditRiskViewContent({ const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [inherentLikelihood, setInherentLikelihood] = - useState("MEDIUM"); - const [inherentImpact, setInherentImpact] = useState("MEDIUM"); + useState("OCCASIONAL"); + const [inherentImpact, setInherentImpact] = useState("MODERATE"); const [residualLikelihood, setResidualLikelihood] = - useState("MEDIUM"); - const [residualImpact, setResidualImpact] = useState("MEDIUM"); + useState("OCCASIONAL"); + const [residualImpact, setResidualImpact] = useState("MODERATE"); const [treatment, setTreatment] = useState("MITIGATED"); const [ownerId, setOwnerId] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -119,34 +119,34 @@ function EditRiskViewContent({ // Helper function to convert float to likelihood string const floatToLikelihood = (value: number): string => { - if (value <= 0.2) return "VERY_LOW"; - if (value <= 0.4) return "LOW"; - if (value <= 0.6) return "MEDIUM"; - if (value <= 0.8) return "HIGH"; - return "VERY_HIGH"; + if (value <= 0.2) return "IMPROBABLE"; + if (value <= 0.4) return "REMOTE"; + if (value <= 0.6) return "OCCASIONAL"; + if (value <= 0.8) return "PROBABLE"; + return "FREQUENT"; }; // Helper function to convert float to impact string const floatToImpact = (value: number): string => { - if (value <= 0.2) return "VERY_LOW"; + if (value <= 0.2) return "NEGLIGIBLE"; if (value <= 0.4) return "LOW"; - if (value <= 0.6) return "MEDIUM"; - if (value <= 0.8) return "HIGH"; - return "VERY_HIGH"; + if (value <= 0.6) return "MODERATE"; + if (value <= 0.8) return "SIGNIFICANT"; + return "CATASTROPHIC"; }; // Map string values to float values const likelihoodToFloat = (value: string): number => { switch (value) { - case "VERY_LOW": + case "IMPROBABLE": return 0.1; - case "LOW": + case "REMOTE": return 0.3; - case "MEDIUM": + case "OCCASIONAL": return 0.5; - case "HIGH": + case "PROBABLE": return 0.7; - case "VERY_HIGH": + case "FREQUENT": return 0.9; default: return 0.5; @@ -155,15 +155,15 @@ function EditRiskViewContent({ const impactToFloat = (value: string): number => { switch (value) { - case "VERY_LOW": + case "NEGLIGIBLE": return 0.1; case "LOW": return 0.3; - case "MEDIUM": + case "MODERATE": return 0.5; - case "HIGH": + case "SIGNIFICANT": return 0.7; - case "VERY_HIGH": + case "CATASTROPHIC": return 0.9; default: return 0.5; @@ -308,15 +308,15 @@ function EditRiskViewContent({ value={inherentLikelihood} onValueChange={setInherentLikelihood} > - + - - Very Low - Low - Medium - High - Very High + + Improbable + Remote + Occasional + Probable + Frequent @@ -327,15 +327,15 @@ function EditRiskViewContent({ value={inherentImpact} onValueChange={setInherentImpact} > - + - - Very Low + + Negligible Low - Medium - High - Very High + Moderate + Significant + Catastrophic @@ -382,15 +382,15 @@ function EditRiskViewContent({ value={residualLikelihood} onValueChange={setResidualLikelihood} > - - + + - - Very Low - Low - Medium - High - Very High + + Improbable + Remote + Occasional + Probable + Frequent @@ -401,15 +401,15 @@ function EditRiskViewContent({ value={residualImpact} onValueChange={setResidualImpact} > - - + + - - Very Low + + Negligible Low - Medium - High - Very High + Moderate + Significant + Catastrophic diff --git a/apps/console/src/pages/organizations/risks/ListRiskView.tsx b/apps/console/src/pages/organizations/risks/ListRiskView.tsx index b4a22b416..c11ee867c 100644 --- a/apps/console/src/pages/organizations/risks/ListRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/ListRiskView.tsx @@ -22,7 +22,7 @@ import { RiskViewSkeleton } from "./ListRiskPage"; import { ListRiskViewPaginationQuery } from "./__generated__/ListRiskViewPaginationQuery.graphql"; import { ListRiskView_risks$key } from "./__generated__/ListRiskView_risks.graphql"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Link } from "react-router"; import { Plus, Trash2, Edit } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; @@ -110,10 +110,19 @@ const deleteRiskMutation = graphql` `; // Helper function to convert float values to percentage +// This is kept for potential future use, like tooltips const floatToPercentage = (value: number): string => { return `${Math.round(value * 100)}%`; }; +// Helper function to convert risk score to severity label +const riskScoreToSeverity = (score: number): string => { + if (score >= 0.75) return "Catastrophic"; + if (score >= 0.5) return "Critical"; + if (score >= 0.25) return "Marginal"; + return "Negligible"; +}; + // Helper function to format treatment value const formatTreatment = (treatment: string): string => { const treatmentMap: Record = { @@ -182,20 +191,16 @@ function LoadBelowButton({ // Define colors for risk matrix cells const riskMatrixColors = { - lowest: "bg-green-500 text-white", - low: "bg-lime-300 text-black", + low: "bg-green-500 text-white", medium: "bg-yellow-300 text-black", - high: "bg-amber-400 text-white", - highest: "bg-red-500 text-white", + high: "bg-red-500 text-white", }; // Empty cell variants (lighter colors) const emptyRiskMatrixColors = { - lowest: "bg-green-50 text-black", - low: "bg-lime-50 text-black", + low: "bg-green-50 text-black", medium: "bg-yellow-50 text-black", - high: "bg-amber-50 text-black", - highest: "bg-red-50 text-black", + high: "bg-red-50 text-black", }; // Risk Matrix Component @@ -213,31 +218,44 @@ function RiskMatrix({ }>; isResidual?: boolean; }): JSX.Element { - // Define likelihood and impact ranges for the 5x5 matrix - const likelihoodRanges: [number, number][] = [ - [0.8, 1], // Highest likelihood - [0.6, 0.8], // High likelihood - [0.4, 0.6], // Medium likelihood - [0.2, 0.4], // Low likelihood - [0, 0.2], // Lowest likelihood - ]; - + // Define impact ranges for the vertical axis (rows) const impactRanges: [number, number][] = [ - [0, 0.2], // Lowest impact - [0.2, 0.4], // Low impact - [0.4, 0.6], // Medium impact + [0.8, 1], // Highest impact - top row [0.6, 0.8], // High impact - [0.8, 1], // Highest impact + [0.4, 0.6], // Medium impact + [0.2, 0.4], // Low impact + [0, 0.2], // Lowest impact - bottom row ]; - const likelihoodLabels = ["Very High", "High", "Medium", "Low", "Very Low"]; + // Define likelihood ranges for the horizontal axis (columns) + const likelihoodRanges: [number, number][] = [ + [0, 0.2], // Lowest likelihood - leftmost column + [0.2, 0.4], // Low likelihood + [0.4, 0.6], // Medium likelihood + [0.6, 0.8], // High likelihood + [0.8, 1], // Highest likelihood - rightmost column + ]; - const impactLabels = ["Very Low", "Low", "Medium", "High", "Very High"]; + const impactLabels = [ + "Catastrophic", + "Significant", + "Moderate", + "Low", + "Negligible", + ]; + + const likelihoodLabels = [ + "Improbable", + "Remote", + "Occasional", + "Probable", + "Frequent", + ]; // Function to get cell content with risks that fall in this cell const getCellContent = ( - likelihoodRange: [number, number], - impactRange: [number, number] + impactRange: [number, number], + likelihoodRange: [number, number] ) => { return risks.filter((risk) => { const likelihood = isResidual @@ -257,48 +275,41 @@ function RiskMatrix({ }; // Helper to determine cell color based on position in matrix - // New matrix has rows indexed from top to bottom (0 = highest likelihood, 4 = lowest likelihood) - // and columns indexed from left to right (0 = lowest impact, 4 = highest impact) + // Matrix has rows indexed from top to bottom (0 = highest impact, 4 = lowest impact) + // and columns indexed from left to right (0 = lowest likelihood, 4 = highest likelihood) const getCellColor = (row: number, col: number, isEmpty: boolean): string => { const colorSet = isEmpty ? emptyRiskMatrixColors : riskMatrixColors; - // Top row (highest likelihood) + // Top row (highest impact - Catastrophic) if (row === 0) { - if (col === 0) return colorSet.lowest; - if (col === 1) return colorSet.high; - return colorSet.highest; + if (col <= 1) return colorSet.medium; // Yellow for first two cells + return colorSet.high; // Red for the rest } - // Second row + // Second row (Significant impact) if (row === 1) { - if (col === 0) return colorSet.lowest; - if (col === 1) return colorSet.medium; - if (col === 2) return colorSet.high; - return colorSet.highest; + if (col === 0) return colorSet.low; // Green for first cell + if (col <= 2) return colorSet.medium; // Yellow for next two + return colorSet.high; // Red for the rest } - // Middle row + // Middle row (Moderate impact) if (row === 2) { - if (col === 0) return colorSet.lowest; - if (col === 1) return colorSet.low; - if (col === 2) return colorSet.medium; - if (col === 3) return colorSet.high; - return colorSet.highest; + if (col === 0) return colorSet.low; // Green for first cell + if (col <= 3) return colorSet.medium; // Yellow for next three + return colorSet.high; // Red for last cell } - // Fourth row + // Fourth row (Low impact) if (row === 3) { - if (col === 0) return colorSet.lowest; - if (col === 1) return colorSet.low; - if (col === 2 || col === 3) return colorSet.medium; - return colorSet.high; + if (col <= 1) return colorSet.low; // Green for first two + return colorSet.medium; // Yellow for the rest } - // Bottom row (lowest likelihood) + // Bottom row (lowest impact - Negligible) if (row === 4) { - if (col <= 1) return colorSet.lowest; - if (col <= 3) return colorSet.low; - return colorSet.medium; + if (col <= 3) return colorSet.low; // Green for first four + return colorSet.medium; // Yellow for last cell } return "bg-gray-100"; @@ -308,15 +319,15 @@ function RiskMatrix({ const RiskCell = ({ rowIndex, colIndex, - likelihoodRange, impactRange, + likelihoodRange, }: { rowIndex: number; colIndex: number; - likelihoodRange: [number, number]; impactRange: [number, number]; + likelihoodRange: [number, number]; }) => { - const cellRisks = getCellContent(likelihoodRange, impactRange); + const cellRisks = getCellContent(impactRange, likelihoodRange); const isEmpty = cellRisks.length === 0; const cellColor = getCellColor(rowIndex, colIndex, isEmpty); @@ -360,43 +371,61 @@ function RiskMatrix({ }; return ( -
-
-
CONSEQUENCE
- - - - - {impactLabels.map((label, index) => ( - - ))} - - - - {likelihoodRanges.map((likelihoodRange, rowIndex) => ( - - - {impactRanges.map((impactRange, colIndex) => ( - +
+
+
+
+
+ Impact +
+
- {label} -
- {likelihoodLabels[rowIndex]} -
+ + {impactRanges.map((impactRange, rowIndex) => ( + + + {likelihoodRanges.map((likelihoodRange, colIndex) => ( + + ))} + ))} - - ))} - -
+ {impactLabels[rowIndex]} +
-
LIKELIHOOD
+ + + + + {likelihoodLabels.map((label, index) => ( + + {label} + + ))} + + + +
+
+
+
+ Likelihood +
+
+
); @@ -519,41 +548,43 @@ function ListRiskViewContent({ {/* Combined Risk Matrix with Toggle */} - - - {showResidualRisk - ? "Residual Risk Matrix" - : "Initial Risk Matrix"} - -
- -
- + +
+
+

+ {showResidualRisk ? "Residual risk" : "Current risk"} +

+
+ + + +
- +
- - - @@ -617,7 +648,7 @@ function ListRiskViewContent({ to={`/organizations/${organizationId}/risks/${risk.id}`} className="block p-4 h-full w-full" > - {floatToPercentage( + {riskScoreToSeverity( risk.inherentLikelihood * risk.inherentImpact )} @@ -628,7 +659,7 @@ function ListRiskViewContent({ className="block p-4 h-full w-full" > {risk.residualLikelihood && risk.residualImpact - ? floatToPercentage( + ? riskScoreToSeverity( risk.residualLikelihood * risk.residualImpact ) : "Not set"} @@ -671,21 +702,6 @@ function ListRiskViewContent({
- { - startTransition(() => { - setSearchParams((prev) => { - prev.set("after", pageInfo?.endCursor || ""); - prev.delete("before"); - return prev; - }); - loadNext(defaultPageSize); - }); - }} - /> - {/* Delete Confirmation Dialog */} + + { + startTransition(() => { + setSearchParams((prev) => { + prev.set("after", pageInfo?.endCursor || ""); + prev.delete("before"); + return prev; + }); + loadNext(defaultPageSize); + }); + }} + />
); @@ -740,7 +771,7 @@ export default function ListRiskView() { last: before ? defaultPageSize : undefined, before: before || undefined, }); - }, [loadQuery, organizationId]); + }, [loadQuery, organizationId, searchParams]); if (!queryRef) { return ; diff --git a/apps/console/src/pages/organizations/risks/NewRiskView.tsx b/apps/console/src/pages/organizations/risks/NewRiskView.tsx index 74b7f228d..510c79acc 100644 --- a/apps/console/src/pages/organizations/risks/NewRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/NewRiskView.tsx @@ -33,7 +33,6 @@ import PeopleSelector from "@/components/PeopleSelector"; import { User } from "lucide-react"; import { Suspense } from "react"; import type { NewRiskViewQuery } from "./__generated__/NewRiskViewQuery.graphql"; -import type { NewRiskViewCreateRiskMutation } from "./__generated__/NewRiskViewCreateRiskMutation.graphql"; interface RiskTemplate { name: string; @@ -89,11 +88,11 @@ function NewRiskForm({ const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [inherentLikelihood, setinherentLikelihood] = - useState("MEDIUM"); - const [inherentImpact, setinherentImpact] = useState("MEDIUM"); + useState("OCCASIONAL"); + const [inherentImpact, setinherentImpact] = useState("MODERATE"); const [residualLikelihood, setResidualLikelihood] = - useState("MEDIUM"); - const [residualImpact, setResidualImpact] = useState("MEDIUM"); + useState("OCCASIONAL"); + const [residualImpact, setResidualImpact] = useState("MODERATE"); const [treatment, setTreatment] = useState("MITIGATED"); const [ownerId, setOwnerId] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -125,15 +124,15 @@ function NewRiskForm({ // Map string values to float values const likelihoodToFloat = (value: string): number => { switch (value) { - case "VERY_LOW": + case "IMPROBABLE": return 0.1; - case "LOW": + case "REMOTE": return 0.3; - case "MEDIUM": + case "OCCASIONAL": return 0.5; - case "HIGH": + case "PROBABLE": return 0.7; - case "VERY_HIGH": + case "FREQUENT": return 0.9; default: return 0.5; @@ -142,15 +141,15 @@ function NewRiskForm({ const impactToFloat = (value: string): number => { switch (value) { - case "VERY_LOW": + case "NEGLIGIBLE": return 0.1; case "LOW": return 0.3; - case "MEDIUM": + case "MODERATE": return 0.5; - case "HIGH": + case "SIGNIFICANT": return 0.7; - case "VERY_HIGH": + case "CATASTROPHIC": return 0.9; default: return 0.5; @@ -165,10 +164,10 @@ function NewRiskForm({ // Clear form if "Select a template" is chosen setName(""); setDescription(""); - setinherentLikelihood("MEDIUM"); - setinherentImpact("MEDIUM"); - setResidualLikelihood("MEDIUM"); - setResidualImpact("MEDIUM"); + setinherentLikelihood("OCCASIONAL"); + setinherentImpact("MODERATE"); + setResidualLikelihood("OCCASIONAL"); + setResidualImpact("MODERATE"); setTreatment("MITIGATED"); return; } @@ -197,20 +196,20 @@ function NewRiskForm({ // Helper function to convert float likelihood to string const floatTolikelihood = (value: number): string => { - if (value <= 0.2) return "VERY_LOW"; - if (value <= 0.4) return "LOW"; - if (value <= 0.6) return "MEDIUM"; - if (value <= 0.8) return "HIGH"; - return "VERY_HIGH"; + if (value <= 0.2) return "IMPROBABLE"; + if (value <= 0.4) return "REMOTE"; + if (value <= 0.6) return "OCCASIONAL"; + if (value <= 0.8) return "PROBABLE"; + return "FREQUENT"; }; // Helper function to convert float impact to string const floatToImpact = (value: number): string => { - if (value <= 0.2) return "VERY_LOW"; + if (value <= 0.2) return "NEGLIGIBLE"; if (value <= 0.4) return "LOW"; - if (value <= 0.6) return "MEDIUM"; - if (value <= 0.8) return "HIGH"; - return "VERY_HIGH"; + if (value <= 0.6) return "MODERATE"; + if (value <= 0.8) return "SIGNIFICANT"; + return "CATASTROPHIC"; }; const handleSubmit = (e: React.FormEvent) => { @@ -361,15 +360,15 @@ function NewRiskForm({ value={inherentLikelihood} onValueChange={setinherentLikelihood} > - - + + - - Very Low - Low - Medium - High - Very High + + Improbable + Remote + Occasional + Probable + Frequent
@@ -380,15 +379,15 @@ function NewRiskForm({ value={inherentImpact} onValueChange={setinherentImpact} > - - + + - - Very Low + + Negligible Low - Medium - High - Very High + Moderate + Significant + Catastrophic @@ -429,15 +428,15 @@ function NewRiskForm({ value={residualLikelihood} onValueChange={setResidualLikelihood} > - - + + - - Very Low - Low - Medium - High - Very High + + Improbable + Remote + Occasional + Probable + Frequent @@ -448,15 +447,15 @@ function NewRiskForm({ value={residualImpact} onValueChange={setResidualImpact} > - - + + - - Very Low + + Negligible Low - Medium - High - Very High + Moderate + Significant + Catastrophic diff --git a/apps/console/src/pages/organizations/risks/ShowRiskView.tsx b/apps/console/src/pages/organizations/risks/ShowRiskView.tsx index d622b1e2c..c45e89ddf 100644 --- a/apps/console/src/pages/organizations/risks/ShowRiskView.tsx +++ b/apps/console/src/pages/organizations/risks/ShowRiskView.tsx @@ -210,10 +210,30 @@ const deleteRiskPolicyMappingMutation = graphql` function getRiskSeverity(likelihood: number, impact: number) { const score = likelihood * impact; - if (score >= 0.75) return { level: "High", class: "bg-red-100 text-red-800" }; - if (score >= 0.4) - return { level: "Medium", class: "bg-yellow-100 text-yellow-800" }; - return { level: "Low", class: "bg-green-100 text-green-800" }; + if (score >= 0.75) + return { level: "Catastrophic", class: "bg-red-100 text-red-800" }; + if (score >= 0.5) + return { level: "Critical", class: "bg-orange-100 text-orange-800" }; + if (score >= 0.25) + return { level: "Marginal", class: "bg-yellow-100 text-yellow-800" }; + return { level: "Negligible", class: "bg-green-100 text-green-800" }; +} + +// Add helper functions to convert numerical values to labels +function getLikelihoodLabel(likelihood: number): string { + if (likelihood >= 0.8) return "Very High"; + if (likelihood >= 0.6) return "High"; + if (likelihood >= 0.4) return "Medium"; + if (likelihood >= 0.2) return "Low"; + return "Very Low"; +} + +function getImpactLabel(impact: number): string { + if (impact >= 0.8) return "Critical"; + if (impact >= 0.6) return "Major"; + if (impact >= 0.4) return "Moderate"; + if (impact >= 0.2) return "Minor"; + return "Insignificant"; } function ShowRiskViewContent({ @@ -755,13 +775,13 @@ function ShowRiskViewContent({ Likelihood

- {(risk.inherentLikelihood! * 100).toFixed(0)}% + {getLikelihoodLabel(risk.inherentLikelihood!)}

Impact

- {(risk.inherentImpact! * 100).toFixed(0)}% + {getImpactLabel(risk.inherentImpact!)}

@@ -830,16 +850,12 @@ function ShowRiskViewContent({ Residual Likelihood

- {(risk.residualLikelihood! * 100).toFixed(0)}% + {getLikelihoodLabel(risk.residualLikelihood!)}

{risk.residualLikelihood !== risk.inherentLikelihood - ? `Reduced by ${( - (risk.inherentLikelihood! - - risk.residualLikelihood!) * - 100 - ).toFixed(0)}%` - : "No reduction"} + ? `Mitigated` + : "No mitigation"}

@@ -847,15 +863,12 @@ function ShowRiskViewContent({ Residual Impact

- {(risk.residualImpact! * 100).toFixed(0)}% + {getImpactLabel(risk.residualImpact!)}

{risk.residualImpact !== risk.inherentImpact - ? `Reduced by ${( - (risk.inherentImpact! - risk.residualImpact!) * - 100 - ).toFixed(0)}%` - : "No reduction"} + ? `Mitigated` + : "No mitigation"}