Update risk matrix

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-15 11:16:24 -07:00
parent 7ff083e339
commit a90de1fdde
12 changed files with 771 additions and 585 deletions

View File

@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
### Changed
- More explicit scale, legend and score for risk matrix
## [0.10.1] - 2025-04-14
### Fixed

View File

@@ -5,8 +5,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.75,
"likelihood": 0.8,
"impact": 4,
"likelihood": 4,
"recommendedTreatment": "Transfer"
}
]
@@ -17,8 +17,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 1.0,
"likelihood": 0.3,
"impact": 5,
"likelihood": 2,
"recommendedTreatment": "Transfer"
}
]
@@ -29,8 +29,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.5,
"impact": 3,
"likelihood": 3,
"recommendedTreatment": "Reduce"
}
]
@@ -41,8 +41,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.15,
"impact": 3,
"likelihood": 1,
"recommendedTreatment": "Accept"
}
]
@@ -53,8 +53,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.8,
"impact": 3,
"likelihood": 4,
"recommendedTreatment": "Transfer"
}
]
@@ -65,8 +65,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.3,
"impact": 3,
"likelihood": 2,
"recommendedTreatment": "Reduce"
}
]
@@ -77,8 +77,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.1,
"likelihood": 0.15,
"impact": 1,
"likelihood": 1,
"recommendedTreatment": "Accept"
}
]
@@ -89,8 +89,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.5,
"impact": 3,
"likelihood": 3,
"recommendedTreatment": "Reduce"
}
]
@@ -101,8 +101,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.8,
"impact": 3,
"likelihood": 4,
"recommendedTreatment": "Transfer"
}
]
@@ -113,8 +113,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.75,
"likelihood": 0.5,
"impact": 4,
"likelihood": 3,
"recommendedTreatment": "Transfer"
}
]
@@ -125,8 +125,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.75,
"likelihood": 0.3,
"impact": 4,
"likelihood": 2,
"recommendedTreatment": "Reduce"
}
]
@@ -137,8 +137,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.15,
"impact": 3,
"likelihood": 1,
"recommendedTreatment": "Accept"
}
]
@@ -149,8 +149,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.25,
"likelihood": 0.5,
"impact": 1,
"likelihood": 3,
"recommendedTreatment": "Reduce"
}
]
@@ -161,8 +161,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.5,
"impact": 3,
"likelihood": 3,
"recommendedTreatment": "Reduce"
}
]
@@ -173,8 +173,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 1.0,
"likelihood": 0.5,
"impact": 5,
"likelihood": 3,
"recommendedTreatment": "Transfer"
}
]
@@ -185,8 +185,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.8,
"impact": 3,
"likelihood": 4,
"recommendedTreatment": "Transfer"
}
]
@@ -197,8 +197,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.25,
"likelihood": 0.3,
"impact": 1,
"likelihood": 2,
"recommendedTreatment": "Reduce"
}
]
@@ -209,8 +209,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.8,
"impact": 3,
"likelihood": 4,
"recommendedTreatment": "Transfer"
}
]
@@ -221,8 +221,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.8,
"impact": 3,
"likelihood": 4,
"recommendedTreatment": "Transfer"
}
]
@@ -233,8 +233,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.25,
"likelihood": 1.0,
"impact": 1,
"likelihood": 5,
"recommendedTreatment": "Reduce"
}
]
@@ -245,8 +245,8 @@
"variations": [
{
"context": "B2B SaaS SMB",
"impact": 0.5,
"likelihood": 0.5,
"impact": 3,
"likelihood": 3,
"recommendedTreatment": "Reduce"
}
]

View File

@@ -104,12 +104,10 @@ function EditRiskViewContent({
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [inherentLikelihood, setInherentLikelihood] =
useState<string>("OCCASIONAL");
const [inherentImpact, setInherentImpact] = useState<string>("MODERATE");
const [residualLikelihood, setResidualLikelihood] =
useState<string>("OCCASIONAL");
const [residualImpact, setResidualImpact] = useState<string>("MODERATE");
const [inherentLikelihood, setInherentLikelihood] = useState<number>(3);
const [inherentImpact, setInherentImpact] = useState<number>(3);
const [residualLikelihood, setResidualLikelihood] = useState<number>(3);
const [residualImpact, setResidualImpact] = useState<number>(3);
const [treatment, setTreatment] = useState<RiskTreatment>("MITIGATED");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -117,68 +115,16 @@ function EditRiskViewContent({
const [updateRisk, isInFlight] =
useMutation<EditRiskViewUpdateRiskMutation>(updateRiskMutation);
// Helper function to convert float to likelihood string
const floatToLikelihood = (value: number): string => {
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 "NEGLIGIBLE";
if (value <= 0.4) return "LOW";
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 "IMPROBABLE":
return 0.1;
case "REMOTE":
return 0.3;
case "OCCASIONAL":
return 0.5;
case "PROBABLE":
return 0.7;
case "FREQUENT":
return 0.9;
default:
return 0.5;
}
};
const impactToFloat = (value: string): number => {
switch (value) {
case "NEGLIGIBLE":
return 0.1;
case "LOW":
return 0.3;
case "MODERATE":
return 0.5;
case "SIGNIFICANT":
return 0.7;
case "CATASTROPHIC":
return 0.9;
default:
return 0.5;
}
};
// Initialize form with risk data
useEffect(() => {
if (risk) {
setName(risk.name || "");
setDescription(risk.description || "");
setInherentLikelihood(floatToLikelihood(risk.inherentLikelihood || 0.5));
setInherentImpact(floatToImpact(risk.inherentImpact || 0.5));
setResidualLikelihood(floatToLikelihood(risk.residualLikelihood || 0.5));
setResidualImpact(floatToImpact(risk.residualImpact || 0.5));
// Set values directly as integers
setInherentLikelihood(risk.inherentLikelihood || 3);
setInherentImpact(risk.inherentImpact || 3);
setResidualLikelihood(risk.residualLikelihood || 3);
setResidualImpact(risk.residualImpact || 3);
setTreatment(risk.treatment || "MITIGATED");
setOwnerId(risk.owner?.id || null);
}
@@ -202,10 +148,10 @@ function EditRiskViewContent({
id: riskId!,
name,
description,
inherentLikelihood: likelihoodToFloat(inherentLikelihood),
inherentImpact: impactToFloat(inherentImpact),
residualLikelihood: likelihoodToFloat(residualLikelihood),
residualImpact: impactToFloat(residualImpact),
inherentLikelihood,
inherentImpact,
residualLikelihood,
residualImpact,
treatment,
ownerId: ownerId || undefined,
};
@@ -302,40 +248,44 @@ function EditRiskViewContent({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="inherentLikelihood">
Inherent Likelihood
Inherent Likelihood (1-5)
</Label>
<Select
value={inherentLikelihood}
onValueChange={setInherentLikelihood}
value={inherentLikelihood.toString()}
onValueChange={(value) =>
setInherentLikelihood(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
<SelectValue placeholder="Select likelihood level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
<SelectItem value="1">1 - Improbable</SelectItem>
<SelectItem value="2">2 - Remote</SelectItem>
<SelectItem value="3">3 - Occasional</SelectItem>
<SelectItem value="4">4 - Probable</SelectItem>
<SelectItem value="5">5 - Frequent</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="inherentImpact">Inherent Impact</Label>
<Label htmlFor="inherentImpact">Inherent Impact (1-5)</Label>
<Select
value={inherentImpact}
onValueChange={setInherentImpact}
value={inherentImpact.toString()}
onValueChange={(value) =>
setInherentImpact(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
<SelectValue placeholder="Select impact level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
<SelectItem value="1">1 - Negligible</SelectItem>
<SelectItem value="2">2 - Low</SelectItem>
<SelectItem value="3">3 - Moderate</SelectItem>
<SelectItem value="4">4 - Significant</SelectItem>
<SelectItem value="5">5 - Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>
@@ -376,40 +326,44 @@ function EditRiskViewContent({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residualLikelihood">
Residual Likelihood
Residual Likelihood (1-5)
</Label>
<Select
value={residualLikelihood}
onValueChange={setResidualLikelihood}
value={residualLikelihood.toString()}
onValueChange={(value) =>
setResidualLikelihood(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
<SelectValue placeholder="Select likelihood level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
<SelectItem value="1">1 - Improbable</SelectItem>
<SelectItem value="2">2 - Remote</SelectItem>
<SelectItem value="3">3 - Occasional</SelectItem>
<SelectItem value="4">4 - Probable</SelectItem>
<SelectItem value="5">5 - Frequent</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="residualImpact">Residual Impact</Label>
<Label htmlFor="residualImpact">Residual Impact (1-5)</Label>
<Select
value={residualImpact}
onValueChange={setResidualImpact}
value={residualImpact.toString()}
onValueChange={(value) =>
setResidualImpact(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
<SelectValue placeholder="Select impact level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
<SelectItem value="1">1 - Negligible</SelectItem>
<SelectItem value="2">2 - Low</SelectItem>
<SelectItem value="3">3 - Moderate</SelectItem>
<SelectItem value="4">4 - Significant</SelectItem>
<SelectItem value="5">5 - Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -109,17 +109,11 @@ 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";
if (score >= 20) return "Catastrophic";
if (score >= 12) return "Critical";
if (score >= 5) return "Marginal";
return "Negligible";
};
@@ -207,6 +201,7 @@ const emptyRiskMatrixColors = {
function RiskMatrix({
risks,
isResidual = false,
organizationId,
}: {
risks: Array<{
id: string;
@@ -217,24 +212,13 @@ function RiskMatrix({
residualImpact?: number;
}>;
isResidual?: boolean;
organizationId: string;
}): JSX.Element {
// Define impact ranges for the vertical axis (rows)
const impactRanges: [number, number][] = [
[0.8, 1], // Highest impact - top row
[0.6, 0.8], // High impact
[0.4, 0.6], // Medium impact
[0.2, 0.4], // Low impact
[0, 0.2], // Lowest impact - bottom row
];
// Define impact values for the vertical axis (rows) - from highest (5) to lowest (1)
const impactValues: number[] = [5, 4, 3, 2, 1];
// 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
];
// Define likelihood values for the horizontal axis (columns) - from lowest (1) to highest (5)
const likelihoodValues: number[] = [1, 2, 3, 4, 5];
const impactLabels = [
"Catastrophic",
@@ -253,10 +237,7 @@ function RiskMatrix({
];
// Function to get cell content with risks that fall in this cell
const getCellContent = (
impactRange: [number, number],
likelihoodRange: [number, number]
) => {
const getCellContent = (impactValue: number, likelihoodValue: number) => {
return risks.filter((risk) => {
const likelihood = isResidual
? risk.residualLikelihood ?? risk.inherentLikelihood
@@ -265,12 +246,7 @@ function RiskMatrix({
? risk.residualImpact ?? risk.inherentImpact
: risk.inherentImpact;
return (
likelihood > likelihoodRange[0] &&
likelihood <= likelihoodRange[1] &&
impact > impactRange[0] &&
impact <= impactRange[1]
);
return likelihood === likelihoodValue && impact === impactValue;
});
};
@@ -280,54 +256,63 @@ function RiskMatrix({
const getCellColor = (row: number, col: number, isEmpty: boolean): string => {
const colorSet = isEmpty ? emptyRiskMatrixColors : riskMatrixColors;
// Top row (highest impact - Catastrophic)
if (row === 0) {
if (col <= 1) return colorSet.medium; // Yellow for first two cells
return colorSet.high; // Red for the rest
}
// Hard-coded color matrix where each element [row][col] represents a specific cell
// This follows the 5x5 grid with rows representing impact (5 to 1) and columns representing likelihood (1 to 5)
const colorMatrix = [
// Impact 5 (Catastrophic) - first row
[
colorSet.medium,
colorSet.medium,
colorSet.high,
colorSet.high,
colorSet.high,
],
// Impact 4 (Significant) - second row
[
colorSet.low,
colorSet.medium,
colorSet.medium,
colorSet.high,
colorSet.high,
],
// Impact 3 (Moderate) - third row
[
colorSet.low,
colorSet.low,
colorSet.medium,
colorSet.medium,
colorSet.high,
],
// Impact 2 (Low) - fourth row
[
colorSet.low,
colorSet.low,
colorSet.low,
colorSet.medium,
colorSet.medium,
],
// Impact 1 (Negligible) - fifth row
[colorSet.low, colorSet.low, colorSet.low, colorSet.low, colorSet.medium],
];
// Second row (Significant impact)
if (row === 1) {
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 (Moderate impact)
if (row === 2) {
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 (Low impact)
if (row === 3) {
if (col <= 1) return colorSet.low; // Green for first two
return colorSet.medium; // Yellow for the rest
}
// Bottom row (lowest impact - Negligible)
if (row === 4) {
if (col <= 3) return colorSet.low; // Green for first four
return colorSet.medium; // Yellow for last cell
}
return "bg-gray-100";
return colorMatrix[row][col];
};
// Instead of using refs and tippy, we'll use a component with popover
const RiskCell = ({
rowIndex,
colIndex,
impactRange,
likelihoodRange,
impactValue,
likelihoodValue,
organizationId,
}: {
rowIndex: number;
colIndex: number;
impactRange: [number, number];
likelihoodRange: [number, number];
impactValue: number;
likelihoodValue: number;
organizationId: string;
}) => {
const cellRisks = getCellContent(impactRange, likelihoodRange);
const cellRisks = getCellContent(impactValue, likelihoodValue);
const isEmpty = cellRisks.length === 0;
const cellColor = getCellColor(rowIndex, colIndex, isEmpty);
@@ -354,17 +339,109 @@ function RiskMatrix({
</div>
</td>
</PopoverTrigger>
<PopoverContent className="w-64 p-3" align="center">
<div className="font-medium mb-2">
{cellRisks.length} Risk{cellRisks.length > 1 ? "s" : ""}:
<PopoverContent className="w-72 p-4" align="center">
<div className="font-semibold text-lg mb-2">
{cellRisks.length} Risk{cellRisks.length > 1 ? "s" : ""}
</div>
<ul className="list-disc pl-4 space-y-1">
{cellRisks.map((risk) => (
<li key={risk.id} className="text-sm">
{risk.name}
</li>
))}
</ul>
<div className="bg-gray-50 p-3 rounded-md mb-3 space-y-1">
<div className="grid grid-cols-2 gap-2">
<div className="text-sm">
<span className="text-muted-foreground">Impact:</span>{" "}
<span className="font-medium">{impactValue}</span>
</div>
<div className="text-sm">
<span className="text-muted-foreground">Likelihood:</span>{" "}
<span className="font-medium">{likelihoodValue}</span>
</div>
</div>
<div className="text-sm pt-1 border-t border-gray-200 mt-1">
<span className="text-muted-foreground">Risk Score:</span>
<span className="font-bold ml-1">
{impactValue * likelihoodValue}
</span>
<span
className="ml-2 px-2 py-0.5 text-xs rounded-full font-medium inline-block"
style={{
backgroundColor:
impactValue * likelihoodValue >= 20
? "#ef4444"
: impactValue * likelihoodValue >= 12
? "#f59e0b"
: impactValue * likelihoodValue >= 5
? "#10b981"
: "#94a3b8",
color:
impactValue * likelihoodValue >= 12 ? "white" : "inherit",
}}
>
{impactValue * likelihoodValue >= 20
? "Catastrophic"
: impactValue * likelihoodValue >= 12
? "Critical"
: impactValue * likelihoodValue >= 5
? "Marginal"
: "Negligible"}
</span>
</div>
</div>
{cellRisks.length > 0 && (
<>
<div className="text-sm font-medium mb-1">Items:</div>
<ul className="space-y-1.5 max-h-48 overflow-y-auto">
{cellRisks.map((risk) => (
<li
key={risk.id}
className="text-sm border-l-2 pl-2"
style={{
borderColor:
(isResidual
? (risk.residualImpact || risk.inherentImpact) *
(risk.residualLikelihood || risk.inherentLikelihood)
: risk.inherentImpact * risk.inherentLikelihood) >= 20
? "#ef4444"
: (isResidual
? (risk.residualImpact || risk.inherentImpact) *
(risk.residualLikelihood ||
risk.inherentLikelihood)
: risk.inherentImpact *
risk.inherentLikelihood) >= 12
? "#f59e0b"
: (isResidual
? (risk.residualImpact || risk.inherentImpact) *
(risk.residualLikelihood ||
risk.inherentLikelihood)
: risk.inherentImpact *
risk.inherentLikelihood) >= 5
? "#10b981"
: "#94a3b8",
}}
>
<Link
to={`/organizations/${organizationId}/risks/${risk.id}`}
className="hover:underline hover:text-primary transition-colors flex items-center w-full py-1 rounded hover:bg-gray-100 px-1"
>
<span className="truncate">{risk.name}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="w-3.5 h-3.5 ml-auto flex-shrink-0 text-muted-foreground"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</Link>
</li>
))}
</ul>
</>
)}
</PopoverContent>
</Popover>
);
@@ -387,18 +464,19 @@ function RiskMatrix({
</div>
<table className="w-full border-collapse table-fixed">
<tbody>
{impactRanges.map((impactRange, rowIndex) => (
{impactValues.map((impactValue, rowIndex) => (
<tr key={rowIndex}>
<th className="p-1 text-xs text-center border-0 font-medium w-14 h-14">
{impactLabels[rowIndex]}
<th className="p-1 text-xs text-right border-0 font-medium w-14 h-14">
{impactLabels[rowIndex]} ({impactValue})
</th>
{likelihoodRanges.map((likelihoodRange, colIndex) => (
{likelihoodValues.map((likelihoodValue, colIndex) => (
<RiskCell
key={colIndex}
rowIndex={rowIndex}
colIndex={colIndex}
impactRange={impactRange}
likelihoodRange={likelihoodRange}
impactValue={impactValue}
likelihoodValue={likelihoodValue}
organizationId={organizationId}
/>
))}
</tr>
@@ -412,7 +490,7 @@ function RiskMatrix({
key={index}
className="p-1 text-xs text-center border-t font-medium w-14"
>
{label}
{label} ({likelihoodValues[index]})
</th>
))}
</tr>
@@ -583,7 +661,11 @@ function ListRiskViewContent({
</Label>
</div>
</div>
<RiskMatrix risks={risks} isResidual={showResidualRisk} />
<RiskMatrix
risks={risks}
isResidual={showResidualRisk}
organizationId={organizationId || ""}
/>
</div>
</CardContent>
</Card>
@@ -648,9 +730,36 @@ function ListRiskViewContent({
to={`/organizations/${organizationId}/risks/${risk.id}`}
className="block p-4 h-full w-full"
>
{riskScoreToSeverity(
risk.inherentLikelihood * risk.inherentImpact
)}
<span
className="px-2 py-0.5 text-xs rounded-full font-medium inline-block"
style={{
backgroundColor:
risk.inherentLikelihood *
risk.inherentImpact >=
20
? "#ef4444"
: risk.inherentLikelihood *
risk.inherentImpact >=
12
? "#f59e0b"
: risk.inherentLikelihood *
risk.inherentImpact >=
5
? "#10b981"
: "#94a3b8",
color:
risk.inherentLikelihood *
risk.inherentImpact >=
12
? "white"
: "inherit",
}}
>
{riskScoreToSeverity(
risk.inherentLikelihood * risk.inherentImpact
)}{" "}
({risk.inherentLikelihood * risk.inherentImpact})
</span>
</Link>
</td>
<td className="p-0 align-middle w-1/6 whitespace-nowrap">
@@ -658,11 +767,41 @@ function ListRiskViewContent({
to={`/organizations/${organizationId}/risks/${risk.id}`}
className="block p-4 h-full w-full"
>
{risk.residualLikelihood && risk.residualImpact
? riskScoreToSeverity(
{risk.residualLikelihood && risk.residualImpact ? (
<span
className="px-2 py-0.5 text-xs rounded-full font-medium inline-block"
style={{
backgroundColor:
risk.residualLikelihood *
risk.residualImpact >=
20
? "#ef4444"
: risk.residualLikelihood *
risk.residualImpact >=
12
? "#f59e0b"
: risk.residualLikelihood *
risk.residualImpact >=
5
? "#10b981"
: "#94a3b8",
color:
risk.residualLikelihood *
risk.residualImpact >=
12
? "white"
: "inherit",
}}
>
{riskScoreToSeverity(
risk.residualLikelihood * risk.residualImpact
)}{" "}
({risk.residualLikelihood * risk.residualImpact}
)
: "Not set"}
</span>
) : (
"Not set"
)}
</Link>
</td>
<td className="p-4 align-middle w-[120px]">

View File

@@ -87,12 +87,10 @@ function NewRiskForm({
const data = usePreloadedQuery<NewRiskViewQuery>(newRiskQuery, queryRef);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [inherentLikelihood, setinherentLikelihood] =
useState<string>("OCCASIONAL");
const [inherentImpact, setinherentImpact] = useState<string>("MODERATE");
const [residualLikelihood, setResidualLikelihood] =
useState<string>("OCCASIONAL");
const [residualImpact, setResidualImpact] = useState<string>("MODERATE");
const [inherentLikelihood, setInherentLikelihood] = useState<number>(3);
const [inherentImpact, setInherentImpact] = useState<number>(3);
const [residualLikelihood, setResidualLikelihood] = useState<number>(3);
const [residualImpact, setResidualImpact] = useState<number>(3);
const [treatment, setTreatment] = useState<string>("MITIGATED");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -121,41 +119,6 @@ function NewRiskForm({
loadRiskTemplates();
}, [toast]);
// Map string values to float values
const likelihoodToFloat = (value: string): number => {
switch (value) {
case "IMPROBABLE":
return 0.1;
case "REMOTE":
return 0.3;
case "OCCASIONAL":
return 0.5;
case "PROBABLE":
return 0.7;
case "FREQUENT":
return 0.9;
default:
return 0.5;
}
};
const impactToFloat = (value: string): number => {
switch (value) {
case "NEGLIGIBLE":
return 0.1;
case "LOW":
return 0.3;
case "MODERATE":
return 0.5;
case "SIGNIFICANT":
return 0.7;
case "CATASTROPHIC":
return 0.9;
default:
return 0.5;
}
};
// Handle template selection and prefill form
const handleTemplateChange = (templateId: string) => {
setSelectedTemplate(templateId);
@@ -164,10 +127,10 @@ function NewRiskForm({
// Clear form if "Select a template" is chosen
setName("");
setDescription("");
setinherentLikelihood("OCCASIONAL");
setinherentImpact("MODERATE");
setResidualLikelihood("OCCASIONAL");
setResidualImpact("MODERATE");
setInherentLikelihood(3);
setInherentImpact(3);
setResidualLikelihood(3);
setResidualImpact(3);
setTreatment("MITIGATED");
return;
}
@@ -176,16 +139,19 @@ function NewRiskForm({
if (template) {
setName(template.name);
setDescription(template.description);
// Convert numeric values to string values for the select components
const likelihoodValue = floatTolikelihood(
template.variations[0].likelihood
);
const impactValue = floatToImpact(template.variations[0].impact);
setinherentLikelihood(likelihoodValue);
setinherentImpact(impactValue);
// Convert template values to 1-5 scale
const likelihoodValue =
Math.round(template.variations[0].likelihood * 5) || 3;
const impactValue = Math.round(template.variations[0].impact * 5) || 3;
// Ensure values are in 1-5 range
setInherentLikelihood(Math.min(Math.max(likelihoodValue, 1), 5));
setInherentImpact(Math.min(Math.max(impactValue, 1), 5));
// Set residual values to be the same as initial values by default
setResidualLikelihood(likelihoodValue);
setResidualImpact(impactValue);
setResidualLikelihood(Math.min(Math.max(likelihoodValue, 1), 5));
setResidualImpact(Math.min(Math.max(impactValue, 1), 5));
// Set recommended treatment if available
if (template.variations[0].recommendedTreatment) {
@@ -194,24 +160,6 @@ function NewRiskForm({
}
};
// Helper function to convert float likelihood to string
const floatTolikelihood = (value: number): string => {
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 "NEGLIGIBLE";
if (value <= 0.4) return "LOW";
if (value <= 0.6) return "MODERATE";
if (value <= 0.8) return "SIGNIFICANT";
return "CATASTROPHIC";
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -230,10 +178,10 @@ function NewRiskForm({
organizationId: organizationId!,
name,
description,
inherentLikelihood: likelihoodToFloat(inherentLikelihood),
inherentImpact: impactToFloat(inherentImpact),
residualLikelihood: likelihoodToFloat(residualLikelihood),
residualImpact: impactToFloat(residualImpact),
inherentLikelihood,
inherentImpact,
residualLikelihood,
residualImpact,
treatment,
ownerId: ownerId || undefined,
};
@@ -355,39 +303,43 @@ function NewRiskForm({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="inherentLikelihood">Initial Likelihood</Label>
<Label htmlFor="inherentLikelihood">
Initial Likelihood (1-5)
</Label>
<Select
value={inherentLikelihood}
onValueChange={setinherentLikelihood}
value={inherentLikelihood.toString()}
onValueChange={(value) =>
setInherentLikelihood(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
<SelectValue placeholder="Select likelihood level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
<SelectItem value="1">1 - Improbable</SelectItem>
<SelectItem value="2">2 - Remote</SelectItem>
<SelectItem value="3">3 - Occasional</SelectItem>
<SelectItem value="4">4 - Probable</SelectItem>
<SelectItem value="5">5 - Frequent</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="inherentImpact">Initial Impact</Label>
<Label htmlFor="inherentImpact">Initial Impact (1-5)</Label>
<Select
value={inherentImpact}
onValueChange={setinherentImpact}
value={inherentImpact.toString()}
onValueChange={(value) => setInherentImpact(parseInt(value))}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
<SelectValue placeholder="Select impact level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
<SelectItem value="1">1 - Negligible</SelectItem>
<SelectItem value="2">2 - Low</SelectItem>
<SelectItem value="3">3 - Moderate</SelectItem>
<SelectItem value="4">4 - Significant</SelectItem>
<SelectItem value="5">5 - Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>
@@ -422,40 +374,44 @@ function NewRiskForm({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residualLikelihood">
Residual Likelihood
Residual Likelihood (1-5)
</Label>
<Select
value={residualLikelihood}
onValueChange={setResidualLikelihood}
value={residualLikelihood.toString()}
onValueChange={(value) =>
setResidualLikelihood(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select likelihood" />
<SelectValue placeholder="Select likelihood level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="IMPROBABLE">Improbable</SelectItem>
<SelectItem value="REMOTE">Remote</SelectItem>
<SelectItem value="OCCASIONAL">Occasional</SelectItem>
<SelectItem value="PROBABLE">Probable</SelectItem>
<SelectItem value="FREQUENT">Frequent</SelectItem>
<SelectItem value="1">1 - Improbable</SelectItem>
<SelectItem value="2">2 - Remote</SelectItem>
<SelectItem value="3">3 - Occasional</SelectItem>
<SelectItem value="4">4 - Probable</SelectItem>
<SelectItem value="5">5 - Frequent</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="residualImpact">Residual Impact</Label>
<Label htmlFor="residualImpact">Residual Impact (1-5)</Label>
<Select
value={residualImpact}
onValueChange={setResidualImpact}
value={residualImpact.toString()}
onValueChange={(value) =>
setResidualImpact(parseInt(value))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select impact" />
<SelectValue placeholder="Select impact level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NEGLIGIBLE">Negligible</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MODERATE">Moderate</SelectItem>
<SelectItem value="SIGNIFICANT">Significant</SelectItem>
<SelectItem value="CATASTROPHIC">Catastrophic</SelectItem>
<SelectItem value="1">1 - Negligible</SelectItem>
<SelectItem value="2">2 - Low</SelectItem>
<SelectItem value="3">3 - Moderate</SelectItem>
<SelectItem value="4">4 - Significant</SelectItem>
<SelectItem value="5">5 - Catastrophic</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -210,30 +210,102 @@ const deleteRiskPolicyMappingMutation = graphql`
function getRiskSeverity(likelihood: number, impact: number) {
const score = likelihood * impact;
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" };
if (score >= 20)
return {
level: "Catastrophic",
score: score,
style: {
backgroundColor: "#ef4444",
color: "white",
},
};
if (score >= 12)
return {
level: "Critical",
score: score,
style: {
backgroundColor: "#f59e0b",
color: "white",
},
};
if (score >= 5)
return {
level: "Marginal",
score: score,
style: {
backgroundColor: "#10b981",
color: "inherit",
},
};
return {
level: "Negligible",
score: score,
style: {
backgroundColor: "#94a3b8",
color: "inherit",
},
};
}
// Add helper functions to convert numerical values to labels
function getLikelihoodLabel(likelihood: number): string {
if (likelihood >= 0.8) return "Frequent";
if (likelihood >= 0.6) return "Probable";
if (likelihood >= 0.4) return "Occasional";
if (likelihood >= 0.2) return "Remote";
return "Improbable";
function getLikelihoodLabel(likelihood: number): {
label: string;
style: React.CSSProperties;
} {
if (likelihood === 5)
return {
label: "Frequent (5)",
style: { backgroundColor: "#ef4444", color: "white" },
};
if (likelihood === 4)
return {
label: "Probable (4)",
style: { backgroundColor: "#f59e0b", color: "white" },
};
if (likelihood === 3)
return {
label: "Occasional (3)",
style: { backgroundColor: "#eab308", color: "black" },
};
if (likelihood === 2)
return {
label: "Remote (2)",
style: { backgroundColor: "#10b981", color: "black" },
};
return {
label: "Improbable (1)",
style: { backgroundColor: "#94a3b8", color: "black" },
};
}
function getImpactLabel(impact: number): string {
if (impact >= 0.8) return "Catastrophic";
if (impact >= 0.6) return "Significant";
if (impact >= 0.4) return "Moderate";
if (impact >= 0.2) return "Low";
return "Negligible";
function getImpactLabel(impact: number): {
label: string;
style: React.CSSProperties;
} {
if (impact === 5)
return {
label: "Catastrophic (5)",
style: { backgroundColor: "#ef4444", color: "white" },
};
if (impact === 4)
return {
label: "Significant (4)",
style: { backgroundColor: "#f59e0b", color: "white" },
};
if (impact === 3)
return {
label: "Moderate (3)",
style: { backgroundColor: "#eab308", color: "black" },
};
if (impact === 2)
return {
label: "Low (2)",
style: { backgroundColor: "#10b981", color: "black" },
};
return {
label: "Negligible (1)",
style: { backgroundColor: "#94a3b8", color: "black" },
};
}
function ShowRiskViewContent({
@@ -765,65 +837,43 @@ function ShowRiskViewContent({
>
<div className="space-y-6">
<Card>
<CardHeader>
<CardHeader className="pb-2">
<CardTitle>Risk Overview</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<h3 className="text-sm font-medium text-secondary">
Likelihood
</h3>
<p className="mt-1 text-lg">
{getLikelihoodLabel(risk.inherentLikelihood!)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-secondary">Impact</h3>
<p className="mt-1 text-lg">
{getImpactLabel(risk.inherentImpact!)}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-secondary">Severity</h3>
<p className="mt-1">
<Badge className={severity.class}>{severity.level}</Badge>
</p>
</div>
</div>
{/* Treatment and Owner section */}
<div className="mt-6 border-t pt-4">
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<h3 className="text-sm font-medium text-secondary">
Treatment
</h3>
<div className="mt-1 text-lg capitalize flex items-center">
{risk.treatment === "MITIGATED" && (
<Shield className="h-5 w-5 mr-2 text-blue-500" />
)}
{risk.treatment === "TRANSFERRED" && (
<Handshake className="h-5 w-5 mr-2 text-purple-500" />
)}
{risk.treatment === "AVOIDED" && (
<Ban className="h-5 w-5 mr-2 text-red-500" />
)}
{risk.treatment === "ACCEPTED" && (
<ShieldCheck className="h-5 w-5 mr-2 text-green-500" />
)}
<span>
<div className="space-y-8">
{/* Risk Management Overview */}
<div className="flex flex-col md:flex-row gap-5">
<div className="flex items-center">
{risk.treatment === "MITIGATED" && (
<Shield className="h-6 w-6 mr-3 text-blue-500" />
)}
{risk.treatment === "TRANSFERRED" && (
<Handshake className="h-6 w-6 mr-3 text-purple-500" />
)}
{risk.treatment === "AVOIDED" && (
<Ban className="h-6 w-6 mr-3 text-red-500" />
)}
{risk.treatment === "ACCEPTED" && (
<ShieldCheck className="h-6 w-6 mr-3 text-green-500" />
)}
<div>
<p className="text-xs text-slate-500 uppercase">
Treatment
</p>
<p className="text-base font-medium capitalize mt-1">
{risk.treatment ? risk.treatment.toLowerCase() : "N/A"}
</span>
</p>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-secondary">
Risk Owner
</h3>
<div className="mt-1 text-lg flex items-center">
<User className="h-5 w-5 mr-2 text-gray-500" />
<span>
<div className="flex items-center">
<User className="h-6 w-6 mr-3 text-slate-500" />
<div>
<p className="text-xs text-slate-500 uppercase">
Risk Owner
</p>
<p className="text-base font-medium mt-1 truncate">
{risk.owner ? (
<Link
to={`/organizations/${organizationId}/people/${risk.owner.id}`}
@@ -834,57 +884,127 @@ function ShowRiskViewContent({
) : (
"Unassigned"
)}
</span>
</p>
</div>
</div>
<div></div>
</div>
</div>
{/* Residual risk section */}
<div className="mt-6 border-t pt-4">
<h3 className="text-md font-semibold mb-3">Residual Risk</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div>
<h3 className="text-sm font-medium text-secondary">
Residual Likelihood
</h3>
<p className="mt-1 text-lg">
{getLikelihoodLabel(risk.residualLikelihood!)}
</p>
<p className="text-xs text-secondary mt-1">
{risk.residualLikelihood !== risk.inherentLikelihood
? `Mitigated`
: "No mitigation"}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-secondary">
Residual Impact
</h3>
<p className="mt-1 text-lg">
{getImpactLabel(risk.residualImpact!)}
</p>
<p className="text-xs text-secondary mt-1">
{risk.residualImpact !== risk.inherentImpact
? `Mitigated`
: "No mitigation"}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-secondary">
Residual Severity
</h3>
<p className="mt-1">
<Badge className={residualSeverity.class}>
{residualSeverity.level}
</Badge>
</p>
<p className="text-xs text-secondary mt-1">
{mesures.length} mesure
{mesures.length !== 1 ? "s" : ""} applied
</p>
{/* Risk Assessment */}
<div>
<h3 className="text-sm font-medium mb-6">Risk Assessment</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Initial Assessment */}
<div>
<h4 className="text-sm font-medium mb-5 flex items-center">
<span className="w-3 h-3 rounded-full bg-orange-500 mr-2"></span>
Initial Assessment
</h4>
<div className="grid grid-cols-3 gap-6">
<div>
<p className="text-sm text-slate-600 mb-2">
Likelihood
</p>
<div
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={
getLikelihoodLabel(risk.inherentLikelihood!).style
}
>
{getLikelihoodLabel(risk.inherentLikelihood!).label}
</div>
</div>
<div>
<p className="text-sm text-slate-600 mb-2">Impact</p>
<div
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={getImpactLabel(risk.inherentImpact!).style}
>
{getImpactLabel(risk.inherentImpact!).label}
</div>
</div>
<div>
<p className="text-sm text-slate-600 mb-2">Severity</p>
<div
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={severity.style}
>
{severity.level} (
{risk.inherentLikelihood! * risk.inherentImpact!})
</div>
</div>
</div>
</div>
{/* Residual Assessment */}
<div>
<h4 className="text-sm font-medium mb-5 flex items-center">
<span className="w-3 h-3 rounded-full bg-green-500 mr-2"></span>
After Treatment
</h4>
<div className="grid grid-cols-3 gap-6">
<div>
<p className="text-sm text-slate-600 mb-2">
Likelihood
</p>
<div
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={
getLikelihoodLabel(risk.residualLikelihood!).style
}
>
{getLikelihoodLabel(risk.residualLikelihood!).label}
</div>
</div>
<div>
<p className="text-sm text-slate-600 mb-2">Impact</p>
<div
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={getImpactLabel(risk.residualImpact!).style}
>
{getImpactLabel(risk.residualImpact!).label}
</div>
</div>
<div>
<p className="text-sm text-slate-600 mb-2">Severity</p>
<div
className="px-5 py-2 rounded-md font-medium text-sm flex justify-center items-center whitespace-nowrap"
style={residualSeverity.style}
>
{residualSeverity.level} (
{risk.residualLikelihood! * risk.residualImpact!})
</div>
</div>
</div>
</div>
</div>
{/* Risk Reduction Summary */}
{(risk.inherentLikelihood !== risk.residualLikelihood ||
risk.inherentImpact !== risk.residualImpact) && (
<div className="mt-6 text-center pt-4 border-t">
<p className="text-sm text-slate-600">
Risk severity reduced from{" "}
<span className="font-semibold">
{severity.level} (
{risk.inherentLikelihood! * risk.inherentImpact!})
</span>{" "}
to{" "}
<span className="font-semibold">
{residualSeverity.level} (
{risk.residualLikelihood! * risk.residualImpact!})
</span>
{risk.treatment === "MITIGATED" &&
risk.mesures?.edges &&
risk.mesures.edges.length > 0 &&
` with ${risk.mesures.edges.length} ${
risk.mesures.edges.length === 1
? "measure"
: "measures"
}`}
</p>
</div>
)}
</div>
</div>
</CardContent>
@@ -910,7 +1030,7 @@ function ShowRiskViewContent({
Link Mesure
</Button>
</div>
{risk.mesures?.edges?.length && risk.mesures?.edges?.length > 0 ? (
{risk.mesures?.edges && risk.mesures.edges.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>

View File

@@ -0,0 +1,44 @@
ALTER TABLE risks ADD COLUMN inherent_likelihood_int INTEGER;
ALTER TABLE risks ADD COLUMN inherent_impact_int INTEGER;
ALTER TABLE risks ADD COLUMN residual_likelihood_int INTEGER;
ALTER TABLE risks ADD COLUMN residual_impact_int INTEGER;
UPDATE risks SET
inherent_likelihood_int = CASE
WHEN inherent_likelihood <= 0.20 THEN 1
WHEN inherent_likelihood <= 0.40 THEN 2
WHEN inherent_likelihood <= 0.60 THEN 3
WHEN inherent_likelihood <= 0.80 THEN 4
ELSE 5
END,
inherent_impact_int = CASE
WHEN inherent_impact <= 0.20 THEN 1
WHEN inherent_impact <= 0.40 THEN 2
WHEN inherent_impact <= 0.60 THEN 3
WHEN inherent_impact <= 0.80 THEN 4
ELSE 5
END,
residual_likelihood_int = CASE
WHEN residual_likelihood <= 0.20 THEN 1
WHEN residual_likelihood <= 0.40 THEN 2
WHEN residual_likelihood <= 0.60 THEN 3
WHEN residual_likelihood <= 0.80 THEN 4
ELSE 5
END,
residual_impact_int = CASE
WHEN residual_impact <= 0.20 THEN 1
WHEN residual_impact <= 0.40 THEN 2
WHEN residual_impact <= 0.60 THEN 3
WHEN residual_impact <= 0.80 THEN 4
ELSE 5
END;
ALTER TABLE risks DROP COLUMN inherent_likelihood;
ALTER TABLE risks DROP COLUMN inherent_impact;
ALTER TABLE risks DROP COLUMN residual_likelihood;
ALTER TABLE risks DROP COLUMN residual_impact;
ALTER TABLE risks RENAME COLUMN inherent_likelihood_int TO inherent_likelihood;
ALTER TABLE risks RENAME COLUMN inherent_impact_int TO inherent_impact;
ALTER TABLE risks RENAME COLUMN residual_likelihood_int TO residual_likelihood;
ALTER TABLE risks RENAME COLUMN residual_impact_int TO residual_impact;

View File

@@ -34,10 +34,10 @@ type (
Description string `db:"description"`
Treatment RiskTreatment `db:"treatment"`
OwnerID *gid.GID `db:"owner_id"`
InherentLikelihood float64 `db:"inherent_likelihood"`
InherentImpact float64 `db:"inherent_impact"`
ResidualLikelihood float64 `db:"residual_likelihood"`
ResidualImpact float64 `db:"residual_impact"`
InherentLikelihood int `db:"inherent_likelihood"`
InherentImpact int `db:"inherent_impact"`
ResidualLikelihood int `db:"residual_likelihood"`
ResidualImpact int `db:"residual_impact"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
@@ -54,11 +54,11 @@ func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey {
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (r *Risk) InherentSeverity() float64 {
func (r *Risk) InherentSeverity() int {
return r.InherentLikelihood * r.InherentImpact
}
func (r *Risk) ResidualSeverity() float64 {
func (r *Risk) ResidualSeverity() int {
return r.ResidualLikelihood * r.ResidualImpact
}

View File

@@ -36,10 +36,10 @@ type (
Description string
Treatment coredata.RiskTreatment
OwnerID *gid.GID
InherentLikelihood float64
InherentImpact float64
ResidualLikelihood *float64
ResidualImpact *float64
InherentLikelihood int
InherentImpact int
ResidualLikelihood *int
ResidualImpact *int
}
UpdateRiskRequest struct {
@@ -48,10 +48,10 @@ type (
Description *string
Treatment *coredata.RiskTreatment
OwnerID *gid.GID
InherentLikelihood *float64
InherentImpact *float64
ResidualLikelihood *float64
ResidualImpact *float64
InherentLikelihood *int
InherentImpact *int
ResidualLikelihood *int
ResidualImpact *int
}
)

View File

@@ -628,12 +628,12 @@ type Risk implements Node {
name: String!
description: String!
treatment: RiskTreatment!
inherentLikelihood: Float!
inherentImpact: Float!
inherentSeverity: Float!
residualLikelihood: Float!
residualImpact: Float!
residualSeverity: Float!
inherentLikelihood: Int!
inherentImpact: Int!
inherentSeverity: Int!
residualLikelihood: Int!
residualImpact: Int!
residualSeverity: Int!
owner: People @goField(forceResolver: true)
@@ -1092,10 +1092,10 @@ input CreateRiskInput {
description: String!
ownerId: ID
treatment: RiskTreatment!
inherentLikelihood: Float!
inherentImpact: Float!
residualLikelihood: Float
residualImpact: Float
inherentLikelihood: Int!
inherentImpact: Int!
residualLikelihood: Int
residualImpact: Int
}
input UpdateRiskInput {
@@ -1104,10 +1104,10 @@ input UpdateRiskInput {
description: String
ownerId: ID
treatment: RiskTreatment
inherentLikelihood: Float
inherentImpact: Float
residualLikelihood: Float
residualImpact: Float
inherentLikelihood: Int
inherentImpact: Int
residualLikelihood: Int
residualImpact: Int
}
input DeleteRiskInput {

View File

@@ -3739,12 +3739,12 @@ type Risk implements Node {
name: String!
description: String!
treatment: RiskTreatment!
inherentLikelihood: Float!
inherentImpact: Float!
inherentSeverity: Float!
residualLikelihood: Float!
residualImpact: Float!
residualSeverity: Float!
inherentLikelihood: Int!
inherentImpact: Int!
inherentSeverity: Int!
residualLikelihood: Int!
residualImpact: Int!
residualSeverity: Int!
owner: People @goField(forceResolver: true)
@@ -4203,10 +4203,10 @@ input CreateRiskInput {
description: String!
ownerId: ID
treatment: RiskTreatment!
inherentLikelihood: Float!
inherentImpact: Float!
residualLikelihood: Float
residualImpact: Float
inherentLikelihood: Int!
inherentImpact: Int!
residualLikelihood: Int
residualImpact: Int
}
input UpdateRiskInput {
@@ -4215,10 +4215,10 @@ input UpdateRiskInput {
description: String
ownerId: ID
treatment: RiskTreatment
inherentLikelihood: Float
inherentImpact: Float
residualLikelihood: Float
residualImpact: Float
inherentLikelihood: Int
inherentImpact: Int
residualLikelihood: Int
residualImpact: Int
}
input DeleteRiskInput {
@@ -16907,9 +16907,9 @@ func (ec *executionContext) _Risk_inherentLikelihood(ctx context.Context, field
}
return graphql.Null
}
res := resTmp.(float64)
res := resTmp.(int)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_inherentLikelihood(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -16919,7 +16919,7 @@ func (ec *executionContext) fieldContext_Risk_inherentLikelihood(_ context.Conte
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
@@ -16951,9 +16951,9 @@ func (ec *executionContext) _Risk_inherentImpact(ctx context.Context, field grap
}
return graphql.Null
}
res := resTmp.(float64)
res := resTmp.(int)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_inherentImpact(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -16963,7 +16963,7 @@ func (ec *executionContext) fieldContext_Risk_inherentImpact(_ context.Context,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
@@ -16995,9 +16995,9 @@ func (ec *executionContext) _Risk_inherentSeverity(ctx context.Context, field gr
}
return graphql.Null
}
res := resTmp.(float64)
res := resTmp.(int)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_inherentSeverity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -17007,7 +17007,7 @@ func (ec *executionContext) fieldContext_Risk_inherentSeverity(_ context.Context
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
@@ -17039,9 +17039,9 @@ func (ec *executionContext) _Risk_residualLikelihood(ctx context.Context, field
}
return graphql.Null
}
res := resTmp.(float64)
res := resTmp.(int)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_residualLikelihood(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -17051,7 +17051,7 @@ func (ec *executionContext) fieldContext_Risk_residualLikelihood(_ context.Conte
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
@@ -17083,9 +17083,9 @@ func (ec *executionContext) _Risk_residualImpact(ctx context.Context, field grap
}
return graphql.Null
}
res := resTmp.(float64)
res := resTmp.(int)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_residualImpact(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -17095,7 +17095,7 @@ func (ec *executionContext) fieldContext_Risk_residualImpact(_ context.Context,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
@@ -17127,9 +17127,9 @@ func (ec *executionContext) _Risk_residualSeverity(ctx context.Context, field gr
}
return graphql.Null
}
res := resTmp.(float64)
res := resTmp.(int)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Risk_residualSeverity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -17139,7 +17139,7 @@ func (ec *executionContext) fieldContext_Risk_residualSeverity(_ context.Context
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
@@ -24054,28 +24054,28 @@ func (ec *executionContext) unmarshalInputCreateRiskInput(ctx context.Context, o
it.Treatment = data
case "inherentLikelihood":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inherentLikelihood"))
data, err := ec.unmarshalNFloat2float64(ctx, v)
data, err := ec.unmarshalNInt2int(ctx, v)
if err != nil {
return it, err
}
it.InherentLikelihood = data
case "inherentImpact":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inherentImpact"))
data, err := ec.unmarshalNFloat2float64(ctx, v)
data, err := ec.unmarshalNInt2int(ctx, v)
if err != nil {
return it, err
}
it.InherentImpact = data
case "residualLikelihood":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("residualLikelihood"))
data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v)
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
it.ResidualLikelihood = data
case "residualImpact":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("residualImpact"))
data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v)
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
@@ -25605,28 +25605,28 @@ func (ec *executionContext) unmarshalInputUpdateRiskInput(ctx context.Context, o
it.Treatment = data
case "inherentLikelihood":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inherentLikelihood"))
data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v)
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
it.InherentLikelihood = data
case "inherentImpact":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inherentImpact"))
data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v)
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
it.InherentImpact = data
case "residualLikelihood":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("residualLikelihood"))
data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v)
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
it.ResidualLikelihood = data
case "residualImpact":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("residualImpact"))
data, err := ec.unmarshalOFloat2ᚖfloat64(ctx, v)
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
@@ -32508,21 +32508,6 @@ var (
}
)
func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v any) (float64, error) {
res, err := graphql.UnmarshalFloatContext(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler {
res := graphql.MarshalFloatContext(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
}
return graphql.WrapContextMarshaler(ctx, res)
}
func (ec *executionContext) marshalNFramework2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐFramework(ctx context.Context, sel ast.SelectionSet, v *types.Framework) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -34555,22 +34540,6 @@ func (ec *executionContext) unmarshalOEvidenceOrder2ᚖgithubᚗcomᚋgetprobo
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v any) (*float64, error) {
if v == nil {
return nil, nil
}
res, err := graphql.UnmarshalFloatContext(ctx, v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOFloat2ᚖfloat64(ctx context.Context, sel ast.SelectionSet, v *float64) graphql.Marshaler {
if v == nil {
return graphql.Null
}
res := graphql.MarshalFloatContext(*v)
return graphql.WrapContextMarshaler(ctx, res)
}
func (ec *executionContext) unmarshalOFrameworkOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐFrameworkOrderBy(ctx context.Context, v any) (*types.FrameworkOrderBy, error) {
if v == nil {
return nil, nil

View File

@@ -152,10 +152,10 @@ type CreateRiskInput struct {
Description string `json:"description"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
Treatment coredata.RiskTreatment `json:"treatment"`
InherentLikelihood float64 `json:"inherentLikelihood"`
InherentImpact float64 `json:"inherentImpact"`
ResidualLikelihood *float64 `json:"residualLikelihood,omitempty"`
ResidualImpact *float64 `json:"residualImpact,omitempty"`
InherentLikelihood int `json:"inherentLikelihood"`
InherentImpact int `json:"inherentImpact"`
ResidualLikelihood *int `json:"residualLikelihood,omitempty"`
ResidualImpact *int `json:"residualImpact,omitempty"`
}
type CreateRiskMesureMappingInput struct {
@@ -562,12 +562,12 @@ type Risk struct {
Name string `json:"name"`
Description string `json:"description"`
Treatment coredata.RiskTreatment `json:"treatment"`
InherentLikelihood float64 `json:"inherentLikelihood"`
InherentImpact float64 `json:"inherentImpact"`
InherentSeverity float64 `json:"inherentSeverity"`
ResidualLikelihood float64 `json:"residualLikelihood"`
ResidualImpact float64 `json:"residualImpact"`
ResidualSeverity float64 `json:"residualSeverity"`
InherentLikelihood int `json:"inherentLikelihood"`
InherentImpact int `json:"inherentImpact"`
InherentSeverity int `json:"inherentSeverity"`
ResidualLikelihood int `json:"residualLikelihood"`
ResidualImpact int `json:"residualImpact"`
ResidualSeverity int `json:"residualSeverity"`
Owner *People `json:"owner,omitempty"`
Mesures *MesureConnection `json:"mesures"`
Policies *PolicyConnection `json:"policies"`
@@ -691,10 +691,10 @@ type UpdateRiskInput struct {
Description *string `json:"description,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
Treatment *coredata.RiskTreatment `json:"treatment,omitempty"`
InherentLikelihood *float64 `json:"inherentLikelihood,omitempty"`
InherentImpact *float64 `json:"inherentImpact,omitempty"`
ResidualLikelihood *float64 `json:"residualLikelihood,omitempty"`
ResidualImpact *float64 `json:"residualImpact,omitempty"`
InherentLikelihood *int `json:"inherentLikelihood,omitempty"`
InherentImpact *int `json:"inherentImpact,omitempty"`
ResidualLikelihood *int `json:"residualLikelihood,omitempty"`
ResidualImpact *int `json:"residualImpact,omitempty"`
}
type UpdateRiskPayload struct {