Add risk charts
Signed-off-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
fc45d2f00d
commit
08bf65d919
@@ -29,6 +29,7 @@ import type { FieldErrors } from "react-hook-form";
|
|||||||
import { useMutationWithToasts } from "../../../hooks/useMutationWithToasts";
|
import { useMutationWithToasts } from "../../../hooks/useMutationWithToasts";
|
||||||
import type { FormRiskDialogMutation } from "./__generated__/FormRiskDialogMutation.graphql";
|
import type { FormRiskDialogMutation } from "./__generated__/FormRiskDialogMutation.graphql";
|
||||||
import type { FormRiskDialogUpdateRiskMutation } from "./__generated__/FormRiskDialogUpdateRiskMutation.graphql";
|
import type { FormRiskDialogUpdateRiskMutation } from "./__generated__/FormRiskDialogUpdateRiskMutation.graphql";
|
||||||
|
import { getRiskImpacts, getRiskLikelihoods } from "@probo/helpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
trigger?: ReactNode;
|
trigger?: ReactNode;
|
||||||
@@ -276,11 +277,11 @@ function ImpactAndLikelihood({
|
|||||||
placeholder={__("Select impact level")}
|
placeholder={__("Select impact level")}
|
||||||
error={errors?.[`${prefix}Impact`]?.message}
|
error={errors?.[`${prefix}Impact`]?.message}
|
||||||
>
|
>
|
||||||
<Option value="1">1 - Negligible</Option>
|
{getRiskImpacts(__).map((i) => (
|
||||||
<Option value="2">2 - Low</Option>
|
<Option key={i.value} value={i.value}>
|
||||||
<Option value="3">3 - Moderate</Option>
|
{i.value} - {i.label}
|
||||||
<Option value="4">4 - Significant</Option>
|
</Option>
|
||||||
<Option value="5">5 - Catastrophic</Option>
|
))}
|
||||||
</ControlledField>
|
</ControlledField>
|
||||||
<ControlledField
|
<ControlledField
|
||||||
control={control}
|
control={control}
|
||||||
@@ -290,11 +291,11 @@ function ImpactAndLikelihood({
|
|||||||
placeholder={__("Select likelihood level")}
|
placeholder={__("Select likelihood level")}
|
||||||
error={errors?.[`${prefix}Likelihood`]?.message}
|
error={errors?.[`${prefix}Likelihood`]?.message}
|
||||||
>
|
>
|
||||||
<Option value="1">1 - Improbable</Option>
|
{getRiskLikelihoods(__).map((l) => (
|
||||||
<Option value="2">2 - Remote</Option>
|
<Option key={l.value} value={l.value}>
|
||||||
<Option value="3">3 - Occasional</Option>
|
{l.value} - {l.label}
|
||||||
<Option value="4">4 - Probable</Option>
|
</Option>
|
||||||
<Option value="5">5 - Frequent</Option>
|
))}
|
||||||
</ControlledField>
|
</ControlledField>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
IconTrashCan,
|
IconTrashCan,
|
||||||
IconPencil,
|
IconPencil,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
RisksChart,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { IconPlusLarge } from "@probo/ui";
|
import { IconPlusLarge } from "@probo/ui";
|
||||||
@@ -127,6 +128,19 @@ export default function RisksPage() {
|
|||||||
onSuccess={() => setEditedRisk(null)}
|
onSuccess={() => setEditedRisk(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<RisksChart
|
||||||
|
organizationId={organizationId}
|
||||||
|
type="inherent"
|
||||||
|
risks={risks}
|
||||||
|
/>
|
||||||
|
<RisksChart
|
||||||
|
organizationId={organizationId}
|
||||||
|
type="residual"
|
||||||
|
risks={risks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
|
|||||||
20
packages/helpers/src/array.ts
Normal file
20
packages/helpers/src/array.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export function times<T>(n: number, cb: (i: number) => T): T[] {
|
||||||
|
return Array.from({ length: n }, (_, i) => cb(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupBy<T>(
|
||||||
|
arr: T[],
|
||||||
|
key: (item: T) => string,
|
||||||
|
): Record<string, T[]> {
|
||||||
|
return arr.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
const k = key(item);
|
||||||
|
if (!acc[k]) {
|
||||||
|
acc[k] = [];
|
||||||
|
}
|
||||||
|
acc[k].push(item);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, T[]>,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
export { objectKeys } from "./object";
|
export { objectKeys } from "./object";
|
||||||
export { sprintf } from "./string";
|
export { sprintf } from "./string";
|
||||||
|
export { getRiskImpacts, getRiskLikelihoods } from "./risk";
|
||||||
|
export { times, groupBy } from "./array";
|
||||||
|
export { randomInt } from "./number";
|
||||||
|
|||||||
3
packages/helpers/src/number.ts
Normal file
3
packages/helpers/src/number.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function randomInt(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
51
packages/helpers/src/risk.ts
Normal file
51
packages/helpers/src/risk.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
type Translator = (s: string) => string;
|
||||||
|
|
||||||
|
export function getRiskImpacts(__: Translator) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: __("Negligible"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: __("Low"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: __("Moderate"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: __("Significant"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: __("Catastrophic"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRiskLikelihoods(__: Translator) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: __("Improbable"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: __("Remote"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: __("Occasional"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: __("Probable"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: __("Frequent"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -37,8 +37,8 @@ const button = tv({
|
|||||||
|
|
||||||
type Props = PropsWithChildren<
|
type Props = PropsWithChildren<
|
||||||
{
|
{
|
||||||
icon?: FC<{ size: number }>;
|
icon?: FC<{ size: number; className?: string }>;
|
||||||
iconAfter?: FC<{ size: number }>;
|
iconAfter?: FC<{ size: number; className?: string }>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
variant?:
|
variant?:
|
||||||
@@ -71,9 +71,11 @@ export const Button = (props: Props) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={button({ ...props, empty: !children })}
|
className={button({ ...props, empty: !children })}
|
||||||
>
|
>
|
||||||
{IconComponent && <IconComponent size={16} />}
|
{IconComponent && <IconComponent size={16} className="flex-none" />}
|
||||||
{children}
|
{children}
|
||||||
{IconAfterComponent && <IconAfterComponent size={16} />}
|
{IconAfterComponent && (
|
||||||
|
<IconAfterComponent size={16} className="flex-none" />
|
||||||
|
)}
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ export function Dropdown({ children, toggle, className }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownSeparator() {
|
export function DropdownSeparator({ className }: { className?: string }) {
|
||||||
return <DropdownMenu.Separator className="h-[1px] bg-border-low my-2" />;
|
return (
|
||||||
|
<DropdownMenu.Separator
|
||||||
|
className={clsx("h-[1px] bg-border-low my-2", className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type DropdownItemProps = PropsWithChildren<{
|
type DropdownItemProps = PropsWithChildren<{
|
||||||
@@ -86,7 +90,9 @@ export function DropdownItem({
|
|||||||
children
|
children
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{IconComponent && <IconComponent size={16} />}
|
{IconComponent && (
|
||||||
|
<IconComponent size={16} className="flex-none" />
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function Table({ children }: PropsWithChildren) {
|
|||||||
|
|
||||||
export function Thead({ children }: PropsWithChildren) {
|
export function Thead({ children }: PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<thead className="text-xs text-txt-tertiary font-semibold">
|
<thead className="text-xs text-txt-tertiary font-semibold border-border-low border-b">
|
||||||
{children}
|
{children}
|
||||||
</thead>
|
</thead>
|
||||||
);
|
);
|
||||||
@@ -30,7 +30,7 @@ export function Tr({ children, to }: PropsWithChildren<{ to?: string }>) {
|
|||||||
<TrContext value={{ to }}>
|
<TrContext value={{ to }}>
|
||||||
<tr
|
<tr
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border-border-low border",
|
"border-border-low border-y first:border-none last:border-none",
|
||||||
to && "hover:bg-subtle",
|
to && "hover:bg-subtle",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -54,7 +54,7 @@ export function Td({
|
|||||||
}: PropsWithChildren<{ noLink?: boolean }>) {
|
}: PropsWithChildren<{ noLink?: boolean }>) {
|
||||||
const { to } = useContext(TrContext);
|
const { to } = useContext(TrContext);
|
||||||
if (!to || noLink) {
|
if (!to || noLink) {
|
||||||
return <td className="first:pl-6 last:pr-6 py-3">{children}</td>;
|
return <td className="first:pl-6 py-3">{children}</td>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<td className="first:*:pl-6 *:block last:*:pr-6 py-3">
|
<td className="first:*:pl-6 *:block last:*:pr-6 py-3">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type Props = PropsWithChildren<{
|
|||||||
|
|
||||||
export function Layout({ header, sidebar, children }: Props) {
|
export function Layout({ header, sidebar, children }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="text-txt-primary bg-level-0">
|
||||||
<header className="absolute z-2 left-0 right-0 px-4 flex items-center border-b border-border-solid h-12 bg-level-1">
|
<header className="absolute z-2 left-0 right-0 px-4 flex items-center border-b border-border-solid h-12 bg-level-1">
|
||||||
<Logo className="w-12 h-5" />
|
<Logo className="w-12 h-5" />
|
||||||
<svg
|
<svg
|
||||||
@@ -27,8 +27,10 @@ export function Layout({ header, sidebar, children }: Props) {
|
|||||||
</header>
|
</header>
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Sidebar>{sidebar}</Sidebar>
|
<Sidebar>{sidebar}</Sidebar>
|
||||||
<main className="p-12 pt-24 max-w-[1200px] w-full mx-auto overflow-y-auto">
|
<main className="overflow-y-auto w-full mt-12">
|
||||||
{children}
|
<div className="py-12 px-8 max-w-[1200px] w-full mx-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Toasts />
|
<Toasts />
|
||||||
|
|||||||
31
packages/ui/src/Molecules/RisksChart/RisksChart.stories.tsx
Normal file
31
packages/ui/src/Molecules/RisksChart/RisksChart.stories.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { randomInt, times } from "@probo/helpers";
|
||||||
|
import { RisksChart } from "./RisksChart";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Molecules/RisksChart",
|
||||||
|
component: RisksChart,
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<typeof RisksChart>;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof RisksChart>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
type: "inherent",
|
||||||
|
organizationId: "1",
|
||||||
|
risks: times(20, (i) => ({
|
||||||
|
id: i.toString(),
|
||||||
|
name: `Risk ${i}`,
|
||||||
|
inherentLikelihood: randomInt(1, 5),
|
||||||
|
inherentImpact: randomInt(1, 5),
|
||||||
|
residualLikelihood: randomInt(1, 5),
|
||||||
|
residualImpact: randomInt(1, 5),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<div style={{ maxWidth: 630 }}>
|
||||||
|
<RisksChart {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
229
packages/ui/src/Molecules/RisksChart/RisksChart.tsx
Normal file
229
packages/ui/src/Molecules/RisksChart/RisksChart.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { Card } from "../../Atoms/Card/Card";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { getRiskImpacts, getRiskLikelihoods, groupBy } from "@probo/helpers";
|
||||||
|
import { Fragment, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownSeparator,
|
||||||
|
} from "../../Atoms/Dropdown/Dropdown";
|
||||||
|
import { IconChevronRight, IconFire3 } from "../../Atoms/Icons";
|
||||||
|
import { Button } from "../../Atoms/Button/Button";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
organizationId: string;
|
||||||
|
type: "inherent" | "residual";
|
||||||
|
risks?: Risk[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Risk = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
inherentLikelihood: number;
|
||||||
|
inherentImpact: number;
|
||||||
|
residualLikelihood: number;
|
||||||
|
residualImpact: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelColors = [
|
||||||
|
{
|
||||||
|
color: "bg-txt-success",
|
||||||
|
bg: "bg-success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "bg-txt-warning",
|
||||||
|
bg: "bg-warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "bg-txt-danger",
|
||||||
|
bg: "bg-danger",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const getLevel = (score: number): 0 | 1 | 2 => {
|
||||||
|
if (score >= 15) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (score > 4) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cellKey = (impact: number, likelihood: number) =>
|
||||||
|
`${impact}-${likelihood}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a grid of risk grouped by impact & likelihood
|
||||||
|
*/
|
||||||
|
export function RisksChart({ organizationId, type, risks }: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const legend = [__("Low"), __("Medium"), __("High")];
|
||||||
|
|
||||||
|
const impacts = getRiskImpacts(__).reverse();
|
||||||
|
const likelihoods = getRiskLikelihoods(__);
|
||||||
|
const impactField =
|
||||||
|
type === "inherent" ? "inherentImpact" : "residualImpact";
|
||||||
|
const likelihoodField =
|
||||||
|
type === "inherent" ? "inherentLikelihood" : "residualLikelihood";
|
||||||
|
|
||||||
|
const riskMap = useMemo(() => {
|
||||||
|
return groupBy(risks ?? [], (risk) =>
|
||||||
|
cellKey(risk[impactField], risk[likelihoodField]),
|
||||||
|
);
|
||||||
|
}, [organizationId, risks]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card padded className="text-txt-primary">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="font-semibold text-lg">
|
||||||
|
{type === "inherent"
|
||||||
|
? __("Inherent Risk")
|
||||||
|
: __("Residual Risk")}
|
||||||
|
</h2>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{legend.map((label, i) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex items-center gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"size-[10px] rounded-xs",
|
||||||
|
levelColors[i].color,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Grid */}
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<div
|
||||||
|
className="text-xs font-medium flex-none text-center"
|
||||||
|
style={{ writingMode: "sideways-lr" }}
|
||||||
|
>
|
||||||
|
{__("Impact")}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[90px_1fr_1fr_1fr_1fr_1fr] gap-1 w-full">
|
||||||
|
{impacts.map((impact) => (
|
||||||
|
<Fragment key={impact.value}>
|
||||||
|
<div className="pr-2 text-right text-xs text-txt-secondary flex items-center">
|
||||||
|
{impact.label} ({impact.value})
|
||||||
|
</div>
|
||||||
|
{likelihoods.map((likelihood) => (
|
||||||
|
<RisksChartCell
|
||||||
|
key={likelihood.value}
|
||||||
|
impact={impact.value}
|
||||||
|
likelihood={likelihood.value}
|
||||||
|
organizationId={organizationId}
|
||||||
|
risks={
|
||||||
|
riskMap[
|
||||||
|
cellKey(
|
||||||
|
impact.value,
|
||||||
|
likelihood.value,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
{/* X axis */}
|
||||||
|
<div></div>
|
||||||
|
{likelihoods.map((likelihood) => (
|
||||||
|
<div
|
||||||
|
className="text-center text-xs text-txt-secondary mt-4"
|
||||||
|
key={likelihood.value}
|
||||||
|
>
|
||||||
|
{likelihood.label} ({likelihood.value})
|
||||||
|
{likelihood.value === 3 && (
|
||||||
|
<div className="text-xs text-txt-primary font-medium flex-none text-center mt-3">
|
||||||
|
{__("Likelihood")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RisksChartCell({
|
||||||
|
risks,
|
||||||
|
impact,
|
||||||
|
likelihood,
|
||||||
|
organizationId,
|
||||||
|
}: {
|
||||||
|
risks?: Risk[];
|
||||||
|
impact: number;
|
||||||
|
likelihood: number;
|
||||||
|
organizationId: string;
|
||||||
|
}) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const level = getLevel(impact * likelihood);
|
||||||
|
const baseClass =
|
||||||
|
"flex items-center justify-center aspect-square rounded-xl text-txt-invert text-sm font-semibold";
|
||||||
|
if (!risks) {
|
||||||
|
return <div className={clsx(baseClass, levelColors[level].bg)}></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const infos = [
|
||||||
|
{ label: __("Number of risks"), value: risks.length },
|
||||||
|
{ label: __("Impact"), value: impact },
|
||||||
|
{ label: __("Likelihood"), value: likelihood },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
className="text-sm w-75 p-4 space-y-1"
|
||||||
|
toggle={
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
baseClass,
|
||||||
|
levelColors[level].color,
|
||||||
|
"cursor-pointer",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{risks.length}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{infos.map((info) => (
|
||||||
|
<div
|
||||||
|
key={info.label}
|
||||||
|
className="flex items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
<div className="text-txt-secondary">{info.label}</div>
|
||||||
|
<div>{info.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<DropdownSeparator className="my-3" />
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="text-txt-secondary">Risk Score</div>
|
||||||
|
<div>{impact * likelihood}</div>
|
||||||
|
</div>
|
||||||
|
<DropdownSeparator className="my-3" />
|
||||||
|
<div className="text-txt-secondary mb-1">{__("Linked Risks")}</div>
|
||||||
|
{risks.map((risk) => (
|
||||||
|
<DropdownItem key={risk.id} asChild>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
||||||
|
>
|
||||||
|
<IconFire3 size={16} className="flex-none" />
|
||||||
|
{risk.name}
|
||||||
|
<IconChevronRight
|
||||||
|
size={16}
|
||||||
|
className="flex-none ml-auto"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ export { Dialog, DialogContent, DialogFooter } from "./Molecules/Dialog/Dialog";
|
|||||||
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
|
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
|
||||||
export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx";
|
export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx";
|
||||||
export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx";
|
export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx";
|
||||||
|
export { RisksChart } from "./Molecules/RisksChart/RisksChart";
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";
|
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";
|
||||||
|
|||||||
Reference in New Issue
Block a user