Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/console/src/_locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2659,6 +2659,7 @@
"unplannedWarning_one": "{{count}} linked risk has no treatment plan yet.",
"unplannedWarning_other": "{{count}} linked risks have no treatment plan yet.",
"filter": "{{state}} · L{{likelihood}} × I{{impact}}",
"asOf": "As of",
"search": "Search risks",
"states": { "inherent": "Inherent", "net": "Net", "residual": "Residual" },
"axes": { "likelihood": "Likelihood", "impact": "Impact" },
Expand All @@ -2678,7 +2679,6 @@
"scoreProgress": "{{inherent}} → {{net}} → {{residual}}",
"progress": "{{done}}/{{total}}",
"progressEmpty": "No measures",
"unassigned": "Unassigned",
"actions": {
"tasks": "Tasks",
"edit": "Edit",
Expand All @@ -2694,12 +2694,12 @@
"riskTreatmentPlanListItem": {
"deleteConfirmation": "This will permanently delete the treatment plan for \"{{name}}\". This action cannot be undone.",
"scoreProgress": "{{inherent}} → {{net}} → {{residual}}",
"unassigned": "Unassigned",
"actions": { "measures": "Measures", "edit": "Edit", "delete": "Delete" }
},
"treatmentPlanMeasureList": {
"title": "Measures",
"empty": "No measures linked yet.",
"deleted": "Deleted measure",
"acceptedEmpty": "Accepted risks cannot have linked measures.",
"actions": { "create": "New measure", "link": "Link measure", "unlink": "Unlink", "showMore": "Show more" }
},
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/_locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -5140,6 +5140,7 @@
"unplannedWarning_one": "{{count}} risque lié n’a pas encore de plan de traitement.",
"unplannedWarning_other": "{{count}} risques liés n’ont pas encore de plan de traitement.",
"filter": "{{state}} · L{{likelihood}} × I{{impact}}",
"asOf": "En date du",
"search": "Rechercher des risques",
"states": {
"inherent": "Inhérent",
Expand Down Expand Up @@ -5168,7 +5169,6 @@
"scoreProgress": "{{inherent}} → {{net}} → {{residual}}",
"progress": "{{done}}/{{total}}",
"progressEmpty": "Aucune mesure",
"unassigned": "Non attribué",
"actions": {
"tasks": "Tâches",
"edit": "Modifier",
Expand All @@ -5189,7 +5189,6 @@
"riskTreatmentPlanListItem": {
"deleteConfirmation": "Cela supprimera définitivement le plan de traitement de « {{name}} ». Cette action est irréversible.",
"scoreProgress": "{{inherent}} → {{net}} → {{residual}}",
"unassigned": "Non attribué",
"actions": {
"measures": "Mesures",
"edit": "Modifier",
Expand All @@ -5199,6 +5198,7 @@
"treatmentPlanMeasureList": {
"title": "Mesures",
"empty": "Aucune mesure liée pour le moment.",
"deleted": "Mesure supprimée",
"acceptedEmpty": "Les risques acceptés ne peuvent pas avoir de mesures liées.",
"actions": {
"create": "Nouvelle mesure",
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/_locales/nl-NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -5130,6 +5130,7 @@
"unplannedWarning_one": "{{count}} gekoppeld risico heeft nog geen behandelplan.",
"unplannedWarning_other": "{{count}} gekoppelde risico’s hebben nog geen behandelplan.",
"filter": "{{state}} · L{{likelihood}} × I{{impact}}",
"asOf": "Per",
"search": "Zoek risico's",
"states": {
"inherent": "Inherent",
Expand Down Expand Up @@ -5158,7 +5159,6 @@
"scoreProgress": "{{inherent}} → {{net}} → {{residual}}",
"progress": "{{done}}/{{total}}",
"progressEmpty": "Geen maatregelen",
"unassigned": "Niet toegewezen",
"actions": {
"tasks": "Taken",
"edit": "Bewerken",
Expand All @@ -5179,7 +5179,6 @@
"riskTreatmentPlanListItem": {
"deleteConfirmation": "Dit verwijdert het behandelplan voor \"{{name}}\" permanent. Deze actie kan niet ongedaan worden gemaakt.",
"scoreProgress": "{{inherent}} → {{net}} → {{residual}}",
"unassigned": "Niet toegewezen",
"actions": {
"measures": "Maatregelen",
"edit": "Bewerken",
Expand All @@ -5189,6 +5188,7 @@
"treatmentPlanMeasureList": {
"title": "Maatregelen",
"empty": "Nog geen maatregelen gekoppeld.",
"deleted": "Verwijderde maatregel",
"acceptedEmpty": "Geaccepteerde risico's kunnen geen maatregelen hebben.",
"actions": {
"create": "Nieuwe maatregel",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
// SOFTWARE.

import { getRiskImpacts, getRiskLikelihoods } from "@probo/helpers";
import { Card, RisksChart } from "@probo/ui";
import { Card, IconClock, Input, RisksChart } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";

import type { RiskAnalysisMatrices_analysis$key } from "#/__generated__/core/RiskAnalysisMatrices_analysis.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";

import { asOfDateBounds, clampDateInput, matrixAsOf } from "../_lib/matrixAsOf";
import {
graphqlScoreTypes,
type MatrixCell,
Expand All @@ -36,12 +37,16 @@ import { useMatrixCellFilter } from "../_lib/useMatrixCellFilter";
const severityColors = ["bg-txt-success", "bg-txt-warning", "bg-txt-danger"] as const;

export const riskAnalysisMatricesFragment = graphql`
fragment RiskAnalysisMatrices_analysis on RiskAnalysis {
fragment RiskAnalysisMatrices_analysis on RiskAnalysis
@argumentDefinitions(
asOf: { type: "Datetime", defaultValue: null }
) {
createdAt
matrixSize {
rows
cols
}
matrixCells {
matrixCells(asOf: $asOf) {
type
likelihood
impact
Expand All @@ -52,6 +57,9 @@ export const riskAnalysisMatricesFragment = graphql`

interface RiskAnalysisMatricesProps {
analysisKey: RiskAnalysisMatrices_analysis$key;
asOfDate: string;
isPending: boolean;
onAsOfDateChange: (date: string) => void;
}

function selectedChartCell(
Expand All @@ -65,26 +73,31 @@ function selectedChartCell(
return { likelihood: cell.likelihood, impact: cell.impact };
}

function cellCountsForScoreType(
counts: ReadonlyArray<{
function cellCountsForType(
cells: ReadonlyArray<{
type: string;
likelihood: number;
impact: number;
count: number;
}>,
type: MatrixScoreType,
) {
const graphqlType = graphqlScoreTypes[type];
return counts
.filter(cell => cell.type === graphqlType)
const scoreType = graphqlScoreTypes[type];
return cells
.filter(cell => cell.type === scoreType)
.map(cell => ({
likelihood: cell.likelihood,
impact: cell.impact,
count: cell.count,
}));
}

export function RiskAnalysisMatrices({ analysisKey }: RiskAnalysisMatricesProps) {
export function RiskAnalysisMatrices({
analysisKey,
asOfDate,
isPending,
onAsOfDateChange,
}: RiskAnalysisMatricesProps) {
const { t } = useTranslation();
const organizationId = useOrganizationId();
const { cell, toggleCell } = useMatrixCellFilter();
Expand All @@ -94,7 +107,14 @@ export function RiskAnalysisMatrices({ analysisKey }: RiskAnalysisMatricesProps)
}

const matrixSize = { rows: analysis.matrixSize.rows, cols: analysis.matrixSize.cols };
const counts = analysis.matrixCells ?? [];
const { minDate, maxDate } = asOfDateBounds(analysis.createdAt);
Comment thread
SachaProbo marked this conversation as resolved.

@cubic-dev-ai cubic-dev-ai Bot Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a viewer west of UTC opens an analysis created just after midnight UTC, this minimum permits an as-of instant before the analysis existed and shows an empty snapshot. Align asOfDateBounds and matrixAsOf to the same timezone so the minimum cannot query before creation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/console/src/pages/organizations/risks/risk-analyses/_components/RiskAnalysisMatrices.tsx, line 110:

<comment>When a viewer west of UTC opens an analysis created just after midnight UTC, this minimum permits an as-of instant before the analysis existed and shows an empty snapshot. Align `asOfDateBounds` and `matrixAsOf` to the same timezone so the minimum cannot query before creation.</comment>

<file context>
@@ -94,7 +107,13 @@ export function RiskAnalysisMatrices({ analysisKey }: RiskAnalysisMatricesProps)
 
   const matrixSize = { rows: analysis.matrixSize.rows, cols: analysis.matrixSize.cols };
-  const counts = analysis.matrixCells ?? [];
+  const { minDate, maxDate } = asOfDateBounds(analysis.createdAt);
+  const cells = analysis.matrixCells;
+
</file context>
Fix with cubic

const cells = analysis.matrixCells;
const hasAsOf = matrixAsOf(asOfDate) != null;

const handleAsOfDateChange = (value: string) => {
onAsOfDateChange(clampDateInput(value, minDate, maxDate));
};

const impacts = getRiskImpacts(t, matrixSize.cols);
const likelihoods = getRiskLikelihoods(t, matrixSize.rows);
const formatScale = (items: ReadonlyArray<{ value: number; label: string }>) =>
Expand All @@ -112,11 +132,30 @@ export function RiskAnalysisMatrices({ analysisKey }: RiskAnalysisMatricesProps)

return (
<Card padded className="space-y-6 overflow-visible">
<div className="grid grid-cols-3 gap-6">
<div className="flex items-center justify-end gap-2">
<IconClock size={16} className="text-txt-secondary" />
<label htmlFor="asOf" className="text-sm font-medium text-txt-primary whitespace-nowrap">
{t("riskAnalysisTreatmentPlansPage.asOf")}
</label>
<Input
type="date"
name="asOf"
id="asOf"
className={hasAsOf ? "w-40" : "w-40 text-txt-secondary"}
value={hasAsOf ? asOfDate : ""}
min={minDate}
Comment thread
SachaProbo marked this conversation as resolved.
max={maxDate}
onChange={event => handleAsOfDateChange(event.target.value)}
/>
</div>
<div
aria-busy={isPending}
className={`grid grid-cols-3 gap-6 transition-opacity ${isPending ? "opacity-60" : ""}`}
>
<RisksChart
organizationId={organizationId}
type="inherent"
cellCounts={cellCountsForScoreType(counts, "inherent")}
cellCounts={cellCountsForType(cells, "inherent")}
matrixSize={matrixSize}
variant="bare"
selectedCell={selectedChartCell(cell, "inherent")}
Expand All @@ -125,7 +164,7 @@ export function RiskAnalysisMatrices({ analysisKey }: RiskAnalysisMatricesProps)
<RisksChart
organizationId={organizationId}
type="net"
cellCounts={cellCountsForScoreType(counts, "net")}
cellCounts={cellCountsForType(cells, "net")}
matrixSize={matrixSize}
variant="bare"
selectedCell={selectedChartCell(cell, "net")}
Expand All @@ -134,7 +173,7 @@ export function RiskAnalysisMatrices({ analysisKey }: RiskAnalysisMatricesProps)
<RisksChart
organizationId={organizationId}
type="residual"
cellCounts={cellCountsForScoreType(counts, "residual")}
cellCounts={cellCountsForType(cells, "residual")}
matrixSize={matrixSize}
variant="bare"
selectedCell={selectedChartCell(cell, "residual")}
Expand Down
Loading
Loading