Add risk assessments tab to risk list page

Surface the organization risk assessments list alongside the risk register
by adding a shared Risks/Risk assessments tab bar to both list pages.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
Sacha Al Himdani
2026-07-10 15:46:15 +02:00
parent 6db2dc68da
commit 22df5742ee
24 changed files with 129 additions and 42 deletions

View File

@@ -1,37 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { lazy } from "@probo/react-lazy";
import type { AppRoute } from "@probo/routes";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
export const riskAssessmentRoutes = [
{
path: "risk-assessments",
Fallback: PageSkeleton,
Component: lazy(
() =>
import("./RiskAssessmentsPageLoader"),
),
},
{
path: "risk-assessments/:riskAssessmentId",
Fallback: PageSkeleton,
Component: lazy(
() =>
import("./RiskAssessmentDetailPageLoader"),
),
},
] satisfies AppRoute[];

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Tabs } from "@probo/ui";
import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { Outlet } from "react-router";
import type { RisksTabsQuery } from "#/__generated__/core/RisksTabsQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { RisksTabs, risksTabsQuery } from "./RisksTabs";
export default function RisksLayoutLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery]
= useQueryLoader<RisksTabsQuery>(risksTabsQuery);
useEffect(() => {
loadQuery({ organizationId });
}, [loadQuery, organizationId]);
return (
<div className="space-y-6">
<Suspense fallback={<Tabs />}>
{queryRef && <RisksTabs queryRef={queryRef} />}
</Suspense>
<Outlet />
</div>
);
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { TabLink, Tabs } from "@probo/ui";
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { RisksTabsQuery } from "#/__generated__/core/RisksTabsQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
export const risksTabsQuery = graphql`
query RisksTabsQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
canListRisks: permission(action: "core:risk:list")
canListRiskAssessments: permission(action: "core:risk-assessment:list")
}
}
}
`;
interface RisksTabsProps {
queryRef: PreloadedQuery<RisksTabsQuery>;
}
export function RisksTabs({ queryRef }: RisksTabsProps) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const data = usePreloadedQuery<RisksTabsQuery>(risksTabsQuery, queryRef);
const canListRisks = data.organization?.canListRisks ?? false;
const canListRiskAssessments
= data.organization?.canListRiskAssessments ?? false;
const baseUrl = `/organizations/${organizationId}`;
return (
<Tabs>
{canListRisks && (
<TabLink to={`${baseUrl}/risks`} end>
{__("Risks")}
</TabLink>
)}
{canListRiskAssessments && (
<TabLink to={`${baseUrl}/risk-assessments`} end>
{__("Risk assessments")}
</TabLink>
)}
</Tabs>
);
}

View File

@@ -23,9 +23,23 @@ import { RisksPageSkeleton } from "#/components/skeletons/RisksPageSkeleton";
export const riskRoutes = [
{
path: "risks",
Fallback: RisksPageSkeleton,
Component: lazy(() => import("./RisksPageLoader")),
Fallback: PageSkeleton,
Component: lazy(() => import("./RisksLayoutLoader")),
children: [
{
path: "risks",
Fallback: RisksPageSkeleton,
Component: lazy(() => import("./RisksPageLoader")),
},
{
path: "risk-assessments",
Fallback: PageSkeleton,
Component: lazy(
() =>
import("./risk-assessments/RiskAssessmentsPageLoader"),
),
},
],
},
{
path: "risks/:riskId",
@@ -72,4 +86,12 @@ export const riskRoutes = [
},
],
},
{
path: "risk-assessments/:riskAssessmentId",
Fallback: PageSkeleton,
Component: lazy(
() =>
import("./risk-assessments/RiskAssessmentDetailPageLoader"),
),
},
] satisfies AppRoute[];

View File

@@ -31,7 +31,6 @@ import { ViewerLayoutLoading } from "./pages/iam/memberships/ViewerLayoutLoading
import { peopleRoutes } from "./pages/iam/organizations/people/routes";
import { compliancePageRoutes } from "./pages/organizations/compliance-page/routes";
import { cookieBannerRoutes } from "./pages/organizations/cookie-banners/routes";
import { riskAssessmentRoutes } from "./pages/organizations/risk-assessments/routes";
import { riskRoutes } from "./pages/organizations/risks/routes";
import { thirdPartyRoutes } from "./pages/organizations/third-parties/routes";
import { CurrentUser } from "./providers/CurrentUser";
@@ -302,7 +301,6 @@ const routes = [
},
...peopleRoutes,
...riskRoutes,
...riskAssessmentRoutes,
...measureRoutes,
...documentsRoutes,
...thirdPartyRoutes,