diff --git a/README.md b/README.md index 7ddbb344e0482132197ac654f24a002de4cad604..2e9ae1cf4b2e9016e1c3a294c2697da4ed79adab 100644 --- a/README.md +++ b/README.md @@ -5,44 +5,45 @@ Be sure that you have node.js (v18 or up) and pnpm installed. Please use pnpm for building and running the scripts of package.json. Due to the way auth works (using a sameSite cookie), the procedure for running locally is a little different than usual. These steps will only have to be done, after that everything should 'just' work. -### MacOS / Linux +## Running Locally -1. `sudo vim /etc/hosts` open the hosts file with your prefered text editor as root -2. Add a new row containing `127.0.0.1 local.graphpolaris.com`, this will route traffic from `local.graphpolaris.com` to `127.0.0.1` -3. `brew install mkcert` install mkcert utility -4. `mkcert -install` generate local CA (certificate authority) -5. Move into the /certs folder at the project root using `cd` -6. `mkcert --key-file local-key.pem --cert-file local-cert.pem local.graphpolaris.com` create certificates for local SSL +### Install -### Windows +First run `pnpm i` on the root of the workspace to install all dependencies. It will create a node_module folder in the root of the workspace as well as one for each app/library within it. -1. Open the `hosts` file under `C:\Windows\System32\drivers\etc` using a text editor, as administrator -2. Add a new row containing `127.0.0.1 local.graphpolaris.com`, this will route traffic from `local.graphpolaris.com` to `127.0.0.1` -3. Install mkcert using any of the ways described [here](https://github.com/FiloSottile/mkcert#windows) -4. Open an elevated Powershell or CMD session -5. Move into the /certs folder at the project root using `cd` -6. `mkcert --key-file local-key.pem --cert-file local-cert.pem local.graphpolaris.com` create certificates for local SSL +If you need to install a new dependency or upgrade an existing one, do so in the respective package.json and rerun `pnpm i` in the root of the workspace. If you find any issues, feel free to delete the node_modules folder from the workspace root and from the app/lib which you are changing the dependency and rerun the `pnpm i` command from the root of the workspace. Most issues are solved by then. -> No idea if the Windows steps work +### Running Storybook.js -## Running Locally +To run the dev storybook (implementing visualizations) simply run `pnpm sb` and once it is running, ctrl-click the link what appears in the terminal. The url should be [http://localhost:6006]. -### Install +### Dev server -First run `pnpm i` on the root of the workspace to install all dependencies. It will create a node_module folder in the root of the workspace as well as one for each app/library within it. +The dev server (as of now) expects the backend to be also running in your machine. Therefore, for it to be useful you will need first to have the Backend +running. After that you also need to be sure tp update your hosts file. Finaly, to run the application simply run `pnpm dev` from the workspace root. -If you need to install a new dependency or upgrade an existing one, do so in the respective package.json and rerun `pnpm i` in the root of the workspace. If you find any issues, feel free to delete the node_modules folder from the workspace root and from the app/lib which you are changing the dependency and rerun the `pnpm i` command from the root of the workspace. Most issues are solved by then. +#### Hosts file -### Commands +To configure the hosts file, you need to first find it in your machine. -You can run pnpm commands (see available ones in packages.json) from the root of the workspace, which will trigger turborepo to run it in all libraries and apps of the workspace. You can run `pnpm test` or `pnpm lint` this way to test or lint the entire workspace. +- Windows: c:\Windows\System32\Drivers\etc\hosts +- Mac/Linux: /etc/hosts -You can also go into a specific lib/app and run pnpm commands from there to scope the task to only that part of the workspace. +Open the file with administrative privileges (e.g. sudo nano, sudo vim, or use vscode and when you try to save it will prompt you to save as an administrator) and add these two lines at the end: -### Dev server +``` +127.0.0.1 local.graphpolaris.com +127.0.0.1 api.graphpolaris.com +``` -To run the application using SSL (with these keys) simply run `pnpm dev` from the workspace root. +This will make the dev server to try to connect to the backend running in your own machine. If you want to make the dev server to call the production (cloud-based) backend of graph polaris, only add the first line of the two like so: -### Running Storybook.js +``` +127.0.0.1 local.graphpolaris.com +``` -To run the dev storybook (implementing visualizations) simply run `pnpm sb` and once it is running, ctrl-click the link what appears in the terminal. +### Other Commands + +You can run pnpm commands (see available ones in packages.json) from the root of the workspace, which will trigger turborepo to run it in all libraries and apps of the workspace. You can run `pnpm test` or `pnpm lint` this way to test or lint the entire workspace. + +You can also go into a specific lib/app and run pnpm commands from there to scope the task to only that part of the workspace. diff --git a/apps/docs/package.json b/apps/docs/package.json index 01d7b1a8c94dd9bcebbe4343213f41f2089958da..97c3bc1f07e9471ed3819ca64aca387d4d3b6432 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -2,12 +2,7 @@ "name": "docs", "version": "0.0.0", "private": true, - "scripts": { - "dev": "next dev --port 3001", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, + "scripts": {}, "dependencies": { "next": "^13.1.1", "react": "18.2.0", diff --git a/apps/web/.env b/apps/web/.env new file mode 100644 index 0000000000000000000000000000000000000000..3748091227f84a56cbfe836fbb081e9635fc1a5c --- /dev/null +++ b/apps/web/.env @@ -0,0 +1,2 @@ +VITE_BACKEND_URL=api.graphpolaris.com +VITE_STAGING=local \ No newline at end of file diff --git a/apps/web/.env.production b/apps/web/.env.production new file mode 100644 index 0000000000000000000000000000000000000000..895239e152994129431d2375a6c22bc0402e4df8 --- /dev/null +++ b/apps/web/.env.production @@ -0,0 +1,2 @@ +VITE_BACKEND_URL=api.graphpolaris.com +VITE_STAGING=prod \ No newline at end of file diff --git a/apps/web/.eslintrc.json b/apps/web/.eslintrc.json index 2707c1f4120744f9e9e9e736c8aad7cf59a0862c..351d714ab66565664d2f52885fdeb9d1d52c0976 100644 --- a/apps/web/.eslintrc.json +++ b/apps/web/.eslintrc.json @@ -7,7 +7,7 @@ "jsx": true } }, - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "node.d.ts"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], diff --git a/apps/web/node.d.ts b/apps/web/node.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..0156111480f412902209f239be01e460baa8192f --- /dev/null +++ b/apps/web/node.d.ts @@ -0,0 +1,6 @@ +interface ImportMeta { + env: { + VITE_BACKEND_URL: string; + VITE_STAGING: string; + } +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 7b5586514ec9f3e800540e537476b8013013dec5..9dc529d75f8c8da5f0b1a1d5b904c8884d4c4fcf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -35,8 +35,11 @@ "@types/styled-components": "^5.1.26", "@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-react-swc": "^3.0.0", + "autoprefixer": "^10.4.14", "graphology-types": "^0.24.7", + "postcss": "^8.4.21", "react-is": "^18.2.0", + "tailwindcss": "^3.3.1", "typescript": "^4.9.3", "vite": "^4.2.0", "vite-plugin-dts": "^2.1.0", diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..2f20e4a741aecf2ad32e5e236c551954c0cb556b --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: { config: './tailwind.config.js' }, + autoprefixer: {}, + }, +}; diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 9ae2badfaf7eceda8e4dcb1a6792d14e5751e0b5..171a7a690c6d1c951bcde069e77c2e6a855a996e 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -1,107 +1,160 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import GridLayout from 'react-grid-layout'; -import LoginScreen from '../components/login/loginScreen'; import Panel from '../components/panels/panel'; import { RawJSONVis } from '@graphpolaris/shared/lib/vis/rawjsonvis/rawjsonvis'; import SemanticSubstrates from '@graphpolaris/shared/lib/vis/semanticsubstrates/semanticsubstrates'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; import { - GetAllDatabases, - GetUserInfo, -} from '@graphpolaris/shared/lib/data-access/api'; -import { QueryBuilder } from '@graphpolaris/shared/lib/querybuilder'; + Query2BackendQuery, + QueryBuilder, +} from '@graphpolaris/shared/lib/querybuilder'; import { assignNewGraphQueryResult, useAppDispatch, } from '@graphpolaris/shared/lib/data-access/store'; +import { Navbar } from '../components/navbar/navbar'; +import { VisualizationPanel } from './panels/Visualization'; import { - AuthorizationHandler, + readInSchemaFromBackend, useAuthorization, -} from '@graphpolaris/shared/lib/data-access/authorization'; -import { Navbar } from '../components/navbar/navbar'; + useAuthorizationCache, + useDatabaseAPI, + useQueryAPI, + useQuerybuilderGraph, + useQuerybuilderGraphology, + useSchemaAPI, + useSessionCache, +} from '@graphpolaris/shared/lib/data-access'; +import LoginScreen from '../components/login/loginScreen'; +import { WebSocketHandler } from '@graphpolaris/shared/lib/data-access/socket'; +import Broker from '@graphpolaris/shared/lib/data-access/socket/broker'; +import { SchemaFromBackend } from '@graphpolaris/shared/lib/model/backend'; +import { domain } from '../environments/variables'; +import { QueryMultiGraphExport } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils'; +import { GraphQueryResultFromBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; export function App() { + const { AuthorizeFromCache, auth } = useAuthorization(domain); + const api = useDatabaseAPI(domain); + const api_schema = useSchemaAPI(domain); + const api_query = useQueryAPI(domain); const dispatch = useAppDispatch(); - const authorization = useAuthorization(); + const session = useSessionCache(); + const query = useQuerybuilderGraph(); + const ws = useRef(new WebSocketHandler(domain)); useEffect(() => { - if (authorization.userAuthorized) { - GetAllDatabases().then((d) => { - console.log(d); + // Default + AuthorizeFromCache(); + Broker.instance().subscribe( + (data: SchemaFromBackend) => dispatch(readInSchemaFromBackend(data)), + 'schema_result' + ); + + Broker.instance().subscribe( + (data: GraphQueryResultFromBackend) => + dispatch(assignNewGraphQueryResult(data)), + 'query_result' + ); + + return () => { + Broker.instance().unSubscribeAll('schema_result'); + Broker.instance().unSubscribeAll('query_result'); + }; + }, []); + + useEffect(() => { + // New active database + if (session.currentDatabase) { + ws.current.useToken(auth.accessToken).connect(() => { + api_schema.RequestSchema(session.currentDatabase); }); } - }, [authorization.userAuthorized]); + }, [session.currentDatabase]); + + useEffect(() => { + // Newly (un)authorized + console.log(auth.authorized); + if (auth.authorized) { + api.GetAllDatabases({ updateSessionCache: true }); + } + }, [auth.authorized]); + + useEffect(() => { + // New query + if (session?.currentDatabase && query) { + api_query.execute(Query2BackendQuery(session.currentDatabase, query)); + } + }, [query]); return ( - <> - {!authorization.userAuthorized && <LoginScreen />} - <Navbar /> - <GridLayout - className="layout" - cols={10} - rowHeight={30} - width={window.innerWidth} + <div className="h-screen w-screen"> + {!auth.authorized && <LoginScreen />} + <div + className={ + 'flex h-screen w-screen overflow-hidden ' + + (!auth.authorized ? 'blur-sm pointer-events-none ' : '') + } > - <div - key="schema-panel" - data-grid={{ x: 0, y: 0, w: 3, h: 30, maxW: 5, isDraggable: false }} - > - <Panel content="Schema Panel" color="red"> - <Schema /> - </Panel> - </div> - <div - key="query-panel" - data-grid={{ x: 3, y: 20, w: 5, h: 10, maxH: 20, isDraggable: false }} - > - <Panel content="Query Panel" color="blue"> - <QueryBuilder /> - </Panel> - </div> - <div - key="visualisation-panel" - data-grid={{ x: 3, y: 0, w: 7, h: 20, isDraggable: false }} - > - <Panel content="Visualisation Panel" color="green"> - <div> - <button - onClick={() => - dispatch( - assignNewGraphQueryResult({ - nodes: [ - { - id: 'agent/007', - attributes: { name: 'Daniel Craig' }, - }, - ], - links: [], - }) - ) - } - > - Load in mock result - </button> - <button - onClick={() => - dispatch(assignNewGraphQueryResult({ nodes: [], links: [] })) - } - > - Remove mock result - </button> + <div className="h-full relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden"> + <div className="h-fit flex-grow-0"> + <Navbar /> + </div> + <main className="h-full flex gap-4 p-10"> + <div className="w-full basis-[33%]"> + <Panel content="Schema"> + <Schema /> + </Panel> </div> - {/* <RawJSONVis /> */} - {/* <SemanticSubstrates /> */} - <div /> - </Panel> - </div> - <div - key="history-panel" - data-grid={{ x: 8, y: 20, w: 2, h: 10, isDraggable: false }} - > - <Panel content="History Channel" color="purple"></Panel> + <div className="w-full basis-[67%] flex flex-col gap-4"> + <div className="h-full basis-[67%] overflow-y-clip"> + <VisualizationPanel /> + </div> + <div className="h-full basis-[33%] flex-grow-0"> + <Panel content="Query Panel"> + <QueryBuilder /> + </Panel> + </div> + </div> + </main> </div> - </GridLayout> - </> + </div> + {/*<GridLayout*/} + {/* className="layout"*/} + {/* cols={10}*/} + {/* rowHeight={30}*/} + {/* width={window.innerWidth}*/} + {/*>*/} + {/* <div*/} + {/* key="schema-panel"*/} + {/* data-grid={{ x: 0, y: 0, w: 3, h: 30, maxW: 5, isDraggable: false }}*/} + {/* >*/} + {/* <Panel content="Schema Panel" color="red">*/} + {/* <Schema />*/} + {/* </Panel>*/} + {/* </div>*/} + {/* <div*/} + {/* key="query-panel"*/} + {/* data-grid={{ x: 3, y: 20, w: 5, h: 10, maxH: 20, isDraggable: false }}*/} + {/* >*/} + {/* <Panel content="Query Panel" color="blue">*/} + {/* <QueryBuilder />*/} + {/* </Panel>*/} + {/* </div>*/} + {/* <div*/} + {/* key="visualisation-panel"*/} + {/* data-grid={{ x: 3, y: 0, w: 7, h: 20, isDraggable: false }}*/} + {/* >*/} + {/* <VisualizationPanel />*/} + {/* </div>*/} + {/* <div*/} + {/* key="history-panel"*/} + {/* data-grid={{ x: 8, y: 20, w: 2, h: 10, isDraggable: false }}*/} + {/* >*/} + {/* <Panel content="History Channel" color="purple"></Panel>*/} + {/* </div>*/} + {/*</GridLayout>*/} + </div> ); } diff --git a/apps/web/src/app/panels/Visualization.tsx b/apps/web/src/app/panels/Visualization.tsx new file mode 100644 index 0000000000000000000000000000000000000000..53fefc57274fc78b68920763ecb2901f21ecd3af --- /dev/null +++ b/apps/web/src/app/panels/Visualization.tsx @@ -0,0 +1,47 @@ +import { RawJSONVis, NodeLinkVis } from '@graphpolaris/shared/lib/vis'; +import Panel from '../../components/panels/panel'; +import { + assignNewGraphQueryResult, + useAppDispatch, +} from '@graphpolaris/shared/lib/data-access'; + +export const VisualizationPanel = () => { + const dispatch = useAppDispatch(); + + return ( + <Panel content="Visualization Panel"> + {/* <div> + <button + onClick={() => + dispatch( + assignNewGraphQueryResult({ + nodes: [ + { + id: 'agent/007', + attributes: { name: 'Daniel Craig' }, + }, + ], + edges: [], + }) + ) + } + > + Load in mock result + </button> + <button + onClick={() => + dispatch(assignNewGraphQueryResult({ nodes: [], edges: [] })) + } + > + Remove mock result + </button> + </div> */} + <div className="h-[48rem] overflow-y-scroll"> + <RawJSONVis /> + {/* <NodeLinkVis /> */} + </div> + {/* <SemanticSubstrates /> */} + {/* <div /> */} + </Panel> + ); +}; diff --git a/apps/web/src/components/login/loginScreen.tsx b/apps/web/src/components/login/loginScreen.tsx index 2e4463709e61f3cef25e3befd1e32855f637c879..9bf756ad5fe82cba5a0ecfa3ca60e1966795f21f 100644 --- a/apps/web/src/components/login/loginScreen.tsx +++ b/apps/web/src/components/login/loginScreen.tsx @@ -1,8 +1,10 @@ -import { AuthorizationHandler } from '@graphpolaris/shared/lib/data-access/authorization'; +import { useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; +import { useAuthorization } from '@graphpolaris/shared/lib/data-access/authorization'; +import { domain } from '../../environments/variables'; import styled from 'styled-components'; const Wrapper = styled.div` - font-family: 'Arial'; + font-family: Arial, serif; position: absolute; left: 0; top: 0; @@ -53,7 +55,7 @@ const Content = styled.div` padding: 0; // Same width flexbox items - flex: 1 1 0px; + flex: 1 1 0; max-height: 3em; @@ -64,6 +66,8 @@ const Content = styled.div` `; const LoginScreen = () => { + const { SetAccessToken } = useAuthorization(domain); + const openSignInWindow = (url: string) => { // remove any existing event listeners window.removeEventListener('auth_message', receiveMessage); @@ -88,17 +92,17 @@ const LoginScreen = () => { if (window.location.hostname !== event.detail.origin) { return; } - console.log(window.location.hostname, event.detail.origin); + console.log(event.detail.token); // Set access token - AuthorizationHandler.instance().SetAccessToken(event.detail.token); + SetAccessToken(event.detail.token); }; return ( <Wrapper> <Background></Background> <Content> - <h1>Sign In</h1> + <h1 className="pointer-events-none">Sign In</h1> <img onClick={() => openSignInWindow( diff --git a/apps/web/src/components/navbar/AddDatabaseForm/add-database-form.module.scss b/apps/web/src/components/navbar/AddDatabaseForm/add-database-form.module.scss index 97642ae3758a844a1c36c7afcb9185993e8c419b..d3db3650042c5437bd2b9539f2b6b6bdb05c2115 100644 --- a/apps/web/src/components/navbar/AddDatabaseForm/add-database-form.module.scss +++ b/apps/web/src/components/navbar/AddDatabaseForm/add-database-form.module.scss @@ -14,7 +14,8 @@ flex-direction: column; flex-wrap: wrap; justify-content: center; - max-width: 400px; + gap: 1em; + max-width: 600px; margin: auto; overflow: auto; min-height: 300px; @@ -29,6 +30,10 @@ } .formWrapper { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 1em; } .loginContainer { @@ -38,6 +43,7 @@ .loginContainerRow { display: flex; flex-direction: row; + gap: 0.5em; margin: 5px 0px; } @@ -72,4 +78,3 @@ passLabel { cancelButton { margin-top: 2.5%; } - diff --git a/apps/web/src/components/navbar/AddDatabaseForm/index.tsx b/apps/web/src/components/navbar/AddDatabaseForm/index.tsx index 54c1b0ac0b9e9aa2066161670b3184a40e82907e..a92c13025ac51739aeede648242f626c89ee4d75 100644 --- a/apps/web/src/components/navbar/AddDatabaseForm/index.tsx +++ b/apps/web/src/components/navbar/AddDatabaseForm/index.tsx @@ -110,7 +110,9 @@ export default function AddDatabaseForm(props: AddDatabaseFormProps) { }} > {databaseNameMapping.map((dbName) => ( - <option value={dbName}>{dbName}</option> + <option value={dbName} key={dbName}> + {dbName} + </option> ))} </NativeSelect> </div> diff --git a/apps/web/src/components/navbar/navbar.module.scss b/apps/web/src/components/navbar/navbar.module.scss index e1faa9a7863fcca1b4c52135300e3951de0e5b8e..339c62c0bdcfd45e3d48f1e0b69cde6fb1519d08 100644 --- a/apps/web/src/components/navbar/navbar.module.scss +++ b/apps/web/src/components/navbar/navbar.module.scss @@ -1,6 +1,7 @@ .root { display: flex; overflow: hidden; + height: 50px; } .appBar { @@ -41,4 +42,3 @@ color: black; font-weight: bold; } - diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index ef3d9c91ec3ddf61b1b4d975fea0285372bd95db..8c78781d500e58d183ec094bd21ad511056ff6d4 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -8,7 +8,7 @@ /* The comment above was added so the code coverage wouldn't count this file towards code coverage. * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AppBar, Toolbar, @@ -26,17 +26,19 @@ import logo_white from './logogpwhite.png'; import AddDatabaseForm from './AddDatabaseForm'; import { useTheme } from '@mui/material/styles'; import styles from './navbar.module.scss'; -import { - AddDatabase, - AddDatabaseRequest, - GetAllDatabases, - RequestSchema, - useAuthorization, -} from '@graphpolaris/shared/lib/data-access'; + import { updateCurrentDatabase, updateDatabaseList, } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; +import { + AddDatabaseRequest, + useAppDispatch, + useAuthorizationCache, + useDatabaseAPI, + useSchemaAPI, + useSessionCache, +} from '@graphpolaris/shared/lib/data-access'; /** NavbarComponentProps is an interface containing the NavbarViewModel. */ export interface NavbarComponentProps { @@ -45,16 +47,11 @@ export interface NavbarComponentProps { /** NavbarComponentState is an interface containing the type of visualizations. */ export interface NavbarComponentState { - isAuthorized: boolean; - clientID: string; - sessionID: string; - databases: string[]; - currentDatabase: string; - // The anchor for rendering the user data (clientID, sessionID, add database, etc) menu userMenuAnchor?: Element; // The anchor for rendering the menu for selecting a database to use selectDatabaseMenuAnchor?: Element; + deleteDatabaseMenuAnchor?: Element; // Determines if the addDatabaseForm will be shown showAddDatabaseForm: boolean; } @@ -62,30 +59,35 @@ export interface NavbarComponentState { /** NavbarComponent is the View implementation for Navbar */ export const Navbar = (props: NavbarComponentProps) => { const theme = useTheme(); - const authorization = useAuthorization(); + const auth = useAuthorizationCache(); + const session = useSessionCache(); + const api = useDatabaseAPI(); + const schemaApi = useSchemaAPI(); + const dispatch = useAppDispatch(); + + useEffect(() => { + console.log(auth); + }, [auth.accessToken]); // const { navbarViewModel, currentColours } = props; // this.navbarViewModel = navbarViewModel; const [state, setState] = useState<NavbarComponentState>({ - isAuthorized: false, - clientID: authorization.userAuthorized ? authorization.userId || '' : '', - sessionID: authorization.sessionId || '', - databases: [], - currentDatabase: '', - userMenuAnchor: undefined, selectDatabaseMenuAnchor: undefined, + deleteDatabaseMenuAnchor: undefined, showAddDatabaseForm: false, }); /** Closes the user menu. Also closes all nested menu's. */ function closeUserMenu(): void { // If a nested window is open, close the main user menu a bit later - if (state.selectDatabaseMenuAnchor != undefined) { - setState({ ...state, selectDatabaseMenuAnchor: undefined }); - setTimeout(() => setState({ ...state, userMenuAnchor: undefined }), 100); - } else setState({ ...state, userMenuAnchor: undefined }); + setState({ + ...state, + selectDatabaseMenuAnchor: undefined, + deleteDatabaseMenuAnchor: undefined, + }); + setTimeout(() => setState({ ...state, userMenuAnchor: undefined }), 100); } function changeColourPalette() { @@ -98,15 +100,11 @@ export const Navbar = (props: NavbarComponentProps) => { function onAddDatabaseFormSubmit( request: AddDatabaseRequest ): Promise<void | Response> { - return AddDatabase(request).then(() => - GetAllDatabases().then((databases) => { - updateDatabaseList(databases); - updateCurrentDatabase(request.name); - // When the database changes, request the new schema - console.log('databases ' + databases); - RequestSchema(request.name); - }) - ); + return api + .AddDatabase(request, { updateDatabaseCache: true, setAsCurrent: true }) + .then(() => { + schemaApi.RequestSchema(request.name); + }); } const currentLogo = theme.palette.custom.logo == 'white' ? logo_white : logo; @@ -117,7 +115,6 @@ export const Navbar = (props: NavbarComponentProps) => { <AppBar title="GraphPolaris" style={{ - zIndex: 1250, backgroundColor: theme.palette.custom.background, }} position="fixed" @@ -128,13 +125,13 @@ export const Navbar = (props: NavbarComponentProps) => { <img src={currentLogo} className={styles.logo} /> </a> <div className={styles.menubox}> - <Button + {/* <Button className={styles.menuText} style={{ color: theme.palette.custom.menuText }} onClick={changeColourPalette} > Change Palette - </Button> + </Button> */} <Button href="https://graphpolaris.com/" className={styles.menuText} @@ -163,7 +160,7 @@ export const Navbar = (props: NavbarComponentProps) => { > Contact </Button> - {state.isAuthorized ? ( + {auth.authorized ? ( <div> <IconButton color="inherit" @@ -186,10 +183,10 @@ export const Navbar = (props: NavbarComponentProps) => { onClose={() => closeUserMenu()} > <MenuItem> - <ListItemText primary={'clientID: ' + state.clientID} /> + <ListItemText primary={'userID: ' + auth.userID} /> </MenuItem> <MenuItem> - <ListItemText primary={'sessionID: ' + state.sessionID} /> + <ListItemText primary={'sessionID: ' + auth.sessionID} /> </MenuItem> {/* <MenuItem onClick={() => @@ -232,16 +229,58 @@ export const Navbar = (props: NavbarComponentProps) => { open={Boolean(state.selectDatabaseMenuAnchor)} onClose={() => closeUserMenu()} > - {state.databases.length > 0 ? ( - state.databases.map((database) => ( + {session.databases.length > 0 ? ( + session.databases.map((database) => ( + <MenuItem + key={database} + selected={database == session.currentDatabase} + onClick={() => { + if (session.currentDatabase != database) { + dispatch(updateCurrentDatabase(database)); + } + closeUserMenu(); + }} + > + <ListItemText primary={database} /> + </MenuItem> + )) + ) : ( + <MenuItem key="placeholder" value="" disabled> + no databases connected + </MenuItem> + )} + </Menu> + <MenuItem + onClick={(event) => + setState({ + ...state, + deleteDatabaseMenuAnchor: event.currentTarget, + }) + } + > + <ListItemText primary={'Delete database'} /> + </MenuItem> + <Menu + id="delete-databases-menus" + anchorOrigin={{ vertical: 'top', horizontal: 'left' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + anchorEl={state.deleteDatabaseMenuAnchor} + keepMounted + disableAutoFocusItem + open={Boolean(state?.deleteDatabaseMenuAnchor)} + onClose={() => closeUserMenu()} + > + {session.databases.length > 0 ? ( + session.databases.map((database) => ( <MenuItem key={database} - selected={database == state.currentDatabase} + selected={database == session.currentDatabase} onClick={() => { - if (state.currentDatabase != database) { - updateCurrentDatabase(database); - closeUserMenu(); + if (session.currentDatabase === database) { + dispatch(updateCurrentDatabase('')); } + api.DeleteDatabase(database); + closeUserMenu(); }} > <ListItemText primary={database} /> diff --git a/apps/web/src/components/panels/panel.tsx b/apps/web/src/components/panels/panel.tsx index 077dbe3662b5daa7df0cb9f88cc64f203702d88e..04f694ecf786428d734d2e2d25ea656910b0ada6 100644 --- a/apps/web/src/components/panels/panel.tsx +++ b/apps/web/src/components/panels/panel.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import { useTheme } from '@mui/material/styles'; interface Props { content: string; - color: string; + color?: string; children?: ReactNode; } @@ -31,15 +31,15 @@ const Content = styled.div` `; const Panel = (props: Props) => { - const theme = useTheme(); - return ( - <Wrapper color={props.color}> - <Content> + <Wrapper color={props?.color || 'white'} className=""> + <Content className=""> <h1 - style={{ - backgroundColor: theme.palette.custom.dataPointColors[1], - }} + style={ + { + // backgroundColor: theme.palette.custom.dataPointColors[1], + } + } > {props.content} </h1> diff --git a/apps/web/src/environments/variables.ts b/apps/web/src/environments/variables.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1a86d7fb58898b82edac581e0fe0c9612664c3d --- /dev/null +++ b/apps/web/src/environments/variables.ts @@ -0,0 +1 @@ +export const domain = import.meta.env.VITE_BACKEND_URL; \ No newline at end of file diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 6bfe07d138e9d28917400adaf2f2c1572df0f135..588f0faf3e145525e8ca14311de2ffe319c92e47 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -11,6 +11,7 @@ import App from './app/app'; import LoginPopupComponent from './components/login/popup'; import { CssBaseline } from '@mui/material'; import { createRoot } from 'react-dom/client'; +import './styles.css'; const domNode = document.getElementById('root'); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..b5c61c956711f981a41e95f7fcf0038436cfbb22 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..d21f1cdae70ca548855964467cc032c21ce5c1bb --- /dev/null +++ b/apps/web/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 55b05d7549e1cc7034047c0c8789cf13e935c748..3dae4981ca079ef523ea4d9b97b4a5835f0acb67 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -25,6 +25,6 @@ }, "exclude": ["node_modules", "public"], "include": ["src", "../../libs/shared/lib/querybuilder/panel/attributepill"], - "files": ["./cssmodule.d.ts", "./image.d.ts"], + "files": ["./node.d.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json index 9d31e2aed93c876bc048cf2f863cb2a847c901e8..a54ca1adb820b30780b53c9b7049fc731bf73452 100644 --- a/apps/web/tsconfig.node.json +++ b/apps/web/tsconfig.node.json @@ -3,7 +3,8 @@ "composite": true, "module": "ESNext", "moduleResolution": "Node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": ["node", "vite/client"] }, "include": ["vite.config.ts"] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 4fcc697290865b9615b091c8566157fe91a4f1c8..bc88eb31f01240d1249fbb16c23e73b1f0d009ec 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -7,10 +7,13 @@ import dts from 'vite-plugin-dts'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), basicSsl(), - dts({ - insertTypesEntry: true, - }),], + plugins: [ + react(), + basicSsl(), + dts({ + insertTypesEntry: true, + }), + ], resolve: { alias: { '@graphpolaris/shared/lib': path.resolve( diff --git a/libs/shared/README.md b/libs/shared/README.md new file mode 100644 index 0000000000000000000000000000000000000000..464090415c47109523e91779d4f40e19495c9cf1 --- /dev/null +++ b/libs/shared/README.md @@ -0,0 +1 @@ +# TODO diff --git a/libs/shared/lib/data-access/api/database.ts b/libs/shared/lib/data-access/api/database.ts index b627ba76dd56946a53719b29f5d66f787bc67c28..5764cfe0f92194e1b7714ef179a0f6454afbce8d 100644 --- a/libs/shared/lib/data-access/api/database.ts +++ b/libs/shared/lib/data-access/api/database.ts @@ -1,6 +1,7 @@ // All database related API calls -import { AuthorizationHandler } from '@graphpolaris/shared/lib/data-access/authorization'; +import { useAppDispatch, useAuthorizationCache, useSessionCache } from "../store"; +import { updateCurrentDatabase, updateDatabaseList } from "../store/sessionSlice"; export enum DatabaseType { ArangoDB = 0, @@ -19,66 +20,100 @@ export type AddDatabaseRequest = { type: DatabaseType; // Database type. 0 = ArangoDB, 1 = Neo4j }; -export function AddDatabase(request: AddDatabaseRequest): Promise<void> { - return new Promise((resolve, reject) => { - fetch('https://api.graphpolaris.com/user/database', { - method: 'POST', - credentials: 'same-origin', - headers: new Headers({ - Authorization: - 'Bearer ' + AuthorizationHandler.instance().AccessToken(), - }), - body: JSON.stringify(request), - }).then((response: Response) => { - if (!response.ok) { - reject(response.statusText); - } +export type AddDatabaseOptions = { + setAsCurrent?: boolean, updateDatabaseCache?: boolean +} - resolve(); - }); - }); +export type GetDatabasesOptions = { + updateSessionCache?: boolean } -export function GetAllDatabases(): Promise<Array<string>> { - return new Promise<Array<string>>((resolve, reject) => { - console.log(AuthorizationHandler.instance().AccessToken()); - fetch('https://api.graphpolaris.com/user/database', { - method: 'GET', - // credentials: 'same-origin', - headers: new Headers({ - Authorization: - 'Bearer ' + AuthorizationHandler.instance().AccessToken(), - }), - }) - .then((response: Response) => { - // if (!response.ok) { - // reject(response.statusText); - // } - - return response.json(); - }) - .then((json: any) => { - console.log(json); - resolve(json.databases); - }); - }); +export type DeleteDatabasesOptions = { + updateSessionCache?: boolean } -export function DeleteDatabase(name: string): Promise<void> { - return new Promise((resolve, reject) => { - fetch('https://api.graphpolaris.com/user/database/' + name, { - method: 'DELETE', +export const useDatabaseAPI = (domain: string) => { + const { accessToken } = useAuthorizationCache(); + const cache = useSessionCache(); + const dispatch = useAppDispatch(); + + function AddDatabase( + request: AddDatabaseRequest, + options: AddDatabaseOptions = {} + ): Promise<void> { + const { setAsCurrent = true, updateDatabaseCache = false } = options; + return new Promise((resolve, reject) => { + fetch(`https://${domain}/user/database`, { + method: 'POST', + credentials: 'same-origin', + headers: new Headers({ + Authorization: + 'Bearer ' + accessToken, + }), + body: JSON.stringify(request), + }).then((response: Response) => { + console.log(response); + + if (!response.ok) { + reject(response.statusText); + } + if (setAsCurrent) + dispatch(updateCurrentDatabase(request.name)); + if (updateDatabaseCache) + GetAllDatabases({ updateSessionCache: true }); + + resolve(); + }); + }); + } + + async function GetAllDatabases(options: GetDatabasesOptions = {}): Promise<Array<string>> { + const { updateSessionCache: updateDatabaseCache = true } = options; + console.log(accessToken); + const response = await fetch(`https://${domain}/user/database`, { + method: 'GET', credentials: 'same-origin', headers: new Headers({ Authorization: - 'Bearer ' + AuthorizationHandler.instance().AccessToken(), + 'Bearer ' + accessToken, }), - }).then((response: Response) => { - if (!response.ok) { - reject(response.statusText); - } + }); + console.log(response); - resolve(); + if (!response.ok) { + const text = await response.text(); + console.error(text); + return []; + // throw Error(response.statusText) + } + const json = await response.json(); + if (updateDatabaseCache) + dispatch(updateDatabaseList(json.databases)); + return json.databases; + } + + function DeleteDatabase(name: string, options: DeleteDatabasesOptions = {}): Promise<void> { + const { updateSessionCache: updateDatabaseCache = true } = options; + return new Promise((resolve, reject) => { + fetch(`https://${domain}/user/database/` + name, { + method: 'DELETE', + credentials: 'same-origin', + headers: new Headers({ + Authorization: + 'Bearer ' + accessToken, + }), + }).then((response: Response) => { + if (!response.ok) { + reject(response.statusText); + } + + if (updateDatabaseCache) + GetAllDatabases({ updateSessionCache: true }); + + resolve(); + }); }); - }); -} + } + + return { DatabaseType, AddDatabase, GetAllDatabases, DeleteDatabase } +}; \ No newline at end of file diff --git a/libs/shared/lib/data-access/api/index.ts b/libs/shared/lib/data-access/api/index.ts index eed07d0e97fe3d9e4353a511b074d44726399bcb..a378760c116118315644d41ba5d271be9fc5ae06 100644 --- a/libs/shared/lib/data-access/api/index.ts +++ b/libs/shared/lib/data-access/api/index.ts @@ -1,3 +1,4 @@ export * from './database' export * from './user' -export * from './schema' \ No newline at end of file +export * from './schema' +export * from './query' \ No newline at end of file diff --git a/libs/shared/lib/data-access/api/query.ts b/libs/shared/lib/data-access/api/query.ts new file mode 100644 index 0000000000000000000000000000000000000000..36aeeb0b9a8baa49c8b5a455ee963d9747a0edab --- /dev/null +++ b/libs/shared/lib/data-access/api/query.ts @@ -0,0 +1,52 @@ +// All database related API calls + +import { BackendQueryFormat } from "../../querybuilder/query-utils/BackendQueryFormat"; +import { useAuthorizationCache, useSessionCache } from "../store"; + +export const useQueryAPI = (domain: string) => { + const cache = useSessionCache(); + const { accessToken } = useAuthorizationCache(); + + async function execute(query: BackendQueryFormat) { + + const response = await fetch(`https://${domain}/query/execute/`, { + method: 'POST', + credentials: 'same-origin', + headers: new Headers({ + Authorization: + 'Bearer ' + accessToken, + }), + body: JSON.stringify(query) + }); + + if (!response?.ok) { + const ret = await response.text(); + console.error(response, ret); + } + const ret = await response.json(); + console.log('Sent Query EXECUTION', ret); + } + + async function retrieveCachedQuery(queryID: string) { + // TODO: check if this method is needed! + + // const response = await fetch(`https://${domain}/query/retrieve-cached/`, { + // method: 'POST', + // credentials: 'same-origin', + // headers: new Headers({ + // Authorization: + // 'Bearer ' + accessToken, + // }), + // body: JSON.stringify({ queryID }) + // }); + + // if (!response?.ok) { + // const ret = await response.text(); + // console.error(response, ret); + // } + // // const ret = await response.json(); + // console.log(response); + } + + return { execute, retrieveCachedQuery }; +}; diff --git a/libs/shared/lib/data-access/api/schema.ts b/libs/shared/lib/data-access/api/schema.ts index 004a8b8e87db1eae3242b84e4f0c7d2e841b66aa..02495e7ca30f7d58a67bf95bb3d2d1985e5674fc 100644 --- a/libs/shared/lib/data-access/api/schema.ts +++ b/libs/shared/lib/data-access/api/schema.ts @@ -1,7 +1,41 @@ // All database related API calls -import { AuthorizationHandler } from '@graphpolaris/shared/lib/data-access/authorization'; +import { useAuthorizationCache, useSessionCache } from "../store"; +export const useSchemaAPI = (domain: string) => { + const cache = useSessionCache(); + const { accessToken } = useAuthorizationCache(); -export function RequestSchema(databaseName: string) { -} \ No newline at end of file + + async function RequestSchema(databaseName?: string) { + if (!databaseName) databaseName = cache.currentDatabase; + if (!databaseName) { + throw Error('Must call with a database name'); + } + + const request = { + databaseName, + cached: false, + }; + + const response = await fetch(`https://${domain}/schema/`, { + method: 'POST', + credentials: 'same-origin', + headers: new Headers({ + Authorization: + 'Bearer ' + accessToken, + }), + body: JSON.stringify(request) + }); + + if (!response?.ok) { + const ret = await response.text(); + console.error(response, ret); + } + // const ret = await response.json(); + console.log(response); + + } + + return { RequestSchema }; +}; diff --git a/libs/shared/lib/data-access/api/user.ts b/libs/shared/lib/data-access/api/user.ts index 825538a8278a9d8d0234c64d01a6e7d074a5c2e4..30a2a6f2ab519d6ee41bb16149a51b49f91c631e 100644 --- a/libs/shared/lib/data-access/api/user.ts +++ b/libs/shared/lib/data-access/api/user.ts @@ -1,6 +1,6 @@ // All user related API calls -import { AuthorizationHandler } from '@graphpolaris/shared/lib/data-access/authorization'; +import { useAuthorizationCache } from "../store"; export type User = { Name: string; @@ -8,29 +8,35 @@ export type User = { SignInProvider: number; }; -export function GetUserInfo(): Promise<User> { - return new Promise<User>((resolve, reject) => { - const auth = AuthorizationHandler.instance(); - fetch('https://api.graphpolaris.com/user/', { - method: 'GET', - credentials: 'same-origin', - headers: new Headers({ - Authorization: 'Bearer ' + auth.AccessToken(), - }), - }) - .then((response: Response) => { - if (!response.ok) { - reject(response.statusText); - } +export const useUserAPI = (domain: string) => { + const { accessToken } = useAuthorizationCache(); - return response.json(); + function GetUserInfo(): Promise<User> { + return new Promise<User>((resolve, reject) => { + fetch(`https://${domain}/user/`, { + method: 'GET', + credentials: 'same-origin', + headers: new Headers({ + Authorization: 'Bearer ' + accessToken, + }), }) - .then((json: any) => { - resolve({ - Name: json.name, - Email: json.email, - SignInProvider: json.sign_in_provider, + .then((response: Response) => { + if (!response.ok) { + reject(response.statusText); + } + + return response.json(); + }) + .then((json: any) => { + resolve({ + Name: json.name, + Email: json.email, + SignInProvider: json.sign_in_provider, + }); }); - }); - }); -} + }); + } + + + return { GetUserInfo }; +}; diff --git a/libs/shared/lib/data-access/authorization/authorizationHandler.ts b/libs/shared/lib/data-access/authorization/authorizationHandler.ts deleted file mode 100644 index d8f2d1b7457571a856eb64f1eec4c6a0e976e8ae..0000000000000000000000000000000000000000 --- a/libs/shared/lib/data-access/authorization/authorizationHandler.ts +++ /dev/null @@ -1,187 +0,0 @@ -export class AuthorizationHandler { - private static _instance: AuthorizationHandler; - private accessToken = ''; - private authorized = false; - private userID = ''; - private sessionID = ''; - private callback?: () => void = undefined; - - // instance gets the AuthorizationHandler singleton instance - public static instance(): AuthorizationHandler { - if (!AuthorizationHandler._instance) { - AuthorizationHandler._instance = new AuthorizationHandler(); - } - - return AuthorizationHandler._instance; - } - - // SetCallback sets a function that will be called when the auth state changes - // TODO: This should be done with a store or custom hook - public setCallback(callback: () => void) { - this.callback = callback; - } - - // MARK: Authorization code - - /** - * Authorize attempts to authorize using a refresh-token set as a cookie. If the user has been inactive for more than 7 days this cookie will be gone. - * @returns true is authorization was successful, else returns false - */ - public async Authorize(): Promise<boolean> { - // Attempt to log in with a refresh-token - const authResponse = await this.getNewAccessToken(); - - // If the request was a success, we have an accessToken, userID and sessionID - if (authResponse.success) { - // Store them - this.userID = authResponse.userID ?? ''; - this.sessionID = authResponse.sessionID ?? ''; - - this.SetAccessToken(authResponse.accessToken ?? ''); - } - - return new Promise((resolve) => { - resolve(authResponse.success); - }); - } - - /** - * getNewAccessToken gets a new access token using the refresh-token cookie - * @returns an authResponse containing details - */ - private async getNewAccessToken(): Promise<authResponse> { - // If we have an access token already, append it to the url as a query param to keep sessionID the same - let url = 'https://api.graphpolaris.com/auth/refresh'; - if (this.accessToken != '') { - url += '?access_token=' + this.accessToken; - } - - return new Promise<authResponse>((resolve) => { - fetch(url, { - method: 'GET', - credentials: 'include', - }) - .then((response) => { - if (!response.ok) { - throw Error(response.statusText); - } - return response.json(); - }) - .then((responseJSON) => { - resolve({ - success: true, - accessToken: responseJSON.accessToken, - userID: responseJSON.userID, - sessionID: responseJSON.sessionID, - }); - }) - .catch(() => { - // User is not authorized - resolve({ success: false }); - }); - }); - } - - /** - * refreshTokens refreshes tokens - */ - private async refreshTokens() { - console.log('refreshing tokens'); - // Get a new access + refresh token pair - const authResponse = await this.getNewAccessToken(); - - if (authResponse.success) { - // Set the new access token - this.accessToken = authResponse.accessToken ?? ''; - - if (this.accessToken !== '') { - // Initialise the new refresh token - this.initializeRefreshToken(); - } - } - } - - /** - * initializeRefreshToken attempts to initialize a refresh token - */ - private async initializeRefreshToken() { - fetch( - 'https://api.graphpolaris.com/auth/refresh?access_token=' + - this.accessToken, - { - method: 'GET', - credentials: 'include', - } - ) - .then((response) => { - if (!response.ok) { - throw Error(response.statusText); - } - }) - .catch((error) => { - console.error(error); - }); - } - - // MARK: Getters - - /** - * Authorized returns the current authorization status - * @returns true if authorized - */ - Authorized(): boolean { - return this.authorized; - } - - /** - * AccessToken returns the current access token - * @returns token - */ - AccessToken(): string { - return this.accessToken; - } - - /** - * UserID returns the current user' ID - * @returns id - */ - UserID(): string { - return this.userID; - } - - /** - * SessionID returns the current session' ID - * @returns id - */ - SessionID(): string { - return this.sessionID; - } - - // MARK: Setters - /** - * SetAccessToken sets the current access token (should only be called by the sign-in component) - * @param accessToken - */ - async SetAccessToken(accessToken: string) { - this.accessToken = accessToken; - - // Activate the refresh token - this.initializeRefreshToken(); - - // Start the automatic refreshing every 10 minutes - setInterval(() => { - this.refreshTokens(); - }, 10 * 60 * 1000); - - if (this.callback) { - this.callback(); - } - } -} - -type authResponse = { - success: boolean; - accessToken?: string; - userID?: string; - sessionID?: string; -}; diff --git a/libs/shared/lib/data-access/authorization/authorizationHook.tsx b/libs/shared/lib/data-access/authorization/authorizationHook.tsx index 4110005e881aa28f62ecda514643609294a6b0fa..76fce13fa6d8bb23afad1e5f5b234b6640cb6933 100644 --- a/libs/shared/lib/data-access/authorization/authorizationHook.tsx +++ b/libs/shared/lib/data-access/authorization/authorizationHook.tsx @@ -1,41 +1,185 @@ import { useEffect, useState } from 'react'; -import { AuthorizationHandler } from '@graphpolaris/shared/lib/data-access'; +import { useAppDispatch, useAuthorizationCache } from '../store'; +import { dispatch } from 'd3'; +import { authorized, updateAccessToken } from '../store/authSlice'; -interface useIsAuthorizedState { - userAuthorized: boolean; - userId?: string; - sessionId?: string; -} +/** + * getNewAccessToken gets a new access token using the refresh-token cookie + * @returns an authResponse containing details + */ +async function getNewAccessToken( + domain: string, + accessToken: string +): Promise<authResponse> { + // If we have an access token already, append it to the url as a query param to keep sessionID the same + let url = `https://${domain}/auth/refresh`; + if (accessToken != '') { + url += '?access_token=' + accessToken; + } -export function useAuthorization() { - const [state, setState] = useState<useIsAuthorizedState>({ - userAuthorized: false, - }); - const authInstance = AuthorizationHandler.instance(); - - const authCallback = async () => { - setState({ - userAuthorized: authInstance.Authorized(), - userId: authInstance.UserID(), - sessionId: authInstance.SessionID(), + try { + const response = await fetch(url, { + method: 'GET', + credentials: 'include', }); - // Print the user that is currently logged in - // const user = await GetUserInfo(); - // console.log(user); - }; + if (!response.ok) { + // User is not authorized + const text = await response.text(); + console.error('User UNAUTHORIZED', text); - authInstance.setCallback(authCallback); + return { success: false }; + // throw Error(response.statusText); + } - // Attempt to Authorize the user - const authorize = async () => { - const authorized = await authInstance.Authorize(); - if (authorized) authCallback(); - }; + const json = await response.json(); + return { + success: true, + accessToken: json.accessToken, + userID: json.userID, + sessionID: json.sessionID, + }; + } catch (error) { + console.error('Error authorizing user', error); + return { success: false }; + } +} + +type authResponse = { + success: boolean; + accessToken?: string; + userID?: string; + sessionID?: string; +}; + +export type useIsAuthorizedState = { + userID?: string; + sessionID?: string; + accessToken: string; + authorized: boolean; +}; + +export function useAuthorization(domain: string) { + const dispatch = useAppDispatch(); + const auth = useAuthorizationCache(); + + /** + * refreshTokens refreshes tokens + */ + async function refreshTokens() { + console.log('refreshing tokens'); + // Get a new access + refresh token pair + const authResponse = await getNewAccessToken(domain, auth.accessToken); + + if (authResponse.success) { + // Set the new access token + dispatch(updateAccessToken(authResponse.accessToken ?? '')); + + if (auth.accessToken !== '') { + // Initialize the new refresh token + initializeRefreshToken(); + } + } + } - useEffect(() => { - authorize(); - }, []); + // /** + // * initializeRefreshToken attempts to initialize a refresh token + // */ + // async function refreshRefreshToken() { + // fetch( + // 'https://${domain}/auth/refresh?access_token=' + + // state.accessToken, + // { + // method: 'GET', + // credentials: 'include', + // } + // ) + // .then((response) => { + // if (!response.ok) { + // throw Error(response.statusText); + // } + // }) + // .catch((error) => { + // console.error(error); + // }); + // } - return state; + /** + * initializeRefreshToken attempts to initialize a refresh token + */ + async function initializeRefreshToken() { + fetch(`https://${domain}/auth/refresh`, { + method: 'POST', + credentials: 'include', + }) + .then((response) => { + if (!response.ok) { + console.error(response.statusText); + // throw Error(response.statusText); + } + }) + .catch((error) => { + console.error(error); + }); + } + + // MARK: Setters + /** + * SetAccessToken sets the current access token (should only be called by the sign-in component) + * @param accessToken + */ + async function SetAccessToken(accessToken: string) { + dispatch(updateAccessToken(accessToken)); + console.log('set access token', auth.accessToken); + + const result = await AuthorizeFromCache(); + if (result) { + // Activate the refresh token TODO + initializeRefreshToken(); + + // Start the automatic refreshing every 10 minutes + setInterval(() => { + refreshTokens(); + }, 10 * 60 * 1000); + } else { + console.error( + 'Failed Logging in due to refresh token initialization', + result + ); + // throw Error('Failed Logging in'); + } + } + + /** + * Authorize attempts to authorize using a refresh-token set as a cookie. If the user has been inactive for more than 7 days this cookie will be gone. + * @returns true is authorization was successful, else returns false + */ + async function AuthorizeFromCache(): Promise<boolean> { + // Attempt to log in with a refresh-token + const authResponse = await getNewAccessToken(domain, auth.accessToken); + + // If the request was a success, we have an accessToken, userID and sessionID + if (authResponse.success) { + console.log('SUCCESS authorize'); + + console.log(auth); + // Store them + await dispatch( + authorized({ + userID: authResponse.userID ?? '', + sessionID: authResponse.sessionID ?? '', + accessToken: authResponse.accessToken ?? '', + authorized: true, + }) + ); + } + + return authResponse.success; + } + + return { + AuthorizeFromCache, + SetAccessToken, + auth, + }; } diff --git a/libs/shared/lib/data-access/authorization/index.ts b/libs/shared/lib/data-access/authorization/index.ts index 0cf2dd57d11433d17c7556f071f558b458c14c2d..521daf5096bc7c9567fca3db52dea7cd6db6a00b 100644 --- a/libs/shared/lib/data-access/authorization/index.ts +++ b/libs/shared/lib/data-access/authorization/index.ts @@ -1,2 +1 @@ -export { AuthorizationHandler } from './authorizationHandler'; -export * from './authorizationHook'; +export * from './authorizationHook'; \ No newline at end of file diff --git a/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/DatabaseRepository.tsx b/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/DatabaseRepository.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e1d3a49b2e5fcbbfc6e6a638e8b8f6b98f47a3b8 --- /dev/null +++ b/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/DatabaseRepository.tsx @@ -0,0 +1,36 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/** Contains interface for adding user databases and fetching the connected databases */ +export type Database = { + // The user specified database, can be anything + databaseName: string; + username: string; + password: string; + port: number; + hostname: string; + // The database for the internal database + internalDatabaseName: string; + // The database type vendor, currently only arangodb and neo4j are supported + databaseType: string; + // Password for speficied databasehost +}; + +// Interface for a database api repository +export default interface DatabaseRepository { + /** + * Adds a database for the currently logged in user. + * @param database {Database} The database to add for the user. + * @returns A promise with a standard Response. + */ + addDatabase(database: Database): Promise<Response>; + + /** + * Fetches the currently connected database of the user. + * @returns The database names. + */ + fetchUserDatabases(): Promise<string[]>; +} diff --git a/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/QueryRepository.tsx b/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/QueryRepository.tsx new file mode 100644 index 0000000000000000000000000000000000000000..244a610146916c858eefa7044d99511663cda3f7 --- /dev/null +++ b/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/QueryRepository.tsx @@ -0,0 +1,27 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +import { TranslatedJSONQuery } from './TranslatedJSONQuery'; + +/** A type for a query to send to the backend. */ +export interface Query extends TranslatedJSONQuery { + databaseName: string; +} + +export default interface QueryRepository { + /** + * Sends a query to the database to execute. + * @param query {Query} The query to execute. + * @returns A promise with the assigned queryID. + */ + sendQuery(query: Query): Promise<string>; + + /** + * Sends a message to the backend to retrieve a cached query + * @param queryID {string} The id of the query to retrieve. + */ + retrieveCachedQuery(queryID: string): Promise<Response>; +} diff --git a/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/TranslatedJSONQuery.tsx b/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/TranslatedJSONQuery.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7d645f7457888aa32c25af47fd8868eaaa74dde5 --- /dev/null +++ b/libs/shared/lib/data-access/socket/DELETE-repository-interfaces/TranslatedJSONQuery.tsx @@ -0,0 +1,85 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/** JSON query format used to send a query to the backend. */ +export interface TranslatedJSONQuery { + return: { + entities: number[]; + relations: number[]; + groupBys: number[]; + }; + entities: Entity[]; + relations: Relation[]; + groupBys: GroupBy[]; + machineLearning: MachineLearning[]; + limit: number; +} + +/** Interface for an entity in the JSON for the query. */ +export interface Entity { + name: string; + ID: number; + constraints: Constraint[]; +} + +/** Interface for an relation in the JSON for the query. */ +export interface Relation { + name: string; + ID: number; + fromType: string; + fromID: number; + toType: string; + toID: number; + depth: { min: number; max: number }; + constraints: Constraint[]; +} + +/** + * Constraint datatypes created from the attributes of a relation or entity. + * + * string MatchTypes: exact/contains/startswith/endswith. + * int MatchTypes: GT/LT/EQ. + * bool MatchTypes: EQ/NEQ. + */ +export interface Constraint { + attribute: string; + dataType: string; + + matchType: string; + value: string; +} + +/** Interface for a function in the JSON for the query. */ +export interface GroupBy { + ID: number; + + groupType: string; + groupID: number[]; + groupAttribute: string; + + byType: string; + byID: number[]; + byAttribute: string; + + appliedModifier: string; + relationID: number; + constraints: Constraint[]; +} + +/** Interface for Machine Learning algorithm */ +export interface MachineLearning { + ID?: number; + queuename: string; + parameters: string[]; +} +/** Interface for what the JSON needs for link predicition */ +export interface LinkPrediction { + queuename: string; + parameters: { + key: string; + value: string; + }[]; +} diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessageReceiver.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessageReceiver.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5d1395012872f3ae0dec6604de5897b1a8d0e7bb --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessageReceiver.tsx @@ -0,0 +1,12 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/** Interface for receiving messages from the backend */ +export default interface BackendMessageReceiver { + connect(onOpen: () => void): void; + close(): void; + isConnected(): boolean; +} diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessageReceiverMock.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessageReceiverMock.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3bb7c4480bb6ac8cd709921d17e4939bffb9b3af --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessageReceiverMock.tsx @@ -0,0 +1,31 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ +/* istanbul ignore file */ +/* The comment above was added so the code coverage wouldn't count this file towards code coverage. + * We do not test mocks. + * See testing plan for more details.*/ + +import BackendMessageReceiver from './BackendMessageReceiver'; + +/** A mock for the backend message receiver used for testing purposes. */ +export default class BackendMessageReceiverMock + implements BackendMessageReceiver +{ + private connected = false; + + public connect(): void { + this.connected = true; + } + public close(): void { + this.connected = false; + } + + public onMessage(msg: string): void {} + + public isConnected(): boolean { + return this.connected; + } +} diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessengerRepository.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessengerRepository.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1b78ea3f015621d8047033ed7d00cec6c605dfaa --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/BackendMessengerRepository.tsx @@ -0,0 +1,13 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/** Interface for sending messages to the backend. */ +export default interface BackendMessengerRepository { + /** throws {Error} if credentials have not passed. */ + SendMessage(body: string, request: string, method: string): Promise<Response>; + + SendRequest(request: string): Promise<Response>; +} diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.test.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3f2e1dfa2ff4194eebe3c6ac1ef5afdce569c7bd --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.test.tsx @@ -0,0 +1,28 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ +import { describe, expect, it } from 'vitest'; + +import { WebSocketHandler } from './WebSocketHandler'; + +//FIXME +describe('WebSocketHandler', () => { + // let websockethandler = new WebSocketHandler('placeholderName'); + + // it('Should connect', () => { + // websockethandler.connect(websockethandler.close); + // expect(websockethandler.isConnected()).toEqual(true); + // }); + + // it('Should disconnect', () => { + // websockethandler.connect(websockethandler.close); + // websockethandler.close(); + // expect(websockethandler.isConnected()).toEqual(false); + // }); + + it('pass', () => { + expect(false).toEqual(false); + }); +}); diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7645f8c008b6742189438fa22986d30cb5d2d3c4 --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx @@ -0,0 +1,84 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +import Broker from '../broker'; +import BackendMessageReceiver from './BackendMessageReceiver'; + +/** The websockethandler creates a websocket and wait for messages send to the socket. */ +export class WebSocketHandler implements BackendMessageReceiver { + private webSocket: WebSocket | undefined; + private url: string; + private connected: boolean; + private token: string | undefined; + + /** @param domain The domain to make the websocket connection with. */ + public constructor(domain: string) { + this.url = 'wss://' + domain + '/socket/'; + this.connected = false; + } + + public useToken(token: string): WebSocketHandler { + this.token = token; + return this; + } + + /** + * Create a websocket to the given URL. + * @param {string} URL is the URL to which the websocket connection is opened. + */ + public connect(onOpen: () => void): void { + // If there already is already a current websocket connection, close it first. + if (this.webSocket) this.close(); + + this.webSocket = new WebSocket( + this.url + (!!this.token ? `?jwt=${this.token}` : '') + ); + this.webSocket.onopen = () => onOpen(); + this.webSocket.onmessage = this.onWebSocketMessage; + this.webSocket.onerror = this.onError; + this.webSocket.onclose = this.onClose; + + this.connected = true; + } + + /** Closes the current websocket connection. */ + public close = (): void => { + if (this.webSocket) this.webSocket.close(); + this.connected = false; + }; + + /** @returns A boolean which indicates if there currently is a socket connection. */ + public isConnected = (): boolean => { + return this.connected; + }; + + /** + * Websocket connection close event handler. + * @param {any} event Contains the event data. + */ + private onClose(event: any): void { + console.log(event.data); + } + + /** + * Websocket connection message event handler. Called if a new message is received through the socket. + * @param {any} event Contains the event data. + */ + public onWebSocketMessage = (event: MessageEvent<any>) => { + let data = JSON.parse(event.data); + console.log('WS message: ', data); + + Broker.instance().publish(data.value, data.type); + }; + + /** + * Websocket connection error event handler. + * @param {any} event contains the event data. + */ + private onError(event: any): void { + console.log(event); + } +} diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/index.ts b/libs/shared/lib/data-access/socket/backend-message-receiver/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1690ebf99a3bd68acf3949ac60fdaf96285a31a9 --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/index.ts @@ -0,0 +1 @@ +export * from './WebSocketHandler' \ No newline at end of file diff --git a/libs/shared/lib/data-access/socket/backend-messenger/BackendMessenger.test.tsx b/libs/shared/lib/data-access/socket/backend-messenger/BackendMessenger.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3d347168e89ff0cd4ef86ebf12c5e26fc694732d --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-messenger/BackendMessenger.test.tsx @@ -0,0 +1,18 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ +import { assert, describe, expect, it, vi } from 'vitest'; + +import BackendMessenger from '.'; +describe('BackendMessenger', () => { + let placeholderDomain = 'placeholderDomain'; + let backendMessenger = new BackendMessenger(placeholderDomain); + + it('should create the the correct url ', () => { + expect(backendMessenger['url']).toEqual( + 'https://' + placeholderDomain + '/' + ); + }); +}); diff --git a/libs/shared/lib/data-access/socket/backend-messenger/BackendMessengerMock.tsx b/libs/shared/lib/data-access/socket/backend-messenger/BackendMessengerMock.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ada2ac8a98dd15e2ab281566c801f0844950311b --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-messenger/BackendMessengerMock.tsx @@ -0,0 +1,87 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/* istanbul ignore file */ +/* The comment above was added so the code coverage wouldn't count this file towards code coverage. + * We do not test mock implementations. + * See testing plan for more details.*/ + +import BackendMessengerRepository from '../../../domain/repository-interfaces/BackendMessengerRepository'; + +/** Mock repository used for testing. */ +export default class BackendMessengerMock implements BackendMessengerRepository { + /** A mock version of the sendMessage function sends a fake message. */ + private url: string; + + /** @param domain The domain to send the messages towards. */ + constructor(domain: string) { + this.url = 'https://' + domain + '/'; + } + public SendMessage(body: string, requestURL: string, method: string): Promise<Response> { + switch (requestURL) { + case 'query/execute/': + return new Promise((resolve, reject) => { + if (body == 'validjsonquery') resolve(this.createResponse({ queryID: 'testID' })); + else if (body == 'no200response') resolve(new Response(undefined, { status: 404 })); + else if (body == 'notvalidjsonquery') reject('invalid body'); + else resolve(this.createResponse({ ok: 'ok' })); + }); + case 'query/retrieve-cached/': + return new Promise((resolve, reject) => { + const b = JSON.parse(body); + if ('queryID' in b) resolve(this.createResponse({})); + else reject('invalid body'); + }); + default: + return new Promise((resolve) => { + resolve(this.createResponse({ ok: 'ok' })); + }); + } + } + + /** A mock version of the sendRequest function sends a fake request. */ + public SendRequest(requestURL: string): Promise<Response> { + switch (requestURL) { + case 'user/callback': + return new Promise((resolve) => { + const result = { + clientID: 'mock-clientID', + }; + resolve(this.createResponse(result)); + }); + + case 'user/session/': + return new Promise((resolve) => { + const result = { + clientID: 'mock-clientID', + sessionID: 'mock-sessionID', + }; + resolve(this.createResponse(result)); + }); + case 'user/databases/': + return new Promise((resolve) => { + resolve(this.createResponse({ databases: ['testdb'] })); + }); + default: + return new Promise((resolve) => { + resolve(new Response(undefined, { status: 404 })); + }); + } + } + + /** A mock response created to test the backend messenger. */ + private createResponse(body: any): Response { + const meta = { + 'Content-Type': 'application/json', + }; + const headers = new Headers(meta); + const responseInit = { status: 200, headers: headers }; + + const response = new Response(JSON.stringify(body), responseInit); + + return response; + } +} diff --git a/libs/shared/lib/data-access/socket/backend-messenger/index.tsx b/libs/shared/lib/data-access/socket/backend-messenger/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d0633bcabb0c07861b29b67b7caf34cfadfa4448 --- /dev/null +++ b/libs/shared/lib/data-access/socket/backend-messenger/index.tsx @@ -0,0 +1,64 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +import BackendMessengerRepository from '../backend-message-receiver/BackendMessengerRepository'; + +/** Sends messages and requests to the backend. */ +export default class BackendMessenger implements BackendMessengerRepository { + private url: string; + + /** @param domain The domain to send the messages towards. */ + constructor(domain: string) { + this.url = 'https://' + domain + '/'; + } + + /** + * Sends a fetch request to the Datastrophe domain. + * @param {string} body The request body you want to send. Should be stringified JSON. + * @param {string} requestURL The URL you want to perform this request to, appended to 'datastrophe.science.uu.nl'. + * @param {string} requestMethod The method of your request. Most used are: POST, GET. + * @returns {Promise<void>} A promise which is resolved once a response with status 200 has been received. + */ + public SendMessage( + body: string, + requestURL: string, + requestMethod: string + ): Promise<Response> { + // Construct the URL we will request from + const req = this.url + requestURL; + + return fetch(req, { + method: requestMethod, + headers: new Headers({ + 'Content-Type': 'application/json', + }), + // Pass this parameter to always send authorization cookies, even on localhost + credentials: 'include', + // Set the body of the request, if this is not a POST request, body is set to undefined + body: requestMethod == 'POST' ? body : undefined, + // Wait for the fetch promise to resolve + }); + } + + /** + * Sendrequest sends a GET request to the backend. + * @param requestURL The URL you want to perform this request to, appended to 'datastrophe.science.uu.nl'. + * @returns {Promise<void>} A promise which is resolved once a response with status 200 has been received. + */ + public SendRequest(requestURL: string): Promise<Response> { + // Construct the URL we will request from + const req = this.url + requestURL; + console.log('backendmessager: ' + req); + return fetch(req, { + method: 'GET', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + // Pass this parameter to always send authorization cookies, even on localhost + credentials: 'include', + }); + } +} diff --git a/libs/shared/lib/data-access/socket/broker/broker.test.tsx b/libs/shared/lib/data-access/socket/broker/broker.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..514d2bb8e9e560024756d6a98a4164e357b1dbe5 --- /dev/null +++ b/libs/shared/lib/data-access/socket/broker/broker.test.tsx @@ -0,0 +1,76 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ +import { assert, describe, expect, it, vi } from 'vitest'; + +import Broker from '.'; + +// FIXME +// /** Testsuite for the testing of the broker and the broker listeners */ +describe('broker', () => { + it('pass', () => { + expect(false).toEqual(false); + }); + + // it('should correctly notify a listener with the correct routingkey', () => { + // const consumeFuncMock = vi.fn(); + + // // Subscribe the mock listener + // Broker.instance().subscribe(consumeFuncMock, 'testkey', 'k1'); + // Broker.instance().publish({ test: 'hoi' }, 'testkey'); + // expect(consumeFuncMock.mock.calls[0][0]).toEqual({ test: 'hoi' }); + + // // Test if it notifies our listener with a different routingkey + // Broker.instance().publish({ test: 'hoi' }, 'differentKey'); + // expect(consumeFuncMock).toBeCalledTimes(1); + + // // When unsubscribed we shouldn't get notified + // Broker.instance().unSubscribe('testkey', 'k1'); + // Broker.instance().publish({ test: 'hoi' }, 'testkey'); + // expect(consumeFuncMock).toBeCalledTimes(1); + // }); + + // it('should not throw when unsubscribing an already unsubbed listener', () => { + // // Test unsubscribing a listener to a routing key it was not subscribed to. + // expect(() => + // Broker.instance().unSubscribe('differentkey', 'k1') + // ).not.toThrow(); + // }); + + // it('should only subscribe a listener once to the same routing key', () => { + // const consumeFuncMock = vi.fn(); + // // Subscribe the mock listener twice. + // Broker.instance().subscribe(consumeFuncMock, 'testkey', 'k1', false); + // Broker.instance().subscribe(consumeFuncMock, 'testkey', 'k2', false); + // Broker.instance().publish({ test: 'hahaha' }, 'testkey'); + + // expect(consumeFuncMock).toBeCalledTimes(1); + + // Broker.instance().unSubscribe('testkey', 'k1'); + // }); + + // it('should console log when there are no listeners to publish to', () => { + // const mockConsoleLog = vi.fn(); + // console.log = mockConsoleLog; + + // Broker.instance().publish({ test: 'hoi' }, 'testkey'); + + // expect(mockConsoleLog).toBeCalledWith( + // 'no listeners for message with routing key %ctestkey', + // 'font-weight:bold; color: blue; background-color: white;', + // { test: 'hoi' } + // ); + // }); + + // it('should notify when there is a previous message available after subscribing', () => { + // const consumeFuncMock = vi.fn(); + // Broker.instance().publish({ test: '⚽' }, 'testkey'); + + // // Subscribe the mock listener twice. + // Broker.instance().subscribe(consumeFuncMock, 'testkey', 'k1', true); + + // expect(consumeFuncMock.mock.calls[0][0]).toEqual({ test: '⚽' }); + // }); +}); diff --git a/libs/shared/lib/data-access/socket/broker/index.tsx b/libs/shared/lib/data-access/socket/broker/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b67aa0da07569fac7730910ebfe7742aed220620 --- /dev/null +++ b/libs/shared/lib/data-access/socket/broker/index.tsx @@ -0,0 +1,102 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ +import BrokerListener from './BrokerListenerInterface'; + +/** + * A broker that handles incoming messages from the backend. + * It works with routingkeys, a listener can subscribe to messages from the backend with a specific routingkey. + * Possible routingkeys: + * - query_result: Contains an object with nodes and edges or a numerical result. + * - query_translation_result: Contains the query translated to the database language. + * - schema_result: Contains the schema of the users database. + * - query_status_update: Contains an update to if a query is being executed. + * - query_database_error: Contains the error received from the database. + * - query_sent: Contains the message that a query has been send. + * - gsa_node_result: Contains a node that has the data for the graph-schema-analytics + * - gsa_edge_result: Contains a edge that has the data for the graph-schema-analytics + */ +export default class Broker { + private static singletonInstance: Broker; + private listeners: Record<string, Record<string, Function>> = {}; + + /** mostRecentMessages is a dictionary with <routingkey, messageObject>. It stores the most recent message for that routingkey. */ + private mostRecentMessages: Record<string, unknown> = {}; + + /** Get the singleton instance of the Broker. */ + public static instance(): Broker { + if (!this.singletonInstance) this.singletonInstance = new Broker(); + return this.singletonInstance; + } + + /** + * Notify all listeners which are subscribed with the specified routingkey. + * @param {unknown} jsonObject An json object with unknown type. + * @param {string} routingKey The routing to publish the message to. + */ + public publish(jsonObject: unknown, routingKey: string): void { + this.mostRecentMessages[routingKey] = jsonObject; + + if ( + this.listeners[routingKey] && + Object.keys(this.listeners[routingKey]).length != 0 + ) + Object.values(this.listeners[routingKey]).forEach((listener) => + listener(jsonObject, routingKey) + ); + // If there are no listeners, log the message + else + console.log( + `no listeners for message with routing key %c${routingKey}`, + 'font-weight:bold; color: blue; background-color: white;', + jsonObject + ); + } + + /** + * Subscribe a listener to messages with the specified routingKey. + * @param {Function} newListener The listener to subscribe. + * @param {string} routingKey The routingkey to subscribe to. + * @param {boolean} consumeMostRecentMessage If true and there is a message for this routingkey available, notify the new listener. Default true. + */ + public subscribe( + newListener: Function, + routingKey: string, + key: string = (Date.now() + Math.floor(Math.random() * 100)).toString(), + consumeMostRecentMessage: boolean = true + ): string { + if (!this.listeners[routingKey]) this.listeners[routingKey] = {}; + + // Don't add a listener twice + if (!(key in this.listeners[routingKey])) { + this.listeners[routingKey][key] = newListener; + + // Consume the most recent message + if (consumeMostRecentMessage && routingKey in this.mostRecentMessages) + newListener(this.mostRecentMessages[routingKey], routingKey); + } + + return key; + } + + /** + * Unsubscribes a listener from messages with specified routingkey. + * @param {string} routingKey The routing key to unsubscribe from + * @param {string} listener key of the listener to unsubscribe. + */ + public unSubscribe(routingKey: string, key: string): void { + if (this.listeners[routingKey] && key in this.listeners[routingKey]) { + delete this.listeners[routingKey][key]; + } + } + + /** + * Unsubscribes all listeners from messages with specified routingkey. + * @param {string} routingKey The routing key to unsubscribe from + */ + public unSubscribeAll(routingKey: string): void { + this.listeners[routingKey] = {}; + } +} diff --git a/libs/shared/lib/data-access/socket/index.ts b/libs/shared/lib/data-access/socket/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..34802e1e5bfe610489cff6836e7b8f7ab2b01799 --- /dev/null +++ b/libs/shared/lib/data-access/socket/index.ts @@ -0,0 +1,3 @@ +export * from './backend-message-receiver'; +export * from './backend-messenger'; +export * from './broker' \ No newline at end of file diff --git a/libs/shared/lib/data-access/socket/listeners/SchemaViewModelImpl.tsx b/libs/shared/lib/data-access/socket/listeners/SchemaViewModelImpl.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8347ca3409bbd099399c413423927ef3ebb164e2 --- /dev/null +++ b/libs/shared/lib/data-access/socket/listeners/SchemaViewModelImpl.tsx @@ -0,0 +1,897 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/** This class is responsible for updating and creating the graph schema. */ +export default class SchemaViewModelImpl + extends AbstractBaseViewModelImpl + implements SchemaViewModel +{ + private reactFlowInstance: any; + private relationCounter: number; + private drawOrderUseCase: DrawOrderUseCase; + private nodeUseCase: NodeUseCase; + private edgeUseCase: EdgeUseCase; + private graphUseCase: GraphUseCase; + private placeInQueryBuilder: (name: string, type: string) => void; + public elements: SchemaElements = { nodes: [], edges: [], selfEdges: [] }; + public zoom: number; + public visible: boolean; + + public nodeQualityPopup: NodeQualityPopupNode; + public attributeAnalyticsPopupMenu: AttributeAnalyticsPopupMenuNode; + + public nodeQualityData: Record< + string, + NodeQualityDataForEntities | NodeQualityDataForRelations + >; + public attributeAnalyticsData: Record<string, AttributeAnalyticsData>; + + private entityPopupOffsets = { + nodeQualityOffset: { + x: SchemaThemeHolder.entity.width, + y: SchemaThemeHolder.entity.height - 3, + }, + attributeQualityOffset: { + x: SchemaThemeHolder.entity.width, + y: -SchemaThemeHolder.entity.height + 12, + }, + }; + + private relationPopupOffsets = { + nodeQualityOffset: { + x: SchemaThemeHolder.relation.width + 50, + y: SchemaThemeHolder.relation.height + 2, + }, + attributeQualityOffset: { + x: SchemaThemeHolder.relation.width + 50, + y: -SchemaThemeHolder.relation.height + 17, + }, + }; + // React flow reference for positioning on drop. + public myRef: React.RefObject<HTMLDivElement>; + + public nodeTypes = { + entity: EntityNode, + relation: RelationNode, + nodeQualityEntityPopup: NodeQualityEntityPopupNode, + nodeQualityRelationPopup: NodeQualityRelationPopupNode, + attributeAnalyticsPopupMenu: AttributeAnalyticsPopupMenu, + }; + + public edgeTypes = { + nodeEdge: NodeEdge, + selfEdge: SelfEdge, + }; + + public constructor( + drawOrderUseCase: DrawOrderUseCase, + nodeUseCase: NodeUseCase, + edgeUseCase: EdgeUseCase, + graphUseCase: GraphUseCase, + addAttribute: (name: string, type: string) => void + ) { + super(); + + this.myRef = React.createRef(); + // TODO: These values need to not be hardcoded. + this.zoom = 1.3; + this.relationCounter = 0; + this.reactFlowInstance = 0; + this.visible = true; + this.edgeUseCase = edgeUseCase; + this.nodeUseCase = nodeUseCase; + this.drawOrderUseCase = drawOrderUseCase; + this.graphUseCase = graphUseCase; + this.placeInQueryBuilder = addAttribute; + + this.nodeQualityPopup = this.emptyNodeQualityPopupNode(); + this.attributeAnalyticsPopupMenu = + this.emptyAttributeAnalyticsPopupMenuNode(); + + this.nodeQualityData = {}; + this.attributeAnalyticsData = {}; + } + + /** + * Containts all function calls to create the graph schema. + * Notifies the view about the changes at the end. + */ + public createSchema = (elements: SchemaElements): SchemaElements => { + let drawOrder: Node[]; + + drawOrder = this.drawOrderUseCase.createDrawOrder(elements); + // Create nodes with start position. + elements.nodes = this.nodeUseCase.setEntityNodePosition( + drawOrder, + { + x: 0, + y: 0, + }, + this.toggleNodeQualityPopup, + this.toggleAttributeAnalyticsPopupMenu + ); + + // Create the relation-nodes. + elements.edges.forEach((relation) => { + this.createRelationNode( + relation.id, + relation.data.attributes, + relation.data.collection, + elements + ); + }); + + elements.selfEdges.forEach((relation) => { + this.createRelationNode( + relation.id, + relation.data.attributes, + relation.data.collection, + elements + ); + }); + + // Complement the relation-nodes with extra data that is now accessible. + elements.edges = this.edgeUseCase.positionEdges( + drawOrder, + elements, + this.setRelationNodePosition + ); + + this.visible = false; + this.notifyViewAboutChanges(); + return elements; + }; + + /** + * consumes the schema send from the backend and uses it to create the SchemaElements for the schema + * @param jsonObject + */ + public consumeMessageFromBackend(jsonObject: unknown): void { + if (isSchemaResult(jsonObject)) { + // This is always the first message to receive, so reset the global variables. + this.visible = false; + this.createSchema({ nodes: [], edges: [], selfEdges: [] }); + + this.relationCounter = 0; + + this.nodeQualityPopup.isHidden = true; + this.attributeAnalyticsPopupMenu.isHidden = true; + + /* Create the graph-schema and add the popup-menu's for as far as possible. + * Runs underlying useCase trice to fix a bug with lingering selfEdges. + TODO: clean this up.*/ + this.elements = this.createSchema({ + nodes: [], + edges: [], + selfEdges: [], + }); + let schemaElements = + this.graphUseCase.createGraphFromInputData(jsonObject); + this.elements = this.createSchema(schemaElements); + this.notifyViewAboutChanges(); + //End weird fix + + this.visible = true; + schemaElements = this.graphUseCase.createGraphFromInputData(jsonObject); + this.elements = this.createSchema(schemaElements); + this.notifyViewAboutChanges(); + + this.addAttributesToAnalyticsPopupMenus(jsonObject); + } else if (isAttributeDataEntity(jsonObject)) { + // Add all information from the received message to the popup-menu's. + this.addAttributeDataToPopupMenusAndElements( + jsonObject, + 'gsa_node_result', + this.elements + ); + } else if (isAttributeDataRelation(jsonObject)) { + // Add all information from the received message to the popup-menu's. + this.addAttributeDataToPopupMenusAndElements( + jsonObject, + 'gsa_edge_result', + this.elements + ); + } else { + // TODO: This should be an error screen eventually. + console.log('This is no valid input!'); + } + this.notifyViewAboutChanges(); + } + + /** + * Create a reference to the reactflow schema component on load + * @param reactFlowInstance reactflow instance + */ + public onLoad = (reactFlowInstance: OnLoadParams<any>): void => { + this.reactFlowInstance = reactFlowInstance; + }; + + /** + * Complements the relation-nodes with data that had default values before. + * It determines the position of the relation-node and the connected entity-nodes. + * @param centerX Used to determine the center of the edge. + * @param centerY Used to determine the center of the edge. + * @param id The id of the relation node. + * @param from The id of the entity where the edge should connect from. + * @param to The id of the entity where the edge should connect to. + * @param attributes The attributes of the relation node. + */ + public setRelationNodePosition = ( + centerX: number, + centerY: number, + id: string, + from: string, + to: string, + attributes: Attribute[] + ): void => { + let width: number; + let overlap: boolean; + let y: number; + + // Check if the relation-node is in the list of nodes. + let relation = this.elements.nodes.find((node) => node.id == id); + if (relation == undefined) + throw new Error('Relation ' + id + ' does not exist.'); + + // Height of relation/entity + external buttons. + const height = 20; + + width = + SchemaThemeHolder.relation.width + + calcWidthRelationNodeBox(attributes.length, 0); + let x = centerX - SchemaThemeHolder.relation.width / 2; + y = centerY - height / 2; + + while (this.CheckForOverlap(x, y, width, height)) { + y = y + 1; + } + + // Replace the default values for the correct values. + relation.position = { x: x, y: y }; + relation.data.from = from; + relation.data.to = to; + + this.relationCounter++; + if (this.relationCounter == this.elements.edges.length) { + this.fitToView(); + } + this.notifyViewAboutChanges(); + }; + + /** + * Creates a new relation-node with some default values. + * @param id The id of the relation node. + * @param attributes The list of attributes that this relation-node has. + * @param collection The collection this relation-node is in. + */ + public createRelationNode = ( + id: string, + attributes: Attribute[], + collection: string, + schemaElements: SchemaElements + ): void => { + // Height of relation/entity + external buttons. + const height = 20; + const width = + SchemaThemeHolder.relation.width + + calcWidthRelationNodeBox(attributes.length, 0); + + schemaElements.nodes.push({ + type: NodeType.relation, + id: id, + position: { x: 0, y: 0 }, + data: { + width: width, + height: height, + collection: collection, + attributes: attributes, + from: '', + to: '', + nodeCount: 0, + summedNullAmount: 0, + fromRatio: 0, + toRatio: 0, + toggleNodeQualityPopup: this.toggleNodeQualityPopup, + toggleAttributeAnalyticsPopupMenu: + this.toggleAttributeAnalyticsPopupMenu, + }, + }); + }; + + /** + * Calculates the width and height of the graph-schema-panel. + * @returns { number; number } The width and the height of the graph-schema-panel. + */ + public getWidthHeight = (): { width: number; height: number } => { + const reactFlow = this.myRef.current as HTMLDivElement; + const reactFlowBounds = reactFlow.getBoundingClientRect(); + const width = reactFlowBounds.right - reactFlowBounds.left; + const height = reactFlowBounds.bottom - reactFlowBounds.top; + return { width, height }; + }; + + /** Placeholder function for fitting the schema into the view. */ + public fitToView = (): void => { + let minX = Infinity; + let maxX = 0; + let minY = Infinity; + let maxY = 0; + let xZoom = 0; + let yZoom = 0; + + let setY = 0; + let setX = 0; + let setZoom = 0; + + this.elements.nodes.forEach((node) => { + const attributeCount: number = node.data.attributes.length; + const nodeCount: number = node.data.nodeCount; + const nodeWidth: number = + node.type == NodeType.entity + ? SchemaThemeHolder.entity.width + + calcWidthEntityNodeBox(attributeCount, nodeCount) + : SchemaThemeHolder.relation.width + + calcWidthRelationNodeBox(attributeCount, nodeCount); + + if (node.position.x < minX) minX = node.position.x; + if (node.position.x + nodeWidth > maxX) + maxX = node.position.x + nodeWidth; + if (node.position.y < minY) minY = node.position.y; + if (node.position.y + node.data.height > maxY) + maxY = node.position.y + node.data.height; + }); + + minX -= 10; + maxX += 90; + + const { width, height } = this.getWidthHeight(); + + // Correct for X and Y position with width and height. + let nodeWidth = Math.abs(maxX - minX); + let nodeHeight = Math.abs(maxY - minY); + + setX = minX * -1; + xZoom = width / nodeWidth; + setY = minY * -1; + yZoom = height / nodeHeight; + + // TODO: Correct position and zoom for selfEdges. + + if (xZoom >= yZoom) { + setZoom = yZoom; + setX = setX + width / 2 - nodeWidth / 2; + } else { + setZoom = xZoom; + setY = setY + height / 2 - nodeHeight / 2; + } + + try { + this.reactFlowInstance.setTransform({ x: setX, y: setY, zoom: setZoom }); + } catch { + console.log('this.reactFlowInstance is undefined!'); + } + }; + + /** + * this function check for a relation node if it overlaps with any of the other nodes in the schema. + * It creates boundingbox for the node and checks with all the other nodes if the boxes overlap. + * @param x Top left x of the node. + * @param y Top left y of the node. + * @param width Width of the node. + * @param height Height of the node. + * @returns {bool} whether it overlaps.*/ + public CheckForOverlap = ( + x: number, + y: number, + width: number, + height: number + ): boolean => { + const boundingBox = makeBoundingBox(x, y, width, height); + let elements = this.elements; + + let boundingTemporary: BoundingBox; + for (let i = 0; i < elements.nodes.length; i++) { + boundingTemporary = makeBoundingBox( + elements.nodes[i].position.x, + elements.nodes[i].position.y, + elements.nodes[i].data.width, + elements.nodes[i].data.height + ); + if (doBoxesOverlap(boundingBox, boundingTemporary)) { + return true; + } + } + return false; + }; + + /** Exports the schema builder to a beautiful png file */ + public exportToPNG(): void { + const { width, height } = this.getWidthHeight(); + exportComponentAsPNG(this.myRef, { + fileName: 'schemaBuilder', + pdfOptions: { + x: 0, + y: 0, + w: width, + h: height, + unit: 'px', + } as Partial<PDFOptions>, + } as Params); + } + + /** Not implemented method for exporting the schema builder visualisation to PDF. */ + public exportToPDF(): void { + console.log('Method not implemented.'); + } + + /** Attach the listener to the broker. */ + public subscribeToSchemaResult(): void { + Broker.instance().subscribe(this, 'schema_result'); + } + + /** Detach the listener to the broker. */ + public unSubscribeFromSchemaResult(): void { + Broker.instance().unSubscribe(this, 'schema_result'); + } + + /** Attach the listeners to the broker. */ + public subscribeToAnalyticsData(): void { + Broker.instance().subscribe(this, 'gsa_node_result'); + Broker.instance().subscribe(this, 'gsa_edge_result'); + } + + /** Detach the listeners to the broker. */ + public unSubscribeFromAnalyticsData(): void { + Broker.instance().unSubscribe(this, 'gsa_node_result'); + Broker.instance().unSubscribe(this, 'gsa_edge_result'); + } + + /** + * This function is used by relation and entity nodes to hide or show the node-quality popup of that node. + * @param id of the node for which the new popup is. + */ + public toggleNodeQualityPopup = (id: string): void => { + const popup = this.nodeQualityPopup; + + // Hide the popup if the current popup is visible and if the popup belongs to the same node as the given id. + if (popup.nodeID == id && !popup.isHidden) popup.isHidden = true; + // Else make and show a new popup for the node with the given id. + else this.updateNodeQualityPopup(id, this.elements); + + this.notifyViewAboutChanges(); + }; + + /** + * This function shows and updates the node-quality popup for the node which has the given id. + * @param id of the node for which the new popup is. + */ + private updateNodeQualityPopup(id: string, schemaElements: SchemaElements) { + let node = schemaElements.nodes.find((node) => node.id == id); + if (node == undefined) { + throw new Error('Node does not exist therefore no popup can be shown.'); + } + + const popup = this.nodeQualityPopup; + popup.nodeID = id; + popup.isHidden = false; + popup.data = this.nodeQualityData[id]; + + if (node.type == 'entity') { + // Make changes to the popup, to make it a popup for entities. + this.updateToNodeQualityEntityPopup(node); + } else { + // Make changes to the popup, to make it a popup for relations. + this.updateToNodeQualityRelationPopup(node); + } + + // Hide the attributeAnalyticsPopupMenu so that only one popup is displayed. + this.attributeAnalyticsPopupMenu.isHidden = true; + + this.notifyViewAboutChanges(); + this.relationCounter++; + if (this.relationCounter == schemaElements.edges.length) { + this.fitToView(); + } + } + + /** + * This displays the new node-quality popup for the given entity. + * @param node This is the entity of which you want to display the popup. + */ + private updateToNodeQualityEntityPopup(node: Node) { + const popup = this.nodeQualityPopup; + const offset = this.entityPopupOffsets.nodeQualityOffset; + + popup.position = { + x: node.position.x + offset.x, + y: node.position.y + offset.y, + }; + + popup.type = 'nodeQualityEntityPopup'; + } + + /** + * This displays the new node-quality popup for the given relation. + * @param node This is the relation of which you want to display the popup. + */ + private updateToNodeQualityRelationPopup(node: Node) { + const popup = this.nodeQualityPopup; + const offset = this.relationPopupOffsets.nodeQualityOffset; + + popup.position = { + x: node.position.x + offset.x, + y: node.position.y + offset.y, + }; + + popup.type = 'nodeQualityRelationPopup'; + } + + /** + * This function is used by relation and entity nodes to hide or show the attribute analyics popup menu of that node. + * @param id of the node for which the popup is. + */ + public toggleAttributeAnalyticsPopupMenu = (id: string): void => { + const popupMenu = this.attributeAnalyticsPopupMenu; + + // Hide the popup menu if the current popup menu is visible and if the popup menu belongs to the same node as the given id. + if (popupMenu.nodeID == id && !popupMenu.isHidden) + popupMenu.isHidden = true; + // Else make and show a new popup menu for the node with the given id. + else this.updateAttributeAnalyticsPopupMenu(id, this.elements); + + this.notifyViewAboutChanges(); + }; + + /** + * This displays the attribute-analytics popup menu for the given node (entity or relation). + * It removes the other menus from the screen. + * @param id This is the id of the node (entity or relation) of which you want to display the menu. + */ + public updateAttributeAnalyticsPopupMenu = ( + id: string, + schemaElements: SchemaElements + ): void => { + const node = schemaElements.nodes.find((node) => node.id == id); + + if (node == undefined) + throw new Error( + 'Node ' + id + ' does not exist therefore no popup menu can be shown.' + ); + + const popupMenu = this.attributeAnalyticsPopupMenu; + // Make new popup menu for the node. + popupMenu.nodeID = id; + popupMenu.isHidden = false; + popupMenu.data = { ...this.attributeAnalyticsData[id] }; + + if (node.type == NodeType.entity) { + const offset = this.entityPopupOffsets.attributeQualityOffset; + popupMenu.position = { + x: node.position.x + offset.x, + y: node.position.y + offset.y, + }; + } else { + const offset = this.relationPopupOffsets.attributeQualityOffset; + popupMenu.position = { + x: node.position.x + offset.x, + y: node.position.y + offset.y, + }; + } + + // Hide the nodeQualityPopup so that only one popup is displayed. + this.nodeQualityPopup.isHidden = true; + + this.notifyViewAboutChanges(); + }; + + /** This removes the node quality popup from the screen. */ + public hideNodeQualityPopup = (): void => { + this.nodeQualityPopup.isHidden = true; + this.notifyViewAboutChanges(); + }; + + /** This removes the attribute-analytics popup menu from the screen. */ + public hideAttributeAnalyticsPopupMenu = (): void => { + this.attributeAnalyticsPopupMenu.isHidden = true; + this.notifyViewAboutChanges(); + }; + + /** + * This sets all the data for the attributesPopupMenu without the attribute data. + * @param schemaResult This is the schema result that you get (so no attribute data yet). + */ + addAttributesToAnalyticsPopupMenus = (schemaResult: Schema): void => { + this.nodeQualityData = {}; + this.attributeAnalyticsData = {}; + + // Firstly, loop over all entities and add the quality-data (as far as possible). + // Then add the attribute-data (as far as possible). + schemaResult.nodes.forEach((node) => { + this.nodeQualityData[node.name] = { + nodeCount: 0, + notConnectedNodeCount: 0, + attributeNullCount: 0, + isAttributeDataIn: false, + onClickCloseButton: this.hideNodeQualityPopup, + }; + let attributes: any = []; + node.attributes.forEach((attribute) => { + attributes.push({ + attribute: attribute, + category: AttributeCategory.undefined, + nullAmount: 0, + }); + }); + this.attributeAnalyticsData[node.name] = { + nodeID: node.name, + nodeType: NodeType.entity, + attributes: attributes, + isAttributeDataIn: false, + onClickCloseButton: this.hideAttributeAnalyticsPopupMenu, + onClickPlaceInQueryBuilderButton: this.placeInQueryBuilder, + searchForAttributes: this.searchForAttributes, + resetAttributeFilters: this.resetAttributeFilters, + applyAttributeFilters: this.applyAttributeFilters, + }; + }); + // Secondly, loop over all relations and add the quality-data (as far as possible). + // Then add the attribute-data (as far as possible). + schemaResult.edges.forEach((edge) => { + this.nodeQualityData[edge.collection] = { + nodeCount: 0, + fromRatio: 0, + toRatio: 0, + attributeNullCount: 0, + notConnectedNodeCount: 0, + isAttributeDataIn: false, + onClickCloseButton: this.hideNodeQualityPopup, + }; + let attributes: any = []; + edge.attributes.forEach((attribute) => { + attributes.push({ + attribute: attribute, + category: AttributeCategory.undefined, + nullAmount: 0, + }); + }); + this.attributeAnalyticsData[edge.collection] = { + nodeID: edge.collection, + nodeType: NodeType.relation, + attributes: attributes, + isAttributeDataIn: false, + onClickCloseButton: this.hideAttributeAnalyticsPopupMenu, + onClickPlaceInQueryBuilderButton: this.placeInQueryBuilder, + searchForAttributes: this.searchForAttributes, + resetAttributeFilters: this.resetAttributeFilters, + applyAttributeFilters: this.applyAttributeFilters, + }; + }); + }; + + /** Returns an empty quality popup node for react-flow. */ + public emptyAttributeAnalyticsPopupMenuNode(): AttributeAnalyticsPopupMenuNode { + return { + id: 'attributeAnalyticsPopupMenu', + nodeID: '', + position: { x: 0, y: 0 }, + data: { + nodeID: '', + nodeType: NodeType.relation, + attributes: [], + isAttributeDataIn: false, + onClickCloseButton: this.hideAttributeAnalyticsPopupMenu, + onClickPlaceInQueryBuilderButton: this.placeInQueryBuilder, + searchForAttributes: this.searchForAttributes, + resetAttributeFilters: this.resetAttributeFilters, + applyAttributeFilters: this.applyAttributeFilters, + }, + type: 'attributeAnalyticsPopupMenu', + isHidden: true, + className: 'attributeAnalyticsPopupMenu', + }; + } + /** Returns an empty quality popup node for react-flow. */ + public emptyNodeQualityPopupNode(): NodeQualityPopupNode { + return { + id: 'nodeQualityPopup', + position: { x: 0, y: 0 }, + data: { + nodeCount: 0, + notConnectedNodeCount: 0, + attributeNullCount: 0, + isAttributeDataIn: false, + onClickCloseButton: this.hideNodeQualityPopup, + }, + type: 'nodeQualityEntityPopup', + isHidden: true, + nodeID: '', + className: 'nodeQualityPopup', + }; + } + + /** + * This function adjusts the values from the new attribute data into + * the attribute-analytics data and the node-quality data. + * @param attributeData The data that comes from the backend with (some) data about the attributes. + */ + public addAttributeDataToPopupMenusAndElements = ( + attributeData: AttributeData, + attributeDataType: string, + schemaElements: SchemaElements + ): void => { + // Check if attributeData is a node/entity. + if (attributeDataType === 'gsa_node_result') { + const entity = attributeData as NodeAttributeData; + // If it is a entity then add the data for the corresponding entity. + if (entity.id in this.attributeAnalyticsData) { + const attributeDataOfEntity = this.attributeAnalyticsData[entity.id]; + attributeDataOfEntity.isAttributeDataIn = true; + + entity.attributes.forEach((attribute) => { + // Check if attribute is in the list with attributes from the correct entity. + const attributeFound = attributeDataOfEntity.attributes.find( + (attribute_) => attribute_.attribute.name == attribute.name + ); + if (attributeFound !== undefined) { + attributeFound.category = attribute.type; + attributeFound.nullAmount = attribute.nullAmount; + } + }); + } // Not throw new error, because it should not crash, a message is enough and not all data will be shown. + else + console.log( + 'entity ' + entity.id + ' is not in attributeAnalyticsData' + ); + + if (entity.id in this.nodeQualityData) { + const qualityDataOfEntity = this.nodeQualityData[ + entity.id + ] as NodeQualityDataForEntities; + qualityDataOfEntity.nodeCount = entity.length; + qualityDataOfEntity.attributeNullCount = entity.summedNullAmount; + qualityDataOfEntity.notConnectedNodeCount = Number( + (1 - entity.connectedRatio).toFixed(2) + ); + qualityDataOfEntity.isAttributeDataIn = true; + } // Not throw new error, because it should not crash, a message is enough and not all data will be shown. + else console.log('entity ' + entity.id + ' is not in nodeQualityData'); + + // Check also if the entity exists in the this.elements-list. + // If so, add the new data to it. + const elementsNode = schemaElements.nodes.find( + (node_) => node_.id == entity.id + ); + if (elementsNode !== undefined) { + elementsNode.data.nodeCount = entity.length; + elementsNode.data.summedNullAmount = entity.summedNullAmount; + elementsNode.data.connectedRatio = entity.connectedRatio; + } + } + + // Check if attributeData is an edge/relation. + else if (attributeDataType === 'gsa_edge_result') { + const relation = attributeData as EdgeAttributeData; + // If it is a relation then add the data for the corresponding relation. + if (relation.id in this.attributeAnalyticsData) { + const attributeDataOfRelation = + this.attributeAnalyticsData[relation.id]; + attributeDataOfRelation.isAttributeDataIn = true; + + relation.attributes.forEach((attribute) => { + // Check if attribute is in the list with attributes from the correct relation. + const attributeFound = attributeDataOfRelation.attributes.find( + (attribute_) => attribute_.attribute.name == attribute.name + ); + if (attributeFound !== undefined) { + attributeFound.category = attribute.type; + attributeFound.nullAmount = attribute.nullAmount; + } + }); + } // Not throw new error, because it should not crash, a message is enough and not all data will be shown. + else + console.log( + 'relation ' + relation.id + ' is not in attributeAnalyticsData' + ); + + if (relation.id in this.nodeQualityData) { + const qualityDataOfRelation = this.nodeQualityData[ + relation.id + ] as NodeQualityDataForRelations; + qualityDataOfRelation.nodeCount = relation.length; + qualityDataOfRelation.attributeNullCount = relation.summedNullAmount; + qualityDataOfRelation.fromRatio = Number(relation.fromRatio.toFixed(2)); + qualityDataOfRelation.toRatio = Number(relation.toRatio.toFixed(2)); + qualityDataOfRelation.isAttributeDataIn = true; + } // Not throw new error, because it should not crash, a message is enough and not all data will be shown. + else + console.log('relation ' + relation.id + ' is not in nodeQualityData'); + + // Check also if the entity exists in the this.elements-list. + // If so, add the new data to it. + const elementsNode = schemaElements.nodes.find( + (node_) => node_.id == relation.id + ); + if (elementsNode !== undefined) { + elementsNode.data.nodeCount = relation.length; + elementsNode.data.summedNullAmount = relation.summedNullAmount; + elementsNode.data.fromRatio = relation.fromRatio; + elementsNode.data.toRatio = relation.toRatio; + } + } else throw new Error('This data is not valid!'); + }; + + /** + * Filter out attributes that do not contain the given searchbar-value. + * @param id The id of the node the attributes are from. + * @param searchbarValue The value of the searchbar. + */ + public searchForAttributes = (id: string, searchbarValue: string): void => { + const data = this.attributeAnalyticsData[id]; + // Check if there is data available. + if (data !== undefined) { + let passedAttributes: AttributeWithData[] = []; + data.attributes.forEach((attribute) => { + if ( + attribute.attribute.name + .toLowerCase() + .includes(searchbarValue.toLowerCase()) + ) + passedAttributes.push(attribute); + }); + this.attributeAnalyticsPopupMenu.data.attributes = passedAttributes; + this.notifyViewAboutChanges(); + } + }; + + /** + * Reset the current used filters for the attribute-list. + * @param id The id of the node the attributes are from. + */ + public resetAttributeFilters = (id: string): void => { + const data = this.attributeAnalyticsData[id]; + // Check if there is data available. + if (data !== undefined) { + this.attributeAnalyticsPopupMenu.data.attributes = data.attributes; + this.notifyViewAboutChanges(); + } + }; + + /** + * Applies the chosen filters on the list of attributes of the particular node. + * @param id The id of the node the attributes are from. + * @param dataType The given type of the data you want to filter on (numerical, categorical, other). + * @param predicate The given predicate. + * @param percentage The given percentage you want to compare the null-values on. + */ + public applyAttributeFilters = ( + id: string, + dataType: AttributeCategory, + predicate: string, + percentage: number + ): void => { + const data = this.attributeAnalyticsData[id]; + // Check if there is data available. + if (data !== undefined) { + let passedAttributes: AttributeWithData[] = []; + data.attributes.forEach((attribute) => { + // If the value is undefined it means that this filter is not chosen, so that must not be taken into account for further filtering. + if ( + attribute.category == dataType || + dataType == AttributeCategory.undefined + ) + if (predicate == '' || percentage == -1) + // If the string is empty it means that this filter is not chosen, so that must not be taken into account for filtering. + passedAttributes.push(attribute); + else if ( + numberPredicates[predicate](attribute.nullAmount, percentage) + ) + passedAttributes.push(attribute); + }); + this.attributeAnalyticsPopupMenu.data.attributes = passedAttributes; + this.notifyViewAboutChanges(); + } + }; +} diff --git a/libs/shared/lib/data-access/socket/query/QueryApi.tsx b/libs/shared/lib/data-access/socket/query/QueryApi.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d4f86112c046831fc5ed0cbd99686071d30e3e0 --- /dev/null +++ b/libs/shared/lib/data-access/socket/query/QueryApi.tsx @@ -0,0 +1,30 @@ +import BackendMessengerRepository from '../../../domain/repository-interfaces/BackendMessengerRepository'; +import QueryRepository, { Query } from '../../../domain/repository-interfaces/QueryRepository'; + +/** + * Implementation of the QueryRepository. + * Contains function for sending a query to the database and retrieving a cached query. + */ +export default class QueryApi implements QueryRepository { + backendMessenger: BackendMessengerRepository; + + /** @param backendMessenger {BackendMessengerRepository} A BackendMessengerRepository implementation. */ + constructor(backendMessenger: BackendMessengerRepository) { + this.backendMessenger = backendMessenger; + } + + async sendQuery(query: Query): Promise<string> { + const body = JSON.stringify(query); + return this.backendMessenger.SendMessage(body, 'query/execute/', 'POST').then((response) => { + return response.json().then((obj) => obj.queryID); + }); + } + + async retrieveCachedQuery(queryID: string): Promise<Response> { + return this.backendMessenger.SendMessage( + JSON.stringify({ queryID }), + 'query/retrieve-cached/', + 'POST', + ); + } +} diff --git a/libs/shared/lib/data-access/store/authSlice.ts b/libs/shared/lib/data-access/store/authSlice.ts new file mode 100644 index 0000000000000000000000000000000000000000..273c498e299f58cbf191b8b4d21f72c909b89d03 --- /dev/null +++ b/libs/shared/lib/data-access/store/authSlice.ts @@ -0,0 +1,37 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './store'; +import { useIsAuthorizedState } from '../authorization'; + + + +// Define the initial state using that type +export const initialState: useIsAuthorizedState = { + authorized: false, + accessToken: '', +}; + +export const authSlice = createSlice({ + name: 'auth', + // `createSlice` will infer the state type from the `initialState` argument + initialState, + reducers: { + updateAccessToken(state, action: PayloadAction<string>) { + state.accessToken = action.payload; + }, + authorized(state, action: PayloadAction<useIsAuthorizedState>) { + state.authorized = action.payload.authorized; + state.accessToken = action.payload.accessToken; + state.sessionID = action.payload.sessionID; + state.userID = action.payload.userID; + } + }, +}); + +export const { + updateAccessToken, authorized +} = authSlice.actions; + +// Other code such as selectors can use the imported `RootState` type +export const authState = (state: RootState) => state.auth; + +export default authSlice.reducer; diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts index edbb2b6eb4c1828b513fd7d19b99d688e6d8b9bd..68ed9621859db5fa677c6a9641fee6dad01ae86a 100644 --- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts +++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts @@ -7,7 +7,7 @@ export interface GraphQueryResultFromBackend { attributes: { [key: string]: unknown }; }[]; - links: { + edges: { attributes: { [key: string]: unknown }; from: string; to: string; @@ -69,7 +69,7 @@ export const graphQueryResultSlice = createSlice({ // Assign new state state.nodes = action.payload.nodes as Node[]; - state.edges = action.payload.links; + state.edges = action.payload.edges; state.nodeTypes = nodeTypes; }, resetGraphQueryResults: (state) => { diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index e1c5935f4bd900657af37422a67b5e24b670acf2..3e40cdb45fdb073b264d49735bf106c6fb504a3b 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -12,6 +12,7 @@ import { selectQuerybuilderGraphology, } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { sessionCacheState } from './sessionSlice'; +import { authState } from './authSlice'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -35,3 +36,4 @@ export const useQuerybuilderGraph = () => // Overall Configuration of the app export const useConfig = () => useAppSelector(configState); export const useSessionCache = () => useAppSelector(sessionCacheState); +export const useAuthorizationCache = () => useAppSelector(authState); diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index f2e3cd62b3cf06a8fc1bddcdfff346be228b065b..9e1f176c7b6e229cd4b3b466bdc5ed74c65932d4 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -49,6 +49,7 @@ export const querybuilderSlice = createSlice({ clearQB: (state) => { state.graphologySerialized = new QueryMultiGraph().export(); }, + // addQuerybuilderNode: ( // state, // action: PayloadAction<{ id: string; attributes: Attributes }> diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index bd3d0b4b70a92774e5fd947a83228e8f9b88d7f3..f2a64c896f94dde193b8e3f5632269b22f9868a6 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -5,6 +5,7 @@ import { SchemaFromBackend } from '@graphpolaris/shared/lib/model/backend'; import type { RootState } from './store'; import { AllLayoutAlgorithms } from '@graphpolaris/shared/lib/graph-layout'; import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/graph/graphology/utils'; +import { SchemaUtils } from '../../schema/schema-utils'; /**************************************************************** */ @@ -34,10 +35,8 @@ export const schemaSlice = createSlice({ state, action: PayloadAction<SchemaFromBackend> ) => { - const { nodes, edges } = action.payload; - // Instantiate a directed graph that allows self loops and parallel edges - const schema = new MultiGraph({ allowSelfLoops: true }); - console.log('Updating schema'); + state.graphologySerialized = SchemaUtils.schemaBackend2Graphology(action.payload).export(); + console.log('Updated schema from backend'); // The graph schema needs a node for each node AND edge. These need then be connected // nodes.forEach((node) => { @@ -69,7 +68,7 @@ export const schemaSlice = createSlice({ // schema.addDirectedEdgeWithKey(edgeID + 't', edgeID, edge.to); // }); - state.graphologySerialized = schema.export(); + // state.graphologySerialized = schema.export(); }, }, }); diff --git a/libs/shared/lib/data-access/store/sessionSlice.ts b/libs/shared/lib/data-access/store/sessionSlice.ts index 979f7f028dd9fa46a7128671f63c92f5ae647874..f246f7e44a5d6a6401a905a0ea9becd19c2bdc10 100644 --- a/libs/shared/lib/data-access/store/sessionSlice.ts +++ b/libs/shared/lib/data-access/store/sessionSlice.ts @@ -1,13 +1,45 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; +import { BackendQueryFormat } from '../../querybuilder'; -// Define the initial state using that type -export const initialState: { - currentDatabase: string; +/** Message format of the error message from the backend */ +export type ErrorMessage = { + errorStatus: any; + queryID: any; +}; + +/** Cache type */ +export type SessionCacheI = { + currentDatabase?: string; databases: string[]; -} = { - currentDatabase: '', + error?: ErrorMessage; + + // queryListOpen: boolean; + // queryStatusList: { + // queries: Record<string, QueryStatusListItem>; + // queryIDsOrder: string[]; + // }; + // functionsMenuOpen: boolean; + // currentDatabaseKey: string; + // currentColours: { key: string; index: number }; + // elementsperDatabaseObject: Record<string, (AnyNode | Edge<any>)[]>; + // autoSendQueries: boolean; + // panelWidthHeights: { + // windowinnerHeight: number; + // windowinnerWidth: number; + // navBarHeight: number; + // schemaDrawerHeight: number; + // queryDrawerHeight: number; + // schemaDrawerWidth: number; + // queryDrawerWidth: number; + // }; +}; + +// Define the initial state using that type +export const initialState: SessionCacheI = { + currentDatabase: undefined, databases: [], + error: undefined, }; export const sessionSlice = createSlice({ @@ -19,14 +51,35 @@ export const sessionSlice = createSlice({ state.currentDatabase = action.payload; }, updateDatabaseList(state, action: PayloadAction<string[]>) { + console.log('Updating database list', action); state.databases = action.payload; + if (state.databases.length > 0) { + if (!state.currentDatabase || !(state.databases.includes(state.currentDatabase))) + state.currentDatabase = state.databases[0] + else state.currentDatabase = undefined; + } + }, + displayError(state, action: PayloadAction<ErrorMessage>): void { + // use a switch, in case certain errors will have side effects in the future + switch (action.payload.errorStatus) { + case 'Bad request': + case 'Bad credentials': + case 'Translation error': + case 'Database error': + case 'ML bad request': + state.error = action.payload + break; + } + }, + closeError(state): void { + state.error = undefined; } }, }); export const { - updateCurrentDatabase, - updateDatabaseList + updateCurrentDatabase, updateDatabaseList, displayError, closeError + } = sessionSlice.actions; // Other code such as selectors can use the imported `RootState` type diff --git a/libs/shared/lib/data-access/store/store.ts b/libs/shared/lib/data-access/store/store.ts index ef639a9206b474aae0bf000ed34b2111fcadbea5..f5dd2dab6fe7d7a2ae023c06e7996c14d5d59380 100644 --- a/libs/shared/lib/data-access/store/store.ts +++ b/libs/shared/lib/data-access/store/store.ts @@ -5,6 +5,7 @@ import querybuilderSlice from './querybuilderSlice'; import schemaSlice from './schemaSlice'; import configSlice from './configSlice'; import sessionSlice from './sessionSlice'; +import authSlice from './authSlice'; export const store = configureStore({ reducer: { @@ -13,6 +14,7 @@ export const store = configureStore({ colorPaletteConfig: colorPaletteConfigSlice, querybuilder: querybuilderSlice, sessionCache: sessionSlice, + auth: authSlice, config: configSlice, }, middleware: (getDefaultMiddleware) => diff --git a/libs/shared/lib/querybuilder/graph/graphology/utils.ts b/libs/shared/lib/querybuilder/graph/graphology/utils.ts index b55a4597dc2822e03d619c09c70212c214a2f79c..4128f61e900e39a76f203b3619199e2b8db1a915 100644 --- a/libs/shared/lib/querybuilder/graph/graphology/utils.ts +++ b/libs/shared/lib/querybuilder/graph/graphology/utils.ts @@ -115,7 +115,7 @@ export function calcWidthHeightOfPill(attributes: Attributes): { } /** Interface for x and y position of node */ -export interface NodePosition extends XYPosition {} +export interface NodePosition extends XYPosition { } /** Returns from-position of relation node */ export function RelationPosToFromEntityPos(position: XYPosition): NodePosition { diff --git a/libs/shared/lib/querybuilder/index.ts b/libs/shared/lib/querybuilder/index.ts index e9ce65c3e91230655ae841d7f2721a41496155ec..35906bceac37fc5a9626ec94bb7e9dce8d453908 100644 --- a/libs/shared/lib/querybuilder/index.ts +++ b/libs/shared/lib/querybuilder/index.ts @@ -1,2 +1,3 @@ export * from './panel'; -export * from './pills'; \ No newline at end of file +export * from './pills'; +export * from './query-utils'; \ No newline at end of file diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.module.scss b/libs/shared/lib/querybuilder/panel/querybuilder.module.scss index e1a5d48d0e6acd88f1c7480891248c9c8fa8c2c7..40d1908c90a44a5d3be6d0352ee48ed0468ec72a 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.module.scss +++ b/libs/shared/lib/querybuilder/panel/querybuilder.module.scss @@ -1,13 +1,12 @@ .reactflow { width: 100%; - height: 500px; + // height: 500px; // :global(.react-flow__edges) { // z-index: 4 !important; // } } - //controls .controls { left: auto !important; @@ -23,10 +22,10 @@ right: 20px; & svg { transform: scale(1.4); - }; + } } .menuText { font-size: small; font-family: Poppins, sans-serif; -} \ No newline at end of file +} diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index aca3c482a87d8d899a5dfd2b3d938eb518d1bb52..7891eed1846898d38d214ff1904cf91ed878f19a 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -208,8 +208,10 @@ export const QueryBuilder: React.FC = () => { x: position.x, y: position.y, depth: { min: 0, max: 1 }, - name: dragData.name, + // name: dragData.name, + name: dragData.collection, // TODO leave collection or use name? fadeIn: dragData.fadeIn, + collection: dragData.collection, }); const leftEntityId = graphologyGraph.addPill2Graphology({ type: QueryElementTypes.Entity, @@ -282,6 +284,7 @@ export const QueryBuilder: React.FC = () => { onNodesChange={onNodesChange} deleteKeyCode="Backspace" className={styles.reactflow} + attributionPosition="top-right" > <Background gap={10} size={0.7} /> <Controls diff --git a/libs/shared/lib/querybuilder/query-utils/BackendQueryFormat.tsx b/libs/shared/lib/querybuilder/query-utils/BackendQueryFormat.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4866d74127fa490916db556a387888c1333fd8d0 --- /dev/null +++ b/libs/shared/lib/querybuilder/query-utils/BackendQueryFormat.tsx @@ -0,0 +1,112 @@ +/** + * This program has been developed by students from the bachelor Computer Science at + * Utrecht University within the Software Project course. + * © Copyright Utrecht University (Department of Information and Computing Sciences) + */ + +/** JSON query format used to send a query to the backend. */ +export interface BackendQueryResultFormat { + databaseName: string; + return: { + entities: number[]; + relations: number[]; + groupBys: number[]; + }; + entities: Entity[]; + relations: Relation[]; + groupBys: GroupBy[]; + machineLearning: MachineLearning[]; + limit: number; + // modifiers: ModifierStruct[]; + // prefix: string; +} + +/** JSON query format used to send a query to the backend. */ +export interface BackendQueryFormat { + databaseName: string; + return: { + entities: number[]; + relations: number[]; + groupBys: number[]; + }; + entities: Entity[]; + relations: Relation[]; + groupBys: GroupBy[]; + machineLearning: MachineLearning[]; + limit: number; + // modifiers: ModifierStruct[]; + // prefix: string; +} + +/** Interface for an entity in the JSON for the query. */ +export interface Entity { + name: string; + ID: number; + constraints: Constraint[]; +} + +/** Interface for an relation in the JSON for the query. */ +export interface Relation { + name: string; + ID: number; + fromType: string; + fromID: number; + toType: string; + toID: number; + depth: { min: number; max: number }; + constraints: Constraint[]; +} + +/** + * Constraint datatypes created from the attributes of a relation or entity. + * + * string MatchTypes: exact/contains/startswith/endswith. + * int MatchTypes: GT/LT/EQ. + * bool MatchTypes: EQ/NEQ. + */ +export interface Constraint { + attribute: string; + dataType: string; + + matchType: string; + value: string; +} + +/** Interface for a function in the JSON for the query. */ +export interface GroupBy { + ID: number; + + groupType: string; + groupID: number[]; + groupAttribute: string; + + byType: string; + byID: number[]; + byAttribute: string; + + appliedModifier: string; + relationID: number; + constraints: Constraint[]; +} + +/** Interface for Machine Learning algorithm */ +export interface MachineLearning { + ID?: number; + queuename: string; + parameters: string[]; +} +/** Interface for what the JSON needs for link predicition */ +export interface LinkPrediction { + queuename: string; + parameters: { + key: string; + value: string; + }[]; +} + +export interface ModifierStruct { + type: string; + selectedType: string; + selectedTypeID: number; + attributeIndex: number; +} diff --git a/libs/shared/lib/querybuilder/query-utils/index.ts b/libs/shared/lib/querybuilder/query-utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a8aba6d7a2ed7ca6ec5de1991d885d9995caab8 --- /dev/null +++ b/libs/shared/lib/querybuilder/query-utils/index.ts @@ -0,0 +1,2 @@ +export * from './query-utils' +export * from './BackendQueryFormat' \ No newline at end of file diff --git a/libs/shared/lib/querybuilder/query-utils/query-utils.ts b/libs/shared/lib/querybuilder/query-utils/query-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..b081210b4dac7a57a3929c58f511d13477183bb1 --- /dev/null +++ b/libs/shared/lib/querybuilder/query-utils/query-utils.ts @@ -0,0 +1,78 @@ +import { EntityNodeAttributes, RelationNodeAttributes } from "../graph/graphology/model"; +import { QueryMultiGraphExport } from "../graph/graphology/utils"; +import { Handles } from "../graph/reactflow/handles"; +import { EntityNode, FunctionNode, QueryElementTypes, RelationData, RelationNode, possibleTypes } from "../graph/reactflow/model"; +import { BackendQueryFormat } from "./BackendQueryFormat"; + +/** + * Converts the ReactFlow query to a json data structure to send the query to the backend. + * @returns {BackendQueryFormat} A JSON object in the `JSONFormat`. + */ +export function Query2BackendQuery(databaseName: string, graph: QueryMultiGraphExport): BackendQueryFormat { + // dict of nodes per type + const entities = graph.nodes.filter(n => n.attributes?.type === QueryElementTypes.Entity) + .map((n) => ({ name: n.attributes!.name, ID: Number(n.key), constraints: [] })); + + const relations = graph.nodes.filter(n => n.attributes?.type === QueryElementTypes.Relation) + .map((n) => { + const attributes = n.attributes as RelationNodeAttributes; + const leftEdge = graph.edges.filter(e => (e.target === n.key && e.attributes!.targetHandle === Handles.RelationLeft))?.[0]; + const rightEdge = graph.edges.filter(e => (e.target === n.key && e.attributes!.targetHandle === Handles.RelationRight))?.[0]; + if (!leftEdge || !rightEdge) throw Error('Malformed Graph! One or more edges of a relation node do not exist'); + + const leftType = graph.nodes.filter(n => n.key === leftEdge.source)?.[0]?.attributes?.type; + const rightType = graph.nodes.filter(n => n.key === rightEdge.source)?.[0]?.attributes?.type; + if (!rightType || !leftType) throw Error('Malformed Graph! edges must have a type'); + + return { + name: attributes!.collection, ID: Number(n.key), depth: attributes!.depth, + fromType: leftType, fromID: Number(leftEdge.source), // need to treat function==groupby + toType: rightType, toID: Number(rightEdge.source), // need to treat function==groupby + constraints: [], + } + }); + + // TODO: Groubby not implemented (constraints) + const result: BackendQueryFormat = { + databaseName: databaseName, + return: { entities: entities.map(e => e.ID), relations: relations.map(r => r.ID), groupBys: [] }, + entities: entities, + relations: relations, + groupBys: [], // TODO + machineLearning: [], // TODO + limit: 5000, // TODO + }; + console.log(result, graph); + + + //note that attribute nodes are not processed here, but are detected if they are connected to any entity/relation/function node + + //add nodes to JSON query + // entityNodes.forEach((node) => { + // this.AddEntityToJsonObject(result, node); + // }); + // relationNodes.forEach((node) => { + // this.AddRelationToJsonObject(result, node); + // }); + + // functionNodes.forEach((functionNode: FunctionNode) => { + // switch (functionNode.data?.functionType) { + // case FunctionTypes.GroupBy: + // this.AddGroupByToJsonObject(result, functionNode); + // break; + // case FunctionTypes.link: + // this.AddLinkPredictionToJsonObject(result, functionNode); + // break; + // case FunctionTypes.communityDetection: + // this.AddCommunityDetectionToJsonObject(result, functionNode); + // break; + // case FunctionTypes.centrality: + // this.AddCentralityToJsonObject(result, functionNode); + // break; + // case FunctionTypes.shortestPath: + // this.addShortestPathToJsonOBject(result, functionNode); + // break; + // } + // }); + return result; +} \ No newline at end of file diff --git a/libs/shared/lib/schema/panel/schema.module.scss b/libs/shared/lib/schema/panel/schema.module.scss index f4d3e74fbb78ea0da17d295d4cf6f5e9b1dc32fa..aeedac18dc5ce359147965ce1b4fd4b9036a5be9 100644 --- a/libs/shared/lib/schema/panel/schema.module.scss +++ b/libs/shared/lib/schema/panel/schema.module.scss @@ -14,9 +14,10 @@ // import { SchemaThemeHolder } from '../../../domain/entity/css-themes/themeHolder'; .schemaPanel { - display: 'flex'; - flex-direction: 'column'; - justify-content: 'center'; + width: 100%; + // display: 'flex'; + // flex-direction: 'column'; + // justify-content: 'center'; // '& div': { // '& svg' { // zindex: 2; @@ -25,11 +26,11 @@ } .controls { - left: 'auto !important'; - bottom: 'auto !important'; - top: '10px'; - right: '20px'; - width: 'auto !important'; + left: auto !important; + bottom: auto !important; + top: 10px; + right: 20px; + width: auto !important; } // export const useStyles = (theme: Theme) => diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index 68245392c939c6530a5cbd8c9ac808a4a17ca01f..cfe8a15d193cd00ace1362d3588dc82680022e9a 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -24,6 +24,7 @@ import ReactFlow, { useNodesState, useEdgesState, ReactFlowInstance, + Background, } from 'reactflow'; import 'reactflow/dist/style.css'; @@ -104,7 +105,7 @@ export const Schema = (props: Props) => { if (schemaGraphology == undefined || schemaGraphology.order == 0) { return; } - console.log(schemaGraphology.export()); + // console.log(schemaGraphology.export()); const expandedSchema = schemaExpandRelation(schemaGraphology); layout.current?.layout(expandedSchema); @@ -170,7 +171,7 @@ export const Schema = (props: Props) => { showFitView={true} className={styles.controls} > - <ControlButton + {/* <ControlButton className={styles.exportButton} title={'Export graph schema'} onClick={(event) => { @@ -181,8 +182,8 @@ export const Schema = (props: Props) => { // }); }} > - {/* <img src={exportIcon} width={21}></img> */} - </ControlButton> + <img src={exportIcon} width={21}></img> + </ControlButton> */} </Controls> </ReactFlow> </ReactFlowProvider> diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx index a33dcaceb8b31acfcbc944521a0cc7d416e67429..4c54a857b261ca95077385b10dc744d4987f9c7a 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx @@ -40,6 +40,7 @@ export const EntityNode = React.memo( * @param event React Mouse drag event */ const onDragStart = (event: React.DragEvent<HTMLDivElement>) => { + // console.log(id, data); event.dataTransfer.setData( 'application/reactflow', JSON.stringify({ type: 'entity', name: id }) diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity.scss b/libs/shared/lib/schema/pills/nodes/entity/entity.scss deleted file mode 100644 index b4ca11567142d6513aa241bc20e8268b343e1232..0000000000000000000000000000000000000000 --- a/libs/shared/lib/schema/pills/nodes/entity/entity.scss +++ /dev/null @@ -1,180 +0,0 @@ - - /** - * When changing the height of the nodes, or the height of the buttons on the nodes, note that there is a hardcoded value 'height' - * in schemaViewModelImpl (in the method 'setRelationNodePosition') that is based on these heights. - */ -.entityNode { - background: SchemaThemeHolder.entity.baseColor; - display: 'flex'; - borderRadius: '1px'; - fontFamily: SchemaThemeHolder.fontFamily; - fontWeight: 'bold'; - fontSize: `${SchemaThemeHolder.fontSize}px`; - width: `${SchemaThemeHolder.entity.width}px`; - lineHeight: `${SchemaThemeHolder.entity.height}px`; -} - - nodeWrapper: { - display: 'inherit', - color: SchemaThemeHolder.entity.textColor, - textAlign: 'center', - justifyContent: 'space-between', - alignItems: 'center', - width: 'inherit', - }, - - nodeSpan: { - margin: '0 0.5em 0 1em', - float: 'right', - }, - - nodeData: { - flexGrow: 2, - }, - - entityNodeAttributesBox: { - height: 20, - position: 'absolute', - left: '115px', - top: '-33%', - transform: 'translateX(50%)', - borderRadius: '1px', - boxShadow: '-1px 2px 5px #888888', - textAlign: 'right', - }, - - entityNodeNodesBox: { - height: 20, - position: 'absolute', - left: '115px', - top: '54%', - transform: 'translateX(50%)', - borderRadius: '1px', - boxShadow: '-1px 2px 5px #888888', - textAlign: 'right', - }, - - /** - * Code for the shape of the relationNodes - */ - relationNode: { - height: 36, - display: 'flex', - fontFamily: SchemaThemeHolder.fontFamily, - fontWeight: 'bold', - fontSize: `${SchemaThemeHolder.fontSize}px`, - width: `${SchemaThemeHolder.relation.width}px`, - lineHeight: `${SchemaThemeHolder.relation.height}px`, - }, - - relationNodeTriangleGeneral: { - position: 'absolute', - width: 0, - height: 0, - margin: 'auto 0', - borderStyle: 'solid', - borderColor: 'transparent', - }, - - relationNodeTriangleLeft: { - transform: 'translateX(-100%)', - top: '0px', - borderWidth: '18px 24px 18px 0', - }, - - relationNodeSmallTriangleLeft: { - transform: 'translateX(-100%)', - top: '30px', - borderWidth: '0 8px 6px 0', - }, - - relationNodeTriangleRight: { - transform: `translateX(${SchemaThemeHolder.relation.width}px)`, - top: '0px', - borderWidth: '18px 0 18px 24px', - }, - - relationNodeSmallTriangleRight: { - transform: `translateX(${SchemaThemeHolder.relation.width}px)`, - top: '30px', - borderWidth: '0 0 6px 8px', - }, - - relationNodeAttributesBox: { - position: 'absolute', - top: '-4px', - transform: `translateX(${SchemaThemeHolder.relation.width + 4}px)`, - clipPath: 'polygon(0 0, 100% 0, 100% 100%, 26.5px 100%)', - height: 20, - textAlign: 'right', - }, - - relationNodeNodesBox: { - position: 'absolute', - top: '20px', - transform: `translateX(${SchemaThemeHolder.relation.width + 4}px)`, - clipPath: 'polygon(26.5px 0, 100% 0, 100% 100%, 0 100%)', - height: 20, - textAlign: 'right', - }, - - arrowup: { - width: 0, - height: 0, - position: 'absolute', - left: '50%', - top: '-20%', - transform: 'translateX(-50%)', - margin: '0 auto', - backgroundColor: 'transparent', - borderStyle: 'solid', - borderTopWidth: 0, - borderRightWidth: 8, - borderBottomWidth: 5, - borderLeftWidth: 8, - borderTopColor: 'transparent', - borderRightColor: 'transparent', - borderLeftColor: 'transparent', - }, - - arrowdown: { - width: 0, - height: 0, - position: 'absolute', - left: '50%', - bottom: '-20%', - transform: 'translateX(-50%)', - margin: '0 auto', - backgroundColor: 'transparent', - borderStyle: 'solid', - borderTopWidth: 5, - borderRightWidth: 8, - borderBottomWidth: 0, - borderLeftWidth: 8, - borderRightColor: 'transparent', - borderBottomColor: 'transparent', - borderLeftColor: 'transparent', - }, - - controls: { - left: 'auto !important', - bottom: 'auto !important', - top: '10px', - right: '20px', - width: 'auto !important', - }, - - exportButton: { - left: 'auto !important', - bottom: 'auto !important', - top: '10px', - right: '20px', - '& svg': { - transform: 'scale(1.4)', - }, - }, - - menuText: { - fontSize: 'small', - fontFamily: 'Poppins, sans-serif', - }, \ No newline at end of file diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx index aa5e1c849973ed5a7d779da77ce281708ff3d015..ca4f7f16d26d4e0f1a83ca064e425bd4eec84227 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx @@ -40,11 +40,12 @@ export const RelationNode = React.memo( * @param event React Mouse drag event. */ const onDragStart = (event: React.DragEvent<HTMLDivElement>) => { + // console.log(id, data); event.dataTransfer.setData( 'application/reactflow', JSON.stringify({ type: 'relation', - name: id, + name: id, //TODO id? from: data.from, to: data.to, collection: data.collection, diff --git a/libs/shared/lib/schema/schema-utils/Types.tsx b/libs/shared/lib/schema/schema-utils/Types.tsx index 2861ca0841af7fc03c851632f37ecc32b8ad06db..c3fde663afe91eca2cb15a760db8bae77768d98f 100644 --- a/libs/shared/lib/schema/schema-utils/Types.tsx +++ b/libs/shared/lib/schema/schema-utils/Types.tsx @@ -32,6 +32,7 @@ export enum AttributeCategory { // }; export interface SchemaGraphData { + name: string; attributes: Attributes[]; nodeCount: number; summedNullAmount: number; diff --git a/libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx b/libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx index e1e0c9fee10fc406bc4c08f4757bac800934c6d1..1a185ac4be0fb6cc6a5fb31d685866156cac5e51 100644 --- a/libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx +++ b/libs/shared/lib/vis/nodelink/ResultNodeLinkParserUseCase.tsx @@ -3,8 +3,8 @@ * Utrecht University within the Software Project course. * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { GraphType, LinkType, NodeType } from "./Types"; -import { Edge, GraphQueryResult } from "../../data-access/store"; +import { GraphType, LinkType, NodeType } from './Types'; +import { Edge, GraphQueryResult } from '../../data-access/store'; /** ResultNodeLinkParserUseCase implements methods to parse and translate websocket messages from the backend into a GraphType. */ /** @@ -37,7 +37,7 @@ export interface Link extends AxisType { /** Gets the group to which the node/edge belongs */ export function getGroupName(axisType: AxisType): string { - return axisType.id.split("/")[0]; + return axisType.id.split('/')[0]; } /** Returns true if the given id belongs to the target group. */ @@ -56,19 +56,19 @@ export function isNodeLinkResult( jsonObject: any ): jsonObject is NodeLinkResultType { if ( - typeof jsonObject === "object" && + typeof jsonObject === 'object' && jsonObject !== null && - "nodes" in jsonObject && - "edges" in jsonObject + 'nodes' in jsonObject && + 'edges' in jsonObject ) { if (!Array.isArray(jsonObject.nodes) || !Array.isArray(jsonObject.edges)) return false; const validNodes = jsonObject.nodes.every( - (node: any) => "id" in node && "attributes" in node + (node: any) => 'id' in node && 'attributes' in node ); const validEdges = jsonObject.edges.every( - (edge: any) => "from" in edge && "to" in edge + (edge: any) => 'from' in edge && 'to' in edge ); return validNodes && validEdges; @@ -119,13 +119,13 @@ export class ParseToUniqueEdges { if (!isLinkPredictionData) { for (let j = 0; j < queryResultEdges.length; j++) { const newLink = - queryResultEdges[j].from + ":" + queryResultEdges[j].to; + queryResultEdges[j].from + ':' + queryResultEdges[j].to; edgesMap.set(newLink, (edgesMap.get(newLink) || 0) + 1); attriMap.set(newLink, queryResultEdges[j].attributes); } edgesMap.forEach((count, key) => { - const fromTo = key.split(":"); + const fromTo = key.split(':'); edges.push({ from: fromTo[0], to: fromTo[1], @@ -168,8 +168,8 @@ export default class ResultNodeLinkParserUseCase { for (let i = 0; i < queryResult.nodes.length; i++) { // Assigns a group to every entity type for color coding - const nodeId = queryResult.nodes[i].id + "/"; - const entityType = nodeId.split("/")[0]; + const nodeId = queryResult.nodes[i].id + '/'; + const entityType = nodeId.split('/')[0]; // The preferred text to be shown on top of the node let preferredText = nodeId; @@ -201,7 +201,7 @@ export default class ResultNodeLinkParserUseCase { let mlExtra = {}; if ( queryResult.nodes[i].mldata && - typeof queryResult.nodes[i].mldata != "number" + typeof queryResult.nodes[i].mldata != 'number' ) { mlExtra = { shortestPathData: queryResult.nodes[i].mldata as Record< @@ -210,7 +210,7 @@ export default class ResultNodeLinkParserUseCase { >, }; shortestPathInResult = true; - } else if (typeof queryResult.nodes[i].mldata == "number") { + } else if (typeof queryResult.nodes[i].mldata == 'number') { // mldata + 1 so you dont get 0, which is interpreted as 'undefined' const numberOfCluster = (queryResult.nodes[i].mldata as number) + 1; mlExtra = { diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx index f3440505600d69f3bebeae02260d128a0d48c2a7..988444d377a29d5d1bd6df8de559bdad08906cc7 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.stories.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { ComponentStory, Meta } from "@storybook/react"; -import { NodeLinkVis } from "./nodelinkvis"; +import React from 'react'; +import { ComponentStory, Meta } from '@storybook/react'; +import { NodeLinkVis } from './nodelinkvis'; import { assignNewGraphQueryResult, @@ -8,21 +8,21 @@ import { graphQueryResultSlice, resetGraphQueryResults, store, -} from "../../data-access/store"; -import { configureStore } from "@reduxjs/toolkit"; -import { Provider } from "react-redux"; -import { GraphPolarisThemeProvider } from "../../data-access/theme"; +} from '../../data-access/store'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { GraphPolarisThemeProvider } from '../../data-access/theme'; import { big2ndChamberQueryResult, smallFlightsQueryResults, -} from "../../mock-data"; +} from '../../mock-data'; const Component: Meta<typeof NodeLinkVis> = { /* 👇 The title prop is optional. * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: "Components/Visualizations/NodeLinkVis", + title: 'Components/Visualizations/NodeLinkVis', component: NodeLinkVis, decorators: [ (story) => ( @@ -30,8 +30,8 @@ const Component: Meta<typeof NodeLinkVis> = { <GraphPolarisThemeProvider> <div style={{ - width: "100%", - height: "100vh", + width: '100%', + height: '100vh', }} > {story()} @@ -62,11 +62,11 @@ TestWithData.play = async () => { dispatch( assignNewGraphQueryResult({ nodes: [ - { id: "agent/007", attributes: { name: "Daniel Craig" } }, - { id: "villain", attributes: { name: "Le Chiffre" } }, + { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, + { id: 'villain', attributes: { name: 'Le Chiffre' } }, ], - links: [ - { from: "agent/007", to: "villain", attributes: { name: "Escape" } }, + edges: [ + { from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }, ], }) ); @@ -81,7 +81,7 @@ TestWithNoData.play = async () => { dispatch( assignNewGraphQueryResult({ nodes: [], - links: [], + edges: [], }) ); }; diff --git a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx index 30b71e803bab89c7b30b009de39b9c2819c4b470..a1805f7b50dfbdde372500669a26d7afa67197dc 100644 --- a/libs/shared/lib/vis/nodelink/nodelinkvis.tsx +++ b/libs/shared/lib/vis/nodelink/nodelinkvis.tsx @@ -15,7 +15,7 @@ import ResultNodeLinkParserUseCase from './ResultNodeLinkParserUseCase'; interface Props { loading?: boolean; - currentColours: any; + // currentColours: any; } const Div = styled.div` diff --git a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx index 688a89dabe3f9be35c7d918f0cc690630e2fb207..e66fde6f44adaefdd3da1b1ca37cf38c7e4c6ce9 100644 --- a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx +++ b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.stories.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import { ComponentStory, Meta } from "@storybook/react"; -import { RawJSONVis } from "./rawjsonvis"; +import React from 'react'; +import { ComponentStory, Meta } from '@storybook/react'; +import { RawJSONVis } from './rawjsonvis'; import { assignNewGraphQueryResult, @@ -8,17 +8,17 @@ import { graphQueryResultSlice, resetGraphQueryResults, store, -} from "../../data-access/store"; -import { configureStore } from "@reduxjs/toolkit"; -import { Provider } from "react-redux"; -import { GraphPolarisThemeProvider } from "../../data-access/theme"; +} from '../../data-access/store'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { GraphPolarisThemeProvider } from '../../data-access/theme'; const Component: Meta<typeof RawJSONVis> = { /* 👇 The title prop is optional. * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: "Components/Visualizations/RawJSONVIS", + title: 'Components/Visualizations/RawJSONVIS', component: RawJSONVis, decorators: [ (story) => ( @@ -49,10 +49,10 @@ TestWithData.play = async () => { dispatch( assignNewGraphQueryResult({ nodes: [ - { id: "agent/007", attributes: { name: "Daniel Craig" } }, - { id: "villain", attributes: { name: "Le Chiffre" } }, + { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, + { id: 'villain', attributes: { name: 'Le Chiffre' } }, ], - links: [], + edges: [], }) ); }; diff --git a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx index 66d63b79e33a168e90d875ac3678add78ac7e56d..61ebc2364a5a9747a01a3861e984a034f68a7afc 100644 --- a/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx +++ b/libs/shared/lib/vis/rawjsonvis/rawjsonvis.tsx @@ -33,7 +33,7 @@ export const RawJSONVis = React.memo((props: RawJSONVisProps) => { const loading = props.loading; return ( - <> + <div className="overflow-scroll"> <input onChange={(v) => dispatch(changePrimary({ main: v.currentTarget.value })) @@ -76,7 +76,7 @@ export const RawJSONVis = React.memo((props: RawJSONVisProps) => { </div> </div> )} - </> + </div> ); }); diff --git a/libs/shared/tsconfig.json b/libs/shared/tsconfig.json index faa063642c87870be531ee89c83de62ee5486c77..855b96076d3e779838cb6d6e577895e04da0f969 100644 --- a/libs/shared/tsconfig.json +++ b/libs/shared/tsconfig.json @@ -29,5 +29,6 @@ }, "exclude": ["dist", "build", "node_modules"], "include": ["src", "lib"], + "files": ["./node.d.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/libs/shared/tsconfig.node.json b/libs/shared/tsconfig.node.json index e5cd6295ceb651a306b4509ace1c88b44419d261..223fa3b61022b657d75c5f0299f6626f57afa30f 100644 --- a/libs/shared/tsconfig.node.json +++ b/libs/shared/tsconfig.node.json @@ -3,7 +3,8 @@ "composite": true, "module": "ESNext", "moduleResolution": "node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": ["node", "vite/client"] }, "include": ["vite.config.ts"] } diff --git a/libs/storybook/.env b/libs/storybook/.env new file mode 100644 index 0000000000000000000000000000000000000000..fd336427cf90b50f5b3b1c372bdd1c3593321dac --- /dev/null +++ b/libs/storybook/.env @@ -0,0 +1,2 @@ +VITE_BACKEND_URL=null +VITE_STAGING=sb \ No newline at end of file diff --git a/package.json b/package.json index 2404d0c909274b6e1f5661765f5b5558bb508561..b88691351ed1f1796b10cd3e57b270ce8e935e61 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "my-turborepo", + "name": "graphpolaris-monorepo", "version": "0.0.0", "private": true, "workspaces": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14b92d621f83e7320fbe089587bac34e31210e2b..8c7b9194bb5454c579784f3b8c0a2b15963a7a8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,43 @@ importers: specifier: latest version: 1.9.3 + apps/docs: + dependencies: + next: + specifier: ^13.1.1 + version: 13.1.1(@babel/core@7.21.3)(react-dom@18.2.0)(react@18.2.0) + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@babel/core': + specifier: ^7.0.0 + version: 7.21.3 + '@types/node': + specifier: ^17.0.12 + version: 17.0.12 + '@types/react': + specifier: ^18.0.22 + version: 18.0.28 + '@types/react-dom': + specifier: ^18.0.7 + version: 18.0.11 + eslint: + specifier: 7.32.0 + version: 7.32.0 + eslint-config-custom: + specifier: workspace:* + version: link:../../libs/workspace/eslint-config-custom + tsconfig: + specifier: workspace:* + version: link:../../libs/workspace/tsconfig + typescript: + specifier: ^4.5.3 + version: 4.9.5 + apps/web: dependencies: '@graphpolaris/shared': @@ -92,12 +129,21 @@ importers: '@vitejs/plugin-react-swc': specifier: ^3.0.0 version: 3.2.0(vite@4.2.1) + autoprefixer: + specifier: ^10.4.14 + version: 10.4.14(postcss@8.4.21) graphology-types: specifier: ^0.24.7 version: 0.24.7 + postcss: + specifier: ^8.4.21 + version: 8.4.21 react-is: specifier: ^18.2.0 version: 18.2.0 + tailwindcss: + specifier: ^3.3.1 + version: 3.3.1(postcss@8.4.21)(ts-node@10.9.1) typescript: specifier: ^4.9.3 version: 4.9.5 @@ -106,7 +152,7 @@ importers: version: 4.2.1(@types/node@18.13.0)(less@4.1.3)(sass@1.59.3) vite-plugin-dts: specifier: ^2.1.0 - version: 2.1.0(@types/node@18.13.0)(vite@4.2.1) + version: 2.1.0(@types/node@17.0.12)(vite@4.2.1) vitest: specifier: ^0.29.2 version: 0.29.4(happy-dom@8.9.0)(jsdom@21.1.1)(sass@1.59.3) @@ -502,7 +548,6 @@ packages: dependencies: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.17 - dev: true /@aw-web-design/x-default-browser@1.4.88: resolution: {integrity: sha512-AkEmF0wcwYC2QkhK703Y83fxWARttIWXDmQN8+cof8FmFZ5BRhnNXGymeb1S73bOCLfWjYELxtujL56idCN/XA==} @@ -525,7 +570,6 @@ packages: /@babel/compat-data@7.21.4: resolution: {integrity: sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==} engines: {node: '>=6.9.0'} - dev: true /@babel/core@7.21.3: resolution: {integrity: sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==} @@ -548,7 +592,6 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true /@babel/generator@7.21.3: resolution: {integrity: sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==} @@ -585,7 +628,6 @@ packages: browserslist: 4.21.5 lru-cache: 5.1.1 semver: 6.3.0 - dev: true /@babel/helper-create-class-features-plugin@7.21.0(@babel/core@7.21.3): resolution: {integrity: sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==} @@ -684,7 +726,6 @@ packages: '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color - dev: true /@babel/helper-optimise-call-expression@7.18.6: resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} @@ -732,7 +773,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.21.4 - dev: true /@babel/helper-skip-transparent-expression-wrappers@7.20.0: resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} @@ -758,7 +798,6 @@ packages: /@babel/helper-validator-option@7.21.0: resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-wrap-function@7.20.5: resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} @@ -781,7 +820,6 @@ packages: '@babel/types': 7.21.4 transitivePeerDependencies: - supports-color - dev: true /@babel/highlight@7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} @@ -1889,15 +1927,15 @@ packages: '@commitlint/execute-rule': 17.4.0 '@commitlint/resolve-extends': 17.4.4 '@commitlint/types': 17.4.4 - '@types/node': 18.13.0 + '@types/node': 17.0.12 chalk: 4.1.2 cosmiconfig: 8.1.3 - cosmiconfig-typescript-loader: 4.3.0(@types/node@18.13.0)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@4.9.5) + cosmiconfig-typescript-loader: 4.3.0(@types/node@17.0.12)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@4.9.5) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@18.13.0)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@17.0.12)(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - '@swc/core' @@ -2511,6 +2549,21 @@ packages: dev: true optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.40.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.40.0 + eslint-visitor-keys: 3.4.1 + dev: true + + /@eslint-community/regexpp@4.5.1: + resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint/eslintrc@0.4.3: resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -2527,10 +2580,43 @@ packages: transitivePeerDependencies: - supports-color + /@eslint/eslintrc@2.0.3: + resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@5.5.0) + espree: 9.5.2 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.40.0: + resolution: {integrity: sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@fal-works/esbuild-plugin-global-externals@2.1.2: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: true + /@humanwhocodes/config-array@0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4(supports-color@5.5.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /@humanwhocodes/config-array@0.5.0: resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} engines: {node: '>=10.10.0'} @@ -2541,6 +2627,11 @@ packages: transitivePeerDependencies: - supports-color + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + /@humanwhocodes/object-schema@1.2.1: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} @@ -2596,7 +2687,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.13.0 + '@types/node': 17.0.12 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -2608,7 +2699,7 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.13.0 + '@types/node': 17.0.12 '@types/yargs': 17.0.23 chalk: 4.1.2 dev: true @@ -2636,7 +2727,6 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.14 - dev: true /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} @@ -2709,6 +2799,16 @@ packages: react: 18.2.0 dev: true + /@microsoft/api-extractor-model@7.26.4(@types/node@17.0.12): + resolution: {integrity: sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 3.55.2(@types/node@17.0.12) + transitivePeerDependencies: + - '@types/node' + dev: true + /@microsoft/api-extractor-model@7.26.4(@types/node@18.13.0): resolution: {integrity: sha512-PDCgCzXDo+SLY5bsfl4bS7hxaeEtnXj7XtuzEE+BtALp7B5mK/NrS2kHWU69pohgsRmEALycQdaQPXoyT2i5MQ==} dependencies: @@ -2719,6 +2819,26 @@ packages: - '@types/node' dev: true + /@microsoft/api-extractor@7.34.4(@types/node@17.0.12): + resolution: {integrity: sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==} + hasBin: true + dependencies: + '@microsoft/api-extractor-model': 7.26.4(@types/node@17.0.12) + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 3.55.2(@types/node@17.0.12) + '@rushstack/rig-package': 0.3.18 + '@rushstack/ts-command-line': 4.13.2 + colors: 1.2.5 + lodash: 4.17.21 + resolve: 1.22.1 + semver: 7.3.8 + source-map: 0.6.1 + typescript: 4.8.4 + transitivePeerDependencies: + - '@types/node' + dev: true + /@microsoft/api-extractor@7.34.4(@types/node@18.13.0): resolution: {integrity: sha512-HOdcci2nT40ejhwPC3Xja9G+WSJmWhCUKKryRfQYsmE9cD+pxmBaKBKCbuS9jUcl6bLLb4Gz+h7xEN5r0QiXnQ==} hasBin: true @@ -2975,11 +3095,132 @@ packages: tar-fs: 2.1.1 dev: true + /@next/env@13.1.1: + resolution: {integrity: sha512-vFMyXtPjSAiOXOywMojxfKIqE3VWN5RCAx+tT3AS3pcKjMLFTCJFUWsKv8hC+87Z1F4W3r68qTwDFZIFmd5Xkw==} + dev: false + /@next/eslint-plugin-next@13.0.0: resolution: {integrity: sha512-z+gnX4Zizatqatc6f4CQrcC9oN8Us3Vrq/OLyc98h7K/eWctrnV91zFZodmJHUjx0cITY8uYM7LXD7IdYkg3kg==} dependencies: glob: 7.1.7 + /@next/swc-android-arm-eabi@13.1.1: + resolution: {integrity: sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@next/swc-android-arm64@13.1.1: + resolution: {integrity: sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-arm64@13.1.1: + resolution: {integrity: sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-x64@13.1.1: + resolution: {integrity: sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-freebsd-x64@13.1.1: + resolution: {integrity: sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm-gnueabihf@13.1.1: + resolution: {integrity: sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-gnu@13.1.1: + resolution: {integrity: sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-musl@13.1.1: + resolution: {integrity: sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-gnu@13.1.1: + resolution: {integrity: sha512-nnjuBrbzvqaOJaV+XgT8/+lmXrSCOt1YYZn/irbDb2fR2QprL6Q7WJNgwsZNxiLSfLdv+2RJGGegBx9sLBEzGA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-musl@13.1.1: + resolution: {integrity: sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-arm64-msvc@13.1.1: + resolution: {integrity: sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-ia32-msvc@13.1.1: + resolution: {integrity: sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-x64-msvc@13.1.1: + resolution: {integrity: sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3577,6 +3818,24 @@ packages: /@rushstack/eslint-patch@1.2.0: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} + /@rushstack/node-core-library@3.55.2(@types/node@17.0.12): + resolution: {integrity: sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@types/node': 17.0.12 + colors: 1.2.5 + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.1 + semver: 7.3.8 + z-schema: 5.0.5 + dev: true + /@rushstack/node-core-library@3.55.2(@types/node@18.13.0): resolution: {integrity: sha512-SaLe/x/Q/uBVdNFK5V1xXvsVps0y7h1sN7aSJllQyFbugyOaxhNRF25bwEDnicARNEjJw0pk0lYnJQ9Kr6ev0A==} peerDependencies: @@ -5189,6 +5448,12 @@ packages: '@swc/core-win32-x64-msvc': 1.3.42 dev: true + /@swc/helpers@0.4.14: + resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} + dependencies: + tslib: 2.5.0 + dev: false + /@testing-library/dom@8.20.0: resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} engines: {node: '>=12'} @@ -5336,7 +5601,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /@types/chai-subset@1.3.3: @@ -5368,7 +5633,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /@types/cookie@0.3.3: @@ -5574,7 +5839,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -5599,20 +5864,20 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /@types/glob@8.1.0: resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /@types/hoist-non-react-statics@3.3.1: @@ -5675,7 +5940,7 @@ packages: /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 form-data: 3.0.1 dev: true @@ -5683,6 +5948,10 @@ packages: resolution: {integrity: sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==} dev: true + /@types/node@17.0.12: + resolution: {integrity: sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==} + dev: true + /@types/node@18.13.0: resolution: {integrity: sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==} dev: true @@ -5771,7 +6040,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /@types/styled-components@5.1.26: @@ -5836,54 +6105,122 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@5.52.0(eslint@7.32.0)(typescript@4.9.5): - resolution: {integrity: sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA==} + /@typescript-eslint/eslint-plugin@5.52.0(@typescript-eslint/parser@5.52.0)(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-lHazYdvYVsBokwCdKOppvYJKaJ4S41CgKBcPvyd0xjZNbvQdhn/pnJlGtQksQ/NhInzdaeaSarlBjDXHuclEbg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: + '@typescript-eslint/parser': ^5.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: + '@typescript-eslint/parser': 5.52.0(eslint@8.40.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.52.0 - '@typescript-eslint/types': 5.52.0 - '@typescript-eslint/typescript-estree': 5.52.0(typescript@4.9.5) + '@typescript-eslint/type-utils': 5.52.0(eslint@8.40.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.52.0(eslint@8.40.0)(typescript@4.9.5) debug: 4.3.4(supports-color@5.5.0) - eslint: 7.32.0 + eslint: 8.40.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + regexpp: 3.2.0 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - supports-color + dev: true - /@typescript-eslint/scope-manager@5.52.0: - resolution: {integrity: sha512-AR7sxxfBKiNV0FWBSARxM8DmNxrwgnYMPwmpkC1Pl1n+eT8/I2NAUPuwDy/FmDcC6F8pBfmOcaxcxRHspgOBMw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.52.0 - '@typescript-eslint/visitor-keys': 5.52.0 - - /@typescript-eslint/type-utils@5.52.0(eslint@7.32.0)(typescript@4.9.5): - resolution: {integrity: sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==} + /@typescript-eslint/parser@5.52.0(eslint@7.32.0)(typescript@4.9.5): + resolution: {integrity: sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - eslint: '*' + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: + '@typescript-eslint/scope-manager': 5.52.0 + '@typescript-eslint/types': 5.52.0 '@typescript-eslint/typescript-estree': 5.52.0(typescript@4.9.5) - '@typescript-eslint/utils': 5.52.0(eslint@7.32.0)(typescript@4.9.5) debug: 4.3.4(supports-color@5.5.0) eslint: 7.32.0 - tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/types@5.52.0: - resolution: {integrity: sha512-oV7XU4CHYfBhk78fS7tkum+/Dpgsfi91IIDy7fjCyq2k6KB63M6gMC0YIvy+iABzmXThCRI6xpCEyVObBdWSDQ==} + /@typescript-eslint/parser@5.52.0(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-e2KiLQOZRo4Y0D/b+3y08i3jsekoSkOYStROYmPUnGMEoA0h+k2qOH5H6tcjIc68WDvGwH+PaOrP1XRzLJ6QlA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.52.0 + '@typescript-eslint/types': 5.52.0 + '@typescript-eslint/typescript-estree': 5.52.0(typescript@4.9.5) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.40.0 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.52.0: + resolution: {integrity: sha512-AR7sxxfBKiNV0FWBSARxM8DmNxrwgnYMPwmpkC1Pl1n+eT8/I2NAUPuwDy/FmDcC6F8pBfmOcaxcxRHspgOBMw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.52.0 + '@typescript-eslint/visitor-keys': 5.52.0 + + /@typescript-eslint/type-utils@5.52.0(eslint@7.32.0)(typescript@4.9.5): + resolution: {integrity: sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.52.0(typescript@4.9.5) + '@typescript-eslint/utils': 5.52.0(eslint@7.32.0)(typescript@4.9.5) + debug: 4.3.4(supports-color@5.5.0) + eslint: 7.32.0 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/type-utils@5.52.0(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-tEKuUHfDOv852QGlpPtB3lHOoig5pyFQN/cUiZtpw99D93nEBjexRLre5sQZlkMoHry/lZr8qDAt2oAHLKA6Jw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.52.0(typescript@4.9.5) + '@typescript-eslint/utils': 5.52.0(eslint@8.40.0)(typescript@4.9.5) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.40.0 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.52.0: + resolution: {integrity: sha512-oV7XU4CHYfBhk78fS7tkum+/Dpgsfi91IIDy7fjCyq2k6KB63M6gMC0YIvy+iABzmXThCRI6xpCEyVObBdWSDQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} /@typescript-eslint/typescript-estree@5.52.0(typescript@4.9.5): @@ -5926,12 +6263,32 @@ packages: - typescript dev: true + /@typescript-eslint/utils@5.52.0(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-As3lChhrbwWQLNk2HC8Ree96hldKIqk98EYvypd3It8Q1f8d5zWyIoaZEp2va5667M4ZyE7X8UUR+azXrFl+NA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.52.0 + '@typescript-eslint/types': 5.52.0 + '@typescript-eslint/typescript-estree': 5.52.0(typescript@4.9.5) + eslint: 8.40.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.40.0) + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@5.52.0: resolution: {integrity: sha512-qMwpw6SU5VHCPr99y274xhbm+PRViK/NATY6qzt+Et7+mThGuFSl/ompj2/hrBlRP/kq+BFdgagnOSgw9TB0eA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: '@typescript-eslint/types': 5.52.0 - eslint-visitor-keys: 3.3.0 + eslint-visitor-keys: 3.4.1 /@vitejs/plugin-basic-ssl@1.0.1(vite@4.2.1): resolution: {integrity: sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==} @@ -6172,6 +6529,14 @@ packages: dependencies: acorn: 7.4.1 + /acorn-jsx@5.3.2(acorn@8.8.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.2 + dev: true + /acorn-walk@7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} engines: {node: '>=0.4.0'} @@ -6293,6 +6658,10 @@ packages: engines: {node: '>=12'} dev: true + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -6320,6 +6689,10 @@ packages: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -6448,6 +6821,22 @@ packages: hasBin: true dev: false + /autoprefixer@10.4.14(postcss@8.4.21): + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.5 + caniuse-lite: 1.0.30001466 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: true + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -6673,7 +7062,6 @@ packages: electron-to-chromium: 1.4.330 node-releases: 2.0.10 update-browserslist-db: 1.0.10(browserslist@4.21.5) - dev: true /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -6702,6 +7090,12 @@ packages: ieee754: 1.2.1 dev: true + /builtins@5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + dependencies: + semver: 7.3.8 + dev: true + /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -6776,7 +7170,6 @@ packages: /caniuse-lite@1.0.30001466: resolution: {integrity: sha512-ewtFBSfWjEmxUgNBSZItFSmVtvk9zkwkl1OfRZlKA8slltRN+/C/tuGVrF9styXkN36Yu3+SeJ1qkXxDEyNZ5w==} - dev: true /canvas@2.11.0: resolution: {integrity: sha512-bdTjFexjKJEwtIo0oRx8eD4G2yWoUOXP9lj279jmQ2zMnTQhT8C3512OKz3s+ZOaQlLbE7TuVvRDYDB3Llyy5g==} @@ -6907,6 +7300,10 @@ packages: string-width: 5.1.2 dev: true + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: @@ -7001,6 +7398,11 @@ packages: /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + /commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -7158,7 +7560,7 @@ packages: layout-base: 2.0.1 dev: true - /cosmiconfig-typescript-loader@4.3.0(@types/node@18.13.0)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@4.9.5): + /cosmiconfig-typescript-loader@4.3.0(@types/node@17.0.12)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@4.9.5): resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: @@ -7167,9 +7569,9 @@ packages: ts-node: '>=10' typescript: '>=3' dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 cosmiconfig: 8.1.3 - ts-node: 10.9.1(@types/node@18.13.0)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@17.0.12)(typescript@4.9.5) typescript: 4.9.5 dev: true @@ -7825,6 +8227,10 @@ packages: - supports-color dev: true + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -7841,6 +8247,10 @@ packages: dependencies: path-type: 4.0.0 + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -7928,7 +8338,6 @@ packages: /electron-to-chromium@1.4.330: resolution: {integrity: sha512-PqyefhybrVdjAJ45HaPLtuVaehiSw7C3ya0aad+rvmV53IVyXmYRk3pwIOb2TxTDTnmgQdn46NjMMaysx79/6Q==} - dev: true /elkjs@0.8.2: resolution: {integrity: sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==} @@ -8165,7 +8574,6 @@ packages: /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} - dev: true /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -8224,6 +8632,52 @@ packages: dependencies: eslint: 7.32.0 + /eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.31.8)(eslint@8.40.0): + resolution: {integrity: sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==} + peerDependencies: + eslint: ^8.8.0 + eslint-plugin-react: ^7.28.0 + dependencies: + eslint: 8.40.0 + eslint-plugin-react: 7.31.8(eslint@8.40.0) + dev: true + + /eslint-config-standard-with-typescript@23.0.0(@typescript-eslint/eslint-plugin@5.52.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-iaaWifImn37Z1OXbNW1es7KI+S7D408F9ys0bpaQf2temeBWlvb0Nc5qHkOgYaRb5QxTZT32GGeN1gtswASOXA==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 5.52.0(@typescript-eslint/parser@5.52.0)(eslint@8.40.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.52.0(eslint@8.40.0)(typescript@4.9.5) + eslint: 8.40.0 + eslint-config-standard: 17.0.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.40.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.52.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) + eslint-plugin-n: 15.7.0(eslint@8.40.0) + eslint-plugin-promise: 6.1.1(eslint@8.40.0) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-config-standard@17.0.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.40.0): + resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + dependencies: + eslint: 8.40.0 + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.52.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) + eslint-plugin-n: 15.7.0(eslint@8.40.0) + eslint-plugin-promise: 6.1.1(eslint@8.40.0) + dev: true + /eslint-config-turbo@1.8.8(eslint@7.32.0): resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} peerDependencies: @@ -8287,6 +8741,17 @@ packages: transitivePeerDependencies: - supports-color + /eslint-plugin-es@4.1.0(eslint@8.40.0): + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.40.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.52.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -8343,6 +8808,32 @@ packages: object.fromentries: 2.0.6 semver: 6.3.0 + /eslint-plugin-n@15.7.0(eslint@8.40.0): + resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + builtins: 5.0.1 + eslint: 8.40.0 + eslint-plugin-es: 4.1.0(eslint@8.40.0) + eslint-utils: 3.0.0(eslint@8.40.0) + ignore: 5.2.4 + is-core-module: 2.11.0 + minimatch: 3.1.2 + resolve: 1.22.1 + semver: 7.3.8 + dev: true + + /eslint-plugin-promise@6.1.1(eslint@8.40.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.40.0 + dev: true + /eslint-plugin-react-hooks@4.6.0(eslint@7.32.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} @@ -8373,6 +8864,29 @@ packages: semver: 6.3.0 string.prototype.matchall: 4.0.8 + /eslint-plugin-react@7.31.8(eslint@8.40.0): + resolution: {integrity: sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + doctrine: 2.1.0 + eslint: 8.40.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.3 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + object.hasown: 1.1.2 + object.values: 1.1.6 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.0 + string.prototype.matchall: 4.0.8 + dev: true + /eslint-plugin-turbo@1.8.8(eslint@7.32.0): resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} peerDependencies: @@ -8387,6 +8901,14 @@ packages: esrecurse: 4.3.0 estraverse: 4.3.0 + /eslint-scope@7.2.0: + resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + /eslint-utils@2.1.0: resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} engines: {node: '>=6'} @@ -8403,6 +8925,16 @@ packages: eslint-visitor-keys: 2.1.0 dev: true + /eslint-utils@3.0.0(eslint@8.40.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.40.0 + eslint-visitor-keys: 2.1.0 + dev: true + /eslint-visitor-keys@1.3.0: resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} engines: {node: '>=4'} @@ -8411,8 +8943,8 @@ packages: resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} engines: {node: '>=10'} - /eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + /eslint-visitor-keys@3.4.1: + resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} /eslint@7.32.0: @@ -8463,6 +8995,55 @@ packages: transitivePeerDependencies: - supports-color + /eslint@8.40.0: + resolution: {integrity: sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) + '@eslint-community/regexpp': 4.5.1 + '@eslint/eslintrc': 2.0.3 + '@eslint/js': 8.40.0 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@5.5.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.0 + eslint-visitor-keys: 3.4.1 + espree: 9.5.2 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.4.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /espree@7.3.1: resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} engines: {node: ^10.12.0 || >=12.0.0} @@ -8471,6 +9052,15 @@ packages: acorn-jsx: 5.3.2(acorn@7.4.1) eslint-visitor-keys: 1.3.0 + /espree@9.5.2: + resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) + eslint-visitor-keys: 3.4.1 + dev: true + /esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -8752,6 +9342,14 @@ packages: path-exists: 4.0.0 dev: true + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: true + /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -8815,6 +9413,10 @@ packages: engines: {node: '>= 0.6'} dev: true + /fraction.js@4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: true + /fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -8904,7 +9506,6 @@ packages: /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - dev: true /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -8937,6 +9538,11 @@ packages: engines: {node: '>=8'} dev: true + /get-stdin@8.0.0: + resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} + engines: {node: '>=10'} + dev: true + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -8986,6 +9592,13 @@ packages: dependencies: is-glob: 4.0.3 + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + /glob-promise@4.2.2(glob@7.2.3): resolution: {integrity: sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==} engines: {node: '>=12'} @@ -9010,6 +9623,17 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + /glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} dependencies: @@ -9815,7 +10439,7 @@ packages: dependencies: '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.6 - '@types/node': 18.13.0 + '@types/node': 17.0.12 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -9833,7 +10457,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 18.13.0 + '@types/node': 17.0.12 dev: true /jest-regex-util@29.4.3: @@ -9846,7 +10470,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 18.13.0 + '@types/node': 17.0.12 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -9857,7 +10481,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -9866,7 +10490,7 @@ packages: resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 18.13.0 + '@types/node': 17.0.12 jest-util: 29.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -9881,6 +10505,10 @@ packages: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} dev: true + /js-sdsl@4.4.0: + resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -9980,6 +10608,10 @@ packages: engines: {node: '>=4'} hasBin: true + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -10002,7 +10634,6 @@ packages: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - dev: true /jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} @@ -10156,6 +10787,22 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + /load-json-file@5.3.0: + resolution: {integrity: sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 4.0.1 + strip-bom: 3.0.0 + type-fest: 0.3.1 + dev: true + + /load-json-file@7.0.1: + resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -10197,6 +10844,13 @@ packages: p-locate: 5.0.0 dev: true + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: true + /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: true @@ -10277,7 +10931,6 @@ packages: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: yallist: 3.1.1 - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -10553,6 +11206,14 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + /nan@2.17.0: resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} requiresBuild: true @@ -10593,6 +11254,50 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true + /next@13.1.1(@babel/core@7.21.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-R5eBAaIa3X7LJeYvv1bMdGnAVF4fVToEjim7MkflceFPuANY3YyvFxXee/A+acrSYwYPvOvf7f6v/BM/48ea5w==} + engines: {node: '>=14.6.0'} + hasBin: true + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^6.0.0 || ^7.0.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.1.1 + '@swc/helpers': 0.4.14 + caniuse-lite: 1.0.30001466 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.21.3)(react@18.2.0) + optionalDependencies: + '@next/swc-android-arm-eabi': 13.1.1 + '@next/swc-android-arm64': 13.1.1 + '@next/swc-darwin-arm64': 13.1.1 + '@next/swc-darwin-x64': 13.1.1 + '@next/swc-freebsd-x64': 13.1.1 + '@next/swc-linux-arm-gnueabihf': 13.1.1 + '@next/swc-linux-arm64-gnu': 13.1.1 + '@next/swc-linux-arm64-musl': 13.1.1 + '@next/swc-linux-x64-gnu': 13.1.1 + '@next/swc-linux-x64-musl': 13.1.1 + '@next/swc-win32-arm64-msvc': 13.1.1 + '@next/swc-win32-ia32-msvc': 13.1.1 + '@next/swc-win32-x64-msvc': 13.1.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /node-dir@0.1.17: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} engines: {node: '>= 0.10.5'} @@ -10634,7 +11339,6 @@ packages: /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} - dev: true /nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} @@ -10667,6 +11371,11 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -10691,6 +11400,11 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -10858,6 +11572,13 @@ packages: p-limit: 3.1.0 dev: true + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: true + /p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -10886,6 +11607,14 @@ packages: dependencies: callsites: 3.1.0 + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -10925,6 +11654,11 @@ packages: engines: {node: '>=8'} dev: true + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -10976,6 +11710,11 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -11022,6 +11761,22 @@ packages: transitivePeerDependencies: - '@pixi/utils' + /pkg-conf@3.1.0: + resolution: {integrity: sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + load-json-file: 5.3.0 + dev: true + + /pkg-conf@4.0.0: + resolution: {integrity: sha512-7dmgi4UY4qk+4mj5Cd8v/GExPo0K+SlY+hulOSdfZ/T6jVH6//y7NtzZo5WrfhDBxuQ0jCa7fLZmNaNh7EWL/w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + find-up: 6.3.0 + load-json-file: 7.0.1 + dev: true + /pkg-dir@3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} engines: {node: '>=6'} @@ -11058,6 +11813,18 @@ packages: '@babel/runtime': 7.21.0 dev: true + /postcss-import@14.1.0(postcss@8.4.21): + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.1 + dev: true + /postcss-js@4.0.1(postcss@8.4.21): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} @@ -11068,6 +11835,24 @@ packages: postcss: 8.4.21 dev: true + /postcss-load-config@3.1.4(postcss@8.4.21)(ts-node@10.9.1): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.21 + ts-node: 10.9.1(@types/node@17.0.12)(typescript@4.9.5) + yaml: 1.10.2 + dev: true + /postcss-load-config@4.0.1(postcss@8.4.21)(ts-node@10.9.1): resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} @@ -11142,6 +11927,16 @@ packages: postcss: 8.4.21 dev: true + /postcss-nested@6.0.0(postcss@8.4.21): + resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: true + /postcss-nesting@11.2.2(postcss@8.4.21): resolution: {integrity: sha512-aOTiUniAB1bcPE6GGiynWRa6PZFPhOTAm5q3q5cem6QeSijIHHkWr6gs65ukCZMXeak8yXeZVbBJET3VM+HlhA==} engines: {node: ^14 || ^16 || >=18} @@ -11187,6 +11982,15 @@ packages: supports-color: 5.5.0 dev: true + /postcss@8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: false + /postcss@8.4.21: resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} engines: {node: ^10 || ^12 || >=14} @@ -11374,6 +12178,11 @@ packages: engines: {node: '>=8'} dev: true + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + /raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} dependencies: @@ -11728,6 +12537,12 @@ packages: - immer dev: false + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -12347,6 +13162,16 @@ packages: dev: false optional: true + /standard-engine@15.0.0: + resolution: {integrity: sha512-4xwUhJNo1g/L2cleysUqUv7/btn7GEbYJvmgKrQ2vd/8pkTmN8cpqAZg+BT8Z1hNeEH787iWUdOpL8fmApLtxA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + get-stdin: 8.0.0 + minimist: 1.2.8 + pkg-conf: 3.1.0 + xdg-basedir: 4.0.0 + dev: true + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -12521,10 +13346,42 @@ packages: supports-color: 5.5.0 dev: false + /styled-jsx@5.1.1(@babel/core@7.21.3)(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + '@babel/core': 7.21.3 + client-only: 0.0.1 + react: 18.2.0 + dev: false + /stylis@4.1.3: resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==} dev: false + /sucrase@3.32.0: + resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 + dev: true + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -12572,6 +13429,41 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /tailwindcss@3.3.1(postcss@8.4.21)(ts-node@10.9.1): + resolution: {integrity: sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==} + engines: {node: '>=12.13.0'} + hasBin: true + peerDependencies: + postcss: ^8.0.9 + dependencies: + arg: 5.0.2 + chokidar: 3.5.3 + color-name: 1.1.4 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.2.12 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.18.2 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-import: 14.1.0(postcss@8.4.21) + postcss-js: 4.0.1(postcss@8.4.21) + postcss-load-config: 3.1.4(postcss@8.4.21)(ts-node@10.9.1) + postcss-nested: 6.0.0(postcss@8.4.21) + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.1 + sucrase: 3.32.0 + transitivePeerDependencies: + - ts-node + dev: true + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -12736,6 +13628,19 @@ packages: /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -12816,6 +13721,10 @@ packages: engines: {node: '>=6.10'} dev: true + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + /ts-morph@17.0.1: resolution: {integrity: sha512-10PkHyXmrtsTvZSL+cqtJLTgFXkU43Gd0JCc0Rw6GchWbqKe0Rwgt1v3ouobTZwQzF1mGhDeAlWYBMGRV7y+3g==} dependencies: @@ -12823,6 +13732,37 @@ packages: code-block-writer: 11.0.3 dev: true + /ts-node@10.9.1(@types/node@17.0.12)(typescript@4.9.5): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 17.0.12 + acorn: 8.8.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node@10.9.1(@types/node@18.13.0)(typescript@4.9.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -12854,6 +13794,32 @@ packages: yn: 3.1.1 dev: true + /ts-standard@12.0.2(typescript@4.9.5): + resolution: {integrity: sha512-XX2wrB9fKKTfBj4yD3ABm9iShzZcS2iWcPK8XzlBvuL20+wMiLgiz/k5tXgZwTaYq5wRhbks1Y9PelhujF/9ag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + peerDependencies: + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 5.52.0(@typescript-eslint/parser@5.52.0)(eslint@8.40.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.52.0(eslint@8.40.0)(typescript@4.9.5) + eslint: 8.40.0 + eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.31.8)(eslint@8.40.0) + eslint-config-standard-with-typescript: 23.0.0(@typescript-eslint/eslint-plugin@5.52.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.40.0)(typescript@4.9.5) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.52.0)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0) + eslint-plugin-n: 15.7.0(eslint@8.40.0) + eslint-plugin-promise: 6.1.1(eslint@8.40.0) + eslint-plugin-react: 7.31.8(eslint@8.40.0) + minimist: 1.2.8 + pkg-conf: 4.0.0 + standard-engine: 15.0.0 + typescript: 4.9.5 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /tsconfck@2.1.0(typescript@4.9.5): resolution: {integrity: sha512-lztI9ohwclQHISVWrM/hlcgsRpphsii94DV9AQtAw2XJSVNiv+3ppdEsrL5J+xc5oTeHXe1qDqlOAGw8VSa9+Q==} engines: {node: ^14.13.1 || ^16 || >=18} @@ -12983,6 +13949,11 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + /type-fest@0.3.1: + resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} + engines: {node: '>=6'} + dev: true + /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -13155,7 +14126,6 @@ packages: browserslist: 4.21.5 escalade: 3.1.1 picocolors: 1.0.0 - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -13307,7 +14277,7 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-node@0.29.4(@types/node@18.13.0)(sass@1.59.3): + /vite-node@0.29.4(@types/node@17.0.12)(sass@1.59.3): resolution: {integrity: sha512-sPhnCzGm3rCI1BMgOUHiGJN4MObLUOzdCjrNU5A2miNTat/7k96hmvVLxKXPLb0wjX160oG1ZhLVYBzF80UJlQ==} engines: {node: '>=v14.16.0'} hasBin: true @@ -13328,6 +14298,29 @@ packages: - terser dev: true + /vite-plugin-dts@2.1.0(@types/node@17.0.12)(vite@4.2.1): + resolution: {integrity: sha512-Vw0FdCuM3VLR4hTFHh0yMEzfwI7NyFvPIMFwvE+Q0t4qtoHIfYOP/JXs7nTnHuQk87FSjlhGeIJ1fLBcktgPgA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: '>=2.9.0' + dependencies: + '@babel/parser': 7.21.3 + '@microsoft/api-extractor': 7.34.4(@types/node@17.0.12) + '@rollup/pluginutils': 5.0.2 + '@rushstack/node-core-library': 3.55.2(@types/node@17.0.12) + debug: 4.3.4(supports-color@5.5.0) + fast-glob: 3.2.12 + fs-extra: 10.1.0 + kolorist: 1.7.0 + magic-string: 0.29.0 + ts-morph: 17.0.1 + vite: 4.2.1(@types/node@18.13.0)(less@4.1.3)(sass@1.59.3) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + dev: true + /vite-plugin-dts@2.1.0(@types/node@18.13.0)(vite@4.2.1): resolution: {integrity: sha512-Vw0FdCuM3VLR4hTFHh0yMEzfwI7NyFvPIMFwvE+Q0t4qtoHIfYOP/JXs7nTnHuQk87FSjlhGeIJ1fLBcktgPgA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -13466,7 +14459,7 @@ packages: dependencies: '@types/chai': 4.3.4 '@types/chai-subset': 1.3.3 - '@types/node': 18.13.0 + '@types/node': 17.0.12 '@vitest/expect': 0.29.4 '@vitest/runner': 0.29.4 '@vitest/spy': 0.29.4 @@ -13488,7 +14481,7 @@ packages: tinypool: 0.4.0 tinyspy: 1.1.1 vite: 4.2.1(@types/node@18.13.0)(less@4.1.3)(sass@1.59.3) - vite-node: 0.29.4(@types/node@18.13.0)(sass@1.59.3) + vite-node: 0.29.4(@types/node@17.0.12)(sass@1.59.3) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -13767,6 +14760,11 @@ packages: optional: true dev: true + /xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + dev: true + /xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -13788,7 +14786,6 @@ packages: /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -13796,7 +14793,6 @@ packages: /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - dev: false /yaml@2.2.1: resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==}