Add detail page

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-05-22 18:27:00 +02:00
committed by Sacha Al Himdani
parent 72c3fe586a
commit fe3128da2a
34 changed files with 969 additions and 922 deletions

View File

@@ -9,23 +9,34 @@ export default {
type Story = StoryObj<typeof Badge>;
const sizes = ["sm", "md"] as const;
const variants = [
"success",
"warning",
"danger",
"info",
"neutral",
"outline",
"highlight",
] as const;
export const Default: Story = {
render: () => (
<>
{(
[
"success",
"warning",
"danger",
"info",
"neutral",
"outline",
] as const
).map((variant) => (
<Badge key={variant} variant={variant}>
{variant}
</Badge>
<div className="space-y-4">
{sizes.map((size) => (
<div key={size} className="space-y-1">
<div className="text-xs text-txt-secondary">
Size : {size}
</div>
<div className="flex gap-2">
{variants.map((variant) => (
<Badge key={variant} variant={variant} size={size}>
{variant}
</Badge>
))}
</div>
</div>
))}
</>
</div>
),
};

View File

@@ -2,11 +2,19 @@ import type { HTMLAttributes } from "react";
import { tv } from "tailwind-variants";
type Props = {
variant?: "success" | "warning" | "danger" | "info" | "neutral" | "outline";
variant?:
| "success"
| "warning"
| "danger"
| "info"
| "neutral"
| "outline"
| "highlight";
size?: "sm" | "md";
} & HTMLAttributes<HTMLDivElement>;
const badge = tv({
base: "text-xs font-medium py-[2px] px-[6px] rounded-lg w-max",
base: "font-medium rounded-lg w-max flex gap-1 items-center",
variants: {
variant: {
success: "bg-success text-txt-success",
@@ -15,10 +23,18 @@ const badge = tv({
info: "bg-info text-txt-info",
neutral: "bg-subtle text-txt-secondary",
outline: "text-txt-tertiary border border-border-low",
highlight: "bg-highlight text-txt-primary",
},
size: {
sm: "text-xs py-[2px] px-[6px]",
md: "text-sm py-[6px] px-2",
},
},
defaultVariants: {
size: "md",
},
});
export function Badge(props: Props) {
return <div className={badge(props)} {...props} />;
return <div {...props} className={badge(props)} />;
}

View File

@@ -61,12 +61,19 @@ const dropdownItem = tv({
},
});
export function ActionDropdown(props: Omit<Props, "toggle">) {
export function ActionDropdown(
props: Omit<Props, "toggle"> & {
variant?: ComponentProps<typeof Button>["variant"];
},
) {
return (
<Dropdown
{...props}
toggle={
<Button variant="tertiary" icon={IconDotGrid1x3Horizontal} />
<Button
variant={props.variant ?? "tertiary"}
icon={IconDotGrid1x3Horizontal}
/>
}
/>
);

View File

@@ -3,7 +3,7 @@ import { Label } from "../Label/Label";
type Props = {
label: string;
id: string;
id?: string;
children: ReactNode;
error?: string;
};

View File

@@ -112,7 +112,7 @@ export function Select({
(c) => c.props.value?.toString() === value?.toString(),
)
: undefined;
console.log(childrenArr, valueNode);
return (
<Root onValueChange={onValueChange} value={value}>
<Trigger className={trigger({ ...props })} {...props}>

View File

@@ -1,4 +1,4 @@
import { Layout } from "./Layout";
import { Layout, Drawer } from "./Layout";
import type { Meta, StoryObj } from "@storybook/react";
const meta = {
@@ -12,4 +12,36 @@ const meta = {
export default meta;
type Story = StoryObj<typeof Layout>;
export const Default: Story = {};
export const Default: Story = {
args: {
children: (
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
Tempora tempore odit at ipsa dignissimos cumque deleniti, illum
rerum sunt laudantium molestias ducimus vero maiores eligendi
necessitatibus nemo animi. Ipsam, fugit.
</p>
),
},
};
export const WithSidebar: Story = {
args: {
children: (
<>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
Tempora tempore odit at ipsa dignissimos cumque deleniti,
illum rerum sunt laudantium molestias ducimus vero maiores
eligendi necessitatibus nemo animi. Ipsam, fugit.
</p>
<Drawer>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Nostrum reiciendis eveniet possimus illo rerum labore nisi
voluptatibus sequi consectetur corrupti, a, sunt dicta ut ad
pariatur. Beatae optio libero pariatur!
</Drawer>
</>
),
},
};

View File

@@ -1,39 +1,84 @@
import type { PropsWithChildren, ReactNode } from "react";
import {
createContext,
useContext,
useEffect,
useState,
type PropsWithChildren,
type ReactNode,
useMemo,
} from "react";
import { Sidebar } from "../Atoms/Sidebar/Sidebar.tsx";
import { Logo } from "../Atoms/Logo/Logo.tsx";
import { Toasts } from "../Atoms/Toasts/Toasts.tsx";
import { createPortal } from "react-dom";
import clsx from "clsx";
type Props = PropsWithChildren<{
header: ReactNode;
sidebar: ReactNode;
}>;
const LayoutContext = createContext({
setDrawer: (() => {}) as (v: boolean) => void,
});
export function Layout({ header, sidebar, children }: Props) {
const [hasDrawer, setDrawer] = useState(false);
return (
<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">
<Logo className="w-12 h-5" />
<svg
className="mx-3"
width="8"
height="18"
viewBox="0 0 8 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 17L7 1" stroke="#C3C8C2" />
</svg>
{header}
</header>
<div className="flex h-screen">
<Sidebar>{sidebar}</Sidebar>
<main className="overflow-y-auto w-full mt-12">
<div className="py-12 px-8 max-w-[1200px] w-full mx-auto">
{children}
</div>
</main>
<LayoutContext
value={useMemo(
() => ({
setDrawer,
}),
[],
)}
>
<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">
<Logo className="w-12 h-5" />
<svg
className="mx-3"
width="8"
height="18"
viewBox="0 0 8 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 17L7 1" stroke="#C3C8C2" />
</svg>
{header}
</header>
<div className="flex h-screen" id="main">
<Sidebar>{sidebar}</Sidebar>
<main
className={clsx(
"overflow-y-auto w-full mt-12 transition-all duration-300",
hasDrawer && "pr-105",
)}
>
<div className="py-12 px-8 max-w-[1200px] w-full mx-auto">
{children}
</div>
</main>
</div>
<Toasts />
</div>
<Toasts />
</div>
</LayoutContext>
);
}
export function Drawer({ children }: PropsWithChildren) {
const { setDrawer } = useContext(LayoutContext);
useEffect(() => {
setDrawer(true);
return () => {
setDrawer(false);
};
}, []);
return createPortal(
<aside className="absolute pt-20 top-0 right-0 w-105 px-6 pb-8 border-border-solid border-l h-screen">
{children}
</aside>,
document.body,
);
}

View File

@@ -2,35 +2,29 @@ import { useTranslate } from "@probo/i18n";
import { Badge } from "../../Atoms/Badge/Badge.tsx";
type Props = {
severity: number;
score: number;
};
const badgeVariant = (severity: number) => {
if (severity >= 75) {
const badgeVariant = (score: number) => {
if (score >= 15) {
return "danger";
}
if (severity >= 50) {
if (score >= 5) {
return "warning";
}
if (severity >= 25) {
return "info";
}
return "success";
return "neutral";
};
export function SeverityBadge({ severity }: Props) {
export function SeverityBadge({ score }: Props) {
const { __ } = useTranslate();
const label = () => {
if (severity >= 75) {
if (score >= 15) {
return __("Critical");
}
if (severity >= 50) {
if (score >= 5) {
return __("High");
}
if (severity >= 25) {
return __("Low");
}
return __("None");
return __("Negligible");
};
return <Badge variant={badgeVariant(severity)}>{label()}</Badge>;
return <Badge variant={badgeVariant(score)}>{label()}</Badge>;
}

View File

@@ -1,7 +1,7 @@
import type { PropsWithChildren } from "react";
import type { PropsWithChildren, ReactNode } from "react";
type Props = PropsWithChildren<{
title: string;
title: ReactNode;
description?: string;
}>;

View File

@@ -0,0 +1,22 @@
import { RiskOverview } from "./RiskOverview";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Molecules/Risks/RiskOverview",
component: RiskOverview,
argTypes: {},
} satisfies Meta<typeof RiskOverview>;
type Story = StoryObj<typeof RiskOverview>;
export const Default: Story = {
args: {
type: "inherent",
risk: {
inherentLikelihood: 5,
inherentImpact: 5,
residualLikelihood: 1,
residualImpact: 1,
},
},
};

View File

@@ -0,0 +1,88 @@
import { useTranslate } from "@probo/i18n";
import { Card } from "../../Atoms/Card/Card";
import { levelColors } from "./constants";
import {
getRiskImpacts,
getRiskLikelihoods,
getSeverity,
} from "@probo/helpers";
import clsx from "clsx";
type Props = {
type: "inherent" | "residual";
risk?: Risk;
};
type Risk = {
inherentLikelihood: number;
inherentImpact: number;
residualLikelihood: number;
residualImpact: number;
};
const getColor = (score: number): string => {
return levelColors[Math.ceil(score / 2) - 1].color;
};
export function RiskOverview({ type, risk }: Props) {
const { __ } = useTranslate();
const impact = risk?.[`${type}Impact`] ?? 0;
const likelihood = risk?.[`${type}Likelihood`] ?? 0;
const severity = getSeverity(__, impact * likelihood);
return (
<Card padded>
<h2 className="font-semibold text-base mb-6">
{type === "inherent" ? __("Initial Risk") : __("Residual Risk")}
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<RiskOverviewBadge
label={__("Impact")}
textCb={getRiskImpacts}
score={impact}
/>
<RiskOverviewBadge
label={__("Likelihood")}
textCb={getRiskLikelihoods}
score={likelihood}
/>
</div>
<div className="space-y-2">
<div className="font-medium text-xs">{__("Severity")}</div>
<div
className={clsx(
severity?.bg,
severity?.color,
"py-2 text-sm font-semibold rounded-lg text-center",
)}
>
{severity?.label}
</div>
</div>
</Card>
);
}
function RiskOverviewBadge({
score,
label,
textCb,
}: {
score: number;
label: string;
textCb: (t: (s: string) => string) => { value: number; label: string }[];
}) {
const { __ } = useTranslate();
return (
<div className="space-y-2">
<div className="font-medium text-xs">{__(label)}</div>
<div
className={clsx(
getColor(score),
"py-2 text-sm font-semibold rounded-lg text-txt-invert text-center",
)}
>
{textCb(__).find((i) => i.value === score)?.label} ({score})
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { RisksChart } from "./RisksChart";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Molecules/RisksChart",
title: "Molecules/Risks/RisksChart",
component: RisksChart,
argTypes: {},
} satisfies Meta<typeof RisksChart>;

View File

@@ -9,8 +9,8 @@ import {
DropdownSeparator,
} from "../../Atoms/Dropdown/Dropdown";
import { IconChevronRight, IconFire3 } from "../../Atoms/Icons";
import { Button } from "../../Atoms/Button/Button";
import { Link } from "react-router";
import { levelColors } from "./constants";
type Props = {
organizationId: string;
@@ -27,21 +27,6 @@ type Risk = {
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;

View File

@@ -0,0 +1,14 @@
export 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;

View File

@@ -1,5 +1,5 @@
// Layouts
export { Layout } from "./Layouts/Layout";
export { Layout, Drawer } from "./Layouts/Layout";
export { AuthLayout } from "./Layouts/AuthLayout";
export { ErrorLayout } from "./Layouts/ErrorLayout";
export {
@@ -14,6 +14,7 @@ export { SidebarItem } from "./Atoms/Sidebar/SidebarItem";
export { Button } from "./Atoms/Button/Button";
export { Card } from "./Atoms/Card/Card";
export { Breadcrumb } from "./Atoms/Breadcrumb/Breadcrumb";
export { Badge } from "./Atoms/Badge/Badge";
export {
Dropdown,
ActionDropdown,
@@ -40,7 +41,8 @@ export { Dialog, DialogContent, DialogFooter } from "./Molecules/Dialog/Dialog";
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx";
export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx";
export { RisksChart } from "./Molecules/RisksChart/RisksChart";
export { RisksChart } from "./Molecules/Risks/RisksChart";
export { RiskOverview } from "./Molecules/Risks/RiskOverview";
// Hooks
export { useToast, Toasts } from "./Atoms/Toasts/Toasts";

View File

@@ -13,7 +13,7 @@
--color-txt-invert: var(--color-invert);
--color-txt-secondary: #6b716a;
--color-txt-tertiary: #818780;
--color-txt-quaternary: #d8dcd8;
--color-txt-quaternary: #c3c8c2;
--color-txt-disabled: #000000;
--color-txt-accent: #93c926;
--color-txt-danger: #cd2b31;