Migrate to vite & new structure

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-19 20:13:50 +02:00
committed by Sacha Al Himdani
parent db9e5f32ac
commit a2d1310780
244 changed files with 9153 additions and 3096 deletions

View File

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

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,4 @@
@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';

View File

@@ -0,0 +1,118 @@
import { Outlet, useParams } from "react-router";
import {
DropdownSeparator,
IconArrowBoxLeft,
IconBank,
IconCircleQuestionmark,
IconFire3,
IconGroup1,
IconInboxEmpty,
IconPageTextLine,
IconSettingsGear2,
IconStore,
IconTodo,
Layout,
SidebarItem,
UserDropdown,
UserDropdownItem,
} 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";
const MainLayoutQuery = graphql`
query MainLayoutQuery {
viewer {
id
user {
fullName
email
}
}
}
`;
export function MainLayout() {
const { organizationId } = useParams();
const { __ } = useTranslate();
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {}).viewer
.user;
const prefix = `/organizations/${organizationId}`;
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>
</>
}
sidebar={
<ul className="space-y-[2px]">
<SidebarItem
label={__("Tasks")}
icon={IconInboxEmpty}
to={`${prefix}/tasks`}
/>
<SidebarItem
label={__("Measures")}
icon={IconTodo}
to={`${prefix}/measures`}
/>
<SidebarItem
label={__("Risks")}
icon={IconFire3}
to={`${prefix}/risks`}
/>
<SidebarItem
label={__("Frameworks")}
icon={IconBank}
to={`${prefix}/frameworks`}
/>
<SidebarItem
label={__("People")}
icon={IconGroup1}
to={`${prefix}/people`}
/>
<SidebarItem
label={__("Vendors")}
icon={IconStore}
to={`${prefix}/vendors`}
/>
<SidebarItem
label={__("Policies")}
icon={IconPageTextLine}
to={`${prefix}/policies`}
/>
<SidebarItem
label={__("Settings")}
icon={IconSettingsGear2}
to={`${prefix}/settings`}
/>
</ul>
}
>
<Outlet />
</Layout>
);
}

View File

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

View File

@@ -0,0 +1,16 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import { App } from "./App.tsx";
import { RelayProvider } from "./providers/RelayProviders.tsx";
import { TranslatorProvider } from "./providers/TranslatorProvider.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RelayProvider>
<TranslatorProvider>
<App />
</TranslatorProvider>
</RelayProvider>
</StrictMode>
);

View File

@@ -0,0 +1,73 @@
import { useTranslate } from "@probo/i18n";
import { Button, Input, useToast } from "@probo/ui";
import type { FormEventHandler } from "react";
import { Link, useLocation, useNavigate } from "react-router";
export function LoginPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
const { toast } = useToast();
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
fetch(import.meta.env.VITE_API_URL + "/api/console/v1/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
})
.then(async (res) => {
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || __("Failed to login"));
}
navigate("/");
})
.catch((e) => {
toast({
title: __("Error"),
description: e.message as string,
variant: "error",
});
});
};
return (
<form className="space-y-4" onSubmit={handleSubmit}>
<h1 className="text-center text-2xl font-bold">
{__("Login to your account")}
</h1>
<p className="text-center text-txt-tertiary mt-1 mb-6">
{__("Enter your email below to login to your account")}
</p>
<Input
required
placeholder={__("Email")}
name="email"
type="email"
label={__("Email")}
/>
<Input
required
placeholder={__("Password")}
name="password"
type="password"
label={__("Password")}
/>
<Button className="w-full">{__("Login")}</Button>
<div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "}
<Link to="/auth/register" className="underline hover:text-txt-primary">
{__("Register")}
</Link>
</div>
</form>
);
}

View File

@@ -0,0 +1,131 @@
import {
Environment,
type FetchFunction,
Network,
RecordSource,
Store,
} from "relay-runtime";
import { GraphQLError } from "graphql";
import type { PropsWithChildren } from "react";
import { RelayEnvironmentProvider } from "react-relay";
export class UnAuthenticatedError extends Error {
constructor() {
super("UNAUTHENTICATED");
this.name = "UnAuthenticatedError";
}
}
export class InternalServerError extends Error {
constructor() {
super("INTERNAL_SERVER_ERROR");
this.name = "InternalServerError";
}
}
const hasUnauthenticatedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHENTICATED";
const fetchRelay: FetchFunction = async (
request,
variables,
_,
uploadables
) => {
const requestInit: RequestInit = {
method: "POST",
credentials: "include",
headers: {},
};
if (uploadables) {
const formData = new FormData();
formData.append(
"operations",
JSON.stringify({
operationName: request.name,
query: request.text,
variables: variables,
})
);
const uploadableMap: {
[key: string]: string[];
} = {};
Object.keys(uploadables).forEach((key, index) => {
uploadableMap[index] = [`variables.${key}`];
});
formData.append("map", JSON.stringify(uploadableMap));
Object.keys(uploadables).forEach((key, index) => {
formData.append(index.toString(), uploadables[key]);
});
requestInit.body = formData;
} else {
requestInit.headers = {
Accept:
"application/graphql-response+json; charset=utf-8, application/json; charset=utf-8",
"Content-Type": "application/json",
};
requestInit.body = JSON.stringify({
operationName: request.name,
query: request.text,
variables,
});
}
const response = await fetch(
import.meta.env.VITE_API_URL + "/api/console/v1/query",
requestInit
);
if (response.status === 500) {
throw new InternalServerError();
}
const json = await response.json();
if (json.errors) {
const errors = json.errors as GraphQLError[];
if (errors.find(hasUnauthenticatedError)) {
throw new UnAuthenticatedError();
}
throw new Error(
`Error fetching GraphQL query '${
request.name
}' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
json.errors
)}`
);
}
return json;
};
const source = new RecordSource();
const store = new Store(source, {
queryCacheExpirationTime: 1 * 60 * 1000,
gcReleaseBufferSize: 20,
});
const environment = new Environment({
network: Network.create(fetchRelay),
store,
});
/**
* Provider for relay with the probo environment
*/
export const RelayProvider = ({ children }: PropsWithChildren) => {
return (
<RelayEnvironmentProvider environment={environment}>
{children}
</RelayEnvironmentProvider>
);
};

View File

@@ -0,0 +1,18 @@
import type { PropsWithChildren } from "react";
import { TranslatorProvider as ProboTranslatorProvider } from "@probo/i18n";
// TODO : implement a way to retrieve translations strings
const loader = () => {
return Promise.resolve({} as Record<string, string>);
};
/**
* Provider for the translator
*/
export function TranslatorProvider({ children }: PropsWithChildren) {
return (
<ProboTranslatorProvider lang="en" loader={loader}>
{children}
</ProboTranslatorProvider>
);
}

View File

@@ -0,0 +1,38 @@
import { createBrowserRouter, useRouteError } from "react-router";
import { MainLayout } from "./layouts/MainLayout";
import { UnAuthenticatedError } from "./providers/RelayProviders";
import { AuthLayout } from "@probo/ui";
import { LoginPage } from "./pages/auth/LoginPage";
function Demo() {
const error = useRouteError();
if (error instanceof UnAuthenticatedError) {
return <div></div>;
}
console.log(error);
return <div>error</div>;
}
export const routes = createBrowserRouter([
{
path: "/auth",
Component: AuthLayout,
children: [
{
path: "login",
element: <LoginPage />,
},
],
},
{
path: "/",
Component: MainLayout,
ErrorBoundary: Demo,
children: [
{
path: "/",
element: <div>Home</div>,
},
],
},
]);

9
apps/console2/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}