Add risks

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-20 18:49:47 +02:00
committed by Sacha Al Himdani
parent a2d1310780
commit a86181c83d
41 changed files with 4533 additions and 1304 deletions

View File

@@ -1,7 +1,12 @@
import { ErrorBoundary } from "react-error-boundary";
import { RouterProvider } from "react-router";
import { routes } from "./routes";
import { router } from "./routes";
import { Toasts } from "@probo/ui";
export function App() {
return <RouterProvider router={routes} />;
return (
<>
<RouterProvider router={router} />
<Toasts />
</>
);
}

View File

@@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap");
@import "tailwindcss";
@import "@probo/ui/src/theme.css";
@source '../../../packages/ui/src';
@import "tw-animate-css";
@source "../../../packages/ui/src";

View File

@@ -1,4 +1,4 @@
import { Outlet, useParams } from "react-router";
import { Link, Navigate, Outlet, useParams } from "react-router";
import {
DropdownSeparator,
IconArrowBoxLeft,
@@ -13,13 +13,21 @@ import {
IconTodo,
Layout,
SidebarItem,
UserDropdown,
UserDropdown as UserDropdownRoot,
UserDropdownItem,
Skeleton,
Dropdown,
Button,
DropdownItem,
IconChevronGrabberVertical,
IconPlusLarge,
Avatar,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import { useLazyLoadQuery } from "react-relay";
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
import { Suspense } from "react";
const MainLayoutQuery = graphql`
query MainLayoutQuery {
@@ -29,42 +37,45 @@ const MainLayoutQuery = graphql`
fullName
email
}
organizations(first: 25) @connection(key: "MainLayout_organizations") {
__id
edges {
node {
id
name
logoUrl
}
}
}
}
}
`;
/**
* Site layout with a header and a sidebar
*/
export function MainLayout() {
const { organizationId } = useParams();
const { __ } = useTranslate();
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {}).viewer
.user;
const prefix = `/organizations/${organizationId}`;
if (!organizationId) {
return <Navigate to="/" />;
}
return (
<Layout
header={
<>
<div className="mr-auto"></div>
<UserDropdown fullName={user.fullName} email={user.email}>
<UserDropdownItem
to="/settings"
icon={IconSettingsGear2}
label={__("Settings")}
/>
<UserDropdownItem
to="/settings"
icon={IconCircleQuestionmark}
label={__("Help")}
/>
<DropdownSeparator />
<UserDropdownItem
variant="danger"
to="/logout"
icon={IconArrowBoxLeft}
label="Logout"
/>
</UserDropdown>
<div className="mr-auto">
<Suspense fallback={<Skeleton className="w-20 h-8" />}>
<OrganizationSelector organizationId={organizationId} />
</Suspense>
</div>
<Suspense fallback={<Skeleton className="w-32 h-8" />}>
<UserDropdown />
</Suspense>
</>
}
sidebar={
@@ -116,3 +127,75 @@ export function MainLayout() {
</Layout>
);
}
function UserDropdown() {
const { __ } = useTranslate();
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {}).viewer
.user;
return (
<UserDropdownRoot fullName={user.fullName} email={user.email}>
<UserDropdownItem
to="/settings"
icon={IconSettingsGear2}
label={__("Settings")}
/>
<UserDropdownItem
to="/settings"
icon={IconCircleQuestionmark}
label={__("Help")}
/>
<DropdownSeparator />
<UserDropdownItem
variant="danger"
to="/logout"
icon={IconArrowBoxLeft}
label="Logout"
/>
</UserDropdownRoot>
);
}
function OrganizationSelector({ organizationId }: { organizationId: string }) {
const organizations = useLazyLoadQuery<MainLayoutQueryType>(
MainLayoutQuery,
{}
).viewer.organizations.edges.map((edge) => edge.node);
const currentOrganization = organizations.find(
(organization) => organization.id === organizationId
);
const { __ } = useTranslate();
return (
<Dropdown
toggle={
<Button
className="-ml-3"
variant="tertiary"
iconAfter={IconChevronGrabberVertical}
>
{currentOrganization?.name}
</Button>
}
>
{organizations.map((organization) => (
<DropdownItem
asChild
key={organization.id}
icon={IconCircleQuestionmark}
>
<Link to={`/organizations/${organization.id}`}>
<Avatar src={organization.logoUrl} name={organization.name} />
{organization.name}
</Link>
</DropdownItem>
))}
<DropdownSeparator />
<DropdownItem asChild icon={IconPlusLarge}>
<Link to="/organizations/new">
<IconPlusLarge size={16} />
{__("Add organization")}
</Link>
</DropdownItem>
</Dropdown>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<998d88d8fcb28932ec470d6afdb49b8e>>
* @generated SignedSource<<d411922830ba632f8c48c98caa435a22>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -13,6 +13,16 @@ export type MainLayoutQuery$variables = Record<PropertyKey, never>;
export type MainLayoutQuery$data = {
readonly viewer: {
readonly id: string;
readonly organizations: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
}>;
};
readonly user: {
readonly email: string;
readonly fullName: string;
@@ -45,7 +55,104 @@ v2 = {
"kind": "ScalarField",
"name": "email",
"storageKey": null
};
},
v3 = [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
v4 = [
{
"kind": "Literal",
"name": "first",
"value": 25
}
];
return {
"fragment": {
"argumentDefinitions": [],
@@ -74,6 +181,16 @@ return {
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": "organizations",
"args": null,
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "__MainLayout_organizations_connection",
"plural": false,
"selections": (v3/*: any*/),
"storageKey": null
}
],
"storageKey": null
@@ -110,6 +227,25 @@ return {
(v0/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": (v3/*: any*/),
"storageKey": "organizations(first:25)"
},
{
"alias": null,
"args": (v4/*: any*/),
"filters": null,
"handle": "connection",
"key": "MainLayout_organizations",
"kind": "LinkedHandle",
"name": "organizations"
}
],
"storageKey": null
@@ -117,16 +253,28 @@ return {
]
},
"params": {
"cacheID": "4b5a08e03bcb26d2edadb57cd94aa7d1",
"cacheID": "9ab80df970b8fa7c4eabff75a6730ec5",
"id": null,
"metadata": {},
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"viewer",
"organizations"
]
}
]
},
"name": "MainLayoutQuery",
"operationKind": "query",
"text": "query MainLayoutQuery {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n }\n}\n"
"text": "query MainLayoutQuery {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "785f7a3ca19b673a157ef0f649de5c1b";
(node as any).hash = "fc37d1b70f1a239abb4c959d47ee28fd";
export default node;

View File

@@ -0,0 +1,95 @@
import { useTranslate } from "@probo/i18n";
import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { OrganizationSelectionPageQuery as OrganizationSelectionPageQueryType } from "./__generated__/OrganizationSelectionPageQuery.graphql";
import { useEffect } from "react";
import { Link, useNavigate } from "react-router";
import { Avatar, Button, Card, IconPlusLarge } from "@probo/ui";
const OrganizationSelectionPageQuery = graphql`
query OrganizationSelectionPageQuery {
viewer {
organizations(first: 25) {
edges {
node {
id
name
logoUrl
}
}
}
}
}
`;
export default function OrganizationSelectionPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const data = useLazyLoadQuery<OrganizationSelectionPageQueryType>(
OrganizationSelectionPageQuery,
{}
);
const organizations = data.viewer.organizations.edges.map(
(edge) => edge.node
);
// Redirect to the first organization if only one exists
useEffect(() => {
if (organizations.length === 1) {
// navigate(`/organizations/${organizations[0].id}`);
}
}, [organizations]);
return (
<>
<title>{__("Select an organization")}</title>
<div className="space-y-6 w-full">
<h1 className="text-3xl font-bold text-center">
{__("Select an organization")}
</h1>
<div className="space-y-4 w-full">
{organizations.map((organization) => (
<Card
asChild
padded
key={organization.id}
className="w-full hover:bg-tertiary-hover cursor-pointer"
>
<Link
to={`/organizations/${organization.id}`}
className="flex items-center justify-between"
>
<div className="flex items-center gap-4">
<Avatar
src={organization.logoUrl}
name={organization.name}
size="l"
/>
<h2 className="font-semibold text-xl">{organization.name}</h2>
</div>
<Button>{__("Select")}</Button>
</Link>
</Card>
))}
<Card padded>
<h2 className="text-xl font-semibold mb-1">
{__("Create an organization")}
</h2>
<p className="text-txt-tertiary mb-4">
{__("Add a new organization to your account")}
</p>
<Button
to="/organizations/new"
variant="quaternary"
icon={IconPlusLarge}
className="w-full"
>
{__("Create organization")}
</Button>
</Card>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,150 @@
/**
* @generated SignedSource<<282a94b76fe5e17bd40b45ced6445ef3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type OrganizationSelectionPageQuery$variables = Record<PropertyKey, never>;
export type OrganizationSelectionPageQuery$data = {
readonly viewer: {
readonly organizations: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
}>;
};
};
};
export type OrganizationSelectionPageQuery = {
response: OrganizationSelectionPageQuery$data;
variables: OrganizationSelectionPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 25
}
],
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "organizations(first:25)"
};
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationSelectionPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v1/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "OrganizationSelectionPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v1/*: any*/),
(v0/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "2938cb693a84c22366862de424a84d4b",
"id": null,
"metadata": {},
"name": "OrganizationSelectionPageQuery",
"operationKind": "query",
"text": "query OrganizationSelectionPageQuery {\n viewer {\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n }\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "c41774fd198ab5f0f4b866d805099baf";
export default node;

View File

@@ -1,9 +1,9 @@
import { useTranslate } from "@probo/i18n";
import { Button, Input, useToast } from "@probo/ui";
import type { FormEventHandler } from "react";
import { Link, useLocation, useNavigate } from "react-router";
import { Link, useNavigate } from "react-router";
export function LoginPage() {
export default function LoginPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const { toast } = useToast();

View File

@@ -0,0 +1,118 @@
import { Button, Card, Input, PageHeader, useToast } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { ConnectionHandler, graphql } from "relay-runtime";
import { useLazyLoadQuery, useMutation } from "react-relay";
import type { NewOrganizationPageQuery as NewOrganizationPageQueryType } from "./__generated__/NewOrganizationPageQuery.graphql";
import type { NewOrganizationPageMutation as NewOrganizationPageMutationType } from "./__generated__/NewOrganizationPageMutation.graphql";
import { useState, type FormEventHandler } from "react";
import { useNavigate } from "react-router";
const createOrganizationMutation = graphql`
mutation NewOrganizationPageMutation(
$input: CreateOrganizationInput!
$connections: [ID!]!
) {
createOrganization(input: $input) {
organizationEdge @appendEdge(connections: $connections) {
node {
id
name
logoUrl
}
}
}
}
`;
const viewerQuery = graphql`
query NewOrganizationPageQuery {
viewer {
id
}
}
`;
export default function NewOrganizationPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const { toast } = useToast();
const [isFetching, setIsFetching] = useState(false);
const data = useLazyLoadQuery<NewOrganizationPageQueryType>(viewerQuery, {});
const [createOrganization] = useMutation<NewOrganizationPageMutationType>(
createOrganizationMutation
);
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const name = formData.get("name")?.toString()!;
setIsFetching(true);
createOrganization({
variables: {
input: {
name,
},
connections: [
ConnectionHandler.getConnectionID(
data.viewer.id,
"NewOrganizationPageQuery"
),
],
},
onCompleted: (r) => {
setIsFetching(false);
const org = r.createOrganization.organizationEdge.node;
navigate(`/organizations/${org.id}`);
toast({
title: __("Success"),
description: __("Organization has been created successfully"),
variant: "success",
});
},
onError: (e) => {
setIsFetching(false);
toast({
title: __("Error"),
description: e.message ?? __("Failed to create organization"),
variant: "error",
});
},
});
};
return (
<div className="space-y-6">
<PageHeader
title={__("Create Organization")}
description={__(
"Create a new organization to manage your compliance and security needs."
)}
/>
<Card padded asChild>
<form onSubmit={handleSubmit} className="space-y-4">
<h2 className="text-xl font-semibold mb-1">
{__("Organization Details")}
</h2>
<p className="text-txt-tertiary text-sm mb-4">
{__("Enter the basic information about your organization.")}
</p>
<Input
required
name="name"
type="text"
placeholder={__("Organization name")}
label={__("Organization name")}
help={__(
"The name of your organization as it will appear throughout the platform."
)}
/>
<Button disabled={isFetching} type="submit" className="w-full">
{__("Create Organization")}
</Button>
</form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,174 @@
/**
* @generated SignedSource<<b15b08fb2d1a0e4e289a9c3c5b9ec7ef>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateOrganizationInput = {
name: string;
};
export type NewOrganizationPageMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateOrganizationInput;
};
export type NewOrganizationPageMutation$data = {
readonly createOrganization: {
readonly organizationEdge: {
readonly node: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
};
};
};
export type NewOrganizationPageMutation = {
response: NewOrganizationPageMutation$data;
variables: NewOrganizationPageMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "organizationEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "NewOrganizationPageMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateOrganizationPayload",
"kind": "LinkedField",
"name": "createOrganization",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "NewOrganizationPageMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateOrganizationPayload",
"kind": "LinkedField",
"name": "createOrganization",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "organizationEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "917f8c27b3faacda07aa6d62e6123677",
"id": null,
"metadata": {},
"name": "NewOrganizationPageMutation",
"operationKind": "mutation",
"text": "mutation NewOrganizationPageMutation(\n $input: CreateOrganizationInput!\n) {\n createOrganization(input: $input) {\n organizationEdge {\n node {\n id\n name\n logoUrl\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "b88831f98e1414c16a18381ab4aeaa38";
export default node;

View File

@@ -0,0 +1,74 @@
/**
* @generated SignedSource<<83489e54447230c6d654a6baa9fb3e9d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type NewOrganizationPageQuery$variables = Record<PropertyKey, never>;
export type NewOrganizationPageQuery$data = {
readonly viewer: {
readonly id: string;
};
};
export type NewOrganizationPageQuery = {
response: NewOrganizationPageQuery$data;
variables: NewOrganizationPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "NewOrganizationPageQuery",
"selections": (v0/*: any*/),
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "NewOrganizationPageQuery",
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "3a624ea532d0ad916c5e4bc883867f5e",
"id": null,
"metadata": {},
"name": "NewOrganizationPageQuery",
"operationKind": "query",
"text": "query NewOrganizationPageQuery {\n viewer {\n id\n }\n}\n"
}
};
})();
(node as any).hash = "c4d28895f85836c0698352ac737720ad";
export default node;

View File

@@ -0,0 +1,37 @@
import { useTranslate } from "@probo/i18n";
import {
Breadcrumb,
Button,
Dialog,
DialogContent,
DialogFooter,
} from "@probo/ui";
import type { ReactNode } from "react";
type Props = {
trigger: ReactNode;
};
export default function NewRiskDialog({ trigger }: Props) {
const { __ } = useTranslate();
return (
<Dialog
onClose={() => {}}
trigger={trigger}
title={<Breadcrumb items={[__("Risks"), __("New Risk")]}></Breadcrumb>}
>
<form>
<DialogContent>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Velit harum
maiores dolor hic nisi repudiandae autem quod rem eveniet iure
quibusdam sint iste numquam, suscipit ad delectus aspernatur. Quo,
amet.
</DialogContent>
<DialogFooter>
<Button type="submit">{__("Create risk")}</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,17 @@
import { Button, PageHeader } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { IconPlusLarge } from "@probo/ui";
import NewRiskDialog from "./NewRiskDialog";
export default function RisksPage() {
const { __ } = useTranslate();
return (
<div className="space-y-6">
<PageHeader title={__("Risks")}>
<NewRiskDialog
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
/>
</PageHeader>
</div>
);
}

View File

@@ -1,10 +1,14 @@
import { createBrowserRouter, useRouteError } from "react-router";
import {
createBrowserRouter,
useRouteError,
type RouteObject,
} from "react-router";
import { MainLayout } from "./layouts/MainLayout";
import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
import { lazy, Suspense, type FC } from "react";
import { UnAuthenticatedError } from "./providers/RelayProviders";
import { AuthLayout } from "@probo/ui";
import { LoginPage } from "./pages/auth/LoginPage";
function Demo() {
function ErrorBoundary() {
const error = useRouteError();
if (error instanceof UnAuthenticatedError) {
return <div></div>;
@@ -13,26 +17,71 @@ function Demo() {
return <div>error</div>;
}
export const routes = createBrowserRouter([
type Route = {
path: string;
Component: FC;
children?: Route[];
ErrorBoundary?: FC;
fallback?: FC;
};
const routes = [
{
path: "/auth",
Component: AuthLayout,
children: [
{
path: "login",
element: <LoginPage />,
Component: lazy(() => import("./pages/auth/LoginPage")),
},
],
},
{
path: "/",
Component: MainLayout,
ErrorBoundary: Demo,
Component: CenteredLayout,
fallback: CenteredLayoutSkeleton,
children: [
{
path: "/",
element: <div>Home</div>,
path: "",
Component: lazy(() => import("./pages/OrganizationsPage")),
},
{
path: "organizations/new",
Component: lazy(
() => import("./pages/organizations/NewOrganizationPage")
),
},
],
},
]);
{
path: "/organizations/:organizationId",
Component: MainLayout,
ErrorBoundary: ErrorBoundary,
children: [
{
path: "risks",
Component: lazy(() => import("./pages/organizations/risks/RisksPage")),
},
],
},
] satisfies Route[];
/**
* Wrap component with a suspense to handle lazy loading & relay loading states
*/
function routeTransformer(route: Route): RouteObject {
if ("fallback" in route && route.fallback) {
return {
...route,
children: route.children?.map(routeTransformer),
Component: () => (
<Suspense fallback={<route.fallback />}>
<route.Component />
</Suspense>
),
} as RouteObject;
}
return route as RouteObject;
}
export const router = createBrowserRouter(routes.map(routeTransformer));