Add risks
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
a2d1310780
commit
a86181c83d
@@ -11,8 +11,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@probo/hooks": "1.0.0",
|
||||
"@probo/i18n": "1.0.0",
|
||||
"@probo/ui": "1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
95
apps/console2/src/pages/OrganizationsPage.tsx
Normal file
95
apps/console2/src/pages/OrganizationsPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
150
apps/console2/src/pages/__generated__/OrganizationSelectionPageQuery.graphql.ts
generated
Normal file
150
apps/console2/src/pages/__generated__/OrganizationSelectionPageQuery.graphql.ts
generated
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
118
apps/console2/src/pages/organizations/NewOrganizationPage.tsx
Normal file
118
apps/console2/src/pages/organizations/NewOrganizationPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
174
apps/console2/src/pages/organizations/__generated__/NewOrganizationPageMutation.graphql.ts
generated
Normal file
174
apps/console2/src/pages/organizations/__generated__/NewOrganizationPageMutation.graphql.ts
generated
Normal 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;
|
||||
74
apps/console2/src/pages/organizations/__generated__/NewOrganizationPageQuery.graphql.ts
generated
Normal file
74
apps/console2/src/pages/organizations/__generated__/NewOrganizationPageQuery.graphql.ts
generated
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
17
apps/console2/src/pages/organizations/risks/RisksPage.tsx
Normal file
17
apps/console2/src/pages/organizations/risks/RisksPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user