diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..b512c09d476623ff4bf8d0d63c29b784925dbdf8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.gitignore b/.gitignore index ee5c9d8336b76b3bb5658d0888bdec86eb7b0990..81298264fd1a8b12bb82d005229e5cfd9b2e9e61 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ testem.log # System Files .DS_Store Thumbs.db + +# Certs +certs/*.pem \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e35789def0cfe075414a549041e95c960b8a2c40..80257ff41b3eeba1268ca17ddecfdbccc4717d93 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ image: node:16-alpine stages: - setup - test + - build install-dependencies: stage: setup @@ -33,14 +34,41 @@ install-dependencies: paths: - node_modules/.cache/nx -build: +test: stage: test extends: .distributed script: - - yarn nx affected --base=HEAD~1 --target=build --parallel --max-parallel=3 + - yarn nx affected --base=HEAD~1 --target=test --parallel --max-parallel=2 -test: +build: stage: test - extends: .distributed + only: + - main + needs: + - install-dependencies + artifacts: + paths: + - node_modules/.cache/nx + - dist/apps/web-graphpolaris script: - - yarn nx affected --base=HEAD~1 --target=test --parallel --max-parallel=2 + # - yarn nx affected --base=HEAD~1 --target=build --parallel --max-parallel=3 + # only build web-graphpolaris + - yarn nx build web-graphpolaris --prod + +build-docker: + image: docker:stable + stage: dockerize + tags: + - docker + only: + - develop + - debug + script: + - docker build --progress plain -t $CI_PROJECT_NAME-webserver-service:latest . + # after_script: + # - docker login datastropheregistry.azurecr.io -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD + # - if [[ ! -z $CI_COMMIT_BRANCH+x ]]; then DOCKER_TAG=$CI_COMMIT_BRANCH; else DOCKER_TAG=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME; fi + # - docker tag $CI_PROJECT_NAME-webserver-service datastropheregistry.azurecr.io/$CI_PROJECT_NAME-webserver-service:$DOCKER_TAG + # - docker push datastropheregistry.azurecr.io/$CI_PROJECT_NAME-webserver-service:$DOCKER_TAG + dependencies: + - build diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..f73ba1c65a316af0257f5848efd24817d1086185 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "configurations": [ + { + "name": "Debug Jest Tests", + "type": "node", + "request": "launch", + "runtimeArgs": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/jest/bin/jest.js", + "--runInBand" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "port": 9229 + }, + { + // Requires the extension Debugger for Chrome: https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome + "type": "chrome", + "request": "launch", + "name": "Storybook Debug", + "breakOnLoad": true, + "url": "http://localhost:4400/?path=/story/", + "sourceMaps": true, + "webRoot": "${workspaceFolder}", + "sourceMapPathOverrides": { + "webpack:///*": "${webRoot}/*", + "webpack:///./*": "${webRoot}/*", + "webpack:///src/*": "${webRoot}/*", + "webpack:///./~/*": "${webRoot}/node_modules/*" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 033495ac56c3906b2b695131f4126cac283b90be..0c27507c7c9016a219a2527fa8d4b61b0302bfad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,18 @@ "vis-nl", "vis-paoh", "vis-schema", - "storybook" + "storybook", + "store", + "libs" + ], + "jest.jestCommandLine": "nx affected:test", + + "jsonColorToken.languages": [ + "json", + "jsonc", + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" ] } diff --git a/Dockerfile b/Dockerfile index 5ae50ee3abb63bbc32251cc96e7094c28d330d66..39c70241463b4eebd6f935ad20eb0dbee962c535 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,12 @@ -# # Prepare nginx -# FROM nginx:1.19-alpine -# WORKDIR /app +# Prepare nginx +FROM nginx:1.19-alpine +WORKDIR /app -# # ! This copy source needs to be changed to reflect the actual app name -# COPY ./dist/apps/frontend /usr/share/nginx/html +COPY ./dist/apps/web-graphpolaris /usr/share/nginx/html -# RUN rm /etc/nginx/conf.d/default.conf -# COPY nginx/nginx.conf /etc/nginx/conf.d +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx/nginx.conf /etc/nginx/conf.d -# # Fire up nginx -# EXPOSE 80 -# CMD ["nginx", "-g", "daemon off;"] +# Fire up nginx +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 3d5c769f3db866fec01e970113a87133e7a07ba7..9f79b01a9bf341c469a185d07e24283bc938c6cc 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,29 @@ +# GraphPolaris Frontend +## Preparing to Run Locally -# Graphpolaris +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. -This project was generated using [Nx](https://nx.dev). +### MacOS / Linux -<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="450"></p> +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.datastrophe.science.uu.nl`, this will route traffic from `local.datastrophe.science.uu.nl` 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.datastrophe.science.uu.nl` create certificates for local SSL -🔎 **Smart, Fast and Extensible Build System** +### Windows -## Adding capabilities to your workspace +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.datastrophe.science.uu.nl`, this will route traffic from `local.datastrophe.science.uu.nl` 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.datastrophe.science.uu.nl` create certificates for local SSL -Nx supports many plugins which add capabilities for developing different types of applications and different tools. +> No idea if the Windows steps work -These capabilities include generating applications, libraries, etc as well as the devtools to test, and build projects as well. +## Running Locally -Below are our core plugins: - -- [React](https://reactjs.org) - - `npm install --save-dev @nrwl/react` -- Web (no framework frontends) - - `npm install --save-dev @nrwl/web` -- [Angular](https://angular.io) - - `npm install --save-dev @nrwl/angular` -- [Nest](https://nestjs.com) - - `npm install --save-dev @nrwl/nest` -- [Express](https://expressjs.com) - - `npm install --save-dev @nrwl/express` -- [Node](https://nodejs.org) - - `npm install --save-dev @nrwl/node` - -There are also many [community plugins](https://nx.dev/community) you could add. - -## Generate an application - -Run `nx g @nrwl/react:app my-app` to generate an application. - -> You can use any of the plugins above to generate applications as well. - -When using Nx, you can create multiple applications and libraries in the same workspace. - -## Generate a library - -Run `nx g @nrwl/react:lib my-lib` to generate a library. - -> You can also use any of the plugins above to generate libraries as well. - -Libraries are shareable across libraries and applications. They can be imported from `@graphpolaris/mylib`. - -## Development server - -Run `nx serve my-app` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. - -## Code scaffolding - -Run `nx g @nrwl/react:component my-component --project=my-app` to generate a new component. - -## Build - -Run `nx build my-app` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. - -## Running unit tests - -Run `nx test my-app` to execute the unit tests via [Jest](https://jestjs.io). - -Run `nx affected:test` to execute the unit tests affected by a change. - -## Running end-to-end tests - -Run `ng e2e my-app` to execute the end-to-end tests via [Cypress](https://www.cypress.io). - -Run `nx affected:e2e` to execute the end-to-end tests affected by a change. - -## Understand your workspace - -Run `nx graph` to see a diagram of the dependencies of your projects. - -## Further help - -Visit the [Nx Documentation](https://nx.dev) to learn more. - - - -## ☠Nx Cloud - -### Distributed Computation Caching & Distributed Task Execution - -<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-cloud-card.png"></p> - -Nx Cloud pairs with Nx in order to enable you to build and test code more rapidly, by up to 10 times. Even teams that are new to Nx can connect to Nx Cloud and start saving time instantly. - -Teams using Nx gain the advantage of building full-stack applications with their preferred framework alongside Nx’s advanced code generation and project dependency graph, plus a unified experience for both frontend and backend developers. - -Visit [Nx Cloud](https://nx.app/) to learn more. +To run the application using SSL (with these keys) simply run `nx run web-graphpolaris:dev`, or `yarn nx run web-graphpolaris:dev` if `nx` is not installed globally. This should open a window to `https://local.datastrophe.science.uu.nl:4200/` automatically. diff --git a/apps/web-graphpolaris/project.json b/apps/web-graphpolaris/project.json index 8f4cc9fa653651d96862e45b3466964b1ecdcc72..dc9c691acc30a77b498ee4ee9c7abf79928cff15 100644 --- a/apps/web-graphpolaris/project.json +++ b/apps/web-graphpolaris/project.json @@ -3,6 +3,20 @@ "sourceRoot": "apps/web-graphpolaris/src", "projectType": "application", "targets": { + "dev": { + "executor": "@nrwl/web:dev-server", + "options": { + "buildTarget": "web-graphpolaris:build", + "host": "local.datastrophe.science.uu.nl", + "port": 4200, + "watch": true, + "hmr": true, + "ssl": true, + "sslCert": "./certs/local-cert.pem", + "sslKey": "./certs/local-key.pem", + "open": true + } + }, "build": { "executor": "@nrwl/web:webpack", "outputs": ["{options.outputPath}"], @@ -27,8 +41,8 @@ "production": { "fileReplacements": [ { - "replace": "apps/web-graphpolaris/src/environments/environment.ts", - "with": "apps/web-graphpolaris/src/environments/environment.prod.ts" + "replace": "apps/graphpolaris/src/environments/environment.ts", + "with": "apps/graphpolaris/src/environments/environment.prod.ts" } ], "optimization": true, @@ -57,12 +71,12 @@ "executor": "@nrwl/linter:eslint", "outputs": ["{options.outputFile}"], "options": { - "lintFilePatterns": ["apps/web-graphpolaris/**/*.{ts,tsx,js,jsx}"] + "lintFilePatterns": ["apps/graphpolaris/**/*.{ts,tsx,js,jsx}"] } }, "test": { "executor": "@nrwl/jest:jest", - "outputs": ["coverage/apps/web-graphpolaris"], + "outputs": ["coverage/apps/graphpolaris"], "options": { "jestConfig": "apps/web-graphpolaris/jest.config.js", "passWithNoTests": true diff --git a/apps/web-graphpolaris/src/app/app.stories.tsx b/apps/web-graphpolaris/src/app/app.stories.tsx index ce5c645ebfa95f6368df7407baa89da871d48bd2..eda4ff21d10ba81251ff1a279231d25a29371928 100644 --- a/apps/web-graphpolaris/src/app/app.stories.tsx +++ b/apps/web-graphpolaris/src/app/app.stories.tsx @@ -1,30 +1,24 @@ -import { configureStore } from '@reduxjs/toolkit'; -import { Meta, Story } from '@storybook/react'; import React from 'react'; -import { Provider } from 'react-redux'; -import graphQueryResultSlice from '../../../../libs/shared/data-access/store/src/lib/graphQueryResultSlice'; +import { Meta, Story } from '@storybook/react'; import { App } from './app'; +import { Provider } from 'react-redux'; +import { store } from '@graphpolaris/shared/data-access/store'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; export default { component: App, title: 'App', decorators: [ + // using the real store here (story) => ( - <div style={{ padding: '3rem' }}> - <Provider store={Mockstore}>{story()}</Provider> - </div> + <Provider store={store}> + <GraphPolarisThemeProvider>{story()}</GraphPolarisThemeProvider> + </Provider> ), ], } as Meta; const Template: Story = (args) => <App {...args} />; -// A super-simple mock of a redux store -const Mockstore = configureStore({ - reducer: { - graphQueryResult: graphQueryResultSlice, - }, -}); - export const Primary = Template.bind({}); Primary.args = {}; diff --git a/apps/web-graphpolaris/src/app/app.tsx b/apps/web-graphpolaris/src/app/app.tsx index c33f6c28dfe1585435a0388de4f5f5e9c1a1dac4..6be79b2f6674c5f19dc0e18e83851ed4f48d9bb8 100644 --- a/apps/web-graphpolaris/src/app/app.tsx +++ b/apps/web-graphpolaris/src/app/app.tsx @@ -1,20 +1,52 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { - assignNewGraphQueryResult, - changeDataPointColors, - useAppDispatch, -} from '@graphpolaris/shared/data-access/store'; +import { useEffect, useState } from 'react'; import GridLayout from 'react-grid-layout'; +import LoginScreen from '../components/login/loginScreen'; import Panel from '../components/panels/panel'; import { RawJSONVis } from '../components/visualisations/rawjsonvis/rawjsonvis'; import SemanticSubstrates from '../components/visualisations/semanticsubstrates/semanticsubstrates'; -import { OurThemeProvider } from '@graphpolaris/shared/data-access/theme'; +import Schema from '../components/schema/schema'; +import { GetUserInfo } from '@graphpolaris/shared/data-access/api'; +import QueryBuilder from '../components/querybuilder/querybuilder'; +import { + assignNewGraphQueryResult, + useAppDispatch, +} from '@graphpolaris/shared/data-access/store'; +import { AuthorizationHandler } from '@graphpolaris/shared/data-access/authorization'; + +function useIsAuthorized() { + const [userAuthorized, setUserAuthorized] = useState(false); + + const authCallback = async () => { + setUserAuthorized(true); + + // Print the user that is currently logged in + const user = await GetUserInfo(); + console.log(user); + }; + + AuthorizationHandler.instance().setCallback(authCallback); + + // Attempt to Authorize the user + const authorize = async () => { + const authorized = await AuthorizationHandler.instance().Authorize(); + setUserAuthorized(authorized); + }; + + useEffect(() => { + authorize(); + }, []); + + return userAuthorized; +} export function App() { const dispatch = useAppDispatch(); + const userIsAuthorized = useIsAuthorized(); return ( - <OurThemeProvider> + <> + {!userIsAuthorized && <LoginScreen />} <GridLayout className="layout" cols={10} @@ -31,7 +63,9 @@ export function App() { key="query-panel" data-grid={{ x: 3, y: 20, w: 5, h: 10, maxH: 20, isDraggable: false }} > - <Panel content="Query Panel" color="blue"></Panel> + <Panel content="Query Panel" color="blue"> + <QueryBuilder /> + </Panel> </div> <div key="visualisation-panel" @@ -76,7 +110,7 @@ export function App() { <Panel content="History Channel" color="purple"></Panel> </div> </GridLayout> - </OurThemeProvider> + </> ); } diff --git a/apps/web-graphpolaris/src/assets/login-screen/github.png b/apps/web-graphpolaris/src/assets/login-screen/github.png new file mode 100644 index 0000000000000000000000000000000000000000..55f90676824d4901f6fe7a50ed11f6d4af3d6fef Binary files /dev/null and b/apps/web-graphpolaris/src/assets/login-screen/github.png differ diff --git a/apps/web-graphpolaris/src/assets/login-screen/google.png b/apps/web-graphpolaris/src/assets/login-screen/google.png new file mode 100644 index 0000000000000000000000000000000000000000..f27bb2433042aea5fc34e19fcf90944430ec331b Binary files /dev/null and b/apps/web-graphpolaris/src/assets/login-screen/google.png differ diff --git a/apps/web-graphpolaris/src/components/login/loginScreen.tsx b/apps/web-graphpolaris/src/components/login/loginScreen.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93339edaadd13653822233ecf9aac3468f9f4435 --- /dev/null +++ b/apps/web-graphpolaris/src/components/login/loginScreen.tsx @@ -0,0 +1,134 @@ +import { AuthorizationHandler } from '@graphpolaris/shared/data-access/authorization'; +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-family: 'Arial'; + position: absolute; + left: 0; + top: 0; + // Cover the screen + width: 100vw; + height: 100vh; + + display: flex; + justify-content: center; + align-items: center; +`; + +const Background = styled.div` + position: absolute; + + width: 100%; + height: 100%; + + z-index: 1; + + // Blur + background: rgba( + 0, + 0, + 0, + 0.4 + ); // Make sure this color has an opacity of less than 1 + backdrop-filter: blur(8px); // This be the blur +`; + +const Content = styled.div` + background-color: white; + box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); + padding: 2em; + z-index: 2; + border-radius: 8px; + + display: flex; + flex-direction: column; + gap: 1em; + align-items: center; + justify-content: center; + + // Give children 0 padding and margin + * { + display: flex; + margin: 0; + padding: 0; + + // Same width flexbox items + flex: 1 1 0px; + + max-height: 3em; + + &:hover { + cursor: pointer; + } + } +`; + +const LoginScreen = () => { + const openSignInWindow = (url: string) => { + // remove any existing event listeners + window.removeEventListener('auth_message', receiveMessage); + + // window features + const strWindowFeatures = + 'toolbar=no, menubar=no, width=600, height=700, top=100, left=100'; + + window.open(url, 'Google Oauth', strWindowFeatures); + + // add the listener for receiving a message from the popup + window.addEventListener( + 'auth_message', + (event) => receiveMessage(event), + false + ); + }; + + const receiveMessage = (event: any) => { + // Do we trust the sender of this message? (might be + // different from what we originally opened) + if (window.location.hostname !== event.detail.origin) { + return; + } + console.log(window.location.hostname, event.detail.origin); + + // Set access token + AuthorizationHandler.instance().SetAccessToken(event.detail.token); + }; + + return ( + <Wrapper> + <Background></Background> + <Content> + <h1>Sign In</h1> + <img + onClick={() => + openSignInWindow( + 'https://datastrophe.science.uu.nl/user/sign-in?provider=1' + ) + } + src="assets/login-screen/google.png" + alt="sign up with google" + /> + <img + onClick={() => + openSignInWindow( + 'https://datastrophe.science.uu.nl/user/sign-in?provider=2' + ) + } + src="assets/login-screen/github.png" + alt="sign up with github" + /> + <p + onClick={() => + openSignInWindow( + 'https://datastrophe.science.uu.nl/user/create_free/' + ) + } + > + Developer + </p> + </Content> + </Wrapper> + ); +}; + +export default LoginScreen; diff --git a/apps/web-graphpolaris/src/components/login/popup.tsx b/apps/web-graphpolaris/src/components/login/popup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3218e6badee1d6797e57bd5153f0937464de4d8b --- /dev/null +++ b/apps/web-graphpolaris/src/components/login/popup.tsx @@ -0,0 +1,21 @@ +const LoginPopupComponent = () => { + if (window.opener) { + // Get the access token from the query params + const urlParams = new URLSearchParams(window.location.search); + const accessToken = urlParams.get('access_token'); + + // Send the access token to the parent window + let c_event = new CustomEvent('auth_message', { + detail: { token: accessToken, origin: window.location.hostname }, + }); + window.opener.dispatchEvent(c_event); + // window.opener.postMessage(accessToken, '*'); + + // Close this window + window.close(); + } + + return <h1>Loading...</h1>; +}; + +export default LoginPopupComponent; diff --git a/apps/web-graphpolaris/src/components/panels/panel.stories.tsx b/apps/web-graphpolaris/src/components/panels/panel.stories.tsx index a172d8f1f4569d624ea280fbb283fbbadb5eea2e..4f570d0fdf85f20d4759e0207cb8045f67f1daa4 100644 --- a/apps/web-graphpolaris/src/components/panels/panel.stories.tsx +++ b/apps/web-graphpolaris/src/components/panels/panel.stories.tsx @@ -1,6 +1,10 @@ import React from 'react'; import Panel from './panel'; import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { configureStore } from '@reduxjs/toolkit'; +import { colorPaletteConfigSlice } from '@graphpolaris/shared/data-access/store'; +import { Provider } from 'react-redux'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; export default { /* 👇 The title prop is optional. @@ -9,9 +13,22 @@ export default { */ title: 'Panel', component: Panel, - decorators: [(story) => <div style={{ padding: '3rem' }}>{story()}</div>], + decorators: [ + (story) => ( + <Provider store={Mockstore}> + <GraphPolarisThemeProvider>{story()}</GraphPolarisThemeProvider> + </Provider> + ), + ], } as ComponentMeta<typeof Panel>; +// A super-simple mock of a redux store +const Mockstore = configureStore({ + reducer: { + colorPaletteConfig: colorPaletteConfigSlice.reducer, + }, +}); + const Template: ComponentStory<typeof Panel> = (args) => <Panel {...args} />; export const Primary = Template.bind({}); diff --git a/apps/web-graphpolaris/src/components/panels/panel.tsx b/apps/web-graphpolaris/src/components/panels/panel.tsx index 5189e4d410f05b865992f6a272fd371c550f91e3..f5214c7a160a96df7015cca2eeb1c09f0b6463fc 100644 --- a/apps/web-graphpolaris/src/components/panels/panel.tsx +++ b/apps/web-graphpolaris/src/components/panels/panel.tsx @@ -9,7 +9,10 @@ interface Props { const Wrapper = styled.div<{ color: string }>` background-color: ${(props) => props.color}; - font: 'Arial'; + font-family: 'Arial'; + + // Light shadow + box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); height: 100%; width: 100%; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowLines/connection.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowLines/connection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..618f67b6bc755f3e1766712d77f3e72dd0981291 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowLines/connection.tsx @@ -0,0 +1,73 @@ +import { handles } from '@graphpolaris/querybuilder/usecases'; +import React from 'react'; +import { EdgeProps, getSmoothStepPath, Position } from 'react-flow-renderer'; + +/** + * A custom query element edge line component. + * @param {EdgeProps} param0 The coordinates for the start and end point, the id and the style. + */ +export default function ConnectionLine({ + id, + sourceX, + sourceY, + targetX, + targetY, + style, + sourceHandleId, + targetHandleId, +}: EdgeProps) { + //Centering the line + sourceY -= 3; + targetY -= 3; + + // Correct line positions with hardcoded numbers, because react flow lacks this functionality + // if (sourceHandleId == ) sourceX += 2; + + // if (targetHandleId == Handles.ToAttributeHandle) targetX += 2; + + let spos: Position = Position.Bottom; + if (sourceHandleId == handles.relation.fromEntity) { + spos = Position.Left; + sourceX += 7; + sourceY += 3; + } else if (sourceHandleId == handles.relation.toEntity) { + spos = Position.Right; + sourceX -= 2; + sourceY -= 3; + } else if ( + sourceHandleId !== undefined && + sourceHandleId !== null && + sourceHandleId.includes('functionHandle') + ) { + spos = Position.Top; + sourceX -= 4; + sourceY += 3; + } + + let tpos: Position = Position.Bottom; + if (targetHandleId == handles.relation.fromEntity) { + tpos = Position.Left; + targetX += 7; + targetY += 3; + } else if (targetHandleId == handles.relation.toEntity) { + tpos = Position.Right; + targetX -= 2; + targetY -= 3; + } + + // Create smoothstep line + const path = getSmoothStepPath({ + sourceX: sourceX, + sourceY: sourceY, + sourcePosition: spos, + targetX: targetX, + targetY: targetY, + targetPosition: tpos, + }); + + return ( + <g stroke="#2e2e2e"> + <path id={id} fill="none" strokeWidth={3} style={style} d={path} /> + </g> + ); +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowLines/connectionDrag.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowLines/connectionDrag.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9932aecdd154e7aa1af622afb9ddbf14fb170c77 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowLines/connectionDrag.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ConnectionLineComponentProps } from 'react-flow-renderer'; + +/** + * A custom query element to render the line when connecting flow elements. + * @param {ConnectionLineComponentProps} param0 Source and target coordinates of the edges. + */ +export default function ConnectionDragLine({ + sourceX, + sourceY, + targetX, + targetY, +}: ConnectionLineComponentProps) { + return ( + <g> + <path + fill="none" + stroke="#222" + strokeWidth={2.5} + className="animated" + d={`M${sourceX},${sourceY}L ${targetX},${targetY}`} + /> + <circle + cx={sourceX} + cy={sourceY} + fill="#fff" + r={3} + stroke="#222" + strokeWidth={1.5} + /> + <circle + cx={targetX} + cy={targetY} + fill="#fff" + r={3} + stroke="#222" + strokeWidth={1.5} + /> + </g> + ); +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..9ba5ba22c49c279617faef06e92d4256e49a36fb --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.module.scss @@ -0,0 +1,60 @@ +@use './variables.module.scss'; + +.attribute { + display: flex; + font-family: monospace; + font-weight: bold; + font-size: variables.$fontsize; + border-radius: 2px; +} + +// .handle { +// border: 0px; +// border-radius: 10px; +// left: 12px; +// width: 7px; +// height: 7px; +// margin-bottom: 11px; +// background: rgba(255, 255, 255, 0.6); +// box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3); +// transform-origin: center; +// } + +.contentWrapper { + display: flex; + align-items: center; + + .content { + padding: variables.$ypad 0 variables.$ypad 1ch; + max-width: 15ch; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } +} + +.attributeInput { + float: right; + padding: 0 1ch 0 0; + display: flex; + align-items: center; + + input { + background-color: rgba(100, 100, 100, 0.1); + font-family: monospace; + font-size: variables.$fontsize; + border: 1px solid rgba(100, 100, 100, 0.3); + border-radius: 2px; + height: variables.$height; + outline: none; + transition: border 0.3s; + color: black; + &::placeholder { + color: black; + } + + &:focus { + border: 1px solid rgba(0, 0, 0, 0.3); + } + } +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41b5803cc251e5f345702e9a364ee9eac4c6b113 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/attributepill.tsx @@ -0,0 +1,96 @@ +import { + CheckDatatypeConstraint, + GetAttributeBoolOperators, +} from '@graphpolaris/querybuilder/usecases'; +import { + updateQBAttributeOperator, + updateQBAttributeValue, + useAppDispatch, +} from '@graphpolaris/shared/data-access/store'; +import { useTheme } from '@mui/material'; +import React, { useMemo, useState } from 'react'; +import styles from './attributepill.module.scss'; +import AttributeOperatorSelect from './operatorselect'; + +/** + * Component to render an attribute flow element + * @param {FlowElement<EntityData>)} param0 The data of an entity flow element. + */ +export const AttributeRFPill = React.memo( + ({ id, data }: { id: string; data: any }) => { + const theme = useTheme(); + const dispatch = useAppDispatch(); + const [value, setValue] = useState(data?.value || ''); + + const onChange = (e: any) => { + setValue(e.target.value); + }; + const validateInput = () => { + const newValue = CheckDatatypeConstraint(data.datatype, value); + setValue(newValue); + dispatch(updateQBAttributeValue({ id, value: newValue })); + }; + + // Calculates the size of the input + const getInputWidth = () => { + if (value == '') return 1; + else if (value.length > 10) return 10; + return value.length; + }; + + const boolOperators = useMemo( + () => GetAttributeBoolOperators(data?.datatype), + [data?.datatype] + ); + + // Determine the backgroundcolor based on if the attribute is connected to a entity or relation + let bgcolor; + if (data?.attributeOfA == 'entity') + bgcolor = theme.palette.queryBuilder.entity.lighterbg; + else if (data?.attributeOfA == 'relation') + bgcolor = theme.palette.queryBuilder.relation.lighterbg; + else bgcolor = theme.palette.queryBuilder.attribute.background; + + return ( + <div + className={styles.attribute} + style={{ + background: bgcolor, + color: theme.palette.queryBuilder.text, + }} + > + {/* <Handle + id={Handles.Attribute} + type="source" + position={Position.Bottom} + className={styles.handle} + /> */} + <div className={styles.contentWrapper}> + <span className={styles.content} title={data.name}> + {data.name} + </span> + <AttributeOperatorSelect + selected={data?.operator} + options={boolOperators} + changed={(o) => + dispatch(updateQBAttributeOperator({ id, operator: o.value })) + } + /> + <span className={styles.attributeInput}> + <input + style={{ maxWidth: `${getInputWidth()}ch` }} + type="string" + placeholder={'?'} + value={value} + onChange={onChange} + onBlur={validateInput} + onKeyDown={(e) => e.key == 'Enter' && validateInput()} + ></input> + </span> + </div> + </div> + ); + } +); + +export default AttributeRFPill; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..1c31d2744f029e77c53efbfac2aae8983640770a --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.module.scss @@ -0,0 +1,70 @@ +@use './variables.module.scss'; + +.container { + position: relative; + vertical-align: baseline; + margin: 0 1ch; + font-weight: normal; + font-size: 7px; +} + +.valueContainer { + color: #6a6a6a; + border: 1px solid rgba(0, 0, 0, 0); + border-radius: 2px; + background-color: transparent; + + transition: border-color 0.2s; + + height: variables.$height; + align-items: center; + display: flex; + padding: 0 1px 1px 1px; + + &.highlighted, + &:hover { + border-color: rgba(0, 0, 0, 0.4); + } +} + +.listbox { + font-size: 10px; + box-sizing: border-box; + padding: 5px; + margin: 5px 0 0 0; + list-style: none; + position: absolute; + height: auto; + box-shadow: 0 5px 13px -3px #e0e3e7; + background: white; + border: 1px solid #cdd2d7; + border-radius: 0.75em; + color: #1a2027; + overflow: auto; + z-index: 1; + outline: 0px; + left: -8px; + + &.hidden { + opacity: 0; + visibility: hidden; + transition: opacity 0.4s 0.1s ease, visibility 0.4s 0.1s step-end; + } + + & > li { + padding: 1px 4px; + border-radius: 2px; + + &.selected { + background: #f1f1f1; + } + + &:hover { + background: #e7ebf0; + } + + &[aria-selected='true'] { + background: #e0e3e7; + } + } +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.tsx new file mode 100644 index 0000000000000000000000000000000000000000..470c87618fdbd49fd8d70b3de926e0d3605cd12a --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/operatorselect.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { SelectOption } from '@mui/base'; +import styles from './operatorselect.module.scss'; +import { useRef, useState } from 'react'; + +// const grey = { +// 100: '#E7EBF0', +// 200: '#E0E3E7', +// 300: '#CDD2D7', +// 400: '#B2BAC2', +// 500: '#A0AAB4', +// 600: '#6F7E8C', +// 700: '#3E5060', +// 800: '#2D3843', +// 900: '#1A2027', +// }; + +interface Props { + options: SelectOption<string>[]; + selected: string; + changed?: (newSelected: SelectOption<string>) => void; +} + +function AttributeOperatorSelect({ + options, + selected, + changed = () => {}, +}: Props) { + const listboxRef = useRef<HTMLUListElement>(null); + const [listboxVisible, setListboxVisible] = useState(false); + const [currSelected, setCurrSelected] = useState( + options.find((o) => o.value == selected)?.label || options[0].label + ); + + React.useEffect(() => { + if (listboxVisible) { + listboxRef.current?.focus(); + } + }, [listboxVisible]); + + const changeSelected = (option: SelectOption<string>) => { + if (option.label != currSelected) { + setCurrSelected(option.label); + changed(option); + } + }; + + return ( + <div + className={styles.container} + // onMouseOver={() => setListboxVisible(true)} + onMouseOut={() => setListboxVisible(false)} + onClick={() => setListboxVisible(true)} + onFocus={() => setListboxVisible(true)} + onBlur={() => setListboxVisible(false)} + > + <div + className={ + styles.valueContainer + ' ' + (listboxVisible && styles.highlighted) + } + > + <span>{currSelected}</span> + </div> + {options.length > 1 && ( + <ul + className={styles.listbox + ' ' + (!listboxVisible && styles.hidden)} + ref={listboxRef} + onMouseOver={() => setListboxVisible(true)} + > + {options.map((option) => ( + <li + className={option.label == currSelected ? styles.selected : ''} + key={option.value} + onClick={() => changeSelected(option)} + > + {option.label} + </li> + ))} + </ul> + )} + </div> + ); +} + +export default AttributeOperatorSelect; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/variables.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/variables.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..08bc31bb67c70a043d0114880fceada930fe14b1 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/attributepill/variables.module.scss @@ -0,0 +1,3 @@ +$height: 5px; +$fontsize: 6px; +$ypad: 2px; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..755d2b41d564abe3f9e4eb41f56865ef7d2432f4 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss @@ -0,0 +1,49 @@ +.entity { + display: flex; + font-family: monospace; + font-weight: bold; + font-size: 10px; + padding: 4px 2ch; + border-radius: 3px; +} + +.highlighted { + box-shadow: black 0 0 2px; +} + +.handleLeft { + border: 0px; + border-radius: 0px; + left: 12px; + width: 7px; + height: 7px; + margin-bottom: 11px; + background: rgba(255, 255, 255, 0.6); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3); + transform-origin: center; +} + +// .handleBottom { +// border: 0px; +// border-radius: 0px; +// width: 7px; +// height: 7px; +// left: 27.5px; +// margin-bottom: 11px; +// background: rgba(255, 255, 255, 0.6); +// box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3); +// transform: rotate(-45deg); +// transform-origin: center; +// } + +.contentWrapper { + margin-left: 3ch; + + span { + max-width: 20ch; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; + } +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1e5f6f7ba5d6a97bd5b47daea744a3a033cd09e7 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx @@ -0,0 +1,63 @@ +import { handles } from '@graphpolaris/querybuilder/usecases'; +import { useTheme } from '@mui/material'; +import React, { useEffect } from 'react'; +import { FlowElement, Handle, Position } from 'react-flow-renderer'; +import styles from './entitypill.module.scss'; +import cn from 'classnames'; + +// export const Handless = { +// entity: { +// attributes: 'attributesHandle', +// relations: 'relationsHandle', +// }, +// }; + +// /** Links need handles to what they are connected to (and which side) */ +// export enum Handles { +// RelationLeft = 'leftEntityHandle', //target +// RelationRight = 'rightEntityHandle', //target +// ToAttributeHandle = 'attributesHandle', //target +// ToRelation = 'relationsHandle', //source +// Attribute = 'AttributeHandle', //source +// ReceiveFunction = 'receiveFunctionHandle', //target +// FunctionBase = 'functionHandle_', // + name from FunctionTypes args //source +// } + +/** + * Component to render an entity flow element + * @param {FlowElement<EntityData>)} param0 The data of an entity flow element. + */ +export const EntityRFPill = React.memo(({ data }: { data: any }) => { + const theme = useTheme(); + + return ( + <div + className={cn(styles.entity, { + [styles.highlighted]: data.suggestedForConnection, + })} + style={{ + background: theme.palette.queryBuilder.entity.background, + color: theme.palette.queryBuilder.text, + }} + > + <Handle + id={handles.entity.relation} + type="source" + position={Position.Bottom} + className={styles.handleLeft} + style={data?.isConnected ? { backgroundColor: '#2e2e2e' } : {}} + /> + {/* <Handle + id={Handles.ToAttributeHandle} + type="target" + position={Position.Bottom} + className={styles.handleBottom} + /> */} + <div className={styles.contentWrapper}> + <span title={data.name}>{data.name}</span> + </div> + </div> + ); +}); + +export default EntityRFPill; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..aff84b2e5d8b9caba6dd9fdd34a99b54614946d4 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss @@ -0,0 +1,110 @@ +.relation { + display: flex; + text-align: center; + font-family: monospace; + font-weight: bold; + font-size: 10px; + background-color: transparent; +} + +.highlighted { + box-shadow: black 0 0 2px; +} + +.contentWrapper { + display: flex; + align-items: center; + + .handleLeft { + position: relative; + z-index: 3; + + top: 25%; + border: 0px; + border-radius: 0px; + + background: transparent; + transform-origin: center; + + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: rgba(255, 255, 255, 0.7) 6px solid; + + &::after { + content: ''; + display: block; + position: absolute; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + border-right: rgba(0, 0, 0, 0.1) 8px solid; + top: -7px; + right: -7px; + } + } + .highlighted { + z-index: -1; + box-shadow: 0 0 2px 1px gray; + } + + .content { + margin: 0 2ch; + padding: 3px 0; + max-width: 20ch; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .handleRight { + position: relative; + top: 25%; + border: 0px; + border-radius: 0px; + + background: transparent; + transform-origin: center; + + width: 0; + height: 0; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: rgba(255, 255, 255, 0.7) 6px solid; + + &::after { + content: ''; + display: block; + position: absolute; + width: 0; + height: 0; + border-top: 7px solid transparent; + border-bottom: 7px solid transparent; + border-left: rgba(0, 0, 0, 0.1) 8px solid; + top: -7px; + left: -7px; + } + } +} + +$height: 10px; +.arrowLeft { + z-index: 2; + width: 0; + height: 0; + border-top: $height solid transparent; + border-bottom: $height solid transparent; + + border-right: $height solid; +} + +.arrowRight { + width: 0; + height: 0; + border-top: $height solid transparent; + border-bottom: $height solid transparent; + + border-left: $height solid; +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f1d4d8592cf367e719ac3db963948b5c06145ef1 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx @@ -0,0 +1,96 @@ +import { handles } from '@graphpolaris/querybuilder/usecases'; +import { useTheme } from '@mui/material'; +import { Handle, Position } from 'react-flow-renderer'; +import cn from 'classnames'; + +import styles from './relationpill.module.scss'; + +/** + * Component to render a relation flow element + * @param { FlowElement<RelationData>} param0 The data of a relation flow element. + */ +export default function RelationRFPill({ data }: { data: any }) { + const theme = useTheme(); + + // const minRef = useRef<HTMLInputElement>(null); + // const maxRef = useRef<HTMLInputElement>(null); + + // const [readOnlyMin, setReadOnlyMin] = useState(true); + // const [readOnlyMax, setReadOnlyMax] = useState(true); + + // const onDepthChanged = (depth: string) => { + // // Don't allow depth above 99 + // const limit = 99; + // if (data != undefined) { + // data.depth.min = data.depth.min >= limit ? limit : data.depth.min; + // data.depth.max = data.depth.max >= limit ? limit : data.depth.max; + + // // Check for for valid depth: min <= max + // if (depth == 'min') { + // if (data.depth.min > data.depth.max) data.depth.max = data.depth.min; + // setReadOnlyMin(true); + // } else if (depth == 'max') { + // if (data.depth.max < data.depth.min) data.depth.min = data.depth.max; + // setReadOnlyMax(true); + // } + + // // Set to the correct width + // if (maxRef.current) + // maxRef.current.style.maxWidth = calcWidth(data.depth.max); + // if (minRef.current) + // minRef.current.style.maxWidth = calcWidth(data.depth.min); + // } + // }; + + // const calcWidth = (data: number) => { + // return data.toString().length + 0.5 + 'ch'; + // }; + + return ( + <div className={styles.relation}> + <div + className={styles.arrowLeft} + style={{ + borderRightColor: theme.palette.queryBuilder.relation.background, + }} + /> + <div + className={cn(styles.contentWrapper, { + [styles.highlighted]: data.suggestedForConnection, + })} + style={{ + color: theme.palette.queryBuilder.text, + background: theme.palette.queryBuilder.relation.background, + }} + > + <Handle + id={handles.relation.fromEntity} + type="target" + position={Position.Left} + className={styles.handleLeft} + style={ + data?.isFromEntityConnected ? { borderRightColor: '#2e2e2e' } : {} // TODO: this should be color from theme + } + /> + <span className={styles.content} title={data.name}> + {data.name} + </span> + <Handle + id={handles.relation.toEntity} + type="source" + position={Position.Right} + className={styles.handleRight} + style={ + data?.isToEntityConnected ? { borderLeftColor: '#2e2e2e' } : {} + } + /> + </div> + <div + className={styles.arrowRight} + style={{ + borderLeftColor: theme.palette.queryBuilder.relation.background, + }} + /> + </div> + ); +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.module.scss b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..db83f46964a46bd398e50188c6d83bd06769b52c --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.module.scss @@ -0,0 +1,8 @@ +.reactflow { + width: 100%; + height: 500px; + + // :global(.react-flow__edges) { + // z-index: 4 !important; + // } +} diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b406d60b5879ace0bfe90fa43b1a151fd83198f8 --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.stories.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { + colorPaletteConfigSlice, + querybuilderSlice, + setQuerybuilderNodes, +} from '@graphpolaris/shared/data-access/store'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; +import { configureStore } from '@reduxjs/toolkit'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Provider } from 'react-redux'; +import QueryBuilder from './querybuilder'; +import { MultiGraph } from 'graphology'; +import { addPill, handles } from '@graphpolaris/querybuilder/usecases'; + +export default { + component: QueryBuilder, +} as ComponentMeta<typeof QueryBuilder>; + +const Template: ComponentStory<typeof QueryBuilder> = (args) => ( + <QueryBuilder {...args} /> +); + +// Mock palette store +const mockStore = configureStore({ + reducer: { + colorPaletteConfig: colorPaletteConfigSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); +const graph = new MultiGraph(); +addPill('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }, graph); +// graph.addNode('0', { type: 'entity', x: 100, y: 100, name: 'Entity Pill' }); +addPill( + '1', + { type: 'relation', x: 140, y: 140, name: 'Relation Pill' }, + graph +); +addPill( + '2', + { + type: 'attribute', + x: 170, + y: 160, + name: 'Attr string', + datatype: 'string', + operator: 'EQ', + value: 'mark', + }, + graph +); +addPill( + '3', + { + type: 'attribute', + x: 170, + y: 170, + name: 'Attr number', + datatype: 'float', + operator: 'EQ', + }, + graph +); +addPill( + '4', + { + type: 'attribute', + x: 130, + y: 120, + name: 'Attr bool', + datatype: 'bool', + operator: 'EQ', + value: 'true', + }, + graph +); +console.log(graph.getNodeAttributes('2')); +graph.addEdge('2', '1', { type: 'attribute_connection' }); +graph.addEdge('3', '1', { type: 'attribute_connection' }); +graph.addEdge('4', '0', { type: 'attribute_connection' }); +graph.addEdge('0', '1', { + type: 'entity_relation', + targetHandle: handles.relation.fromEntity, +}); +// graph.addEdge('1', '0', { +// type: 'entity_relation', +// sourceHandle: handles.relation.entity, +// }); +mockStore.dispatch(setQuerybuilderNodes(graph.export())); + +export const Simple = Template.bind({}); +Simple.decorators = [ + (story) => ( + <Provider store={mockStore}> + <GraphPolarisThemeProvider>{story()}</GraphPolarisThemeProvider> + </Provider> + ), +]; diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3870f76de645a2d47757c5e0a40e2c8bea89503b --- /dev/null +++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx @@ -0,0 +1,104 @@ +import { + createReactFlowElements, + dragPill, + dragPillStarted, + dragPillStopped, +} from '@graphpolaris/querybuilder/usecases'; +import { + setQuerybuilderNodes, + useAppDispatch, + useQuerybuilderNodes, +} from '@graphpolaris/shared/data-access/store'; +import { useMemo, useRef } from 'react'; +import ReactFlow, { + ReactFlowProvider, + Background, + Node, + isNode, +} from 'react-flow-renderer'; +import styles from './querybuilder.module.scss'; +import ConnectionLine from './customFlowLines/connection'; +import ConnectionDragLine from './customFlowLines/connectionDrag'; +import AttributeRFPill from './customFlowPills/attributepill/attributepill'; +import EntityRFPill from './customFlowPills/entitypill/entitypill'; +import RelationRFPill from './customFlowPills/relationpill/relationpill'; + +const nodeTypes = { + entity: EntityRFPill, + relation: RelationRFPill, + attribute: AttributeRFPill, +}; +const edgeTypes = { + connection: ConnectionLine, +}; + +const onLoad = (reactFlowInstance: any) => { + setTimeout(() => reactFlowInstance.fitView(), 0); +}; + +const QueryBuilder = (props: {}) => { + const nodes = useQuerybuilderNodes(); + const dispatch = useAppDispatch(); + const isDraggingPill = useRef(false); + + const elements = useMemo(() => createReactFlowElements(nodes), [nodes]); + + const onNodeDrag = ( + event: React.MouseEvent<Element, MouseEvent>, + node: Node<any> + ) => { + // Get the node in the elements list to get the previous location + const pNode = elements.find((e) => e.id == node.id); + if (!(pNode && isNode(pNode))) return; + // This is then used to calculate the delta position + const dx = node.position.x - pNode.position.x; + const dy = node.position.y - pNode.position.y; + + // Check if we started dragging, if so, call the drag started usecase + if (!isDraggingPill.current) { + dragPillStarted(node.id, nodes); + isDraggingPill.current = true; + } + + // Call the drag usecase + dragPill(node.id, nodes, dx, dy, node.position); + + // Dispatch the new graphology object, so reactflow will get rerendered + dispatch(setQuerybuilderNodes(nodes.export())); + }; + const onNodeDragStop = ( + event: React.MouseEvent<Element, MouseEvent>, + node: Node<any> + ) => { + isDraggingPill.current = false; + + // Call the drag pill stopped usecase + dragPillStopped(node.id, nodes); + + // Dispatch the new graphology object, so reactflow will get rerendered + dispatch(setQuerybuilderNodes(nodes.export())); + }; + + return ( + <div> + <ReactFlowProvider> + <ReactFlow + elements={elements} + snapGrid={[10, 10]} + // snapToGrid + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + connectionLineComponent={ConnectionDragLine} + onLoad={onLoad} + onNodeDrag={onNodeDrag} + onNodeDragStop={onNodeDragStop} + className={styles.reactflow} + > + <Background gap={10} size={0.7} /> + </ReactFlow> + </ReactFlowProvider> + </div> + ); +}; + +export default QueryBuilder; diff --git a/apps/web-graphpolaris/src/components/schema/schema.stories.tsx b/apps/web-graphpolaris/src/components/schema/schema.stories.tsx index 4eb22a1ea0aebd2992b35da61d977480d5b3f782..873afdf6b36983cd9d6a493235df20b3ed654452 100644 --- a/apps/web-graphpolaris/src/components/schema/schema.stories.tsx +++ b/apps/web-graphpolaris/src/components/schema/schema.stories.tsx @@ -1,10 +1,11 @@ +import { SchemaUtils } from '@graphpolaris/schema-utils'; import { - handleSchemaLayout, - parseSchemaFromBackend -} from '@graphpolaris/schema/schema-usecases'; -import { - store + colorPaletteConfigSlice, + schemaSlice, + setSchema, } from '@graphpolaris/shared/data-access/store'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; +import { configureStore } from '@reduxjs/toolkit'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import { Provider } from 'react-redux'; @@ -19,28 +20,29 @@ export default { component: Schema, decorators: [ (story) => ( - <div style={{ padding: '3rem' }}> - <Provider store={store}>{story()}</Provider> - </div> + <Provider store={Mockstore}> + <GraphPolarisThemeProvider>{story()}</GraphPolarisThemeProvider> + </Provider> ), ], } as ComponentMeta<typeof Schema>; const Template: ComponentStory<typeof Schema> = (args) => <Schema {...args} />; -// // A super-simple mock of a redux store -// const Mockstore = configureStore({ -// reducer: { -// schema: schemaSlice, -// }, -// }); +// Mock the schema and palette store +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + colorPaletteConfig: colorPaletteConfigSlice.reducer, + }, +}); export const TestWithSchema = Template.bind({}); TestWithSchema.play = async () => { - const dispatch = store.dispatch; + const dispatch = Mockstore.dispatch; - const schema = parseSchemaFromBackend({ + const schema = SchemaUtils.ParseSchemaFromBackend({ nodes: [ { name: 'Thijs', @@ -142,6 +144,7 @@ TestWithSchema.play = async () => { ], }); - //dispatch(setSchema(schema)); - handleSchemaLayout(schema); + console.info('dispatch dummy schema', schema.order); + dispatch(setSchema(schema.export())); + // handleSchemaLayout(schema); }; diff --git a/apps/web-graphpolaris/src/components/schema/schema.tsx b/apps/web-graphpolaris/src/components/schema/schema.tsx index e005a8543bb79779c7765d5e1ba71f6bfa6df0c0..4167487adf58fa5068e4a8698a1490bf26ad8421 100644 --- a/apps/web-graphpolaris/src/components/schema/schema.tsx +++ b/apps/web-graphpolaris/src/components/schema/schema.tsx @@ -1,35 +1,79 @@ -import { useSchema } from '@graphpolaris/shared/data-access/store'; -//import { useEffect } from 'react'; -import ReactFlow from 'react-flow-renderer'; -import styled from 'styled-components'; -import { createReactFlowElements } from '@graphpolaris/schema/schema-usecases'; +import { AllLayoutAlgorithms, LayoutFactory } from '@graphpolaris/graph-layout'; +import { createReactFlowElements } from '@graphpolaris/schema/usecases'; +import { + useSchema, + useSchemaLayout, +} from '@graphpolaris/shared/data-access/store'; +import { MultiGraph } from 'graphology'; +// import { AllLayoutAlgorithms, LayoutFactory } from '@graphpolaris/graph-layout'; +import { useEffect, useState } from 'react'; +import ReactFlow, { FlowElement, ReactFlowProvider } from 'react-flow-renderer'; interface Props { - content: string; + // content: string; } -const Div = styled.div` - background-color: red; - font: 'Arial'; - width: 300px; - height: 600px; -`; +const onLoad = (reactFlowInstance: any) => { + setTimeout(() => reactFlowInstance.fitView(), 0); +}; const Schema = (props: Props) => { + const [elements, setElements] = useState([] as FlowElement[]); + // In case the schema is updated const dbschema = useSchema(); + // const [dbschema, setSchema] = useState(useSchema()); + const [schemaLayout, setSchemaLayout] = useState(useSchemaLayout()); - // In case the schema is updated - // useEffect(() => { - // console.log('update schema useEffect'); - // }, [dbschema]); + useEffect(() => { + console.log('dbSchema', dbschema, dbschema.order); + }, [dbschema]); + + useEffect(() => { + if (dbschema == undefined || dbschema.order == 0) { + return; + } + const layoutFactory = new LayoutFactory(); + console.log('schema Layout', schemaLayout, 'order', dbschema.order); + const layout = layoutFactory.createLayout( + schemaLayout as AllLayoutAlgorithms + ); + layout?.layout(dbschema); + + // dbschema.forEachNode((node, attr) => { + // console.log('x', dbschema.getNodeAttribute(node, 'x')); + // console.log('y', dbschema.getNodeAttribute(node, 'y')); + // }); + + const flowElements = createReactFlowElements(dbschema); + setElements(flowElements); + console.log( + 'update schema useEffect', + dbschema, + dbschema.order, + flowElements + ); + }, [dbschema, schemaLayout]); + + const graphStyles = { width: '100%', height: '500px' }; - console.log(dbschema); return ( - <Div> - <p>hey</p> - <ReactFlow elements={createReactFlowElements(dbschema)} /> - <p>hoi</p> - </Div> + <div + style={{ + width: '100%', + height: '100%', + }} + > + {elements.length == 0 && <p>DEBUG: No Elements</p>} + <ReactFlowProvider> + <ReactFlow elements={elements} style={graphStyles} onLoad={onLoad} /> + {/* // onElementsRemove={onElementsRemove} + // onConnect={onConnect} + // + snapToGrid={true} + snapGrid={[15, 15]} + ></ReactFlow> */} + </ReactFlowProvider> + </div> ); }; diff --git a/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.stories.tsx b/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.stories.tsx index b384a6d3e879ec9c452194f170be0bb8877866e8..3b10ca2d12d79022af4ed24afd69c60c3235f4e0 100644 --- a/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.stories.tsx +++ b/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.stories.tsx @@ -2,14 +2,16 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { RawJSONVis } from './rawjsonvis'; -import { Provider } from 'react-redux'; -import { configureStore, createSlice } from '@reduxjs/toolkit'; - import { - graphQueryResultSlice, assignNewGraphQueryResult, + colorPaletteConfigSlice, + graphQueryResultSlice, resetGraphQueryResults, + store, } from '@graphpolaris/shared/data-access/store'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; export default { /* 👇 The title prop is optional. @@ -20,30 +22,30 @@ export default { component: RawJSONVis, decorators: [ (story) => ( - <div style={{ padding: '3rem' }}> - <Provider store={Mockstore}>{story()}</Provider> - </div> + <Provider store={Mockstore}> + <GraphPolarisThemeProvider>{story()}</GraphPolarisThemeProvider> + </Provider> ), ], } as ComponentMeta<typeof RawJSONVis>; -const Template: ComponentStory<typeof RawJSONVis> = (args) => ( - <RawJSONVis {...args} /> -); - -// A super-simple mock of a redux store const Mockstore = configureStore({ reducer: { + colorPaletteConfig: colorPaletteConfigSlice.reducer, graphQueryResult: graphQueryResultSlice.reducer, }, }); +const Template: ComponentStory<typeof RawJSONVis> = (args) => ( + <RawJSONVis {...args} /> +); + export const TestWithData = Template.bind({}); TestWithData.args = { loading: false, }; TestWithData.play = async () => { - const dispatch = Mockstore.dispatch; + const dispatch = store.dispatch; dispatch( assignNewGraphQueryResult({ nodes: [ @@ -68,6 +70,6 @@ Empty.args = { loading: false, }; Empty.play = async () => { - const dispatch = Mockstore.dispatch; + const dispatch = store.dispatch; dispatch(resetGraphQueryResults()); }; diff --git a/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.tsx b/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.tsx index 12347185858d587d4bbdf09017286fec0bb7322a..85c5d46c0aa17b470181cee7841673204e5527ff 100644 --- a/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.tsx +++ b/apps/web-graphpolaris/src/components/visualisations/rawjsonvis/rawjsonvis.tsx @@ -1,6 +1,12 @@ -import { useGraphQueryResult } from '@graphpolaris/shared/data-access/store'; +import { + changePrimary, + useAppDispatch, + useGraphQueryResult, +} from '@graphpolaris/shared/data-access/store'; +import { useTheme } from '@mui/material'; import React, { useEffect, useState } from 'react'; import ReactJSONView from 'react-json-view'; + import styles from './rawjsonvis.module.scss'; /* eslint-disable-next-line */ @@ -10,6 +16,8 @@ export interface RawJSONVisProps { export const RawJSONVis = React.memo((props: RawJSONVisProps) => { const graphQueryResult = useGraphQueryResult(); + const dispatch = useAppDispatch(); + const theme = useTheme(); console.log('update rawjson'); useEffect(() => { @@ -26,6 +34,14 @@ export const RawJSONVis = React.memo((props: RawJSONVisProps) => { return ( <> + <input + onChange={(v) => + dispatch(changePrimary({ main: v.currentTarget.value })) + } + type="color" + name="head" + value={theme.palette.primary.main} + /> {loading && ( <div style={{ diff --git a/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.stories.tsx b/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.stories.tsx index 5cb0c4c84ee1df4dd153d3f0dd9a2d84e66c4875..944011b532a61cef8159459e8922b79f496dbb5c 100644 --- a/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.stories.tsx +++ b/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.stories.tsx @@ -1,8 +1,12 @@ -import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { + colorPaletteConfigSlice, + graphQueryResultSlice, +} from '@graphpolaris/shared/data-access/store'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; import { configureStore } from '@reduxjs/toolkit'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import colorPaletteConfigSlice from 'libs/shared/data-access/store/src/lib/colorPaletteConfigSlice'; import { Provider } from 'react-redux'; + import SemanticSubstrates from './semanticsubstrates'; export default { @@ -10,49 +14,23 @@ export default { * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading * to learn how to generate automatic titles */ - title: 'SemanticSubstrates', + title: 'SemanticSubstrates', component: SemanticSubstrates, decorators: [ - (story) => <div style={{ padding: '3rem' }}>{story()}</div>, (story) => ( - <div style={{ padding: '3rem' }}> - <Provider store={Mockstore}>{story()}</Provider> - </div> + <Provider store={Mockstore}> + <GraphPolarisThemeProvider>{story()}</GraphPolarisThemeProvider> + </Provider> ), - (story) => <ThemeProvider theme={mockTheme}>{story()}</ThemeProvider>, ], } as ComponentMeta<typeof SemanticSubstrates>; -declare module '@mui/material/styles' { - interface Theme { - graphpolaris: { - danger: string; - dataPointColors: string[]; - }; - } - // allow configuration using `createTheme` - interface ThemeOptions { - graphpolaris: { - danger: string; - dataPointColors: string[]; - }; - } -} - -const mockTheme = createTheme({ - graphpolaris: { - danger: 'orange', - dataPointColors: ['blue', 'green'], - }, -}); - -// A super-simple mock of a redux store const Mockstore = configureStore({ reducer: { - colorPaletteConfig: colorPaletteConfigSlice, + colorPaletteConfig: colorPaletteConfigSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, }, }); - const Template: ComponentStory<typeof SemanticSubstrates> = (args) => ( <SemanticSubstrates /> ); diff --git a/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.tsx b/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.tsx index 12f850c74fde2d9bd8787b1c72c7c9ed6e0c56b5..cb6c9e564d6d571df6698b755a7ab2f3f182a390 100644 --- a/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.tsx +++ b/apps/web-graphpolaris/src/components/visualisations/semanticsubstrates/semanticsubstrates.tsx @@ -1,75 +1,84 @@ -import { styled, Theme, useTheme } from '@mui/material/styles'; -import Box from '@mui/material/Box'; -import { useDispatch } from 'react-redux'; +import { Theme, useTheme } from '@mui/material/styles'; +import Box from '@mui/system/Box'; import { changeDataPointColors, changePrimary, + toggleDarkMode, + useAppDispatch, } from '@graphpolaris/shared/data-access/store'; +import styled from 'styled-components'; +import { Button } from '@mui/material'; -// Basically a styled-component -const Div = styled('div')(({ theme }) => ({ - backgroundColor: theme.palette.primary.main, -})); +const Div = styled.div` + // add :{ theme: Theme } if you want to have typing (autocomplete) + background-color: ${({ theme }: { theme: Theme }) => + theme.palette.primary.main}; +`; const SemanticSubstrates = () => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const theme = useTheme(); - console.log('theme here ', theme); return ( <> <Div> - <h1>semantic substrates div</h1> + <h1>semantic substrates, primary.main</h1> </Div> - <Box - sx={{ - // Using our custom palette dataPointColors property, cast theme to any because Theme doesn't know of this custom property - backgroundColor: (theme: Theme) => - theme.graphpolaris.dataPointColors[0], + <div + style={{ + backgroundColor: theme.palette.primary.dark, }} > - <h1>semantic substrates h1</h1> - </Box> + <h1>semantic substrates, primary.dark</h1> + </div> <input onChange={(v) => - dispatch(changePrimary({ main: v.currentTarget.value })) + dispatch( + changePrimary({ + main: v.currentTarget.value, + darkMode: theme.palette.mode, + }) + ) } type="color" name="head" value={theme.palette.primary.main} - />{' '} - Change Primary Color + /> + <p>Change Primary Color</p> <input onChange={(v) => dispatch( changeDataPointColors([ v.currentTarget.value, - ...theme.graphpolaris.dataPointColors.slice(1), + ...theme.palette.dataPointColors.slice(1), ]) ) } type="color" id="head" name="head" - value={theme.graphpolaris.dataPointColors[0]} + value={theme.palette.dataPointColors[0]} /> - Change dataPointColors reflects to local + <p>Change dataPointColor 0</p> <input onChange={(v) => dispatch( changeDataPointColors([ - theme.graphpolaris.dataPointColors[0], + theme.palette.dataPointColors[0], v.currentTarget.value, - ...theme.graphpolaris.dataPointColors.slice(2), + ...theme.palette.dataPointColors.slice(2), ]) ) } type="color" id="head" name="head" - value={theme.graphpolaris.dataPointColors[1]} + value={theme.palette.dataPointColors[1]} /> - Change dataPointColors reflects to global + <p>Change dataPointColor 1</p> + <Button variant="contained" onClick={() => dispatch(toggleDarkMode())}> + toggle dark mode + </Button> </> ); }; diff --git a/apps/web-graphpolaris/src/main.tsx b/apps/web-graphpolaris/src/main.tsx index 0eb1b52efbd1c4aab21e9ad90fe9845c7f697abd..5c5a5a841f508e8f59e86911e2b27521dd42bbca 100644 --- a/apps/web-graphpolaris/src/main.tsx +++ b/apps/web-graphpolaris/src/main.tsx @@ -1,15 +1,27 @@ -import { store } from '@graphpolaris/shared/data-access/store'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; +import { store } from '@graphpolaris/shared/data-access/store'; +import { GraphPolarisThemeProvider } from '@graphpolaris/shared/data-access/theme'; import App from './app/app'; +import LoginPopupComponent from './components/login/popup'; +import { CssBaseline } from '@mui/material'; ReactDOM.render( - <StrictMode> - <Provider store={store}> - <App /> - </Provider> - </StrictMode>, + <Provider store={store}> + <GraphPolarisThemeProvider> + <CssBaseline /> + <Router> + <Routes> + {/* Route to auth component in popup */} + <Route path="/auth" element={<LoginPopupComponent />}></Route> + {/* App */} + <Route path="/" element={<App />}></Route> + </Routes> + </Router> + </GraphPolarisThemeProvider> + </Provider>, document.getElementById('root') ); diff --git a/apps/web-graphpolaris/.storybook/preview.js b/certs/.gitkeep similarity index 100% rename from apps/web-graphpolaris/.storybook/preview.js rename to certs/.gitkeep diff --git a/libs/schema/schema-usecases/.babelrc b/libs/querybuilder/usecases/.babelrc similarity index 100% rename from libs/schema/schema-usecases/.babelrc rename to libs/querybuilder/usecases/.babelrc diff --git a/libs/schema/schema-usecases/.eslintrc.json b/libs/querybuilder/usecases/.eslintrc.json similarity index 100% rename from libs/schema/schema-usecases/.eslintrc.json rename to libs/querybuilder/usecases/.eslintrc.json diff --git a/libs/querybuilder/usecases/README.md b/libs/querybuilder/usecases/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f919d5ec5c4710fe9263506b367251e7e6a92baa --- /dev/null +++ b/libs/querybuilder/usecases/README.md @@ -0,0 +1,7 @@ +# querybuilder-usecases + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test querybuilder-usecases` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/schema/schema-usecases/jest.config.js b/libs/querybuilder/usecases/jest.config.js similarity index 69% rename from libs/schema/schema-usecases/jest.config.js rename to libs/querybuilder/usecases/jest.config.js index 3ce30ca8d9b273a15c9fa74b349d6dd9df8be839..941f4fbc2eed65bcc6b5a914c29364dcd1fff9bb 100644 --- a/libs/schema/schema-usecases/jest.config.js +++ b/libs/querybuilder/usecases/jest.config.js @@ -1,5 +1,5 @@ module.exports = { - displayName: 'schema-schema-usecases', + displayName: 'querybuilder-usecases', preset: '../../../jest.preset.js', globals: { 'ts-jest': { @@ -10,5 +10,5 @@ module.exports = { '^.+\\.[tj]sx?$': 'ts-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - coverageDirectory: '../../../coverage/libs/schema/schema-usecases', + coverageDirectory: '../../../coverage/libs/querybuilder/usecases', }; diff --git a/libs/querybuilder/usecases/project.json b/libs/querybuilder/usecases/project.json new file mode 100644 index 0000000000000000000000000000000000000000..271a62582eacd5887d6adbf6a5027c3b12618eaf --- /dev/null +++ b/libs/querybuilder/usecases/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/querybuilder/usecases", + "sourceRoot": "libs/querybuilder/usecases/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/querybuilder/usecases/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/querybuilder/usecases"], + "options": { + "jestConfig": "libs/querybuilder/usecases/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/querybuilder/usecases/src/index.ts b/libs/querybuilder/usecases/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..32e0c81f2ecfe335cb61852728753c148b9c6c8b --- /dev/null +++ b/libs/querybuilder/usecases/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/attribute/getAttributeBoolOperators'; +export * from './lib/attribute/checkInput'; +export * from './lib/createReactFlowElements'; +export * from './lib/pillHandles'; +export * from './lib/dragging/dragPill'; +export * from './lib/addPill'; diff --git a/libs/querybuilder/usecases/src/lib/addPill.ts b/libs/querybuilder/usecases/src/lib/addPill.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7b359d8f0fc77f1281fa57a7c200f13acfb820e --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/addPill.ts @@ -0,0 +1,94 @@ +import { + setQuerybuilderNodes, + store, +} from '@graphpolaris/shared/data-access/store'; +import Graph from 'graphology'; +import { Attributes } from 'graphology-types'; + +/** monospace fontsize table */ +const widthPerFontsize = { + 6: 3.6167, + 7: 4.2167, + 10: 6.0167, +}; + +/** Adds a query builder pill to the graphology nodes object. */ +export function addPill( + id: string, + attributes: Attributes, + nodes: Graph +): boolean { + const { type, name } = attributes; + if (!type || !name) return false; + let { x, y } = attributes; + + // Check if x and y are present, otherwise set them to 0 + if (!x) x = 0; + if (!y) y = 0; + + // Get the width and height of a node + const { w, h } = calcWidthHeightOfPill(attributes); + + // Add a node to the graphology object + nodes.addNode(id, { ...attributes, x, y, w, h }); + + // Set the new nodes in the query builder slice + store.dispatch(setQuerybuilderNodes(nodes.export())); + + return true; +} + +/** Calculates the width and height of a query builder pill. + * DEPENDS ON STYLING, if styling changed, change this. + */ +function calcWidthHeightOfPill(attributes: Attributes): { + w: number; + h: number; +} { + const { type, name } = attributes; + + let w = 0; + let h = 0; + switch (type) { + case 'entity': { + // calculate width and height of entity pill + w = Math.min(name.length, 20) * widthPerFontsize[10]; // for fontsize 10px + + const widthOfPillWithoutText = 42.1164; // WARNING: depends on styling + w += widthOfPillWithoutText; + h = 20; + break; + } + case 'relation': { + // calculate width and height of relation pill + w = Math.min(name.length, 20) * widthPerFontsize[10]; // for fontsize 10px + + const widthOfPillWithoutText = 56.0666; // WARNING: depends on styling + w += widthOfPillWithoutText; + h = 20; + break; + } + case 'attribute': { + // calculate width and height of relation pill + const pixelsPerChar = widthPerFontsize[6]; // for fontsize 10px + w = name.length * pixelsPerChar; + + const { datatype, operator } = attributes; + let value = attributes['value']; + if (!datatype || !operator) return { w: 0, h: 0 }; + if (!value) value = '?'; + + // Add width of operator + w += operator.length * widthPerFontsize[7]; + // use a max of 10, because max-width is set to 10ch; + w += Math.min(value.length, 10) * widthPerFontsize[6]; + + const widthOfPillWithoutText = 25.6666; // WARNING: depends on styling + w += widthOfPillWithoutText; + h = 12; + break; + } + } + + return { w, h }; +} diff --git a/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1c29ab1f05d62105afaf80faaa405c30ce60535 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts @@ -0,0 +1,34 @@ +/** Checks if the string input is a number. */ +function isNumber(x: string): boolean { + if (typeof x != 'string') return false; + return !Number.isNaN(x) && !Number.isNaN(parseFloat(x)); +} +function isBoolean(s: string): boolean { + return s == 'true' || s == 'false' || s == '0' || s == '1'; +} +function toBoolean(s: string): string { + if (s == '1' || s == 'true') return 'true'; + return 'false'; +} + +/** Checks if the provided value has the same as the datatype of the attribute. */ +export function CheckDatatypeConstraint(type: string, str: string): string { + let res = ''; + switch (type) { + case 'string': + res = str; + break; + case 'bool': + isBoolean(str) ? (res = toBoolean(str)) : (res = ''); + break; + case 'int': + case 'float': + case 'number': + isNumber(str) ? (res = '' + parseFloat(str)) : (res = ''); + break; + default: + res = str; + break; + } + return res; +} diff --git a/libs/querybuilder/usecases/src/lib/attribute/getAttributeBoolOperators.ts b/libs/querybuilder/usecases/src/lib/attribute/getAttributeBoolOperators.ts new file mode 100644 index 0000000000000000000000000000000000000000..22809a7cde969f5be4da3978e650231a38ca1c4f --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/attribute/getAttributeBoolOperators.ts @@ -0,0 +1,63 @@ +/** Determines the available boolean operators for a certain datatype. */ +export function GetAttributeBoolOperators( + datatype: string +): { label: string; value: string }[] { + switch (datatype) { + case 'text': + case 'string': + return [ + { + label: '=', + value: 'EQ', + }, + { + label: '≠', + value: 'NEQ', + }, + { + label: 'inc', + value: 'includes', + }, + { + label: 'exc', + value: 'excludes', + }, + ]; + case 'int': + case 'float': + return [ + { + label: '=', + value: 'EQ', + }, + { + label: '≠', + value: 'NEQ', + }, + { + label: '>', + value: 'GT', + }, + { + label: '≥', + value: 'GTE', + }, + { + label: '<', + value: 'LT', + }, + { + label: '≤', + value: 'LTE', + }, + ]; + case 'bool': + default: + return [ + { + label: '=', + value: 'EQ', + }, + ]; + } +} diff --git a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts new file mode 100644 index 0000000000000000000000000000000000000000..eba3bf06477f9bd53ac9e1c87ea13126ecd58e02 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts @@ -0,0 +1,130 @@ +import Graph, { MultiGraph } from 'graphology'; +import { Attributes } from 'graphology-types'; +import { Elements, Node, Edge, XYPosition } from 'react-flow-renderer'; + +// Takes the querybuilder graph as an input and creates react flow elements for them. +export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { + const elements: Elements<Node | Edge> = []; + + graph.forEachNode((node: string, attributes: Attributes): void => { + let data; + let position = { x: attributes?.x || 0, y: attributes?.y || 0 }; + + switch (attributes.type) { + case 'entity': + data = { + isConnected: graph + .neighbors(node) + .some((nb) => graph.getNodeAttribute(nb, 'type') == 'relation'), + }; + break; + case 'relation': + data = { + isFromEntityConnected: graph + .inNeighbors(node) + .some((nb) => graph.getNodeAttribute(nb, 'type') == 'entity'), + isToEntityConnected: graph + .outNeighbors(node) + .some((nb) => graph.getNodeAttribute(nb, 'type') == 'entity'), + }; + break; + case 'attribute': { + const ERNeighbors = graph.outNeighbors(node).filter((nb) => { + const type = graph.getNodeAttribute(nb, 'type'); + return type == 'entity' || type == 'relation'; + }); + let attributeOfA = ''; + if (ERNeighbors.length > 0) + attributeOfA = graph.getNodeAttribute(ERNeighbors[0], 'type'); + data = { + datatype: attributes.datatype, + operator: attributes.operator, + value: attributes.value, + attributeOfA: attributeOfA, + }; + // Get the position of the attribute, based on the connection to entity or relation + const p = getAttributePosition(node, graph); + if (p) position = p; + break; + } + } + // Each pill should have a name and type + data = { + ...data, + name: attributes.name, + suggestedForConnection: attributes.suggestedForConnection, // Highlights the pill, with shadow or something + }; + + const RFNode: Node = { + id: node, + type: attributes.type, + position: position, + data: data, + }; + elements.push(RFNode); + }); + + // Add the reactflow edges + graph.forEachEdge((edge, attributes, source, target): void => { + // connection from attributes don't have visible connection lines + if (attributes.type == 'attribute_connection') return; + + const RFEdge: Edge = { + id: edge, + source: source, + target: target, + type: 'connection', + sourceHandle: attributes.sourceHandle, + targetHandle: attributes.targetHandle, + }; + elements.push(RFEdge); + }); + + return elements; +} + +/** Gets the position of an attribute based on the connection to an entity or relation. + * It uses the position of the parent pill and what the index is of this attribute in all + * the connected attributes to the parent. + */ +function getAttributePosition( + id: string, + nodes: MultiGraph +): XYPosition | undefined { + const nbs = nodes.filterOutNeighbors(id, (_, { type }) => + ['entity', 'relation'].includes(type) + ); + + if (nbs.length > 1) + console.log( + 'WARNING: attribute connected to more than one entity or relation' + ); + else if (nbs.length == 1) { + const nb = nbs[0]; + const connectedAttributes = nodes.filterInNeighbors( + nb, + (_, { type }) => type == 'attribute' + ); + + // An entity can have more attributes, what is the attributes index in the attributes array of that entity? + let nthAttibute = -1; + for (let i = 0; i < connectedAttributes.length; i++) { + if (connectedAttributes[i] == id) { + nthAttibute = i; + break; + } + } + + const nbAttr = nodes.getNodeAttributes(nb); + + const pos = { x: nbAttr.x + 30, y: nbAttr.y + nbAttr.h }; + // ASSUMES THAT EACH ATTRIBUTE HAS THE SAME HEIGHT + const heightOfAttributes = nodes.getNodeAttribute(id, 'h') - 1; + pos.y += nthAttibute * heightOfAttributes; + + return pos; + } + + // If the attribute has no (attribute_)connection, don't position it. + return undefined; +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts b/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts new file mode 100644 index 0000000000000000000000000000000000000000..df89ab57dcc526b42c151e868a50add049efbe7d --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts @@ -0,0 +1,30 @@ +import { MultiGraph } from 'graphology'; +import { GetClosestPill } from './getClosestPill'; + +export function DragAttributePillStarted(id: string, nodes: MultiGraph) { + // if the attribute is still connected to an entity or relation pill, disconnect + const es = nodes.outEdges(id); + es.forEach((e) => nodes.dropEdge(e)); +} + +export function DragAttributePill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number +) { + // Get the closes entity or relation node + const closestNode = GetClosestPill(id, nodes, ['entity', 'relation']); + // If we found one, highlight it by adding an attribute + if (closestNode) + nodes.setNodeAttribute(closestNode, 'suggestedForConnection', true); +} + +export function DragAttibutePillStopped(id: string, nodes: MultiGraph) { + // If there is currently a node with the suggestedForConnection attribute + // connect this attribute to it + nodes.forEachNode((node, { suggestedForConnection }) => { + if (suggestedForConnection) + nodes.addEdge(id, node, { type: 'attribute_connection' }); + }); +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragAttributesAlong.ts b/libs/querybuilder/usecases/src/lib/dragging/dragAttributesAlong.ts new file mode 100644 index 0000000000000000000000000000000000000000..56697ab3f9cd125629c309c9df699419365a4a49 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragAttributesAlong.ts @@ -0,0 +1,27 @@ +import Graph from 'graphology'; + +/** + * Changes the position of connected attributes. + * @param id The id of the node which could have attributes connected to it (entity or relation) + * @param nodes The graphology query builder object + * @param dx The change in x + * @param dy The change in y + * @returns True if any attribute positions were changed + */ +export function DragAttributesAlong( + id: string, + nodes: Graph, + dx: number, + dy: number +): boolean { + let didChangeAttributes = false; + nodes.forEachInNeighbor(id, (nb) => { + if (nodes.getNodeAttribute(nb, 'type') == 'attribute') { + nodes.updateNodeAttribute(nb, 'x', (x) => x + dx); + nodes.updateNodeAttribute(nb, 'y', (y) => y + dy); + didChangeAttributes = true; + } + }); + + return didChangeAttributes; +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragEntity.ts b/libs/querybuilder/usecases/src/lib/dragging/dragEntity.ts new file mode 100644 index 0000000000000000000000000000000000000000..171124ac9330a007b531311fec85dceeb4ccf945 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragEntity.ts @@ -0,0 +1,18 @@ +import { MultiGraph } from 'graphology'; + +export function DragEntityPillStarted(id: string, nodes: MultiGraph) { + // Started dragging entity usecase +} + +export function DragEntityPill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number +) { + // Code for dragging an entity pill should go here +} + +export function DragEntityPillStopped(id: string, nodes: MultiGraph) { + // Stopped dragging entity pill +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragPill.ts b/libs/querybuilder/usecases/src/lib/dragging/dragPill.ts new file mode 100644 index 0000000000000000000000000000000000000000..3832c103d94fbb2d759edc082cd643051580712a --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragPill.ts @@ -0,0 +1,90 @@ +import { MultiGraph } from 'graphology'; +import { XYPosition } from 'react-flow-renderer'; +import { + DragAttibutePillStopped, + DragAttributePill, + DragAttributePillStarted, +} from './dragAttribute'; +import { DragAttributesAlong } from './dragAttributesAlong'; +import { + DragEntityPill, + DragEntityPillStarted, + DragEntityPillStopped, +} from './dragEntity'; +import { + DragRelationPill, + DragRelationPillStarted, + DragRelationPillStopped, +} from './dragRelation'; + +export function dragPillStarted(id: string, nodes: MultiGraph) { + switch (nodes.getNodeAttribute(id, 'type')) { + case 'attribute': + DragAttributePillStarted(id, nodes); + break; + case 'entity': + DragEntityPillStarted(id, nodes); + break; + case 'relation': + DragRelationPillStarted(id, nodes); + break; + } +} + +/** + * A general drag usecase for any pill, it will select the correct usecase for each pill + * @param id + * @param nodes The graphology query builder nodes object + * @param dx Delta x + * @param dy Delta y + * @param position The already updated positiong (dx dy are already applied) + */ +export function dragPill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number, + position: XYPosition +) { + // Update the position of the node in the graphology object + nodes.setNodeAttribute(id, 'x', position.x); + nodes.setNodeAttribute(id, 'y', position.y); + + // Remove the highlighted attribute from each node + nodes.forEachNode((node) => + nodes.removeNodeAttribute(node, 'suggestedForConnection') + ); + + switch (nodes.getNodeAttribute(id, 'type')) { + case 'attribute': + DragAttributePill(id, nodes, dx, dy); + break; + case 'entity': + DragAttributesAlong(id, nodes, dx, dy); + DragEntityPill(id, nodes, dx, dy); + break; + case 'relation': + DragAttributesAlong(id, nodes, dx, dy); + DragRelationPill(id, nodes, dx, dy); + break; + } +} + +export function dragPillStopped(id: string, nodes: MultiGraph) { + switch (nodes.getNodeAttribute(id, 'type')) { + case 'attribute': + DragAttibutePillStopped(id, nodes); + break; + case 'entity': + DragEntityPillStopped(id, nodes); + break; + case 'relation': + DragRelationPillStopped(id, nodes); + break; + } + + // Remove all suggestedForConnection attributes + nodes.forEachNode((node) => + nodes.removeNodeAttribute(node, 'suggestedForConnection') + ); +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/dragRelation.ts b/libs/querybuilder/usecases/src/lib/dragging/dragRelation.ts new file mode 100644 index 0000000000000000000000000000000000000000..12bf51961ae8cff27113c73a14c81d1e6c346b91 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/dragRelation.ts @@ -0,0 +1,18 @@ +import { MultiGraph } from 'graphology'; + +export function DragRelationPillStarted(id: string, nodes: MultiGraph) { + // Started dragging relation usecase +} + +export function DragRelationPill( + id: string, + nodes: MultiGraph, + dx: number, + dy: number +) { + // Code for dragging an relation pill should go here +} + +export function DragRelationPillStopped(id: string, nodes: MultiGraph) { + // Stopped dragging relation pill +} diff --git a/libs/querybuilder/usecases/src/lib/dragging/getClosestPill.ts b/libs/querybuilder/usecases/src/lib/dragging/getClosestPill.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a3fd44874fa8de1f5b3d1508d456dfae0848faa --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/dragging/getClosestPill.ts @@ -0,0 +1,40 @@ +import { MultiGraph } from 'graphology'; + +/** + * Gets the closest node to id + * @param id + * @param nodes Graphology querybuilder MultiGraph object + * @param allowedNodeTypes An array of the node types which are included in the search + * @param maxDistance The maximum distance + * @returns the closest node if within range + */ +export function GetClosestPill( + id: string, + nodes: MultiGraph, + allowedNodeTypes: string[], + maxDistance = 150 +): string | undefined { + const { x, y, w, h } = nodes.getNodeAttributes(id); + const center: { x: number; y: number } = { x: x + w / 2, y: y + h / 2 }; + + let minDist = maxDistance * maxDistance; + let closestNode: string | undefined = undefined; + nodes.forEachNode((node, { x, y, w, h, type }) => { + if (allowedNodeTypes.includes(type)) { + const nodeCenter: { x: number; y: number } = { + x: x + w / 2, + y: y + h / 2, + }; + + const dx = center.x - nodeCenter.x; + const dy = center.y - nodeCenter.y; + const dist = dx * dx + dy * dy; + if (dist < minDist) { + minDist = dist; + closestNode = node; + } + } + }); + + return closestNode; +} diff --git a/libs/querybuilder/usecases/src/lib/pillHandles.ts b/libs/querybuilder/usecases/src/lib/pillHandles.ts new file mode 100644 index 0000000000000000000000000000000000000000..355bdd8e5ce2cd821e49d3bef74bb9d925637b50 --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/pillHandles.ts @@ -0,0 +1,14 @@ +// This file describes the connection points (handles) on a query builder pill +// For example the connection from entity to left relation handle + +export const handles = { + entity: { + /** The handle for a connection from an entity to a relation pill */ + relation: 'entity:to_relation', + }, + relation: { + /** The handle for a connection from a relation to an entity pill */ + toEntity: 'relation:to_entity', + fromEntity: 'relation:from_entity', + }, +}; diff --git a/libs/querybuilder/usecases/src/lib/querybuilder-usecases.spec.ts b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..684984e68f06f7cff30cc3f8c00749fd61c1aa9b --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.spec.ts @@ -0,0 +1,7 @@ +import { querybuilderUsecases } from './querybuilder-usecases'; + +describe('querybuilderUsecases', () => { + it('should work', () => { + expect(querybuilderUsecases()).toEqual('querybuilder-usecases'); + }); +}); diff --git a/libs/querybuilder/usecases/src/lib/querybuilder-usecases.ts b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.ts new file mode 100644 index 0000000000000000000000000000000000000000..06d687eb90fbdd6d6e752c90417a12a854aff52b --- /dev/null +++ b/libs/querybuilder/usecases/src/lib/querybuilder-usecases.ts @@ -0,0 +1,3 @@ +export function querybuilderUsecases(): string { + return 'querybuilder-usecases'; +} diff --git a/libs/querybuilder/usecases/tsconfig.json b/libs/querybuilder/usecases/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..6ebadfb9de07f71cd20ee1102f3512550505ad2a --- /dev/null +++ b/libs/querybuilder/usecases/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true + } +} diff --git a/libs/schema/schema-usecases/tsconfig.lib.json b/libs/querybuilder/usecases/tsconfig.lib.json similarity index 100% rename from libs/schema/schema-usecases/tsconfig.lib.json rename to libs/querybuilder/usecases/tsconfig.lib.json diff --git a/libs/schema/schema-usecases/tsconfig.spec.json b/libs/querybuilder/usecases/tsconfig.spec.json similarity index 100% rename from libs/schema/schema-usecases/tsconfig.spec.json rename to libs/querybuilder/usecases/tsconfig.spec.json diff --git a/libs/schema/schema-usecases/src/index.ts b/libs/schema/schema-usecases/src/index.ts deleted file mode 100644 index 5d984685980c34d4a947c278f84a8ea4b755a323..0000000000000000000000000000000000000000 --- a/libs/schema/schema-usecases/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/schema-schema-usecases'; diff --git a/libs/schema/schema-usecases/src/lib/schema-schema-usecases.spec.ts b/libs/schema/schema-usecases/src/lib/schema-schema-usecases.spec.ts deleted file mode 100644 index f2130094ed2bbfa865b7f61af9d6c130eb32dfe2..0000000000000000000000000000000000000000 --- a/libs/schema/schema-usecases/src/lib/schema-schema-usecases.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { schemaSchemaUsecases } from './schema-schema-usecases'; - -describe('schemaSchemaUsecases', () => { - it('should work', () => { - expect(schemaSchemaUsecases()).toEqual('schema-schema-usecases'); - }); -}); diff --git a/libs/schema/schema-usecases/src/lib/schema-schema-usecases.ts b/libs/schema/schema-usecases/src/lib/schema-schema-usecases.ts deleted file mode 100644 index e12f733f038d0303da9cd95657b46d98c96a5d56..0000000000000000000000000000000000000000 --- a/libs/schema/schema-usecases/src/lib/schema-schema-usecases.ts +++ /dev/null @@ -1,221 +0,0 @@ -import Graph from 'graphology'; -import cytoscape from 'cytoscape'; // eslint-disable-line -import { setSchema, store } from '@graphpolaris/shared/data-access/store'; -import { Elements, Node, Edge } from 'react-flow-renderer'; -import { SchemaFromBackend } from '@graphpolaris/shared/data-access/store'; - -type CytoNode = { - data: { - id: string; - type: string; - source?: string; - target?: string; - position?: { - x: number; - y: number; - }; - }; -}; - -// Layouts a given schema -export function handleSchemaLayout(graph: Graph): void { - const layout = createSchemaLayout(graph); - - layout.then((cy) => { - //cy.cy.elements().forEach((elem) => { - cy.cy.nodes().forEach((elem) => { - const position = elem.position(); - console.log(elem.id()); - - graph.setNodeAttribute(elem.id(), 'x', position.x); - graph.setNodeAttribute(elem.id(), 'y', position.y); - }); - - store.dispatch(setSchema(graph)); - }); -} - -// Creates a schema layout (async) -function createSchemaLayout(graph: Graph): Promise<cytoscape.EventObject> { - const cytonodes: CytoNode[] = trimSchema(graph); - - const cy = cytoscape({ - elements: cytonodes, - }); - - const options = { - name: 'cose', - - // Whether to animate while running the layout - // true : Animate continuously as the layout is running - // false : Just show the end result - // 'end' : Animate with the end result, from the initial positions to the end positions - animate: true, - - // Easing of the animation for animate:'end' - animationEasing: undefined, - - // The duration of the animation for animate:'end' - animationDuration: undefined, - - // A function that determines whether the node should be animated - // All nodes animated by default on animate enabled - // Non-animated nodes are positioned immediately when the layout starts - // animateFilter: function (node: any, i: any) { - // return true; - // }, - - // The layout animates only after this many milliseconds for animate:true - // (prevents flashing on fast runs) - animationThreshold: 250, - - // Number of iterations between consecutive screen positions update - refresh: 20, - - // Whether to fit the network view after when done - fit: true, - - // Padding on fit - padding: 30, - - // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } - boundingBox: undefined, - - // Excludes the label when calculating node bounding boxes for the layout algorithm - nodeDimensionsIncludeLabels: false, - - // Randomize the initial positions of the nodes (true) or use existing positions (false) - randomize: false, - - // Extra spacing between components in non-compound graphs - componentSpacing: 200, // 40 - - // Node repulsion (non overlapping) multiplier - nodeRepulsion: function (node: any) { - return 2048; - }, - - // Node repulsion (overlapping) multiplier - nodeOverlap: 4, - - // Ideal edge (non nested) length - idealEdgeLength: function (edge: any) { - return 32; - }, - - // Divisor to compute edge forces - edgeElasticity: function (edge: any) { - return 32; - }, - - // Nesting factor (multiplier) to compute ideal edge length for nested edges - nestingFactor: 1.2, - - // Gravity force (constant) - gravity: 1, - - // Maximum number of iterations to perform - numIter: 1000, - - // Initial temperature (maximum node displacement) - initialTemp: 1000, - - // Cooling factor (how the temperature is reduced between consecutive iterations - coolingFactor: 0.99, - - // Lower temperature threshold (below this point the layout will end) - minTemp: 1.0, - }; - - const layout = cy.layout(options); - - layout.run(); - - return layout.pon('layoutstop'); -} - -// Takes the schema as input and creates a list of nodes and edges in a format that the layouting algorithm can use. -function trimSchema(graph: Graph): CytoNode[] { - const cytonodes: CytoNode[] = []; - - graph.forEachNode((node) => { - cytonodes.push({ - data: { id: node, type: 'node' }, - }); - }); - - graph.forEachEdge((edge, _attributes, source, target) => { - cytonodes.push({ - data: { id: edge, type: 'edge', source: source, target: target }, - }); - }); - - return cytonodes; -} - -// Takes the schema as an imput and creates basic react flow elements for them. -export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { - const initialElements: Elements<Node | Edge> = []; - - graph.forEachNode((node, attributes) => { - const newNode: Node = { - id: node, - data: { - label: attributes.name, - }, - position: { x: attributes.x, y: attributes.y }, - }; - initialElements.push(newNode); - }); - - graph.forEachEdge((edge, _attributes, source, target) => { - const newEdge: Edge = { - id: edge, - source: source, - target: target, - }; - initialElements.push(newEdge); - }); - - return initialElements; -} - -export function parseSchemaFromBackend( - schemaFromBackend: SchemaFromBackend -): Graph { - const { nodes, edges } = schemaFromBackend; - // Instantiate a directed graph that allows self loops and parallel edges - const schema = new Graph({ allowSelfLoops: true, multi: true }); - console.log('Updating schema'); - // The graph schema needs a node for each node AND edge. These need then be connected - - nodes.forEach((node) => { - schema.addNode(node.name, { - name: node.name, - attributes: node.attributes, - x: 0, - y: 0, - }); - }); - - // The name of the edge will be name + from + to, since edge names are not unique - edges.forEach((edge) => { - const edgeID = edge.name + edge.from + edge.to; - - // This node is the actual edge - schema.addNode(edgeID, { - name: edge.name, - attributes: edge.attributes, - from: edge.from, - to: edge.to, - collection: edge.collection, - x: 0, - y: 0, - }); - - // These lines are simply for keeping the schema together - schema.addDirectedEdgeWithKey(edgeID + 'f', edge.from, edgeID); - schema.addDirectedEdgeWithKey(edgeID + 't', edgeID, edge.to); - }); - return schema; -} diff --git a/libs/schema/usecases/.babelrc b/libs/schema/usecases/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..cf7ddd99c615a064ac18eb3109eee4f394ab1faf --- /dev/null +++ b/libs/schema/usecases/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/libs/schema/usecases/.eslintrc.json b/libs/schema/usecases/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..3456be9b9036a42c593c82b050281230e4ca0ae4 --- /dev/null +++ b/libs/schema/usecases/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/schema/usecases/README.md b/libs/schema/usecases/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9df85b49c2d08927c6f1af920c477bdda6b4136c --- /dev/null +++ b/libs/schema/usecases/README.md @@ -0,0 +1,7 @@ +# schema-usecases + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test schema-usecases` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/schema/usecases/jest.config.js b/libs/schema/usecases/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..62be23864e3ee4b19d3f04173f72f4c4caafe1cf --- /dev/null +++ b/libs/schema/usecases/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'schema-usecases', + preset: '../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/schema/usecases', +}; diff --git a/libs/schema/usecases/project.json b/libs/schema/usecases/project.json new file mode 100644 index 0000000000000000000000000000000000000000..1231999dd8bad8d5c0e54de1bfba44c7ef586d29 --- /dev/null +++ b/libs/schema/usecases/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/schema/usecases", + "sourceRoot": "libs/schema/usecases/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/schema/usecases/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/schema/usecases"], + "options": { + "jestConfig": "libs/schema/usecases/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/schema/usecases/src/index.ts b/libs/schema/usecases/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1aa5377958fa9eb0665084bc6ed92f3b62d4d5e9 --- /dev/null +++ b/libs/schema/usecases/src/index.ts @@ -0,0 +1 @@ +export * from './lib/schema-usecases'; diff --git a/libs/schema/usecases/src/lib/schema-usecases.spec.ts b/libs/schema/usecases/src/lib/schema-usecases.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..99ec73f4c8b2b1bd660b11f7f73a965f83a34f79 --- /dev/null +++ b/libs/schema/usecases/src/lib/schema-usecases.spec.ts @@ -0,0 +1,73 @@ +import { SchemaFromBackend } from '@graphpolaris/models'; +import { SchemaUtils } from '@graphpolaris/schema-utils'; +import { MultiGraph } from 'graphology'; +import { Attributes } from 'graphology-types'; +import { + movieSchemaRaw, + northwindSchemaRaw, + simpleSchemaRaw, + twitterSchemaRaw, +} from 'libs/shared/mock-data/src'; + +describe('SchemaUsecases', () => { + test.each([ + { data: simpleSchemaRaw }, + { data: movieSchemaRaw }, + { data: northwindSchemaRaw }, + { data: twitterSchemaRaw }, + ])('parseSchemaFromBackend parsing should work', ({ data }) => { + // console.log('testinput', input); + + const parsed = SchemaUtils.ParseSchemaFromBackend( + data as SchemaFromBackend + ); + expect(parsed).toBeDefined(); + + const parsedNodeAttributes: Attributes[] = []; + parsed.forEachNode((node, attr) => { + // console.log('Node', node, attr); + parsedNodeAttributes.push(attr.attributes); + }); + + const parsedEdgeAttributes: Attributes = []; + parsed.forEachEdge((edge, attr, source, target, sa, ta, undirected) => { + // console.log('Edge', edge, attr, source, target, sa, ta, undirected); + parsedEdgeAttributes.push(attr.attribute); + }); + + expect(data.nodes.length).toEqual(parsed.order); + expect(data.edges.length).toEqual(parsed.size); + + const inputNodeAttributes: Attributes = []; + data.nodes.forEach((node) => { + inputNodeAttributes.push(node.attributes as Attributes); + }); + + const inputEdgeAttributes: Attributes = []; + data.edges.forEach((edge) => { + inputEdgeAttributes.push(edge.attributes as Attributes); + }); + + expect(inputNodeAttributes).toEqual(parsedNodeAttributes); + expect(inputEdgeAttributes).toEqual(parsedEdgeAttributes); + }); + + it('should export and reimport', () => { + const parsed = SchemaUtils.ParseSchemaFromBackend( + simpleSchemaRaw as SchemaFromBackend + ); + const reload = MultiGraph.from(parsed.export()); + + expect(parsed).toStrictEqual(reload); + }); + + test.each([ + { data: simpleSchemaRaw }, + { data: movieSchemaRaw }, + { data: northwindSchemaRaw }, + { data: twitterSchemaRaw }, + ])('should load my test json $data', ({ data }) => { + expect(data).toBeDefined(); + expect(data.nodes).toBeDefined(); + }); +}); diff --git a/libs/schema/usecases/src/lib/schema-usecases.ts b/libs/schema/usecases/src/lib/schema-usecases.ts new file mode 100644 index 0000000000000000000000000000000000000000..5efe7de805e90dcc79a9d4e468c75026b5d18899 --- /dev/null +++ b/libs/schema/usecases/src/lib/schema-usecases.ts @@ -0,0 +1,190 @@ +import Graph from 'graphology'; +import { Attributes } from 'graphology-types'; +import { Edge, Elements, Node } from 'react-flow-renderer'; + +// type CytoNode = { +// data: { +// id: string; +// type: string; +// source?: string; +// target?: string; +// position?: { +// x: number; +// y: number; +// }; +// }; +// }; + +// // Layouts a given schema +// export function handleSchemaLayout(graph: Graph): void { +// const layout = createSchemaLayout(graph); + +// layout.then((cy) => { +// //cy.cy.elements().forEach((elem) => { +// cy.cy.nodes().forEach((elem: any) => { +// const position = elem.position(); +// console.log(elem.id()); + +// graph.setNodeAttribute(elem.id(), 'x', position.x); +// graph.setNodeAttribute(elem.id(), 'y', position.y); +// }); + +// store.dispatch(setSchema(graph)); +// }); +// } + +// // Creates a schema layout (async) +// function createSchemaLayout(graph: Graph): Promise<cytoscape.EventObject> { +// const cytonodes: CytoNode[] = trimSchema(graph); + +// const cy = cytoscape({ +// elements: cytonodes, +// }); + +// const options = { +// name: 'cose', + +// // Whether to animate while running the layout +// // true : Animate continuously as the layout is running +// // false : Just show the end result +// // 'end' : Animate with the end result, from the initial positions to the end positions +// animate: true, + +// // Easing of the animation for animate:'end' +// animationEasing: undefined, + +// // The duration of the animation for animate:'end' +// animationDuration: undefined, + +// // A function that determines whether the node should be animated +// // All nodes animated by default on animate enabled +// // Non-animated nodes are positioned immediately when the layout starts +// // animateFilter: function (node: any, i: any) { +// // return true; +// // }, + +// // The layout animates only after this many milliseconds for animate:true +// // (prevents flashing on fast runs) +// animationThreshold: 250, + +// // Number of iterations between consecutive screen positions update +// refresh: 20, + +// // Whether to fit the network view after when done +// fit: true, + +// // Padding on fit +// padding: 30, + +// // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } +// boundingBox: undefined, + +// // Excludes the label when calculating node bounding boxes for the layout algorithm +// nodeDimensionsIncludeLabels: false, + +// // Randomize the initial positions of the nodes (true) or use existing positions (false) +// randomize: false, + +// // Extra spacing between components in non-compound graphs +// componentSpacing: 200, // 40 + +// // Node repulsion (non overlapping) multiplier +// nodeRepulsion: function (node: any) { +// return 2048; +// }, + +// // Node repulsion (overlapping) multiplier +// nodeOverlap: 4, + +// // Ideal edge (non nested) length +// idealEdgeLength: function (edge: any) { +// return 32; +// }, + +// // Divisor to compute edge forces +// edgeElasticity: function (edge: any) { +// return 32; +// }, + +// // Nesting factor (multiplier) to compute ideal edge length for nested edges +// nestingFactor: 1.2, + +// // Gravity force (constant) +// gravity: 1, + +// // Maximum number of iterations to perform +// numIter: 1000, + +// // Initial temperature (maximum node displacement) +// initialTemp: 1000, + +// // Cooling factor (how the temperature is reduced between consecutive iterations +// coolingFactor: 0.99, + +// // Lower temperature threshold (below this point the layout will end) +// minTemp: 1.0, +// }; + +// const layout = cy.layout(options); + +// layout.run(); + +// return layout.pon('layoutstop'); +// } + +// Takes the schema as an imput and creates basic react flow elements for them. +export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { + const initialElements: Elements<Node | Edge> = []; + + graph.forEachNode((node: string, attributes: Attributes): void => { + const newNode: Node = { + id: node, + data: { + label: attributes.name, + }, + position: { x: attributes.x, y: attributes.y }, + }; + initialElements.push(newNode); + }); + + graph.forEachEdge((edge, _attributes, source, target): void => { + const newEdge: Edge = { + id: edge, + source: source, + target: target, + }; + initialElements.push(newEdge); + }); + + return initialElements; +} + +// export function parseSchemaFromBackend( +// schemaFromBackend: SchemaFromBackend +// ): Graph { +// const { nodes, edges } = schemaFromBackend; +// // Instantiate a directed graph that allows self loops and parallel edges +// const schemaGraph = new MultiGraph({ allowSelfLoops: true }); +// // console.log('parsing schema'); +// // The graph schema needs a node for each node AND edge. These need then be connected + +// nodes.forEach((node) => { +// schemaGraph.addNode(node.name, { +// name: node.name, +// attributes: node.attributes, +// x: 0, +// y: 0, +// }); +// }); + +// // The name of the edge will be name + from + to, since edge names are not unique +// edges.forEach((edge) => { +// const edgeID = [edge.name, '_', edge.from, edge.to].join(''); //ensure that all interpreted as string + +// // This node is the actual edge +// schemaGraph.addDirectedEdgeWithKey(edgeID, edge.from, edge.to, { +// attribute: edge.attributes, +// }); +// }); +// return schemaGraph; +// } diff --git a/libs/schema/usecases/tsconfig.json b/libs/schema/usecases/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..6ebadfb9de07f71cd20ee1102f3512550505ad2a --- /dev/null +++ b/libs/schema/usecases/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true + } +} diff --git a/libs/schema/usecases/tsconfig.lib.json b/libs/schema/usecases/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..efdd77fbf5b34f06e8efa8ad8bc87e11a3c1e9af --- /dev/null +++ b/libs/schema/usecases/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/schema/usecases/tsconfig.spec.json b/libs/schema/usecases/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..d8716fecfa3b7929f162b71e7a966c579a63c071 --- /dev/null +++ b/libs/schema/usecases/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/data-access/api/.babelrc b/libs/shared/data-access/api/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..cf7ddd99c615a064ac18eb3109eee4f394ab1faf --- /dev/null +++ b/libs/shared/data-access/api/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/libs/shared/data-access/api/.eslintrc.json b/libs/shared/data-access/api/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..632e9b0e22253922989d1153e06f7ba996c72d38 --- /dev/null +++ b/libs/shared/data-access/api/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/schema/schema-usecases/README.md b/libs/shared/data-access/api/README.md similarity index 55% rename from libs/schema/schema-usecases/README.md rename to libs/shared/data-access/api/README.md index 52ab1f83a954f953decb97a40f2afd07239d9a12..3156f01c6b6befd68fe400d0c03a01d52ac3b5e7 100644 --- a/libs/schema/schema-usecases/README.md +++ b/libs/shared/data-access/api/README.md @@ -1,7 +1,7 @@ -# schema-schema-usecases +# shared-data-access-api This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test schema-schema-usecases` to execute the unit tests via [Jest](https://jestjs.io). +Run `nx test shared-data-access-api` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/data-access/api/jest.config.js b/libs/shared/data-access/api/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..d6f5553b2f38e80cf671a056bd731ff218828fc7 --- /dev/null +++ b/libs/shared/data-access/api/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'shared-data-access-api', + preset: '../../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../../coverage/libs/shared/data-access/api', +}; diff --git a/libs/schema/schema-usecases/project.json b/libs/shared/data-access/api/project.json similarity index 53% rename from libs/schema/schema-usecases/project.json rename to libs/shared/data-access/api/project.json index 39846a002e9e098d8027a092f82630812391f264..362413c86100b967aa87d647e7a416bfd1b1d8c6 100644 --- a/libs/schema/schema-usecases/project.json +++ b/libs/shared/data-access/api/project.json @@ -1,20 +1,20 @@ { - "root": "libs/schema/schema-usecases", - "sourceRoot": "libs/schema/schema-usecases/src", + "root": "libs/shared/data-access/api", + "sourceRoot": "libs/shared/data-access/api/src", "projectType": "library", "targets": { "lint": { "executor": "@nrwl/linter:eslint", "outputs": ["{options.outputFile}"], "options": { - "lintFilePatterns": ["libs/schema/schema-usecases/**/*.ts"] + "lintFilePatterns": ["libs/shared/data-access/api/**/*.ts"] } }, "test": { "executor": "@nrwl/jest:jest", - "outputs": ["coverage/libs/schema/schema-usecases"], + "outputs": ["coverage/libs/shared/data-access/api"], "options": { - "jestConfig": "libs/schema/schema-usecases/jest.config.js", + "jestConfig": "libs/shared/data-access/api/jest.config.js", "passWithNoTests": true } } diff --git a/libs/shared/data-access/api/src/index.ts b/libs/shared/data-access/api/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..136d9ebb4b0d0d0dc89d1ac6330a62d19d681908 --- /dev/null +++ b/libs/shared/data-access/api/src/index.ts @@ -0,0 +1 @@ +export { User, GetUserInfo } from './lib/user'; diff --git a/libs/shared/data-access/api/src/lib/database.ts b/libs/shared/data-access/api/src/lib/database.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d6700de6f55dde670227d1cedf1080c44637e28 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/database.ts @@ -0,0 +1,75 @@ +// All database related API calls + +import { AuthorizationHandler } from '@graphpolaris/shared/data-access/authorization'; + +export type AddDatabaseRequest = { + name: string; + internal_database_name: string; + url: string; + port: number; + username: string; + password: string; + type: number; // Database type. 0 = ArangoDB, 1 = Neo4j +}; + +export function AddDatabase(request: AddDatabaseRequest): Promise<void> { + return new Promise((resolve, reject) => { + fetch('https://datastrophe.science.uu.nl/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); + } + + resolve(); + }); + }); +} + +export function GetAllDatabases(): Promise<Array<string>> { + return new Promise<Array<string>>((resolve, reject) => { + fetch('https://datastrophe.science.uu.nl/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) => { + resolve(json.databases); + }); + }); +} + +export function DeleteDatabase(name: string): Promise<void> { + return new Promise((resolve, reject) => { + fetch('https://datastrophe.science.uu.nl/user/database/' + name, { + method: 'DELETE', + credentials: 'same-origin', + headers: new Headers({ + Authorization: + 'Bearer ' + AuthorizationHandler.instance().AccessToken(), + }), + }).then((response: Response) => { + if (!response.ok) { + reject(response.statusText); + } + + resolve(); + }); + }); +} diff --git a/libs/shared/data-access/api/src/lib/user.ts b/libs/shared/data-access/api/src/lib/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..457ce20f6af6c8be31727f2b9757134827b4fc70 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/user.ts @@ -0,0 +1,36 @@ +// All user related API calls + +import { AuthorizationHandler } from '@graphpolaris/shared/data-access/authorization'; + +export type User = { + Name: string; + Email: string; + SignInProvider: number; +}; + +export function GetUserInfo(): Promise<User> { + return new Promise<User>((resolve, reject) => { + fetch('https://datastrophe.science.uu.nl/user/', { + 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) => { + resolve({ + Name: json.name, + Email: json.email, + SignInProvider: json.sign_in_provider, + }); + }); + }); +} diff --git a/libs/shared/data-access/api/tsconfig.json b/libs/shared/data-access/api/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..d0953a0f8b8084fd7ca99de4a00f82435f8679cd --- /dev/null +++ b/libs/shared/data-access/api/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/libs/shared/data-access/api/tsconfig.lib.json b/libs/shared/data-access/api/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..2ef844c42b4d526dd97ef3b18591cc5c652781e5 --- /dev/null +++ b/libs/shared/data-access/api/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/shared/data-access/api/tsconfig.spec.json b/libs/shared/data-access/api/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..315a5b0bbebaca96617a8dd5353901287ebd8e68 --- /dev/null +++ b/libs/shared/data-access/api/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/data-access/authorization/.babelrc b/libs/shared/data-access/authorization/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..cf7ddd99c615a064ac18eb3109eee4f394ab1faf --- /dev/null +++ b/libs/shared/data-access/authorization/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/libs/shared/data-access/authorization/.eslintrc.json b/libs/shared/data-access/authorization/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..632e9b0e22253922989d1153e06f7ba996c72d38 --- /dev/null +++ b/libs/shared/data-access/authorization/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/data-access/authorization/README.md b/libs/shared/data-access/authorization/README.md new file mode 100644 index 0000000000000000000000000000000000000000..009a2ead17f3f87e2bc4b246ad9ceae1423f9813 --- /dev/null +++ b/libs/shared/data-access/authorization/README.md @@ -0,0 +1,7 @@ +# shared-data-access-authorization + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-data-access-authorization` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/data-access/authorization/jest.config.js b/libs/shared/data-access/authorization/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..6bae255b477a179c550eade1547895b0cd8372d0 --- /dev/null +++ b/libs/shared/data-access/authorization/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'shared-data-access-authorization', + preset: '../../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: + '../../../../coverage/libs/shared/data-access/authorization', +}; diff --git a/libs/shared/data-access/authorization/project.json b/libs/shared/data-access/authorization/project.json new file mode 100644 index 0000000000000000000000000000000000000000..7225438fd977b6401038fbabfffa797328d1b7c1 --- /dev/null +++ b/libs/shared/data-access/authorization/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/shared/data-access/authorization", + "sourceRoot": "libs/shared/data-access/authorization/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/data-access/authorization/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/data-access/authorization"], + "options": { + "jestConfig": "libs/shared/data-access/authorization/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/shared/data-access/authorization/src/index.ts b/libs/shared/data-access/authorization/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0309d93b41a3f18d0294efe7999d9038b98f42ee --- /dev/null +++ b/libs/shared/data-access/authorization/src/index.ts @@ -0,0 +1 @@ +export { AuthorizationHandler } from './lib/authorizationHandler'; diff --git a/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts b/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc92922419ad609e85767e55dd35b06b2a581012 --- /dev/null +++ b/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts @@ -0,0 +1,184 @@ +import { Cookies } from 'react-cookie'; + +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://datastrophe.science.uu.nl/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 ?? ''; + + // Initialise the new refresh token + this.initialiseRefreshToken(); + } + } + + /** + * initialiseRefreshToken attempts to initialise a refresh token + */ + private async initialiseRefreshToken() { + fetch('https://datastrophe.science.uu.nl/auth/refresh', { + method: 'POST', + 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; + + console.log(this.accessToken); + + // Activate the refresh token + this.initialiseRefreshToken(); + + // 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/data-access/authorization/tsconfig.json b/libs/shared/data-access/authorization/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..d0953a0f8b8084fd7ca99de4a00f82435f8679cd --- /dev/null +++ b/libs/shared/data-access/authorization/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/libs/shared/data-access/authorization/tsconfig.lib.json b/libs/shared/data-access/authorization/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..2ef844c42b4d526dd97ef3b18591cc5c652781e5 --- /dev/null +++ b/libs/shared/data-access/authorization/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/shared/data-access/authorization/tsconfig.spec.json b/libs/shared/data-access/authorization/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..315a5b0bbebaca96617a8dd5353901287ebd8e68 --- /dev/null +++ b/libs/shared/data-access/authorization/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/data-access/store/src/index.ts b/libs/shared/data-access/store/src/index.ts index 125019fc2b523e117ccb6d365f1382b58fe68eac..3e2aec2be9ea77de09c1c598a15a1ef30fe68713 100644 --- a/libs/shared/data-access/store/src/index.ts +++ b/libs/shared/data-access/store/src/index.ts @@ -5,7 +5,14 @@ export { setSchema, readInSchemaFromBackend, schemaSlice, + selectSchemaLayout } from './lib/schemaSlice'; +export { + querybuilderSlice, + setQuerybuilderNodes, + updateQBAttributeOperator, + updateQBAttributeValue, +} from './lib/querybuilderSlice'; export { selectGraphQueryResult, selectGraphQueryResultLinks, @@ -17,7 +24,9 @@ export { export { changePrimary, changeDataPointColors, + toggleDarkMode, selectColorPaletteConfig, + colorPaletteConfigSlice, } from './lib/colorPaletteConfigSlice'; // Exported types @@ -26,4 +35,3 @@ export type { ColorPaletteConfig, ExtraColorsForMui5, } from './lib/colorPaletteConfigSlice'; -export type { SchemaFromBackend } from './lib/schemaSlice'; diff --git a/libs/shared/data-access/store/src/lib/colorPaletteConfigSlice.ts b/libs/shared/data-access/store/src/lib/colorPaletteConfigSlice.ts index b311ccee957853b4e5b1a57b7b80f8d2f15c6d13..64d340262ec623a4f427a7c5e149c350beaff8d5 100644 --- a/libs/shared/data-access/store/src/lib/colorPaletteConfigSlice.ts +++ b/libs/shared/data-access/store/src/lib/colorPaletteConfigSlice.ts @@ -1,26 +1,117 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -/** Extra properties that are not present in the default PaletteOptions of mui. */ +/** Extra properties that are not present in the default PaletteOptions of mui. For autocompletion */ export interface ExtraColorsForMui5 { /** Colors that can be used for data visualisation, e.g. nodes, edges */ dataPointColors: string[]; + + queryBuilder: { + text: string; + entity: { + background: string; + lighterbg?: string; + }; + relation: { + background: string; + lighterbg?: string; + }; + attribute: { + background: string; + lighterbg?: string; + }; + }; } -/** Our custom color palette config. */ -export interface ColorPaletteConfig extends ExtraColorsForMui5 { - primary: { - main: string; +/** Our custom color palette config. With the palette options from MUI that we are going to use. */ +export interface ColorPaletteConfig { + lightPalette: { + // Custom colors + custom: ExtraColorsForMui5; + // MUI colors + primary: { + light?: string; + main: string; + dark?: string; + }; + secondary: { + light?: string; + main: string; + dark?: string; + }; + }; + darkPalette: { + custom: ExtraColorsForMui5; + primary: { + light?: string; + main: string; + dark?: string; + }; + secondary: { + light?: string; + main: string; + dark?: string; + }; }; + darkMode: boolean; } // This looks very much like the mui Palette interface, // But we don't reference that type directly here to stay decoupled export const initialState: ColorPaletteConfig = { - dataPointColors: ['#ff0000', '#00ff00', '#0000ff'], - primary: { - main: '#9999ff', + lightPalette: { + custom: { + dataPointColors: ['#ff0000', '#00ff00', '#0000ff'], + queryBuilder: { + text: 'black', + entity: { + background: '#FC4F4F', + }, + relation: { + background: '#FF9F45', + }, + attribute: { + background: '#C7C7C7', + }, + }, + }, + // If light and dark are not set, these will be calculated using main. + // light/dark have nothing with darkmode, these are just a light and dark variation + primary: { + // light: '#42a5f5', + main: '#1976d2', + // dark: '#1565c0', + }, + secondary: { + // light: '#ba68c8', + main: '#9c27b0', + // dark: '#7b1fa2', + }, }, + darkPalette: { + custom: { + dataPointColors: ['#ff0000', '#00ff00', '#0000ff'], + queryBuilder: { + text: 'black', + entity: { + background: '#FC4F4F', + }, + relation: { + background: '#FF9F45', + }, + attribute: { + background: '#C7C7C7', + }, + }, + }, + primary: { + main: '#e3f3fd', + }, + secondary: { + main: '#f3e5f5', + }, + }, + darkMode: false, }; export const colorPaletteConfigSlice = createSlice({ @@ -28,16 +119,33 @@ export const colorPaletteConfigSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - changePrimary: (state, action: PayloadAction<{ main: string }>) => { - state.primary = action.payload; + changePrimary: ( + state, + action: PayloadAction<{ + main?: string; + light?: string; + dark?: string; + darkMode?: 'light' | 'dark'; + }> + ) => { + const { main, light, dark, darkMode } = action.payload; + let palette = state.lightPalette; + if (darkMode == 'dark') palette = state.darkPalette; + + if (main) palette.primary.main = main; + if (light) palette.primary.light = light; + if (dark) palette.primary.dark = dark; }, changeDataPointColors: (state, action: PayloadAction<string[]>) => { - state.dataPointColors = action.payload; + state.lightPalette.custom.dataPointColors = action.payload; + }, + toggleDarkMode: (state) => { + state.darkMode = !state.darkMode; }, }, }); -export const { changePrimary, changeDataPointColors } = +export const { changePrimary, changeDataPointColors, toggleDarkMode } = colorPaletteConfigSlice.actions; // Select the schema and convert it to a graphology object diff --git a/libs/shared/data-access/store/src/lib/hooks.ts b/libs/shared/data-access/store/src/lib/hooks.ts index df0faef46e155463bea444790b3ba91dcc076b76..6b67cbead6799c62e78c19c23f58830593e2d195 100644 --- a/libs/shared/data-access/store/src/lib/hooks.ts +++ b/libs/shared/data-access/store/src/lib/hooks.ts @@ -1,14 +1,20 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { selectGraphQueryResult } from './graphQueryResultSlice'; -import { selectSchema } from './schemaSlice'; +import { selectSchema, selectSchemaLayout } from './schemaSlice'; +import { selectQuerybuilderNodes } from './querybuilderSlice'; import type { RootState, AppDispatch } from './store'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; -// Gives the graphQueryResult from the store +/** Gives the graphQueryResult from the store */ export const useGraphQueryResult = () => useAppSelector(selectGraphQueryResult); // Gives the schema form the store (as a graphology object) export const useSchema = () => useAppSelector(selectSchema); + +// Gives the schema form the store (as a graphology object) +export const useSchemaLayout = () => useAppSelector(selectSchemaLayout); +export const useQuerybuilderNodes = () => + useAppSelector(selectQuerybuilderNodes); diff --git a/libs/shared/data-access/store/src/lib/querybuilderSlice.ts b/libs/shared/data-access/store/src/lib/querybuilderSlice.ts new file mode 100644 index 0000000000000000000000000000000000000000..877444a06b9ebe70c06a866e9ecc6d28efc001b5 --- /dev/null +++ b/libs/shared/data-access/store/src/lib/querybuilderSlice.ts @@ -0,0 +1,79 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './store'; +import { MultiGraph } from 'graphology'; +import { Attributes, SerializedGraph } from 'graphology-types'; + +// Define the initial state using that type +export const initialState = { + graphologySerialized: new MultiGraph().export(), + // schemaLayout: 'Graphology_noverlap', +}; + +export const querybuilderSlice = createSlice({ + name: 'querybuilder', + // `createSlice` will infer the state type from the `initialState` argument + initialState, + reducers: { + setQuerybuilderNodes: ( + state, + action: PayloadAction<SerializedGraph<Attributes, Attributes, Attributes>> + ) => { + state.graphologySerialized = action.payload; + }, + updateQBAttributeOperator: ( + state, + action: PayloadAction<{ id: string; operator: string }> + ) => { + const graph = MultiGraph.from(state.graphologySerialized); + graph.setNodeAttribute( + action.payload.id, + 'operator', + action.payload.operator + ); + state.graphologySerialized = graph.export(); + }, + updateQBAttributeValue: ( + state, + action: PayloadAction<{ id: string; value: string }> + ) => { + const graph = MultiGraph.from(state.graphologySerialized); + graph.setNodeAttribute(action.payload.id, 'value', action.payload.value); + state.graphologySerialized = graph.export(); + }, + // addQuerybuilderNode: ( + // state, + // action: PayloadAction<{ id: string; attributes: Attributes }> + // ) => { + // const graph = MultiGraph.from(state.graphologySerialized); + // graph.addNode(action.payload.id, action.payload.attributes); + // state.graphologySerialized = graph.export(); + // }, + // setGraphLayout: (state, action: PayloadAction<AllLayoutAlgorithms>) => { + // state.schemaLayout = action.payload; + // }, + }, +}); + +export const { + setQuerybuilderNodes, + updateQBAttributeOperator, + updateQBAttributeValue, +} = querybuilderSlice.actions; + +/** Select the querybuilder nodes and convert it to a graphology object */ +export const selectQuerybuilderNodes = (state: RootState): MultiGraph => { + // This is really weird but for some reason all the attributes appeared as read-only otherwise + return MultiGraph.from( + MultiGraph.from(state.querybuilder.graphologySerialized).export() + ); +}; + +// /** +// * selects the SchemaLayout enum +// * @param {GraphLayout} state +// * @returns {GraphLayout} enum of type GraphLayout +// */ +// export const selectSchemaLayout = (state: RootState) => +// state.schema.schemaLayout; + +export default querybuilderSlice.reducer; diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts b/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a6b17a5275478904fd98b29d47ab2a6708062eb --- /dev/null +++ b/libs/shared/data-access/store/src/lib/schemaSlice.spec.ts @@ -0,0 +1,94 @@ +import Graph from 'graphology'; +import AbstractGraph, { DirectedGraph, MultiGraph } from 'graphology'; +import { useSchema } from '..'; +import reducer, { selectSchema, setSchema, initialState } from './schemaSlice'; +// import { deleteBook, updateBook, addNewBook } from '../redux/bookSlice'; +import { store } from './store'; + +describe('SchemaSlice Tests', () => { + it('should make a graphology graph', () => { + const graph = new MultiGraph({ allowSelfLoops: true }); + expect(graph); + + const graph2 = new MultiGraph(); + expect(graph2); + + const exported = graph.export(); + expect(exported); + }); + + it('export and reimport equality check for graphology graph', () => { + const graph = new MultiGraph({ allowSelfLoops: true }); + expect(graph); + const exported = graph.export(); + expect(exported); + + const graphReloaded = MultiGraph.from(exported); + expect(graphReloaded).toStrictEqual(graph); + }); + + it('get the initial state', () => { + expect(initialState); + }); + + it('should return the initial state', () => { + let state = store.getState(); + + const schema = state.schema; + expect(schema); + + const graph = MultiGraph.from(schema.graphologySerialized); + + // console.log(graph); + // console.log(initialState); + expect(graph); + }); + + // it('should handle a todo being added to an empty list', () => { + // let state = store.getState().schema; + // let schema = useSchema(); + + // // const unchangedBook = state.bookList.find((book) => book.id === '1'); + // // expect(unchangedBook?.title).toBe('1984'); + // // expect(unchangedBook?.author).toBe('George Orwell'); + + // // store.dispatch(updateBook({ id: '1', title: '1985', author: 'George Bush' })); + // // state = store.getState().book; + // // let changeBook = state.bookList.find((book) => book.id === '1'); + // // expect(changeBook?.title).toBe('1985'); + // // expect(changeBook?.author).toBe('George Bush'); + + // // store.dispatch( + // // updateBook({ id: '1', title: '1984', author: 'George Orwell' }) + // // ); + // // state = store.getState().book; + // // const backToUnchangedBook = state.bookList.find((book) => book.id === '1'); + + // // expect(backToUnchangedBook).toEqual(unchangedBook); + // // ]); + // }); +}); + +// test('Deletes a book from list with id', () => { +// let state = store.getState().book; +// const initialBookCount = state.bookList.length; + +// store.dispatch(deleteBook({ id: '1' })); +// state = store.getState().book; + +// expect(state.bookList.length).toBeLessThan(initialBookCount); // Checking if new length smaller than inital length, which is 3 +// }); + +// test('Adds a new book', () => { +// let state = store.getState().book; +// const initialBookCount = state.bookList.length; + +// store.dispatch( +// addNewBook({ id: '4', author: 'Tester', title: 'Testers manual' }) +// ); +// state = store.getState().book; +// const newlyAddedBook = state.bookList.find((book) => book.id === '4'); +// expect(newlyAddedBook?.author).toBe('Tester'); +// expect(newlyAddedBook?.title).toBe('Testers manual'); +// expect(state.bookList.length).toBeGreaterThan(initialBookCount); +// }); diff --git a/libs/shared/data-access/store/src/lib/schemaSlice.ts b/libs/shared/data-access/store/src/lib/schemaSlice.ts index b44303d1cb77b4b501360d90f251d4ea388f2b0c..9ff253e0a0611c3f3ff91edef3f19a6d015aaf73 100644 --- a/libs/shared/data-access/store/src/lib/schemaSlice.ts +++ b/libs/shared/data-access/store/src/lib/schemaSlice.ts @@ -1,40 +1,15 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import Graph, { MultiGraph } from 'graphology'; +import { SerializedGraph } from 'graphology-types'; +import { SchemaFromBackend } from 'libs/shared/models/src'; import type { RootState } from './store'; -import Graph from 'graphology'; -/*************** schema format from the backend *************** */ -// TODO: should probably not live here -/** Schema type, consist of nodes and edges */ -export type SchemaFromBackend = { - edges: Edge[]; - nodes: Node[]; -}; - -/** Attribute type, consist of a name */ -export type Attribute = { - name: string; - type: 'string' | 'int' | 'bool' | 'float'; -}; - -/** Node type, consist of a name and a list of attributes */ -export type Node = { - name: string; - attributes: Attribute[]; -}; - -/** Edge type, consist of a name, start point, end point and a list of attributes */ -export type Edge = { - name: string; - to: string; - from: string; - collection: string; - attributes: Attribute[]; -}; /**************************************************************** */ // Define the initial state using that type export const initialState = { - graphologySerialized: new Graph().export(), + graphologySerialized: new MultiGraph().export(), + layoutName: 'Cytoscape_fcose', }; export const schemaSlice = createSlice({ @@ -42,8 +17,15 @@ export const schemaSlice = createSlice({ // `createSlice` will infer the state type from the `initialState` argument initialState, reducers: { - setSchema: (state, action: PayloadAction<Graph>) => { - state.graphologySerialized = action.payload.export(); + setSchema: (state, action: PayloadAction<SerializedGraph>) => { + console.log('setSchema', action); + state.graphologySerialized = action.payload; + }, + + setSchemaLayout: (state, action: PayloadAction<string>) => { + console.log('setSchemaLayout', action); + + state.layoutName = action.payload; }, readInSchemaFromBackend: ( @@ -52,38 +34,38 @@ export const schemaSlice = createSlice({ ) => { const { nodes, edges } = action.payload; // Instantiate a directed graph that allows self loops and parallel edges - const schema = new Graph({ allowSelfLoops: true, multi: true }); + const schema = new MultiGraph({ allowSelfLoops: true }); console.log('Updating schema'); // The graph schema needs a node for each node AND edge. These need then be connected - nodes.forEach((node) => { - schema.addNode(node.name, { - name: node.name, - attributes: node.attributes, - x: 0, - y: 0, - }); - }); + // nodes.forEach((node) => { + // schema.addNode(node.name, { + // name: node.name, + // attributes: node.attributes, + // x: 0, + // y: 0, + // }); + // }); - // The name of the edge will be name + from + to, since edge names are not unique - edges.forEach((edge) => { - const edgeID = edge.name + edge.from + edge.to; + // // The name of the edge will be name + from + to, since edge names are not unique + // edges.forEach((edge) => { + // const edgeID = edge.name + edge.from + edge.to; - // This node is the actual edge - schema.addNode(edgeID, { - name: edge.name, - attributes: edge.attributes, - from: edge.from, - to: edge.to, - collection: edge.collection, - x: 0, - y: 0, - }); + // // This node is the actual edge + // schema.addNode(edgeID, { + // name: edge.name, + // attributes: edge.attributes, + // from: edge.from, + // to: edge.to, + // collection: edge.collection, + // x: 0, + // y: 0, + // }); - // These lines are simply for keeping the schema together - schema.addDirectedEdgeWithKey(edgeID + 'f', edge.from, edgeID); - schema.addDirectedEdgeWithKey(edgeID + 't', edgeID, edge.to); - }); + // // These lines are simply for keeping the schema together + // schema.addDirectedEdgeWithKey(edgeID + 'f', edge.from, edgeID); + // schema.addDirectedEdgeWithKey(edgeID + 't', edgeID, edge.to); + // }); state.graphologySerialized = schema.export(); }, @@ -92,8 +74,21 @@ export const schemaSlice = createSlice({ export const { readInSchemaFromBackend, setSchema } = schemaSlice.actions; -// Select the schema and convert it to a graphology object -export const selectSchema = (state: RootState) => - Graph.from(state.schema.graphologySerialized); +/** + * Select the schema and convert it to a graphology object + * */ +export const selectSchema = (state: RootState) => { + // This is really weird but for some reason all the attributes appeared as read-only otherwise + return MultiGraph.from( + MultiGraph.from(state.schema.graphologySerialized).export() + ); +}; + +// /** +// * selects the SchemaLayout enum +// * @param {GraphLayout} state +// * @returns {GraphLayout} enum of type GraphLayout +// */ +export const selectSchemaLayout = (state: RootState) => state.schema.layoutName; export default schemaSlice.reducer; diff --git a/libs/shared/data-access/store/src/lib/store.ts b/libs/shared/data-access/store/src/lib/store.ts index 7198337bde3e8b98b11d6336552743c71d8e413a..30a41ddb43d55a86517bb818054405da4e45daf6 100644 --- a/libs/shared/data-access/store/src/lib/store.ts +++ b/libs/shared/data-access/store/src/lib/store.ts @@ -1,6 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; import colorPaletteConfigSlice from './colorPaletteConfigSlice'; import graphQueryResultSlice from './graphQueryResultSlice'; +import querybuilderSlice from './querybuilderSlice'; import schemaSlice from './schemaSlice'; export const store = configureStore({ @@ -8,6 +9,7 @@ export const store = configureStore({ graphQueryResult: graphQueryResultSlice, schema: schemaSlice, colorPaletteConfig: colorPaletteConfigSlice, + querybuilder: querybuilderSlice, }, }); diff --git a/libs/shared/data-access/store/tsconfig.lib.json b/libs/shared/data-access/store/tsconfig.lib.json index 2ef844c42b4d526dd97ef3b18591cc5c652781e5..94c28cbd1b3e19a79ac8bca76709896f18c34531 100644 --- a/libs/shared/data-access/store/tsconfig.lib.json +++ b/libs/shared/data-access/store/tsconfig.lib.json @@ -5,6 +5,6 @@ "declaration": true, "types": [] }, - "include": ["**/*.ts"], + "include": ["**/*.ts", "../theme/src/lib/colorPaletteConfigSlice.ts"], "exclude": ["**/*.spec.ts"] } diff --git a/libs/shared/data-access/theme/src/index.ts b/libs/shared/data-access/theme/src/index.ts index fae1f6948cf3d9f4fd40ea0c309bf39ea9f07f89..009824eca141e2b77653f611d6e8a03899061be2 100644 --- a/libs/shared/data-access/theme/src/index.ts +++ b/libs/shared/data-access/theme/src/index.ts @@ -1 +1 @@ -export * from './lib/ourThemeProvider'; +export * from './lib/graphPolarisThemeProvider'; diff --git a/libs/shared/data-access/theme/src/lib/ourThemeProvider.tsx b/libs/shared/data-access/theme/src/lib/graphPolarisThemeProvider.tsx similarity index 89% rename from libs/shared/data-access/theme/src/lib/ourThemeProvider.tsx rename to libs/shared/data-access/theme/src/lib/graphPolarisThemeProvider.tsx index 04f36156807eafc3dcf4d4f569da172f00792d38..3a1512f8c63af1aba262414799df202e365292b9 100644 --- a/libs/shared/data-access/theme/src/lib/ourThemeProvider.tsx +++ b/libs/shared/data-access/theme/src/lib/graphPolarisThemeProvider.tsx @@ -19,7 +19,11 @@ declare module '@mui/material/styles' { } } -export function OurThemeProvider({ children }: { children: ReactNode }) { +export function GraphPolarisThemeProvider({ + children, +}: { + children: ReactNode; +}) { const colorPaletteConfig = useAppSelector(selectColorPaletteConfig); // Create a new theme when our custom color palette in redux changed @@ -32,4 +36,4 @@ export function OurThemeProvider({ children }: { children: ReactNode }) { return <ThemeProvider theme={theme}>{children}</ThemeProvider>; } -export default OurThemeProvider; +export default GraphPolarisThemeProvider; diff --git a/libs/shared/data-access/theme/src/lib/mapColorsConfigToMuiTheme.ts b/libs/shared/data-access/theme/src/lib/mapColorsConfigToMuiTheme.ts index f94556c8842f61c8588bf38aa89295ed3920bc2b..aa9a6de51d703d3d79d1c03ab6f573e24d5e5cf4 100644 --- a/libs/shared/data-access/theme/src/lib/mapColorsConfigToMuiTheme.ts +++ b/libs/shared/data-access/theme/src/lib/mapColorsConfigToMuiTheme.ts @@ -1,5 +1,6 @@ import { ColorPaletteConfig } from '@graphpolaris/shared/data-access/store'; import { ThemeOptions } from '@mui/material/styles'; +import Color from 'color'; /** Maps our color palette config (stored in redux) to the mui 5 theme format */ export default function MapColorsConfigToMuiTheme( @@ -7,8 +8,78 @@ export default function MapColorsConfigToMuiTheme( ): ThemeOptions { return { palette: { - primary: { main: colorsConfig.primary.main }, - dataPointColors: colorsConfig.dataPointColors, + mode: colorsConfig.darkMode ? 'dark' : 'light', + + ...(colorsConfig.darkMode + ? { + primary: colorsConfig.darkPalette.primary, + secondary: colorsConfig.darkPalette.secondary, + dataPointColors: colorsConfig.darkPalette.custom.dataPointColors, + queryBuilder: { + text: colorsConfig.darkPalette.custom.queryBuilder.text, + entity: { + background: + colorsConfig.darkPalette.custom.queryBuilder.entity + .background, + lighterbg: Color( + colorsConfig.darkPalette.custom.queryBuilder.entity.background + ) + .lighten(0.2) + .toString(), + }, + relation: { + background: + colorsConfig.darkPalette.custom.queryBuilder.relation + .background, + lighterbg: Color( + colorsConfig.darkPalette.custom.queryBuilder.relation + .background + ) + .lighten(0.2) + .toString(), + }, + attribute: { + background: + colorsConfig.darkPalette.custom.queryBuilder.attribute + .background, + }, + }, + } + : { + primary: colorsConfig.lightPalette.primary, + secondary: colorsConfig.lightPalette.secondary, + dataPointColors: colorsConfig.lightPalette.custom.dataPointColors, + queryBuilder: { + text: colorsConfig.lightPalette.custom.queryBuilder.text, + entity: { + background: + colorsConfig.lightPalette.custom.queryBuilder.entity + .background, + lighterbg: Color( + colorsConfig.lightPalette.custom.queryBuilder.entity + .background + ) + .lighten(0.2) + .toString(), + }, + relation: { + background: + colorsConfig.lightPalette.custom.queryBuilder.relation + .background, + lighterbg: Color( + colorsConfig.lightPalette.custom.queryBuilder.relation + .background + ) + .lighten(0.2) + .toString(), + }, + attribute: { + background: + colorsConfig.lightPalette.custom.queryBuilder.attribute + .background, + }, + }, + }), }, }; } diff --git a/libs/shared/data-access/theme/tsconfig.lib.json b/libs/shared/data-access/theme/tsconfig.lib.json index 1ab8e06a7111b168e84debca80bb4ac82f72f2aa..5286d47e4655c882bd8544a2e154bb8cf419a203 100644 --- a/libs/shared/data-access/theme/tsconfig.lib.json +++ b/libs/shared/data-access/theme/tsconfig.lib.json @@ -18,5 +18,11 @@ "**/*.spec.jsx", "**/*.test.jsx" ], - "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] + "include": [ + "**/*.js", + "**/*.jsx", + "**/*.ts", + "**/*.tsx", + "../store/src/lib/colorPaletteConfigSlice.ts" + ] } diff --git a/libs/shared/graph-layout/.babelrc b/libs/shared/graph-layout/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..ccae900be42788285eb6e1b3a2af4b81f56f14fe --- /dev/null +++ b/libs/shared/graph-layout/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/graph-layout/.eslintrc.json b/libs/shared/graph-layout/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..50e59482cfd41dfef0c19bd2027efcfb182f6bc2 --- /dev/null +++ b/libs/shared/graph-layout/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/graph-layout/.gitignore b/libs/shared/graph-layout/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6dd3a48abe9fd65f8a96bc29dc00b1cedab442f8 --- /dev/null +++ b/libs/shared/graph-layout/.gitignore @@ -0,0 +1,171 @@ + +<<<<<<< HEAD +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node +======= +# Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode +>>>>>>> feat/refactor-schema-panel + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope + +# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode diff --git a/libs/shared/graph-layout/README.md b/libs/shared/graph-layout/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9925976b0df4b219612f8e6c9c29f0517911ed7c --- /dev/null +++ b/libs/shared/graph-layout/README.md @@ -0,0 +1,7 @@ +# shared-graph-layout + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-graph-layout` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/graph-layout/jest.config.js b/libs/shared/graph-layout/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..ae3d05af50147b0e99ce13f1af409e0ca0752c64 --- /dev/null +++ b/libs/shared/graph-layout/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + displayName: 'shared-graph-layout', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/shared/graph-layout', +}; diff --git a/libs/shared/graph-layout/package.json b/libs/shared/graph-layout/package.json new file mode 100644 index 0000000000000000000000000000000000000000..52928d5e97f1f340d2b653e45eedea657b842575 --- /dev/null +++ b/libs/shared/graph-layout/package.json @@ -0,0 +1,22 @@ +{ + "name": "@graphpolaris/graph-layout", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "cytoscape": "^3.21.0", + "cytoscape-cise": "^1.0.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-dagre": "^2.4.0", + "cytoscape-elk": "^2.0.2", + "cytoscape-fcose": "^2.1.0", + "cytoscape-klay": "^3.1.4", + "graphology": "^0.24.1", + "graphology-layout": "^0.5.0", + "graphology-layout-forceatlas2": "^0.8.2", + "graphology-layout-noverlap": "^0.4.2", + "web-worker": "^1.2.0" + }, + "devDependencies": { + "graphology-generators": "^0.11.2" + } +} diff --git a/libs/shared/graph-layout/project.json b/libs/shared/graph-layout/project.json new file mode 100644 index 0000000000000000000000000000000000000000..2dff301f3f3d902bcd8817ad261a8213e573c2ba --- /dev/null +++ b/libs/shared/graph-layout/project.json @@ -0,0 +1,43 @@ +{ + "root": "libs/shared/graph-layout", + "sourceRoot": "libs/shared/graph-layout/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/web:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/shared/graph-layout", + "tsConfig": "libs/shared/graph-layout/tsconfig.lib.json", + "project": "libs/shared/graph-layout/package.json", + "entryFile": "libs/shared/graph-layout/src/index.ts", + "external": ["react/jsx-runtime"], + "rollupConfig": "@nrwl/react/plugins/bundle-rollup", + "compiler": "babel", + "assets": [ + { + "glob": "libs/shared/graph-layout/README.md", + "input": ".", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/graph-layout/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/graph-layout"], + "options": { + "jestConfig": "libs/shared/graph-layout/jest.config.js", + "passWithNoTests": true + } + } + } +} diff --git a/libs/shared/graph-layout/src/index.ts b/libs/shared/graph-layout/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dda465c87a51929594b4caa5f6a8a721757742a0 --- /dev/null +++ b/libs/shared/graph-layout/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/layout'; +export * from './lib/graphology-layouts'; +export * from './lib/cytoscape-layouts'; +export * from './lib/layout-creator-usecase'; diff --git a/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5190896b5916d8652368bff7f39477f62b17a47 --- /dev/null +++ b/libs/shared/graph-layout/src/lib/cytoscape-layouts.ts @@ -0,0 +1,576 @@ +import cytoscape from 'cytoscape'; +// @ts-ignore +import cise from 'cytoscape-cise'; +// @ts-ignore +import coseBilkent from 'cytoscape-cose-bilkent'; +// @ts-ignore +import dagre from 'cytoscape-dagre'; +// @ts-ignore +import elk from 'cytoscape-elk'; +// @ts-ignore +import fcose from 'cytoscape-fcose'; +// @ts-ignore +import klay from 'cytoscape-klay'; +cytoscape.use(elk); + +import Graph from 'graphology'; +import { Attributes } from 'graphology-types'; +import { Layout } from './layout'; +import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase'; + +cytoscape.use(klay); + +export type CytoscapeProvider = 'Cytoscape'; + +export type CytoscapeLayoutAlgorithms = + | `${CytoscapeProvider}_klay` + | `${CytoscapeProvider}_dagre` + | `${CytoscapeProvider}_elk` + | `${CytoscapeProvider}_fcose` + | `${CytoscapeProvider}_cose-bilkent` + | `${CytoscapeProvider}_cise`; + +type CytoNode = { + data: { + id: string; + type?: string; + source?: string; + target?: string; + position?: { + x: number; + y: number; + }; + label?: string; + count?: number; + color?: string; + }; +}; +/** + * This is the Cytoscape Factory + */ +export class CytoscapeFactory + implements ILayoutFactory<CytoscapeLayoutAlgorithms> +{ + createLayout(LayoutAlgorithm: CytoscapeLayoutAlgorithms): Cytoscape | null { + switch (LayoutAlgorithm) { + case 'Cytoscape_klay': + //https://github.com/cytoscape/cytoscape.js-klay + return new CytoscapeKlay(); + case 'Cytoscape_dagre': + //https://github.com/cytoscape/cytoscape.js-dagre + return new CytoscapeDagre(); + case 'Cytoscape_elk': + //https://github.com/cytoscape/cytoscape.js-elk + return new CytoscapeElk(); + + case 'Cytoscape_fcose': + //https://github.com/iVis-at-Bilkent/cytoscape.js-fcose + return new CytoscapeFCose(); + + case 'Cytoscape_cose-bilkent': + //https://github.com/cytoscape/cytoscape.js-cose-bilkent + return new CytoscapeCoseBilkent(); + + case 'Cytoscape_cise': + //https://github.com/iVis-at-Bilkent/cytoscape.js-cise + return new CytoscapeCise(); + default: + return null; + } + } +} + +export abstract class Cytoscape extends Layout<CytoscapeProvider> { + public verbose = false; + + constructor(public override algorithm: LayoutAlgorithm<CytoscapeProvider>) { + super('Cytoscape', algorithm); + } + + // public specialCytoscapeFunction() { + // console.log('Only Cytoscape Layouts can do this.'); + // } + + // Takes the schema as input and creates a list of nodes and edges in a format that the layouting algorithm can use. + public convertToCytoscapeModel(graph: Graph): CytoNode[] { + const cytonodes: CytoNode[] = []; + + graph.forEachNode((node) => { + cytonodes.push({ + data: { + id: node, + // type: 'node', + label: 'start', + count: 50, + color: 'green', + }, + }); + }); + + graph.forEachEdge((edge, _attributes, source, target) => { + cytonodes.push({ + data: { + id: edge, + // type: 'edge', + source: source, + target: target, + }, + }); + }); + + return cytonodes; + } + + public setVerbose(verbose: boolean) { + this.verbose = verbose; + } + + makeCytoscapeInstance(cytonodes: CytoNode[]) { + return cytoscape({ + elements: cytonodes, + + headless: true, + styleEnabled: true, + + style: [ + // the stylesheet for the graph + { + selector: 'node', + style: { + label: 'data(id)', + width: (node: any) => { + const ctx = document.createElement('canvas').getContext('2d'); + const fStyle = node.pstyle('font-style').strValue; + const size = node.pstyle('font-size').pfValue + 'px'; + const family = node.pstyle('font-family').strValue; + const weight = node.pstyle('font-weight').strValue; + + // const padding = node.pstyle('padding-left').; + + ctx!.font = fStyle + ' ' + weight + ' ' + size + ' ' + family; + return ctx!.measureText(node.data('id')).width; + }, + height: '15px', + shape: 'round-rectangle', + 'background-color': '#fff', + 'text-valign': 'center', + color: '#333333', + 'border-width': 1, + 'border-color': '#2E1A61', + 'font-size': '10px', + 'padding-left': '30px', + 'padding-right': '30px', + 'padding-top': '50px', + 'padding-bottom': '50px', + }, + // selector: 'node', + // style: { + // content: 'data(id)', + // // label: 'data(id)', + // shape: 'round-rectangle', + // // width: 'label', + // // height: 200, + // 'font-size': '10px', + // 'padding-left': '5px', + // 'padding-right': '5px', + // 'padding-top': '5px', + // 'padding-bottom': '5px', + // }, + }, + + { + selector: 'edge', + style: { + 'line-color': '#ccc', + 'target-arrow-color': '#ccc', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + }, + }, + ], + }); + } + + updateNodePositions(nodes: cytoscape.NodeCollection, graph: Graph) { + nodes.forEach((node: cytoscape.NodeSingular) => { + if (this.verbose) { + console.log(node.id(), node.position()); + } + + // graph.setNodeAttribute(node.id(), 'x', node.position().x); + // graph.setNodeAttribute(node.id(), 'y', node.position().y); + + graph.updateNode(node.id(), (attr) => { + return { + ...attr, + x: node.position().x, + y: node.position().y, + }; + }); + + console.log( + 'width', + node.numericStyle('width'), + 'height', + node.numericStyle('height') + ); + }); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeKlay extends Cytoscape { + constructor() { + super('Cytoscape_klay'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); + + cytoscape.use(klay); + + const cy = this.makeCytoscapeInstance(cytonodes); + + const layout = cy.layout({ + name: 'klay', + + nodeDimensionsIncludeLabels: true, + + // Constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } + // boundingBox: undefined, + + ready: function () { + console.log('Layout.ready'); + }, // on layoutready + stop: function () { + console.log('Layout.stop'); + }, // on layoutstop + } as any); + layout.run(); + + const nodes = cy.nodes(); + this.updateNodePositions(nodes, graph); + + // var options = { + // nodeDimensionsIncludeLabels: false, // Boolean which changes whether label dimensions are included when calculating node dimensions + // fit: true, // Whether to fit + // padding: 20, // Padding on fit + // animate: false, // Whether to transition the node positions + // animateFilter: function( node, i ){ return true; }, // Whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions + // animationDuration: 500, // Duration of animation in ms if enabled + // animationEasing: undefined, // Easing of animation if enabled + // transform: function( node, pos ){ return pos; }, // A function that applies a transform to the final node position + // ready: undefined, // Callback on layoutready + // stop: undefined, // Callback on layoutstop + // klay: { + // // Following descriptions taken from http://layout.rtsys.informatik.uni-kiel.de:9444/Providedlayout.html?algorithm=de.cau.cs.kieler.klay.layered + // addUnnecessaryBendpoints: false, // Adds bend points even if an edge does not change direction. + // aspectRatio: 1.6, // The aimed aspect ratio of the drawing, that is the quotient of width by height + // borderSpacing: 20, // Minimal amount of space to be left to the border + // compactComponents: false, // Tries to further compact components (disconnected sub-graphs). + // crossingMinimization: 'LAYER_SWEEP', // Strategy for crossing minimization. + // /* LAYER_SWEEP The layer sweep algorithm iterates multiple times over the layers, trying to find node orderings that minimize the number of crossings. The algorithm uses randomization to increase the odds of finding a good result. To improve its results, consider increasing the Thoroughness option, which influences the number of iterations done. The Randomization seed also influences results. + // INTERACTIVE Orders the nodes of each layer by comparing their positions before the layout algorithm was started. The idea is that the relative order of nodes as it was before layout was applied is not changed. This of course requires valid positions for all nodes to have been set on the input graph before calling the layout algorithm. The interactive layer sweep algorithm uses the Interactive Reference Point option to determine which reference point of nodes are used to compare positions. */ + // cycleBreaking: 'GREEDY', // Strategy for cycle breaking. Cycle breaking looks for cycles in the graph and determines which edges to reverse to break the cycles. Reversed edges will end up pointing to the opposite direction of regular edges (that is, reversed edges will point left if edges usually point right). + // /* GREEDY This algorithm reverses edges greedily. The algorithm tries to avoid edges that have the Priority property set. + // INTERACTIVE The interactive algorithm tries to reverse edges that already pointed leftwards in the input graph. This requires node and port coordinates to have been set to sensible values.*/ + // direction: 'UNDEFINED', // Overall direction of edges: horizontal (right / left) or vertical (down / up) + // /* UNDEFINED, RIGHT, LEFT, DOWN, UP */ + // edgeRouting: 'ORTHOGONAL', // Defines how edges are routed (POLYLINE, ORTHOGONAL, SPLINES) + // edgeSpacingFactor: 0.5, // Factor by which the object spacing is multiplied to arrive at the minimal spacing between edges. + // feedbackEdges: false, // Whether feedback edges should be highlighted by routing around the nodes. + // fixedAlignment: 'NONE', // Tells the BK node placer to use a certain alignment instead of taking the optimal result. This option should usually be left alone. + // /* NONE Chooses the smallest layout from the four possible candidates. + // LEFTUP Chooses the left-up candidate from the four possible candidates. + // RIGHTUP Chooses the right-up candidate from the four possible candidates. + // LEFTDOWN Chooses the left-down candidate from the four possible candidates. + // RIGHTDOWN Chooses the right-down candidate from the four possible candidates. + // BALANCED Creates a balanced layout from the four possible candidates. */ + // inLayerSpacingFactor: 1.0, // Factor by which the usual spacing is multiplied to determine the in-layer spacing between objects. + // layoutHierarchy: false, // Whether the selected layouter should consider the full hierarchy + // linearSegmentsDeflectionDampening: 0.3, // Dampens the movement of nodes to keep the diagram from getting too large. + // mergeEdges: false, // Edges that have no ports are merged so they touch the connected nodes at the same points. + // mergeHierarchyCrossingEdges: true, // If hierarchical layout is active, hierarchy-crossing edges use as few hierarchical ports as possible. + // nodeLayering:'NETWORK_SIMPLEX', // Strategy for node layering. + // /* NETWORK_SIMPLEX This algorithm tries to minimize the length of edges. This is the most computationally intensive algorithm. The number of iterations after which it aborts if it hasn't found a result yet can be set with the Maximal Iterations option. + // LONGEST_PATH A very simple algorithm that distributes nodes along their longest path to a sink node. + // INTERACTIVE Distributes the nodes into layers by comparing their positions before the layout algorithm was started. The idea is that the relative horizontal order of nodes as it was before layout was applied is not changed. This of course requires valid positions for all nodes to have been set on the input graph before calling the layout algorithm. The interactive node layering algorithm uses the Interactive Reference Point option to determine which reference point of nodes are used to compare positions. */ + // nodePlacement:'BRANDES_KOEPF', // Strategy for Node Placement + // /* BRANDES_KOEPF Minimizes the number of edge bends at the expense of diagram size: diagrams drawn with this algorithm are usually higher than diagrams drawn with other algorithms. + // LINEAR_SEGMENTS Computes a balanced placement. + // INTERACTIVE Tries to keep the preset y coordinates of nodes from the original layout. For dummy nodes, a guess is made to infer their coordinates. Requires the other interactive phase implementations to have run as well. + // SIMPLE Minimizes the area at the expense of... well, pretty much everything else. */ + // randomizationSeed: 1, // Seed used for pseudo-random number generators to control the layout algorithm; 0 means a new seed is generated + // routeSelfLoopInside: false, // Whether a self-loop is routed around or inside its node. + // separateConnectedComponents: true, // Whether each connected component should be processed separately + // spacing: 20, // Overall setting for the minimal amount of space to be left between objects + // thoroughness: 7 // How much effort should be spent to produce a nice layout.. + // }, + // priority: function( edge ){ return null; }, // Edges with a non-nil value are skipped when greedy edge cycle breaking is enabled + // }; + // cy.layout( options ).run(); + } +} + +function getWidth(node: any) { + /** + Calculate the width of a node given its text label `node.data('lbl')` + */ + + // Create element with attributes needed to calculate text size + const ctx = document.createElement('canvas').getContext('2d'); + const fStyle = node.pstyle('font-style').strValue; + const size = node.pstyle('font-size').pfValue + 'px'; + const family = node.pstyle('font-family').strValue; + const weight = node.pstyle('font-weight').strValue; + ctx!.font = fStyle + ' ' + weight + ' ' + size + ' ' + family; + + // For multiple lines, evaluate the width of the largest line + const lines = node.data('lbl').split('\n'); + const lengths = lines.map((a: any) => a.length); + const max_line = lengths.indexOf(Math.max(...lengths)); + + // User-defined padding + const padding = 30; + + return ctx!.measureText(lines[max_line]).width + padding; +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeElk extends Cytoscape { + constructor() { + super('Cytoscape_elk'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes>, + verbose?: boolean + ): void { + const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); + + const cy = this.makeCytoscapeInstance(cytonodes); + + // const options = { + // name: 'elk', + // nodeDimensionsIncludeLabels: false, // Boolean which changes whether label dimensions are included when calculating node dimensions + // fit: true, // Whether to fit + // padding: 20, // Padding on fit + // animate: false, // Whether to transition the node positions + // animateFilter: function (node: any, i: number) { + // return true; + // }, // Whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions + // animationDuration: 500, // Duration of animation in ms if enabled + // animationEasing: undefined, // Easing of animation if enabled + // transform: function (node: any, pos: Position) { + // return pos; + // }, // A function that applies a transform to the final node position + // ready: undefined, // Callback on layoutready + // stop: undefined, // Callback on layoutstop + // klay: { + // // Following descriptions taken from http://layout.rtsys.informatik.uni-kiel.de:9444/Providedlayout.html?algorithm=de.cau.cs.kieler.klay.layered + // addUnnecessaryBendpoints: false, // Adds bend points even if an edge does not change direction. + // aspectRatio: 1.6, // The aimed aspect ratio of the drawing, that is the quotient of width by height + // borderSpacing: 20, // Minimal amount of space to be left to the border + // compactComponents: false, // Tries to further compact components (disconnected sub-graphs). + // crossingMinimization: 'LAYER_SWEEP', // Strategy for crossing minimization. + // /* LAYER_SWEEP The layer sweep algorithm iterates multiple times over the layers, trying to find node orderings that minimize the number of crossings. The algorithm uses randomization to increase the odds of finding a good result. To improve its results, consider increasing the Thoroughness option, which influences the number of iterations done. The Randomization seed also influences results. + // INTERACTIVE Orders the nodes of each layer by comparing their positions before the layout algorithm was started. The idea is that the relative order of nodes as it was before layout was applied is not changed. This of course requires valid positions for all nodes to have been set on the input graph before calling the layout algorithm. The interactive layer sweep algorithm uses the Interactive Reference Point option to determine which reference point of nodes are used to compare positions. */ + // cycleBreaking: 'GREEDY', // Strategy for cycle breaking. Cycle breaking looks for cycles in the graph and determines which edges to reverse to break the cycles. Reversed edges will end up pointing to the opposite direction of regular edges (that is, reversed edges will point left if edges usually point right). + // /* GREEDY This algorithm reverses edges greedily. The algorithm tries to avoid edges that have the Priority property set. + // INTERACTIVE The interactive algorithm tries to reverse edges that already pointed leftwards in the input graph. This requires node and port coordinates to have been set to sensible values.*/ + // direction: 'UNDEFINED', // Overall direction of edges: horizontal (right / left) or vertical (down / up) + // /* UNDEFINED, RIGHT, LEFT, DOWN, UP */ + // edgeRouting: 'ORTHOGONAL', // Defines how edges are routed (POLYLINE, ORTHOGONAL, SPLINES) + // edgeSpacingFactor: 0.5, // Factor by which the object spacing is multiplied to arrive at the minimal spacing between edges. + // feedbackEdges: false, // Whether feedback edges should be highlighted by routing around the nodes. + // fixedAlignment: 'NONE', // Tells the BK node placer to use a certain alignment instead of taking the optimal result. This option should usually be left alone. + // /* NONE Chooses the smallest layout from the four possible candidates. + // LEFTUP Chooses the left-up candidate from the four possible candidates. + // RIGHTUP Chooses the right-up candidate from the four possible candidates. + // LEFTDOWN Chooses the left-down candidate from the four possible candidates. + // RIGHTDOWN Chooses the right-down candidate from the four possible candidates. + // BALANCED Creates a balanced layout from the four possible candidates. */ + // inLayerSpacingFactor: 1.0, // Factor by which the usual spacing is multiplied to determine the in-layer spacing between objects. + // layoutHierarchy: false, // Whether the selected layouter should consider the full hierarchy + // linearSegmentsDeflectionDampening: 0.3, // Dampens the movement of nodes to keep the diagram from getting too large. + // mergeEdges: false, // Edges that have no ports are merged so they touch the connected nodes at the same points. + // mergeHierarchyCrossingEdges: true, // If hierarchical layout is active, hierarchy-crossing edges use as few hierarchical ports as possible. + // nodeLayering: 'NETWORK_SIMPLEX', // Strategy for node layering. + // /* NETWORK_SIMPLEX This algorithm tries to minimize the length of edges. This is the most computationally intensive algorithm. The number of iterations after which it aborts if it hasn't found a result yet can be set with the Maximal Iterations option. + // LONGEST_PATH A very simple algorithm that distributes nodes along their longest path to a sink node. + // INTERACTIVE Distributes the nodes into layers by comparing their positions before the layout algorithm was started. The idea is that the relative horizontal order of nodes as it was before layout was applied is not changed. This of course requires valid positions for all nodes to have been set on the input graph before calling the layout algorithm. The interactive node layering algorithm uses the Interactive Reference Point option to determine which reference point of nodes are used to compare positions. */ + // nodePlacement: 'BRANDES_KOEPF', // Strategy for Node Placement + // /* BRANDES_KOEPF Minimizes the number of edge bends at the expense of diagram size: diagrams drawn with this algorithm are usually higher than diagrams drawn with other algorithms. + // LINEAR_SEGMENTS Computes a balanced placement. + // INTERACTIVE Tries to keep the preset y coordinates of nodes from the original layout. For dummy nodes, a guess is made to infer their coordinates. Requires the other interactive phase implementations to have run as well. + // SIMPLE Minimizes the area at the expense of... well, pretty much everything else. */ + // randomizationSeed: 1, // Seed used for pseudo-random number generators to control the layout algorithm; 0 means a new seed is generated + // routeSelfLoopInside: false, // Whether a self-loop is routed around or inside its node. + // separateConnectedComponents: true, // Whether each connected component should be processed separately + // spacing: 20, // Overall setting for the minimal amount of space to be left between objects + // thoroughness: 7, // How much effort should be spent to produce a nice layout.. + // }, + // priority: function (edge: any) { + // return null; + // }, // Edges with a non-nil value are skipped when greedy edge cycle breaking is enabled + // }; + + const layout = cy.layout({ + name: 'elk', + fit: true, + ranker: 'longest-path', + animate: false, + elk: { + zoomToFit: true, + algorithm: 'mrtree', + separateConnectedComponents: false, + }, + ready: function () { + console.log('Layout.ready'); + }, // on layoutready + stop: function () { + console.log('Layout.stop'); + }, // on layoutstop + } as any); + layout.run(); + + this.updateNodePositions(cy.nodes(), graph); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeDagre extends Cytoscape { + constructor() { + super('Cytoscape_dagre'); + cytoscape.use(dagre); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes>, + verbose?: boolean + ): void { + const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); + + const cy = this.makeCytoscapeInstance(cytonodes); + + const layout = cy.layout({ + name: 'dagre', + ready: function () { + console.log('Layout.ready'); + }, // on layoutready + stop: function () { + console.log('Layout.stop'); + }, // on layoutstop + } as any); + layout.run(); + + this.updateNodePositions(cy.nodes(), graph); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeFCose extends Cytoscape { + constructor() { + super('Cytoscape_fcose'); + cytoscape.use(fcose); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes>, + verbose?: boolean + ): void { + const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); + + const cy = this.makeCytoscapeInstance(cytonodes); + + const layout = cy.layout({ + name: 'fcose', + animate: false, + ready: function () { + console.log('Layout.ready'); + }, // on layoutready + stop: function () { + console.log('Layout.stop'); + }, // on layoutstop + } as any); + layout.run(); + + const nodes = cy.nodes(); + this.updateNodePositions(nodes, graph); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeCoseBilkent extends Cytoscape { + constructor() { + super('Cytoscape_cose-bilkent'); + cytoscape.use(coseBilkent); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes>, + verbose?: boolean + ): void { + const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); + + const cy = this.makeCytoscapeInstance(cytonodes); + + const layout = cy.layout({ + name: 'cose-bilkent', + animate: false, + ready: function () { + console.log('Layout.ready'); + }, // on layoutready + stop: function () { + console.log('Layout.stop'); + }, // on layoutstop + } as any); + layout.run(); + + this.updateNodePositions(cy.nodes(), graph); + } +} + +/** + * This is a ConcreteProduct + */ +class CytoscapeCise extends Cytoscape { + constructor() { + super('Cytoscape_cise'); + cytoscape.use(cise); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes>, + verbose?: boolean + ): void { + const cytonodes: CytoNode[] = this.convertToCytoscapeModel(graph); + + const cy = this.makeCytoscapeInstance(cytonodes); + + const layout = cy.layout({ + name: 'cise', + ready: function () { + console.log('Layout.ready'); + }, // on layoutready + stop: function () { + console.log('Layout.stop'); + }, // on layoutstop + } as any); + layout.run(); + + this.updateNodePositions(cy.nodes(), graph); + } +} diff --git a/libs/shared/graph-layout/src/lib/graphology-layouts.ts b/libs/shared/graph-layout/src/lib/graphology-layouts.ts new file mode 100644 index 0000000000000000000000000000000000000000..74585977cfb0878c290b300f407d3598c5a2f840 --- /dev/null +++ b/libs/shared/graph-layout/src/lib/graphology-layouts.ts @@ -0,0 +1,168 @@ +import Graph from 'graphology'; +import { circular, random } from 'graphology-layout'; +import forceAtlas2, { + ForceAtlas2Settings, +} from 'graphology-layout-forceatlas2'; +import noverlap from 'graphology-layout-noverlap'; +import { RandomLayoutOptions } from 'graphology-layout/random'; +import { Attributes } from 'graphology-types'; +import { Layout } from './layout'; +import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase'; + +export type GraphologyProvider = 'Graphology'; + +export type GraphologyLayoutAlgorithms = + | `${GraphologyProvider}_circular` + | `${GraphologyProvider}_random` + | `${GraphologyProvider}_noverlap` + | `${GraphologyProvider}_forceAtlas2`; + +/** + * This is the Graphology Constructor for the main layouts available at + * https://graphology.github.io/ + */ +export class GraphologyFactory + implements ILayoutFactory<GraphologyLayoutAlgorithms> +{ + createLayout(layoutAlgorithm: GraphologyLayoutAlgorithms): Graphology | null { + switch (layoutAlgorithm) { + case 'Graphology_random': + return new GraphologyRandom(); + case 'Graphology_circular': + return new GraphologyCircular(); + case 'Graphology_noverlap': + return new GraphologyNoverlap(); + case 'Graphology_forceAtlas2': + return new GraphologyForceAtlas2(); + default: + return null; + } + } +} + +export abstract class Graphology extends Layout<GraphologyProvider> { + height = 100; + width = 100; + constructor(public override algorithm: LayoutAlgorithm<GraphologyProvider>) { + super('Graphology', algorithm); + this.setDimensions(100, 200); + } + + public specialGraphologyFunction() { + // graph.forEachNode((node, attr) => { + // graph.setNodeAttribute(node, 'x', 0); + // graph.setNodeAttribute(node, 'y', 0); + // }); + } + + public setDimensions(width = 100, height = 100) { + this.width = width; + this.height = height; + } +} + +/** + * This is a ConcreteProduct + */ +export class GraphologyCircular extends Graphology { + constructor() { + super('Graphology_circular'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // To directly assign the positions to the nodes: + circular.assign(graph, { + scale: 100, + }); + } +} + +const DEFAULT_RANDOM_SETTINGS: RandomLayoutOptions = { + scale: 250, +}; +/** + * This is a ConcreteProduct + */ +export class GraphologyRandom extends Graphology { + constructor() { + super('Graphology_random'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // const positions = random(graph); + + // To directly assign the positions to the nodes: + random.assign(graph, DEFAULT_RANDOM_SETTINGS); + } +} + +const DEFAULT_NOVERLAP_SETTINGS = { + margin: 40, + ratio: 40, + // gridSize: 50, + + // gridSize ?number 20: number of grid cells horizontally and vertically subdivising the graph’s space. This is used as an optimization scheme. Set it to 1 and you will have O(n²) time complexity, which can sometimes perform better with very few nodes. + // margin ?number 5: margin to keep between nodes. + // expansion ?number 1.1: percentage of current space that nodes could attempt to move outside of. + // ratio ?number 1.0: ratio scaling node sizes. + // speed ?number 3: dampening factor that will slow down node movements to ease the overall process. +}; + +/** + * This is a ConcreteProduct + */ +export class GraphologyNoverlap extends Graphology { + constructor() { + super('Graphology_noverlap'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + // To directly assign the positions to the nodes: + noverlap.assign(graph, { + maxIterations: 10000, + settings: DEFAULT_NOVERLAP_SETTINGS, + }); + } +} + +const DEFAULT_FORCEATLAS2_SETTINGS: ForceAtlas2Settings = { + gravity: 1, + adjustSizes: true, + linLogMode: true, + strongGravityMode: true, + + // adjustSizes ?boolean false: should the node’s sizes be taken into account? + // barnesHutOptimize ?boolean false: whether to use the Barnes-Hut approximation to compute repulsion in O(n*log(n)) rather than default O(n^2), n being the number of nodes. + // barnesHutTheta ?number 0.5: Barnes-Hut approximation theta parameter. + // edgeWeightInfluence ?number 1: influence of the edge’s weights on the layout. To consider edge weight, don’t forget to pass weighted as true when applying the synchronous layout or when instantiating the worker. + // gravity ?number 1: strength of the layout’s gravity. + // linLogMode ?boolean false: whether to use Noack’s LinLog model. + // outboundAttractionDistribution ?boolean false + // scalingRatio ?number 1 + // slowDown ?number 1 + // strongGravityMode ?boolean false +}; + +/** + * This is a ConcreteProduct + */ +export class GraphologyForceAtlas2 extends Graphology { + constructor() { + super('Graphology_forceAtlas2'); + } + + public override layout( + graph: Graph<Attributes, Attributes, Attributes> + ): void { + forceAtlas2.assign(graph, { + iterations: 500, + settings: DEFAULT_FORCEATLAS2_SETTINGS, + }); + } +} diff --git a/libs/shared/graph-layout/src/lib/graphology.spec.ts b/libs/shared/graph-layout/src/lib/graphology.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b790b6fb3cd8f27ac13c2377b5644106b71c397 --- /dev/null +++ b/libs/shared/graph-layout/src/lib/graphology.spec.ts @@ -0,0 +1,22 @@ +import Graph, { UndirectedGraph } from 'graphology'; +import { MultiGraph } from 'graphology'; +import connectedCaveman from 'graphology-generators/community/connected-caveman'; +import ladder from 'graphology-generators/classic/ladder'; + +describe('graphology connection', () => { + it('should create a graphology caveman', () => { + // Creating a connected caveman graph + const graph = connectedCaveman(Graph, 6, 8); + }); + + + it('should create a graphology ladder', () => { + // Creating a connected caveman graph + const graph = ladder(Graph, 6, 8); + }); + + it('should create a graphology graph', () => { + const graph = new MultiGraph(); + expect(graph); + }); +}); diff --git a/libs/shared/graph-layout/src/lib/layout-creator-usecase.spec.ts b/libs/shared/graph-layout/src/lib/layout-creator-usecase.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e86bb3556715997545768ad8d2822a5a0f9e45d --- /dev/null +++ b/libs/shared/graph-layout/src/lib/layout-creator-usecase.spec.ts @@ -0,0 +1,387 @@ +// import { +// movieSchemaRaw, +// northwindSchemaRaw, +// simpleSchemaRaw, +// twitterSchemaRaw, +// } from '@graphpolaris/shared/mock-data'; +import Graph, { MultiGraph } from 'graphology'; + +import connectedCaveman from 'graphology-generators/community/connected-caveman'; +import ladder from 'graphology-generators/classic/ladder'; +import { LayoutFactory } from './layout-creator-usecase'; + +const TIMEOUT = 10; + +/** + * @jest-environment jsdom + */ +describe('LayoutFactory Graphology Libries', () => { + /** + * @jest-environment jsdom + */ + it( + 'should work with noverlap from graphology ', + () => { + const graph = new MultiGraph(); + + // Adding some nodes + // graph.addNode('John', { x: 0, y: 0, width: 200, height: 200 }); + // graph.addNode('Martha', { x: 0, y: 0 }); + graph.addNode('John'); + graph.addNode('Martha'); + + // Adding an edge + graph.addEdge('John', 'Martha'); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Graphology_noverlap'); + layoutAlgorithm?.layout(graph); + + // const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + // const pos = '' + attr['x'] + '' + attr['y']; + // expect(positionMap.has(pos)).toBeFalsy(); + // positionMap.add(pos); + }); + }, + TIMEOUT + ); + + test( + 'should work with noverlap from graphology on generated graph', + () => { + // Creating a ladder graph + const graph = ladder(Graph, 10); + + graph.forEachNode((node, attr) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_noverlap'); + layout?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + const pos = '' + attr['x'] + '' + attr['y']; + + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + test( + 'should work with random from graphology on generated graph', + () => { + // Creating a ladder graph + const graph = ladder(Graph, 10); + + graph.forEachNode((node, attr) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_random'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + const pos = '' + attr['x'] + '' + attr['y']; + + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + test( + 'should work with circular from graphology on generated graph', + () => { + // Creating a ladder graph + const graph = ladder(Graph, 100); + + graph.forEachNode((node) => { + graph.setNodeAttribute(node, 'x', 0); + graph.setNodeAttribute(node, 'y', 0); + }); + + const layoutFactory = new LayoutFactory(); + + const layout = layoutFactory.createLayout('Graphology_circular'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + const pos = '' + attr['x'] + '' + attr['y']; + + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + test( + 'should work with Graphology_forceAtlas2 from graphology on generated graph', + () => { + // console.log(Object.keys(AllLayoutAlgorithms)) + + const graph = connectedCaveman(Graph, 6, 8); + + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeUndefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeUndefined(); + }); + + // console.log('before'); + const layoutFactory = new LayoutFactory(); + const layout = layoutFactory.createLayout('Graphology_forceAtlas2'); + layout?.setDimensions(100, 100); + layout?.layout(graph); + + // console.log('after'); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + console.log(pos); + + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + + expect(isNaN(graph.getNodeAttribute(node, 'x'))).toBeFalsy(); + expect(isNaN(graph.getNodeAttribute(node, 'y'))).toBeFalsy(); + }); + }, + TIMEOUT + ); +}); + +describe('LayoutFactory Cytoscape Libraries', () => { + it('should convert between models Graphology <-> Cytoscape ', () => { + const graph = connectedCaveman(Graph, 10, 10); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Cytoscape_klay'); + + const cytonodes = layoutAlgorithm?.convertToCytoscapeModel(graph); + + let nodes = 0; + let edges = 0; + cytonodes?.forEach((element) => { + if (element.data.type == 'node') { + nodes++; + } else if (element.data.type == 'edge') { + edges++; + } + }); + + // console.log('Number of nodes', graph.order); + // console.log('Number of edges', graph.size); + expect(nodes).toBe(graph.order); + expect(edges).toBe(graph.size); + }); + + it( + 'should execute Cytoscape_klay from Cytoscape ', + () => { + /** + * l number: number of components in the graph. + * k number: number of nodes of the components. + */ + const graph = connectedCaveman(Graph, 5, 5); + graph.addNode('Martha', { occurrences: 1 }); + + // graph.forEachNode(node => { + // console.log(node); + // }); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Cytoscape_klay'); + layoutAlgorithm?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + it( + 'should execute Cytoscape_elk from Cytoscape ', + () => { + /** + * l number: number of components in the graph. + * k number: number of nodes of the components. + */ + const graph = connectedCaveman(Graph, 5, 5); + graph.addNode('Martha', { occurrences: 1 }); + + // graph.forEachNode(node => { + // console.log(node); + // }); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Cytoscape_elk'); + layoutAlgorithm?.layout(graph, false); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + it( + 'should execute Cytoscape_dagre from Cytoscape ', + () => { + /** + * l number: number of components in the graph. + * k number: number of nodes of the components. + */ + const graph = connectedCaveman(Graph, 5, 5); + graph.addNode('Martha', { occurrences: 1 }); + + // graph.forEachNode(node => { + // console.log(node); + // }); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Cytoscape_dagre'); + layoutAlgorithm?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + it( + 'should execute Cytoscape_fcose from Cytoscape ', + () => { + /** + * l number: number of components in the graph. + * k number: number of nodes of the components. + */ + const graph = connectedCaveman(Graph, 5, 5); + graph.addNode('Martha', { occurrences: 1 }); + + // graph.forEachNode(node => { + // console.log(node); + // }); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Cytoscape_fcose'); + layoutAlgorithm?.layout(graph, false); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + it( + 'should execute Cytoscape_cose-bilkent from Cytoscape ', + () => { + /** + * l number: number of components in the graph. + * k number: number of nodes of the components. + */ + const graph = connectedCaveman(Graph, 5, 5); + graph.addNode('Martha', { occurrences: 1 }); + + // graph.forEachNode(node => { + // console.log(node); + // }); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout( + 'Cytoscape_cose-bilkent' + ); + layoutAlgorithm?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); + + it( + 'should execute Cytoscape_cise from Cytoscape ', + () => { + /** + * l number: number of components in the graph. + * k number: number of nodes of the components. + */ + const graph = connectedCaveman(Graph, 5, 5); + graph.addNode('Martha', { occurrences: 1 }); + + // graph.forEachNode(node => { + // console.log(node); + // }); + + const layoutFactory = new LayoutFactory(); + const layoutAlgorithm = layoutFactory.createLayout('Cytoscape_cise'); + layoutAlgorithm?.layout(graph); + + const positionMap = new Set<string>(); + graph.forEachNode((node, attr) => { + expect(graph.getNodeAttribute(node, 'x')).toBeDefined(); + expect(graph.getNodeAttribute(node, 'y')).toBeDefined(); + + const pos = '' + attr['x'] + '' + attr['y']; + expect(positionMap.has(pos)).toBeFalsy(); + positionMap.add(pos); + }); + }, + TIMEOUT + ); +}); diff --git a/libs/shared/graph-layout/src/lib/layout-creator-usecase.ts b/libs/shared/graph-layout/src/lib/layout-creator-usecase.ts new file mode 100644 index 0000000000000000000000000000000000000000..930200eea101f2b6c6c977a3705f5986ef2d864d --- /dev/null +++ b/libs/shared/graph-layout/src/lib/layout-creator-usecase.ts @@ -0,0 +1,77 @@ +import { + Cytoscape, + CytoscapeFactory, + CytoscapeLayoutAlgorithms, + CytoscapeProvider, +} from './cytoscape-layouts'; +import { + Graphology, + GraphologyFactory, + GraphologyLayoutAlgorithms, + GraphologyProvider, +} from './graphology-layouts'; +import { Layout } from './layout'; + +export type Providers = GraphologyProvider | CytoscapeProvider; +export type LayoutAlgorithm<Provider extends Providers> = + `${Provider}_${string}`; + +export type AllLayoutAlgorithms = + | GraphologyLayoutAlgorithms + | CytoscapeLayoutAlgorithms; + +export type AlgorithmToLayoutProvider<Algorithm extends AllLayoutAlgorithms> = + Algorithm extends GraphologyLayoutAlgorithms + ? Graphology + : Algorithm extends CytoscapeLayoutAlgorithms + ? Cytoscape + : Cytoscape | Graphology; + +export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> { + createLayout: ( + Algorithm: Algorithm + ) => AlgorithmToLayoutProvider<Algorithm> | null; +} + +/** + * This is our Creator + */ +export class LayoutFactory implements ILayoutFactory<AllLayoutAlgorithms> { + private graphologyFactory = new GraphologyFactory(); + private cytoscapeFactory = new CytoscapeFactory(); + + private isSpecificAlgorithm<Algorithm extends AllLayoutAlgorithms>( + LayoutAlgorithm: AllLayoutAlgorithms, + startsWith: string + ): LayoutAlgorithm is Algorithm { + return LayoutAlgorithm.startsWith(startsWith); + } + + createLayout<Algorithm extends AllLayoutAlgorithms>( + layoutAlgorithm: Algorithm + ): AlgorithmToLayoutProvider<Algorithm> | null { + if ( + this.isSpecificAlgorithm<GraphologyLayoutAlgorithms>( + layoutAlgorithm, + 'Graphology' + ) + ) { + return this.graphologyFactory.createLayout( + layoutAlgorithm + ) as AlgorithmToLayoutProvider<Algorithm>; + } + + if ( + this.isSpecificAlgorithm<CytoscapeLayoutAlgorithms>( + layoutAlgorithm, + 'Cytoscape' + ) + ) { + return this.cytoscapeFactory.createLayout( + layoutAlgorithm + ) as AlgorithmToLayoutProvider<Algorithm>; + } + + return null; + } +} diff --git a/libs/shared/graph-layout/src/lib/layout.ts b/libs/shared/graph-layout/src/lib/layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..caa834f545a7e013965f979ce71839498f4c6b4a --- /dev/null +++ b/libs/shared/graph-layout/src/lib/layout.ts @@ -0,0 +1,31 @@ +import Graph from 'graphology'; +import { Providers, LayoutAlgorithm } from './layout-creator-usecase'; + +/** + * This is our Product + */ + +export abstract class Layout<provider extends Providers> { + constructor( + public provider: provider, + public algorithm: LayoutAlgorithm<provider> + ) { + console.log( + `Created the following Layout: ${provider} - ${this.algorithm}` + ); + } + + public layout(graph: Graph, verbose?: boolean) { + console.log(`${this.provider} [${this.algorithm}] layouting now`); + + graph.forEachNode((node) => { + const attr = graph.getNodeAttributes(node); + if (!attr.hasOwnProperty('x')) { + graph.setNodeAttribute(node, 'x', Math.random()); + } + if (!attr.hasOwnProperty('y')) { + graph.setNodeAttribute(node, 'y', Math.random()); + } + }); + } +} diff --git a/libs/shared/graph-layout/src/lib/mockdata-layout.spec.ts b/libs/shared/graph-layout/src/lib/mockdata-layout.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c54f4ac8eefec9dc105661791f0d2587a7fb10ce --- /dev/null +++ b/libs/shared/graph-layout/src/lib/mockdata-layout.spec.ts @@ -0,0 +1,13 @@ +// import { +// movieSchema, +// northWindSchema, +// simpleSchemaRaw, +// twitterSchemaRaw, +// } from '@graphpolaris/shared/mock-data'; + +it('should layout the mock-data movieSchema', () => { + // Creating a connected caveman graph + // const schema = movieSchema; + // expect(schema).toBeDefined(); + expect(true).toBeTruthy(); +}); diff --git a/libs/shared/graph-layout/tsconfig.json b/libs/shared/graph-layout/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..b3eaee077ca790ec299fe3ac9dd427bc04d21613 --- /dev/null +++ b/libs/shared/graph-layout/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], +} diff --git a/libs/shared/graph-layout/tsconfig.lib.json b/libs/shared/graph-layout/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..0a86e9a4481e7c1f10b111060bf05bcd90a5329e --- /dev/null +++ b/libs/shared/graph-layout/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"], + "composite": true + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/shared/graph-layout/tsconfig.spec.json b/libs/shared/graph-layout/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..4afc999ad429eea63e777308527c4e8f629e4198 --- /dev/null +++ b/libs/shared/graph-layout/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "composite": true + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/graph-layout/yarn.lock b/libs/shared/graph-layout/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..a648f2038763e7f89799b53b4144232254ac577a --- /dev/null +++ b/libs/shared/graph-layout/yarn.lock @@ -0,0 +1,245 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@yomguithereal/helpers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@yomguithereal/helpers/-/helpers-1.1.1.tgz#185dfb0f88ca2beec53d0adf6eed15c33b1c549d" + integrity sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg== + +avsdf-base@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/avsdf-base/-/avsdf-base-1.0.0.tgz#80c437d7d15d2bd201d9c31804e7b7a15a84781a" + integrity sha512-APhZNUFJwIwrLsSfE95QjobEntdUhFQgfNtC/BrYmjUpwHh5Y2fbRv8lxAlMr1hdf/CuQYsqJxK3dRzcCL77qw== + dependencies: + layout-base "^1.0.0" + +cose-base@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a" + integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg== + dependencies: + layout-base "^1.0.0" + +cose-base@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.1.0.tgz#89b2d4a59d7bd0cde3138a4689825f3e8a5abd6a" + integrity sha512-HTMm07dhxq1dIPGWwpiVrIk9n+DH7KYmqWA786mLe8jDS+1ZjGtJGIIsJVKoseZXS6/FxiUWCJ2B7XzqUCuhPw== + dependencies: + layout-base "^2.0.0" + +cytoscape-cise@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cytoscape-cise/-/cytoscape-cise-1.0.0.tgz#29ac061c74e036441c0b5687e7ebc25e919d507c" + integrity sha512-Y1NPaUo4fN992XJTEIDd4oPVkv8BsDSrFBHSB38caDu8PcmHUyl8/Q8K5wvqdTeti1mLR9IX4/o2RyuObh+P7Q== + dependencies: + avsdf-base "^1.0.0" + cose-base "^1.0.0" + +cytoscape-cose-bilkent@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b" + integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ== + dependencies: + cose-base "^1.0.0" + +cytoscape-dagre@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cytoscape-dagre/-/cytoscape-dagre-2.4.0.tgz#abf145b1c675afe3b7d531166e6727dc39dc350d" + integrity sha512-jfOtKzKduCnruBs3YMHS9kqWjZKqvp6loSJwlotPO5pcU4wLUhkx7ZBIyW3VWZXa8wfkGxv/zhWoBxWtYrUxKQ== + dependencies: + dagre "^0.8.5" + +cytoscape-elk@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/cytoscape-elk/-/cytoscape-elk-2.0.2.tgz#dc8cf9b3ac3c9dfb16a67273f2ef6fd62d676cfc" + integrity sha512-P47PY63YECC42JVJm/PozKDZMLmx/BPGEhmfAEQzMRAz03DC09CsGrMC4TM4ggErnI8uPMzmI42F1oP6Zu4iUw== + dependencies: + elkjs "^0.7.0" + +cytoscape-fcose@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.1.0.tgz#04c3093776ea6b71787009de641607db7d4edf55" + integrity sha512-Q3apPl66jf8/2sMsrCjNP247nbDkyIPjA9g5iPMMWNLZgP3/mn9aryF7EFY/oRPEpv7bKJ4jYmCoU5r5/qAc1Q== + dependencies: + cose-base "^2.0.0" + +cytoscape-klay@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz#0e5bbb5c482b384b2ff2485150173aaecebc324b" + integrity sha512-VwPj0VR25GPfy6qXVQRi/MYlZM/zkdvRhHlgqbM//lSvstgM6fhp3ik/uM8Wr8nlhskfqz/M1fIDmR6UckbS2A== + dependencies: + klayjs "^0.4.1" + +cytoscape@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.21.0.tgz#8a73f6f0f3a44f948e266ac7df9b2eff65e8bd3c" + integrity sha512-xPINMzQNGN4WIog93gYRScT4y/CyZMmqunnhU59/8LynhWwzepJtUydMfvIPuz5TmJ9rSCMdw6rmxhfbb1eofw== + dependencies: + heap "^0.2.6" + lodash.debounce "^4.0.8" + lodash.get "^4.4.2" + lodash.set "^4.3.2" + lodash.topath "^4.5.2" + +dagre@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" + integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw== + dependencies: + graphlib "^2.1.8" + lodash "^4.17.15" + +elkjs@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.7.1.tgz#4751c5e918a4988139baf7f214e010aea22de969" + integrity sha512-lD86RWdh480/UuRoHhRcnv2IMkIcK6yMDEuT8TPBIbO3db4HfnVF+1lgYdQi99Ck0yb+lg5Eb46JCHI5uOsmAw== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +graphlib@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" + integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== + dependencies: + lodash "^4.17.15" + +graphology-generators@^0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/graphology-generators/-/graphology-generators-0.11.2.tgz#eff2c97e4f5bf401e86ab045470dded95f2ebe24" + integrity sha512-hx+F0OZRkVdoQ0B1tWrpxoakmHZNex0c6RAoR0PrqJ+6fz/gz6CQ88Qlw78C6yD9nlZVRgepIoDYhRTFV+bEHg== + dependencies: + graphology-metrics "^2.0.0" + graphology-utils "^2.3.0" + +graphology-indices@^0.16.3: + version "0.16.6" + resolved "https://registry.yarnpkg.com/graphology-indices/-/graphology-indices-0.16.6.tgz#0de112ef0367e44041490933e34ad2075cb24e80" + integrity sha512-tozTirLb7pd37wULJ5qeIZfZqKuVln/V+bWmUWJ7MmoTU8YkW5dehOkRz2by/O+5MdJ52imqL8LH4+GCd0yEVw== + dependencies: + graphology-utils "^2.4.2" + mnemonist "^0.39.0" + +graphology-layout-forceatlas2@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.8.2.tgz#7cb5b2fa00fd5445cb2b73c333e36ef22c8a82a8" + integrity sha512-OsmOuQP0xiav5Iau9W9G4eb4cGx5tDcdzx9NudG6fhi6japqD+Z45zUBcwnp/12BPBXp/PKc5pvUe3Va6AsOUA== + dependencies: + graphology-utils "^2.1.0" + +graphology-layout-noverlap@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz#2ffa054ceeebaa31fcffe695d271fc55707cd29c" + integrity sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA== + dependencies: + graphology-utils "^2.3.0" + +graphology-layout@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/graphology-layout/-/graphology-layout-0.5.0.tgz#a0a54861cebae5f486c778dbdafc6294859f23b5" + integrity sha512-aIeXYPLeGMLvXIkO41TlhBv0ROFWUx1bqR2VQoJ7Mp2IW+TF+rxqMeRUrmyLHoe3HtKo8jhloB2KHp7g6fcDSg== + dependencies: + graphology-utils "^2.3.0" + pandemonium "^1.5.0" + +graphology-metrics@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/graphology-metrics/-/graphology-metrics-2.1.0.tgz#7d00bae92d8970583afd020e6d40d8a16c378002" + integrity sha512-E+y4kgVGxhYl/+bPHEftJeWLS8LgVno4/Wvg+C7IoDIjY6OlIZghgMKDR8LKsxU6GC43mlx08FTZs229cvEkwQ== + dependencies: + graphology-shortest-path "^2.0.0" + graphology-utils "^2.4.4" + mnemonist "^0.39.0" + +graphology-shortest-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/graphology-shortest-path/-/graphology-shortest-path-2.0.0.tgz#27a01b3a9980872bd44a197fc77114623dd2b302" + integrity sha512-6dJWgbr7w4YQKb7Y0w7vhZn2qAkqP+J0IhE9F3vz/HZcx7VSOqnNfTGtYr44BQ5ohdXj0l9iKjlWCb+3vqEINQ== + dependencies: + "@yomguithereal/helpers" "^1.1.1" + graphology-indices "^0.16.3" + graphology-utils "^2.4.3" + mnemonist "^0.39.0" + +graphology-utils@^2.1.0, graphology-utils@^2.3.0, graphology-utils@^2.4.2, graphology-utils@^2.4.3, graphology-utils@^2.4.4: + version "2.5.1" + resolved "https://registry.yarnpkg.com/graphology-utils/-/graphology-utils-2.5.1.tgz#93916ead84ec7896959b4033b94cd6994ae9952c" + integrity sha512-N6zjqvBHgJvulYnwdDgdJoeuhKXZBNm1zedC2asdN+rexfbJylhey/PVT8Bwr8B1aVxKuK+zQqMbQ50kKikjew== + +graphology@^0.24.1: + version "0.24.1" + resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.24.1.tgz#035e452e294b01168cf5c85d5dd0a4b7e4837d87" + integrity sha512-6lNz1PNTAe9Q6ioHKrXu0Lp047sgvOoHa4qmP/8mnJWCGv2iIZPQkuHPUb2/OWDWCqHpw2hKgJLJ55X/66xmHg== + dependencies: + events "^3.3.0" + obliterator "^2.0.2" + +heap@^0.2.6: + version "0.2.7" + resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" + integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== + +klayjs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/klayjs/-/klayjs-0.4.1.tgz#5bf9fadc7a3e44b94082bba501e7d803076dcfc2" + integrity sha1-W/n63Ho+RLlAgrulAefYAwdtz8I= + +layout-base@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" + integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== + +layout-base@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285" + integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + +lodash.topath@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009" + integrity sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak= + +lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +mnemonist@^0.39.0: + version "0.39.0" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.0.tgz#4c83dd22e8d9d05dfb721ff66a905fec4c460041" + integrity sha512-7v08Ldk1lnlywnIShqfKYN7EW4WKLUnkoWApdmR47N1xA2xmEtWERfEvyRCepbuFCETG5OnfaGQpp/p4Bus6ZQ== + dependencies: + obliterator "^2.0.1" + +obliterator@^2.0.1, obliterator@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21" + integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg== + +pandemonium@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/pandemonium/-/pandemonium-1.5.0.tgz#93f35af555de1420022b341e730215c51c725be3" + integrity sha512-9PU9fy93rJhZHLMjX+4M1RwZPEYl6g7DdWKGmGNhkgBZR5+tOBVExNZc00kzdEGMxbaAvWdQy9MqGAScGwYlcA== + +web-worker@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da" + integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA== diff --git a/libs/shared/mock-data/.eslintrc.json b/libs/shared/mock-data/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..3456be9b9036a42c593c82b050281230e4ca0ae4 --- /dev/null +++ b/libs/shared/mock-data/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/mock-data/.gitignore b/libs/shared/mock-data/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4eb78c54b0e23b5fdb84ed6c7bc5534217836b58 --- /dev/null +++ b/libs/shared/mock-data/.gitignore @@ -0,0 +1,145 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/libs/shared/mock-data/README.md b/libs/shared/mock-data/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3e701fff79d7765684c78442e3bf3e84c4a12322 --- /dev/null +++ b/libs/shared/mock-data/README.md @@ -0,0 +1,7 @@ +# shared-mock-data + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-mock-data` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/mock-data/jest.config.js b/libs/shared/mock-data/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..4e791eac5390d85f4736399f4f238f6db90e02ae --- /dev/null +++ b/libs/shared/mock-data/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'shared-mock-data', + preset: '../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/shared/mock-data', +}; diff --git a/libs/shared/mock-data/project.json b/libs/shared/mock-data/project.json new file mode 100644 index 0000000000000000000000000000000000000000..0a0ec6c23a302fa25f3782cc9443a60d0b895857 --- /dev/null +++ b/libs/shared/mock-data/project.json @@ -0,0 +1,29 @@ +{ + "root": "libs/shared/mock-data", + "sourceRoot": "libs/shared/mock-data/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/mock-data/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/mock-data"], + "options": { + "jestConfig": "libs/shared/mock-data/jest.config.js", + "passWithNoTests": true + } + }, + "version": { + "executor": "@jscutlery/semver:version", + "options": { + "commitMessageFormat": "chore(${projectName}): release version ${version}" + } + } + }, + "tags": [] +} diff --git a/libs/shared/mock-data/src/index.ts b/libs/shared/mock-data/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab66e18ef08fcda54d1db1bfb65b8119697452dc --- /dev/null +++ b/libs/shared/mock-data/src/index.ts @@ -0,0 +1,4 @@ +export * from './schema/simpleRaw'; +export * from './schema/moviesSchemaRaw'; +export * from './schema/northwindSchemaRaw'; +export * from './schema/twitterSchemaRaw'; diff --git a/libs/shared/mock-data/src/schema/mock-data.spec.ts b/libs/shared/mock-data/src/schema/mock-data.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f86e6d316a9867914bc99061442e2b3f8f56e854 --- /dev/null +++ b/libs/shared/mock-data/src/schema/mock-data.spec.ts @@ -0,0 +1,55 @@ +import Graph from 'graphology'; +import { movieSchemaRaw, movieSchema } from './moviesSchemaRaw'; +import { northWindSchema, northwindSchemaRaw } from './northwindSchemaRaw'; +import { simpleSchema, simpleSchemaRaw } from './simpleRaw'; +import { twitterSchema, twitterSchemaRaw } from './twitterSchemaRaw'; + +describe('MockData Tests', () => { + it('should have raw data available movie', () => { + const graph = movieSchemaRaw; + expect(graph); + }); + + it('should have raw data available northwind', () => { + const graph = northwindSchemaRaw; + expect(graph); + }); + + it('should have raw data available simpleSchemaRaw', () => { + const graph = simpleSchemaRaw; + expect(graph); + }); + + it('should have raw data available twitterSchemaRaw', () => { + const graph = twitterSchemaRaw; + expect(graph); + }); + + it('should have data available as graphology model movie', () => { + const graph = movieSchema; + expect(graph); + + expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy(); + }); + + it('should have data available as graphology model northwind', () => { + const graph = northWindSchema; + expect(graph); + + expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy(); + }); + + it('should have data available as graphology model simpleSchemaRaw', () => { + const graph = simpleSchema; + expect(graph); + + expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy(); + }); + + it('should have data available as graphology model twitterSchemaRaw', () => { + const graph = twitterSchema; + expect(graph); + + expect(graph.constructor.name.toLowerCase().indexOf('graph') != -1).toBeTruthy(); + }); +}); diff --git a/libs/shared/mock-data/src/schema/moviesSchemaRaw.ts b/libs/shared/mock-data/src/schema/moviesSchemaRaw.ts new file mode 100644 index 0000000000000000000000000000000000000000..762bce33c65af64ba2bdac7928697224afd5d651 --- /dev/null +++ b/libs/shared/mock-data/src/schema/moviesSchemaRaw.ts @@ -0,0 +1,103 @@ +import { SchemaUtils } from '@graphpolaris/schema-utils'; +import { SchemaFromBackend } from 'libs/shared/models/src'; + +export const movieSchemaRaw = { + nodes: [ + { + name: 'Movie', + attributes: [ + { + name: 'tagline', + type: 'string', + }, + { + name: 'title', + type: 'string', + }, + { + name: 'released', + type: 'int', + }, + { + name: 'votes', + type: 'int', + }, + ], + }, + { + name: 'Person', + attributes: [ + { + name: 'born', + type: 'int', + }, + { + name: 'name', + type: 'string', + }, + ], + }, + ], + edges: [ + { + name: 'ACTED_IN', + collection: 'ACTED_IN', + from: 'Person', + to: 'Movie', + attributes: [ + { + name: 'roles', + type: 'string', + }, + ], + }, + { + name: 'REVIEWED', + collection: 'REVIEWED', + from: 'Person', + to: 'Movie', + attributes: [ + { + name: 'summary', + type: 'string', + }, + { + name: 'rating', + type: 'int', + }, + ], + }, + { + name: 'PRODUCED', + collection: 'PRODUCED', + from: 'Person', + to: 'Movie', + attributes: [], + }, + { + name: 'WROTE', + collection: 'WROTE', + from: 'Person', + to: 'Movie', + attributes: [], + }, + { + name: 'FOLLOWS', + collection: 'FOLLOWS', + from: 'Person', + to: 'Person', + attributes: [], + }, + { + name: 'DIRECTED', + collection: 'DIRECTED', + from: 'Person', + to: 'Movie', + attributes: [], + }, + ], +}; + +export const movieSchema = SchemaUtils.ParseSchemaFromBackend( + movieSchemaRaw as SchemaFromBackend +); diff --git a/libs/shared/mock-data/src/schema/northwindSchemaRaw.ts b/libs/shared/mock-data/src/schema/northwindSchemaRaw.ts new file mode 100644 index 0000000000000000000000000000000000000000..59f248f5fbd9117a1021cc1852100e24c02f85ec --- /dev/null +++ b/libs/shared/mock-data/src/schema/northwindSchemaRaw.ts @@ -0,0 +1,291 @@ +import { SchemaUtils } from '@graphpolaris/schema-utils'; +import { SchemaFromBackend } from 'libs/shared/models/src'; + +export const northwindSchemaRaw = { + nodes: [ + { + name: 'Order', + attributes: [ + { + name: 'customerID', + type: 'string', + }, + { + name: 'shipCity', + type: 'string', + }, + { + name: 'orderID', + type: 'string', + }, + { + name: 'freight', + type: 'string', + }, + { + name: 'requiredDate', + type: 'string', + }, + { + name: 'employeeID', + type: 'string', + }, + { + name: 'shipName', + type: 'string', + }, + { + name: 'shipPostalCode', + type: 'string', + }, + { + name: 'orderDate', + type: 'string', + }, + { + name: 'shipRegion', + type: 'string', + }, + { + name: 'shipCountry', + type: 'string', + }, + { + name: 'shippedDate', + type: 'string', + }, + { + name: 'shipVia', + type: 'string', + }, + { + name: 'shipAddress', + type: 'string', + }, + ], + }, + { + name: 'Category', + attributes: [ + { + name: 'categoryID', + type: 'string', + }, + { + name: 'description', + type: 'string', + }, + { + name: 'categoryName', + type: 'string', + }, + { + name: 'picture', + type: 'string', + }, + ], + }, + { + name: 'Customer', + attributes: [ + { + name: 'country', + type: 'string', + }, + { + name: 'address', + type: 'string', + }, + { + name: 'contactTitle', + type: 'string', + }, + { + name: 'city', + type: 'string', + }, + { + name: 'phone', + type: 'string', + }, + { + name: 'contactName', + type: 'string', + }, + { + name: 'postalCode', + type: 'string', + }, + { + name: 'companyName', + type: 'string', + }, + { + name: 'fax', + type: 'string', + }, + { + name: 'region', + type: 'string', + }, + { + name: 'customerID', + type: 'string', + }, + ], + }, + { + name: 'Product', + attributes: [ + { + name: 'reorderLevel', + type: 'int', + }, + { + name: 'unitsInStock', + type: 'int', + }, + { + name: 'unitPrice', + type: 'float', + }, + { + name: 'supplierID', + type: 'string', + }, + { + name: 'productID', + type: 'string', + }, + { + name: 'discontinued', + type: 'bool', + }, + { + name: 'quantityPerUnit', + type: 'string', + }, + { + name: 'categoryID', + type: 'string', + }, + { + name: 'unitsOnOrder', + type: 'int', + }, + { + name: 'productName', + type: 'string', + }, + ], + }, + { + name: 'Supplier', + attributes: [ + { + name: 'supplierID', + type: 'string', + }, + { + name: 'country', + type: 'string', + }, + { + name: 'address', + type: 'string', + }, + { + name: 'contactTitle', + type: 'string', + }, + { + name: 'city', + type: 'string', + }, + { + name: 'phone', + type: 'string', + }, + { + name: 'contactName', + type: 'string', + }, + { + name: 'postalCode', + type: 'string', + }, + { + name: 'companyName', + type: 'string', + }, + { + name: 'fax', + type: 'string', + }, + { + name: 'region', + type: 'string', + }, + { + name: 'homePage', + type: 'string', + }, + ], + }, + ], + edges: [ + { + name: 'ORDERS', + collection: 'ORDERS', + from: 'Order', + to: 'Product', + attributes: [ + { + name: 'unitPrice', + type: 'string', + }, + { + name: 'productID', + type: 'string', + }, + { + name: 'orderID', + type: 'string', + }, + { + name: 'discount', + type: 'string', + }, + { + name: 'quantity', + type: 'int', + }, + ], + }, + { + name: 'PART_OF', + collection: 'PART_OF', + from: 'Product', + to: 'Category', + attributes: [], + }, + { + name: 'SUPPLIES', + collection: 'SUPPLIES', + from: 'Supplier', + to: 'Product', + attributes: [], + }, + { + name: 'PURCHASED', + collection: 'PURCHASED', + from: 'Customer', + to: 'Order', + attributes: [], + }, + ], +}; + +export const northWindSchema = SchemaUtils.ParseSchemaFromBackend( + northwindSchemaRaw as SchemaFromBackend +); diff --git a/libs/shared/mock-data/src/schema/simpleRaw.ts b/libs/shared/mock-data/src/schema/simpleRaw.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8fee0ca1eea766231f802433ee7f52ecd7b273d --- /dev/null +++ b/libs/shared/mock-data/src/schema/simpleRaw.ts @@ -0,0 +1,108 @@ +import { SchemaUtils } from '@graphpolaris/schema-utils'; +import { SchemaFromBackend } from '@graphpolaris/models'; + +export const simpleSchemaRaw = { + nodes: [ + { + name: 'Thijs', + attributes: [], + }, + { + name: 'Airport', + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + }, + { + name: 'Airport2', + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + }, + { + name: 'Plane', + attributes: [ + { name: 'type', type: 'string' }, + { name: 'maxFuelCapacity', type: 'int' }, + ], + }, + { name: 'Staff', attributes: [] }, + ], + edges: [ + { + name: 'Airport2:Airport', + from: 'Airport2', + to: 'Airport', + collection: 'flights', + attributes: [ + { name: 'arrivalTime', type: 'int' }, + { name: 'departureTime', type: 'int' }, + ], + }, + { + name: 'Airport:Staff', + from: 'Airport', + to: 'Staff', + collection: 'flights', + attributes: [{ name: 'salary', type: 'int' }], + }, + { + name: 'Plane:Airport', + from: 'Plane', + to: 'Airport', + collection: 'flights', + attributes: [], + }, + { + name: 'Airport:Thijs', + from: 'Airport', + to: 'Thijs', + collection: 'flights', + attributes: [{ name: 'hallo', type: 'string' }], + }, + { + name: 'Thijs:Airport', + from: 'Thijs', + to: 'Airport', + collection: 'flights', + attributes: [{ name: 'hallo', type: 'string' }], + }, + { + name: 'Staff:Plane', + from: 'Staff', + to: 'Plane', + collection: 'flights', + attributes: [{ name: 'hallo', type: 'string' }], + }, + { + name: 'Staff:Airport2', + from: 'Staff', + to: 'Airport2', + collection: 'flights', + attributes: [{ name: 'hallo', type: 'string' }], + }, + { + name: 'Airport2:Plane', + from: 'Airport2', + to: 'Plane', + collection: 'flights', + attributes: [{ name: 'hallo', type: 'string' }], + }, + + { + name: 'Airport:Airport', + from: 'Airport', + to: 'Airport', + collection: 'flights', + attributes: [{ name: 'test', type: 'string' }], + }, + ], +}; + +export const simpleSchema = SchemaUtils.ParseSchemaFromBackend( + simpleSchemaRaw as SchemaFromBackend +); diff --git a/libs/shared/mock-data/src/schema/twitterSchemaRaw.ts b/libs/shared/mock-data/src/schema/twitterSchemaRaw.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba922222c3fa2d9600f1318649ae59c7d0033f90 --- /dev/null +++ b/libs/shared/mock-data/src/schema/twitterSchemaRaw.ts @@ -0,0 +1,320 @@ +import { SchemaUtils } from '@graphpolaris/schema-utils'; +import { SchemaFromBackend } from 'libs/shared/models/src'; + +export const twitterSchemaRaw = { + nodes: [ + { + name: 'Me', + attributes: [ + { + name: 'screen_name', + type: 'string', + }, + { + name: 'name', + type: 'string', + }, + { + name: 'location', + type: 'string', + }, + { + name: 'followers', + type: 'int', + }, + { + name: 'following', + type: 'int', + }, + { + name: 'url', + type: 'string', + }, + { + name: 'profile_image_url', + type: 'string', + }, + ], + }, + { + name: 'Link', + attributes: [ + { + name: 'url', + type: 'string', + }, + ], + }, + { + name: 'Source', + attributes: [ + { + name: 'name', + type: 'string', + }, + ], + }, + { + name: 'Hashtag', + attributes: [ + { + name: 'name', + type: 'string', + }, + ], + }, + { + name: 'User', + attributes: [ + { + name: 'screen_name', + type: 'string', + }, + { + name: 'name', + type: 'string', + }, + { + name: 'location', + type: 'string', + }, + { + name: 'followers', + type: 'int', + }, + { + name: 'following', + type: 'int', + }, + { + name: 'url', + type: 'string', + }, + { + name: 'profile_image_url', + type: 'string', + }, + { + name: 'screen_name', + type: 'string', + }, + { + name: 'name', + type: 'string', + }, + { + name: 'location', + type: 'string', + }, + { + name: 'followers', + type: 'int', + }, + { + name: 'following', + type: 'int', + }, + { + name: 'statuses', + type: 'int', + }, + { + name: 'url', + type: 'string', + }, + { + name: 'profile_image_url', + type: 'string', + }, + ], + }, + { + name: 'Tweet', + attributes: [ + { + name: 'id', + type: 'int', + }, + { + name: 'id_str', + type: 'string', + }, + { + name: 'text', + type: 'string', + }, + { + name: 'favorites', + type: 'int', + }, + { + name: 'import_method', + type: 'string', + }, + ], + }, + ], + edges: [ + { + name: 'USING', + collection: 'USING', + from: 'Tweet', + to: 'Source', + attributes: [], + }, + { + name: 'SIMILAR_TO', + collection: 'SIMILAR_TO', + from: 'User', + to: 'User', + attributes: [ + { + name: 'score', + type: 'float', + }, + ], + }, + { + name: 'SIMILAR_TO', + collection: 'SIMILAR_TO', + from: 'User', + to: 'Me', + attributes: [ + { + name: 'score', + type: 'float', + }, + ], + }, + { + name: 'AMPLIFIES', + collection: 'AMPLIFIES', + from: 'Me', + to: 'User', + attributes: [], + }, + { + name: 'AMPLIFIES', + collection: 'AMPLIFIES', + from: 'User', + to: 'User', + attributes: [], + }, + { + name: 'RT_MENTIONS', + collection: 'RT_MENTIONS', + from: 'Me', + to: 'User', + attributes: [], + }, + { + name: 'RT_MENTIONS', + collection: 'RT_MENTIONS', + from: 'User', + to: 'User', + attributes: [], + }, + { + name: 'FOLLOWS', + collection: 'FOLLOWS', + from: 'User', + to: 'Me', + attributes: [], + }, + { + name: 'FOLLOWS', + collection: 'FOLLOWS', + from: 'Me', + to: 'User', + attributes: [], + }, + { + name: 'FOLLOWS', + collection: 'FOLLOWS', + from: 'User', + to: 'User', + attributes: [], + }, + { + name: 'FOLLOWS', + collection: 'FOLLOWS', + from: 'Me', + to: 'Me', + attributes: [], + }, + { + name: 'INTERACTS_WITH', + collection: 'INTERACTS_WITH', + from: 'User', + to: 'User', + attributes: [], + }, + { + name: 'INTERACTS_WITH', + collection: 'INTERACTS_WITH', + from: 'Me', + to: 'User', + attributes: [], + }, + { + name: 'RETWEETS', + collection: 'RETWEETS', + from: 'Tweet', + to: 'Tweet', + attributes: [], + }, + { + name: 'REPLY_TO', + collection: 'REPLY_TO', + from: 'Tweet', + to: 'Tweet', + attributes: [], + }, + { + name: 'CONTAINS', + collection: 'CONTAINS', + from: 'Tweet', + to: 'Link', + attributes: [], + }, + { + name: 'MENTIONS', + collection: 'MENTIONS', + from: 'Tweet', + to: 'User', + attributes: [], + }, + { + name: 'MENTIONS', + collection: 'MENTIONS', + from: 'Tweet', + to: 'Me', + attributes: [], + }, + { + name: 'TAGS', + collection: 'TAGS', + from: 'Tweet', + to: 'Hashtag', + attributes: [], + }, + { + name: 'POSTS', + collection: 'POSTS', + from: 'User', + to: 'Tweet', + attributes: [], + }, + { + name: 'POSTS', + collection: 'POSTS', + from: 'Me', + to: 'Tweet', + attributes: [], + }, + ], +}; + +export const twitterSchema = SchemaUtils.ParseSchemaFromBackend( + twitterSchemaRaw as SchemaFromBackend +); diff --git a/libs/schema/schema-usecases/tsconfig.json b/libs/shared/mock-data/tsconfig.json similarity index 100% rename from libs/schema/schema-usecases/tsconfig.json rename to libs/shared/mock-data/tsconfig.json diff --git a/libs/shared/mock-data/tsconfig.lib.json b/libs/shared/mock-data/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..6eb3eb9eabd5e4bb38ac7c366304f7ba7a6a491c --- /dev/null +++ b/libs/shared/mock-data/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": [], + "composite": true + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/shared/mock-data/tsconfig.spec.json b/libs/shared/mock-data/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..4afc999ad429eea63e777308527c4e8f629e4198 --- /dev/null +++ b/libs/shared/mock-data/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "composite": true + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/models/.babelrc b/libs/shared/models/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..ccae900be42788285eb6e1b3a2af4b81f56f14fe --- /dev/null +++ b/libs/shared/models/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/models/.eslintrc.json b/libs/shared/models/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..50e59482cfd41dfef0c19bd2027efcfb182f6bc2 --- /dev/null +++ b/libs/shared/models/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/models/.gitignore b/libs/shared/models/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4eb78c54b0e23b5fdb84ed6c7bc5534217836b58 --- /dev/null +++ b/libs/shared/models/.gitignore @@ -0,0 +1,145 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/libs/shared/models/README.md b/libs/shared/models/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ec693074dd1c5faa437707045ccc5396f8cf6544 --- /dev/null +++ b/libs/shared/models/README.md @@ -0,0 +1,7 @@ +# shared-models + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-models` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/models/jest.config.js b/libs/shared/models/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..67188b1bbb706872be6261eb30de2be67e54167d --- /dev/null +++ b/libs/shared/models/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + displayName: 'shared-models', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/shared/models', +}; diff --git a/libs/shared/models/package.json b/libs/shared/models/package.json new file mode 100644 index 0000000000000000000000000000000000000000..19f478f8b79a11a627b55297a3d2cd190048977f --- /dev/null +++ b/libs/shared/models/package.json @@ -0,0 +1,4 @@ +{ + "name": "@graphpolaris/models", + "version": "0.0.1" +} diff --git a/libs/shared/models/project.json b/libs/shared/models/project.json new file mode 100644 index 0000000000000000000000000000000000000000..16ea9e35534ac1536ac3ddc08b0ddfde33691e94 --- /dev/null +++ b/libs/shared/models/project.json @@ -0,0 +1,43 @@ +{ + "root": "libs/shared/models", + "sourceRoot": "libs/shared/models/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/web:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/shared/models", + "tsConfig": "libs/shared/models/tsconfig.lib.json", + "project": "libs/shared/models/package.json", + "entryFile": "libs/shared/models/src/index.ts", + "external": ["react/jsx-runtime"], + "rollupConfig": "@nrwl/react/plugins/bundle-rollup", + "compiler": "babel", + "assets": [ + { + "glob": "libs/shared/models/README.md", + "input": ".", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/models/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/models"], + "options": { + "jestConfig": "libs/shared/models/jest.config.js", + "passWithNoTests": true + } + } + } +} diff --git a/libs/shared/models/src/index.ts b/libs/shared/models/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..11a865736e6ed5d85963d39e8b33bc7803c1df0f --- /dev/null +++ b/libs/shared/models/src/index.ts @@ -0,0 +1 @@ +export * from './lib/shared-models'; diff --git a/libs/shared/models/src/lib/shared-models.spec.ts b/libs/shared/models/src/lib/shared-models.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..08a29a089432208f807f885f6069ae25f8b6201e --- /dev/null +++ b/libs/shared/models/src/lib/shared-models.spec.ts @@ -0,0 +1,7 @@ + + +describe('SharedModels', () => { + it('should render successfully', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/libs/shared/models/src/lib/shared-models.ts b/libs/shared/models/src/lib/shared-models.ts new file mode 100644 index 0000000000000000000000000000000000000000..b54d87d4913348e49d02d2531e0eb018b9397b16 --- /dev/null +++ b/libs/shared/models/src/lib/shared-models.ts @@ -0,0 +1,27 @@ +/*************** schema format from the backend *************** */ +/** Schema type, consist of nodes and edges */ +export type SchemaFromBackend = { + edges: Edge[]; + nodes: Node[]; +}; + +/** Attribute type, consist of a name */ +export type Attribute = { + name: string; + type: 'string' | 'int' | 'bool' | 'float'; +}; + +/** Node type, consist of a name and a list of attributes */ +export type Node = { + name: string; + attributes: Attribute[]; +}; + +/** Edge type, consist of a name, start point, end point and a list of attributes */ +export type Edge = { + name: string; + to: string; + from: string; + collection: string; + attributes: Attribute[]; +}; \ No newline at end of file diff --git a/libs/shared/models/tsconfig.json b/libs/shared/models/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..4b421814593c06b901b07a205f079cc08998a1e0 --- /dev/null +++ b/libs/shared/models/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/shared/models/tsconfig.lib.json b/libs/shared/models/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..36549a100ee959618b742d13c61bd6b40af0f53a --- /dev/null +++ b/libs/shared/models/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": [], + "composite": true + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/shared/models/tsconfig.spec.json b/libs/shared/models/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..d8716fecfa3b7929f162b71e7a966c579a63c071 --- /dev/null +++ b/libs/shared/models/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/models/yarn.lock b/libs/shared/models/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..fb57ccd13afbd082ad82051c2ffebef4840661ec --- /dev/null +++ b/libs/shared/models/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/libs/shared/schema-utils/.babelrc b/libs/shared/schema-utils/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..ccae900be42788285eb6e1b3a2af4b81f56f14fe --- /dev/null +++ b/libs/shared/schema-utils/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/schema-utils/.eslintrc.json b/libs/shared/schema-utils/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..50e59482cfd41dfef0c19bd2027efcfb182f6bc2 --- /dev/null +++ b/libs/shared/schema-utils/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/schema-utils/.gitignore b/libs/shared/schema-utils/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4eb78c54b0e23b5fdb84ed6c7bc5534217836b58 --- /dev/null +++ b/libs/shared/schema-utils/.gitignore @@ -0,0 +1,145 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/libs/shared/schema-utils/README.md b/libs/shared/schema-utils/README.md new file mode 100644 index 0000000000000000000000000000000000000000..99b8629120ccc1f0dafb2ac461ff40de9d45f654 --- /dev/null +++ b/libs/shared/schema-utils/README.md @@ -0,0 +1,7 @@ +# shared-schema-utils + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-schema-utils` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/schema-utils/jest.config.js b/libs/shared/schema-utils/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e351d1abc92c54e097dfd6e58aa1610a9f33c846 --- /dev/null +++ b/libs/shared/schema-utils/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + displayName: 'shared-schema-utils', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/shared/schema-utils', +}; diff --git a/libs/shared/schema-utils/package.json b/libs/shared/schema-utils/package.json new file mode 100644 index 0000000000000000000000000000000000000000..72dadd855d67b38ba862216d0180ffe6626ceabc --- /dev/null +++ b/libs/shared/schema-utils/package.json @@ -0,0 +1,4 @@ +{ + "name": "@graphpolaris/schema-utils", + "version": "0.0.1" +} diff --git a/libs/shared/schema-utils/project.json b/libs/shared/schema-utils/project.json new file mode 100644 index 0000000000000000000000000000000000000000..2414f349be5dbc92606704d842ba58a33e9240c2 --- /dev/null +++ b/libs/shared/schema-utils/project.json @@ -0,0 +1,43 @@ +{ + "root": "libs/shared/schema-utils", + "sourceRoot": "libs/shared/schema-utils/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/web:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/shared/schema-utils", + "tsConfig": "libs/shared/schema-utils/tsconfig.lib.json", + "project": "libs/shared/schema-utils/package.json", + "entryFile": "libs/shared/schema-utils/src/index.ts", + "external": ["react/jsx-runtime"], + "rollupConfig": "@nrwl/react/plugins/bundle-rollup", + "compiler": "babel", + "assets": [ + { + "glob": "libs/shared/schema-utils/README.md", + "input": ".", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/schema-utils/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/schema-utils"], + "options": { + "jestConfig": "libs/shared/schema-utils/jest.config.js", + "passWithNoTests": true + } + } + } +} diff --git a/libs/shared/schema-utils/src/index.ts b/libs/shared/schema-utils/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..70bd0a41b24658ef1f5a4223f3172bc62f69aa25 --- /dev/null +++ b/libs/shared/schema-utils/src/index.ts @@ -0,0 +1 @@ +export * from './lib/schema-utils'; diff --git a/libs/shared/schema-utils/src/lib/schema-utils.spec.ts b/libs/shared/schema-utils/src/lib/schema-utils.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..21f5c4cda1dd36bf007886829531eb8b8a3ff978 --- /dev/null +++ b/libs/shared/schema-utils/src/lib/schema-utils.spec.ts @@ -0,0 +1,10 @@ +import { SchemaFromBackend } from "@graphpolaris/shared/data-access/store"; +import { SchemaUtils } from "./schema-utils"; + +describe('Schema Utils', () => { + it('should expose a class SchemaUtils', () => { + const clazz = new SchemaUtils() + expect(clazz); + }); + }); + \ No newline at end of file diff --git a/libs/shared/schema-utils/src/lib/schema-utils.ts b/libs/shared/schema-utils/src/lib/schema-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ca15baedb7cdbe011e12d9eaf688c0fa23a73ed --- /dev/null +++ b/libs/shared/schema-utils/src/lib/schema-utils.ts @@ -0,0 +1,34 @@ +import { SchemaFromBackend, Node, Edge } from '@graphpolaris/models'; +import Graph, { MultiGraph } from 'graphology'; + +export class SchemaUtils { + public static ParseSchemaFromBackend( + schemaFromBackend: SchemaFromBackend + ): Graph { + const { nodes, edges } = schemaFromBackend; + // Instantiate a directed graph that allows self loops and parallel edges + const schemaGraph = new MultiGraph({ allowSelfLoops: true }); + // console.log('parsing schema'); + // The graph schema needs a node for each node AND edge. These need then be connected + + nodes.forEach((node: Node) => { + schemaGraph.addNode(node.name, { + name: node.name, + attributes: node.attributes, + x: 0, + y: 0, + }); + }); + + // The name of the edge will be name + from + to, since edge names are not unique + edges.forEach((edge: Edge) => { + const edgeID = [edge.name, '_', edge.from, edge.to].join(''); //ensure that all interpreted as string + + // This node is the actual edge + schemaGraph.addDirectedEdgeWithKey(edgeID, edge.from, edge.to, { + attribute: edge.attributes, + }); + }); + return schemaGraph; + } +} diff --git a/libs/shared/schema-utils/tsconfig.json b/libs/shared/schema-utils/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..4b421814593c06b901b07a205f079cc08998a1e0 --- /dev/null +++ b/libs/shared/schema-utils/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/shared/schema-utils/tsconfig.lib.json b/libs/shared/schema-utils/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..9c1ac43e8efdd93ba3faef900cc1ececbf0a8eb0 --- /dev/null +++ b/libs/shared/schema-utils/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/shared/schema-utils/tsconfig.spec.json b/libs/shared/schema-utils/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..d8716fecfa3b7929f162b71e7a966c579a63c071 --- /dev/null +++ b/libs/shared/schema-utils/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/shared/schema-utils/yarn.lock b/libs/shared/schema-utils/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..fb57ccd13afbd082ad82051c2ffebef4840661ec --- /dev/null +++ b/libs/shared/schema-utils/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/package.json b/package.json index d8191b152a48038e070cfe56bdcd2c5a613d7f2c..9d07a7788766fc39c5af14ad5142bf853f2776fe 100644 --- a/package.json +++ b/package.json @@ -6,24 +6,27 @@ "start": "nx serve", "build": "nx build", "test": "nx test", + "test:all": "nx affected:test --all --codeCoverage --skip-nx-cache", "prepare": "husky install" }, "private": true, "dependencies": { "@commitlint/config-conventional": "^16.0.0", - "@emotion/react": "^11.7.1", - "@emotion/styled": "^11.6.0", "@mui/material": "^5.4.1", - "@mui/styles": "^5.4.1", + "@mui/styled-engine-sc": "^5.4.1", + "@mui/system": "^5.4.1", "@reduxjs/toolkit": "^1.7.1", "@types/cytoscape": "^3.19.4", "@types/react-grid-layout": "^1.3.0", "@types/styled-components": "^5.1.21", + "classnames": "^2.3.1", + "color": "^4.2.1", "core-js": "^3.6.5", "cytoscape": "^3.21.0", - "graphology": "^0.23.2", - "graphology-types": "^0.23.0", + "graphology": "^0.24.0", + "graphology-types": "^0.24.0", "react": "17.0.2", + "react-cookie": "^4.1.1", "react-dom": "17.0.2", "react-flow-renderer": "^9.7.4", "react-grid-layout": "^1.3.3", @@ -56,6 +59,7 @@ "@svgr/webpack": "^5.4.0", "@testing-library/react": "12.1.2", "@testing-library/react-hooks": "7.0.2", + "@types/color": "^3.0.3", "@types/jest": "27.0.2", "@types/node": "16.11.7", "@types/react": "17.0.30", diff --git a/tsconfig.base.json b/tsconfig.base.json index 4f689f610a680e42f9bc6583c8ec06d80b46ee87..a5bafb69c61a39d4c988430267936172aeb36df7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,15 +15,27 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@graphpolaris/schema/schema-usecases": [ - "libs/schema/schema-usecases/src/index.ts" + "@graphpolaris/graph-layout": ["libs/shared/graph-layout/src/index.ts"], + "@graphpolaris/models": ["libs/shared/models/src/index.ts"], + "@graphpolaris/schema-utils": ["libs/shared/schema-utils/src/index.ts"], + "@graphpolaris/schema/usecases": ["libs/schema/usecases/src/index.ts"], + "@graphpolaris/querybuilder/usecases": [ + "libs/querybuilder/usecases/src/index.ts" + ], + "@graphpolaris/shared/data-access/api": [ + "libs/shared/data-access/api/src/index.ts" + ], + "@graphpolaris/shared/data-access/authorization": [ + "libs/shared/data-access/authorization/src/index.ts" ], "@graphpolaris/shared/data-access/store": [ "libs/shared/data-access/store/src/index.ts" ], "@graphpolaris/shared/data-access/theme": [ "libs/shared/data-access/theme/src/index.ts" - ] + ], + "@graphpolaris/shared/mock-data": ["libs/shared/mock-data/src/index.ts"], + "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 1369ffa1cca6a284a3d2dc70381d94ff38ddb536..d829d3ca0515a309a5a268383dcf3764953f2251 100644 --- a/workspace.json +++ b/workspace.json @@ -1,9 +1,16 @@ { "version": 2, "projects": { - "schema-schema-usecases": "libs/schema/schema-usecases", + "querybuilder-usecases": "libs/querybuilder/usecases", + "schema-usecases": "libs/schema/usecases", + "shared-data-access-api": "libs/shared/data-access/api", + "shared-data-access-authorization": "libs/shared/data-access/authorization", "shared-data-access-store": "libs/shared/data-access/store", "shared-data-access-theme": "libs/shared/data-access/theme", + "shared-graph-layout": "libs/shared/graph-layout", + "shared-mock-data": "libs/shared/mock-data", + "shared-models": "libs/shared/models", + "shared-schema-utils": "libs/shared/schema-utils", "web-graphpolaris": "apps/web-graphpolaris", "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e" } diff --git a/yarn.lock b/yarn.lock index 4b4bf132a0a4039f25d9efd6340ba1bb48626f2d..ad3e6305a7719eb8dc8d0e2a9ad48f4ff0bc32e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -664,7 +664,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.16.7": +"@babel/plugin-syntax-jsx@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== @@ -1202,7 +1202,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.16.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.0.tgz#b8d142fc0f7664fb3d9b5833fd40dcbab89276c0" integrity sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ== @@ -1216,7 +1216,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.3": +"@babel/runtime@^7.17.0", "@babel/runtime@^7.8.7": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1519,24 +1519,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA== -"@emotion/babel-plugin@^11.3.0": - version "11.7.2" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz#fec75f38a6ab5b304b0601c74e2a5e77c95e5fa0" - integrity sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ== - dependencies: - "@babel/helper-module-imports" "^7.12.13" - "@babel/plugin-syntax-jsx" "^7.12.13" - "@babel/runtime" "^7.13.10" - "@emotion/hash" "^0.8.0" - "@emotion/memoize" "^0.7.5" - "@emotion/serialize" "^1.0.2" - babel-plugin-macros "^2.6.1" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "4.0.13" - "@emotion/cache@^10.0.27": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -1579,7 +1561,7 @@ "@emotion/utils" "0.11.3" babel-plugin-emotion "^10.0.27" -"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0": +"@emotion/hash@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== @@ -1603,24 +1585,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== -"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": +"@emotion/memoize@^0.7.4": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== -"@emotion/react@^11.7.1": - version "11.7.1" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07" - integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw== - dependencies: - "@babel/runtime" "^7.13.10" - "@emotion/cache" "^11.7.1" - "@emotion/serialize" "^1.0.2" - "@emotion/sheet" "^1.1.0" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" - hoist-non-react-statics "^3.3.1" - "@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": version "0.11.16" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" @@ -1632,17 +1601,6 @@ "@emotion/utils" "0.11.3" csstype "^2.5.7" -"@emotion/serialize@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" - integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== - dependencies: - "@emotion/hash" "^0.8.0" - "@emotion/memoize" "^0.7.4" - "@emotion/unitless" "^0.7.5" - "@emotion/utils" "^1.0.0" - csstype "^3.0.2" - "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" @@ -1671,23 +1629,12 @@ "@emotion/styled-base" "^10.3.0" babel-plugin-emotion "^10.0.27" -"@emotion/styled@^11.6.0": - version "11.6.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d" - integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw== - dependencies: - "@babel/runtime" "^7.13.10" - "@emotion/babel-plugin" "^11.3.0" - "@emotion/is-prop-valid" "^1.1.1" - "@emotion/serialize" "^1.0.2" - "@emotion/utils" "^1.0.0" - "@emotion/stylis@0.8.5", "@emotion/stylis@^0.8.4": version "0.8.5" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4", "@emotion/unitless@^0.7.5": +"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.4": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== @@ -2123,36 +2070,20 @@ "@mui/utils" "^5.4.1" prop-types "^15.7.2" -"@mui/styled-engine@^5.4.1": +"@mui/styled-engine-sc@^5.4.1": version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.4.1.tgz#1427738e71c087f7005547e17d4a59de75597850" - integrity sha512-CFLNJkopRoAuShkgUZOTBVxdTlKu4w6L4kOwPi4r3QB2XXS6O5kyLHSsg9huUbtOYk5Dv5UZyUSc5pw4J7ezdg== + resolved "https://registry.yarnpkg.com/@mui/styled-engine-sc/-/styled-engine-sc-5.4.1.tgz#e52c9c1f330c8f2fb2ebf2acccbda8287cfbbb2d" + integrity sha512-7PPSwPh7Kugdapi5uQ/1PojZ6hVPpF4mB77pTm8BMm+WT/noRv3ESfAEP4Ic51LZWy95oeK0GgqsyPRjDiyHLA== dependencies: - "@babel/runtime" "^7.17.0" - "@emotion/cache" "^11.7.1" prop-types "^15.7.2" -"@mui/styles@^5.4.1": +"@mui/styled-engine@^5.4.1": version "5.4.1" - resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.1.tgz#994171da902267184fffa19896ee5bbb07d4d783" - integrity sha512-ekw2NBC06re0H9SvCA1XgtFcghB8AQdGPXD3mjIz5ik+X+LvR+f2TeoCpJpkKp7UQdcNn6uuYi6BO6irTiQhdw== + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.4.1.tgz#1427738e71c087f7005547e17d4a59de75597850" + integrity sha512-CFLNJkopRoAuShkgUZOTBVxdTlKu4w6L4kOwPi4r3QB2XXS6O5kyLHSsg9huUbtOYk5Dv5UZyUSc5pw4J7ezdg== dependencies: "@babel/runtime" "^7.17.0" - "@emotion/hash" "^0.8.0" - "@mui/private-theming" "^5.4.1" - "@mui/types" "^7.1.1" - "@mui/utils" "^5.4.1" - clsx "^1.1.1" - csstype "^3.0.10" - hoist-non-react-statics "^3.3.2" - jss "^10.8.2" - jss-plugin-camel-case "^10.8.2" - jss-plugin-default-unit "^10.8.2" - jss-plugin-global "^10.8.2" - jss-plugin-nested "^10.8.2" - jss-plugin-props-sort "^10.8.2" - jss-plugin-rule-value-function "^10.8.2" - jss-plugin-vendor-prefixer "^10.8.2" + "@emotion/cache" "^11.7.1" prop-types "^15.7.2" "@mui/system@^5.4.1": @@ -4141,7 +4072,7 @@ dependencies: "@types/node" "*" -"@types/color-convert@^2.0.0": +"@types/color-convert@*", "@types/color-convert@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== @@ -4153,6 +4084,13 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/color@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.3.tgz#e6d8d72b7aaef4bb9fe80847c26c7c786191016d" + integrity sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA== + dependencies: + "@types/color-convert" "*" + "@types/connect-history-api-fallback@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" @@ -4168,6 +4106,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" + integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== + "@types/cytoscape@^3.19.4": version "3.19.4" resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.4.tgz#f41214103b80ff3d7d8741bacc32265ed90e45b5" @@ -4247,7 +4190,7 @@ dependencies: "@types/unist" "*" -"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.0.1", "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -5684,7 +5627,7 @@ babel-plugin-jest-hoist@^27.4.0: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.6.1, babel-plugin-macros@^2.8.0: +babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== @@ -6484,6 +6427,11 @@ classcat@^5.0.3: resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.3.tgz#38eaa0ec6eb1b10faf101bbcef2afb319c23c17b" integrity sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ== +classnames@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-css@^4.2.3: version "4.2.4" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" @@ -6635,16 +6583,32 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color-support@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" + integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colord@^2.9.1: version "2.9.2" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" @@ -7018,6 +6982,11 @@ cookie@0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -7364,14 +7333,6 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-vendor@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" - integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== - dependencies: - "@babel/runtime" "^7.8.3" - is-in-browser "^1.0.2" - css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -9634,18 +9595,18 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== -graphology-types@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/graphology-types/-/graphology-types-0.23.0.tgz#76a0564baf31891044a7b0cc6cd028810541bb7a" - integrity sha512-6Je1NWU3el7YmybAhRzrOEi79Blhx05EU3wGUCvP5ikaxRXEflrW/5unfw5q/wqfwjryM9tcwUv4M7TZ8yTBYQ== +graphology-types@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/graphology-types/-/graphology-types-0.24.0.tgz#81aaef55226edb692dd63a9ce5eaecc80694cd93" + integrity sha512-3qSanRtucm6rwBjpwuAc18GQcl68NqIRE4OA3wFUzdB2HRVXYoCAUsUJVS898bW+byEgd+BTcwR26CbltNvSWQ== -graphology@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.23.2.tgz#b09a33a9408a7615c3c9cff98e7404cc70a21820" - integrity sha512-RHcLpAP4M+KPShLQEvgkT1Y4vxl+FFbmmy3D0mupO+VXIuYC8zdmMcHs40D9m3mmN067zGS+lUaHjDq06Td7PQ== +graphology@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/graphology/-/graphology-0.24.0.tgz#c3c78b197f8ff6d8d3422a2d705c16e637b295f6" + integrity sha512-tEtz8n+rrx19l7muKh9VJwiRcFPu3FL9brJk4ilQR6tt+yoYsgomQHYnJaebkldIZPOXZ1mP8DEEnF0rpk8eNQ== dependencies: events "^3.3.0" - obliterator "^2.0.0" + obliterator "^2.0.2" handle-thing@^2.0.0: version "2.0.1" @@ -9874,7 +9835,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -10115,11 +10076,6 @@ husky@^7.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== -hyphenate-style-name@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" - integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== - iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -10377,6 +10333,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -10557,11 +10518,6 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== -is-in-browser@^1.0.2, is-in-browser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" - integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= - is-installed-globally@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -11571,76 +11527,6 @@ jsprim@^2.0.2: json-schema "0.4.0" verror "1.10.0" -jss-plugin-camel-case@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7" - integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww== - dependencies: - "@babel/runtime" "^7.3.1" - hyphenate-style-name "^1.0.3" - jss "10.9.0" - -jss-plugin-default-unit@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991" - integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - -jss-plugin-global@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f" - integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - -jss-plugin-nested@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3" - integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - tiny-warning "^1.0.2" - -jss-plugin-props-sort@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d" - integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - -jss-plugin-rule-value-function@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67" - integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.9.0" - tiny-warning "^1.0.2" - -jss-plugin-vendor-prefixer@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a" - integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA== - dependencies: - "@babel/runtime" "^7.3.1" - css-vendor "^2.0.8" - jss "10.9.0" - -jss@10.9.0, jss@^10.8.2: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b" - integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw== - dependencies: - "@babel/runtime" "^7.3.1" - csstype "^3.0.2" - is-in-browser "^1.1.3" - tiny-warning "^1.0.2" - "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" @@ -12848,10 +12734,10 @@ objectorarray@^1.0.5: resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== -obliterator@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.1.tgz#fbdd873bf39fc4f365a53b1fc86617a22526987c" - integrity sha512-XnkiCrrBcIZQitJPAI36mrrpEUvatbte8hLcTcQwKA1v9NkCKasSi+UAguLsLDs/out7MoRzAlmz7VXvY6ph6w== +obliterator@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.2.tgz#25f50dc92e1181371b9d8209d11890f1a3c2fc21" + integrity sha512-g0TrA7SbUggROhDPK8cEu/qpItwH2LSKcNl4tlfBNT54XY+nOsqrs0Q68h1V9b3HOSpIWv15jb1lax2hAggdIg== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" @@ -14063,6 +13949,15 @@ react-colorful@^5.1.2: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784" integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg== +react-cookie@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-4.1.1.tgz#832e134ad720e0de3e03deaceaab179c4061a19d" + integrity sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A== + dependencies: + "@types/hoist-non-react-statics" "^3.0.1" + hoist-non-react-statics "^3.0.0" + universal-cookie "^4.0.0" + react-docgen-typescript@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c" @@ -15197,6 +15092,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -16062,11 +15964,6 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-warning@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -16553,6 +16450,14 @@ unist-util-visit@2.0.3, unist-util-visit@^2.0.0: unist-util-is "^4.0.0" unist-util-visit-parents "^3.0.0" +universal-cookie@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" + integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== + dependencies: + "@types/cookie" "^0.3.3" + cookie "^0.4.0" + universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"