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/.gitlab-ci.yml b/.gitlab-ci.yml index e35789def0cfe075414a549041e95c960b8a2c40..3a29f10fe025a48e2859a3b0f067bf9061d59475 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,8 @@ image: node:16-alpine stages: - setup - test + - build + - dockerize install-dependencies: stage: setup @@ -33,14 +35,40 @@ install-dependencies: paths: - node_modules/.cache/nx -build: - stage: test - extends: .distributed - script: - - yarn nx affected --base=HEAD~1 --target=build --parallel --max-parallel=3 - test: stage: test extends: .distributed script: - yarn nx affected --base=HEAD~1 --target=test --parallel --max-parallel=2 + +build: + stage: build + only: + - main + needs: + - install-dependencies + artifacts: + paths: + - node_modules/.cache/nx + - dist/apps/web-graphpolaris + script: + # - 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: + - main + script: + - docker build --progress plain -t datastropheregistry.azurecr.io/frontend:latest: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/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 9f79b01a9bf341c469a185d07e24283bc938c6cc..ebcaddb86091a83d3a8e2df8a5d1defc53f03b89 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,23 @@ Due to the way auth works (using a sameSite cookie), the procedure for running l ### MacOS / Linux 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` +2. Add a new row containing `127.0.0.1 local.graphpolaris.com`, this will route traffic from `local.graphpolaris.com` to `127.0.0.1` 3. `brew install mkcert` install mkcert utility 4. `mkcert -install` generate local CA (certificate authority) 5. Move into the /certs folder at the project root using `cd` -6. `mkcert --key-file local-key.pem --cert-file local-cert.pem local.datastrophe.science.uu.nl` create certificates for local SSL +6. `mkcert --key-file local-key.pem --cert-file local-cert.pem local.graphpolaris.com` create certificates for local SSL ### Windows 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` +2. Add a new row containing `127.0.0.1 local.graphpolaris.com`, this will route traffic from `local.graphpolaris.com` to `127.0.0.1` 3. Install mkcert using any of the ways described [here](https://github.com/FiloSottile/mkcert#windows) 4. Open an elevated Powershell or CMD session 5. Move into the /certs folder at the project root using `cd` -6. `mkcert --key-file local-key.pem --cert-file local-cert.pem local.datastrophe.science.uu.nl` create certificates for local SSL +6. `mkcert --key-file local-key.pem --cert-file local-cert.pem local.graphpolaris.com` create certificates for local SSL > No idea if the Windows steps work ## Running Locally -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. +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.graphpolaris.com:4200/` automatically. diff --git a/apps/web-graphpolaris/project.json b/apps/web-graphpolaris/project.json index dc9c691acc30a77b498ee4ee9c7abf79928cff15..86403889595b911f74d0f30838cc3bc428eb2251 100644 --- a/apps/web-graphpolaris/project.json +++ b/apps/web-graphpolaris/project.json @@ -7,7 +7,7 @@ "executor": "@nrwl/web:dev-server", "options": { "buildTarget": "web-graphpolaris:build", - "host": "local.datastrophe.science.uu.nl", + "host": "local.graphpolaris.com", "port": 4200, "watch": true, "hmr": true, @@ -19,7 +19,9 @@ }, "build": { "executor": "@nrwl/web:webpack", - "outputs": ["{options.outputPath}"], + "outputs": [ + "{options.outputPath}" + ], "defaultConfiguration": "production", "options": { "compiler": "babel", @@ -33,18 +35,18 @@ "apps/web-graphpolaris/src/favicon.ico", "apps/web-graphpolaris/src/assets" ], - "styles": ["apps/web-graphpolaris/src/styles.scss"], + "styles": [ + "apps/web-graphpolaris/src/styles.scss" + ], "scripts": [], "webpackConfig": "@nrwl/react/plugins/webpack" }, "configurations": { "production": { - "fileReplacements": [ - { - "replace": "apps/graphpolaris/src/environments/environment.ts", - "with": "apps/graphpolaris/src/environments/environment.prod.ts" - } - ], + "fileReplacements": [{ + "replace": "apps/graphpolaris/src/environments/environment.ts", + "with": "apps/graphpolaris/src/environments/environment.prod.ts" + }], "optimization": true, "outputHashing": "all", "sourceMap": false, @@ -69,14 +71,20 @@ }, "lint": { "executor": "@nrwl/linter:eslint", - "outputs": ["{options.outputFile}"], + "outputs": [ + "{options.outputFile}" + ], "options": { - "lintFilePatterns": ["apps/graphpolaris/**/*.{ts,tsx,js,jsx}"] + "lintFilePatterns": [ + "apps/graphpolaris/**/*.{ts,tsx,js,jsx}" + ] } }, "test": { "executor": "@nrwl/jest:jest", - "outputs": ["coverage/apps/graphpolaris"], + "outputs": [ + "coverage/apps/graphpolaris" + ], "options": { "jestConfig": "apps/web-graphpolaris/jest.config.js", "passWithNoTests": true @@ -105,7 +113,9 @@ }, "build-storybook": { "executor": "@nrwl/storybook:build", - "outputs": ["{options.outputPath}"], + "outputs": [ + "{options.outputPath}" + ], "options": { "uiFramework": "@storybook/react", "outputPath": "dist/storybook/graphpolaris", @@ -121,4 +131,4 @@ } }, "tags": [] -} +} \ No newline at end of file diff --git a/apps/web-graphpolaris/src/components/login/loginScreen.tsx b/apps/web-graphpolaris/src/components/login/loginScreen.tsx index 93339edaadd13653822233ecf9aac3468f9f4435..fc1cf4a89f19194a218396348dbcc5cf4dc1e608 100644 --- a/apps/web-graphpolaris/src/components/login/loginScreen.tsx +++ b/apps/web-graphpolaris/src/components/login/loginScreen.tsx @@ -102,7 +102,7 @@ const LoginScreen = () => { <img onClick={() => openSignInWindow( - 'https://datastrophe.science.uu.nl/user/sign-in?provider=1' + 'https://api.graphpolaris.com/user/sign-in?provider=1' ) } src="assets/login-screen/google.png" @@ -111,7 +111,7 @@ const LoginScreen = () => { <img onClick={() => openSignInWindow( - 'https://datastrophe.science.uu.nl/user/sign-in?provider=2' + 'https://api.graphpolaris.com/user/sign-in?provider=2' ) } src="assets/login-screen/github.png" @@ -119,9 +119,7 @@ const LoginScreen = () => { /> <p onClick={() => - openSignInWindow( - 'https://datastrophe.science.uu.nl/user/create_free/' - ) + openSignInWindow('https://api.graphpolaris.com/user/create_free/') } > Developer 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 index e774208e2edc900f4b6a14a50e8a742cc85745e6..755d2b41d564abe3f9e4eb41f56865ef7d2432f4 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.module.scss @@ -7,6 +7,10 @@ border-radius: 3px; } +.highlighted { + box-shadow: black 0 0 2px; +} + .handleLeft { border: 0px; border-radius: 0px; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx index 2598bf4581edd6c9ca1225b2b8351155289350e0..1e5f6f7ba5d6a97bd5b47daea744a3a033cd09e7 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/entitypill/entitypill.tsx @@ -3,6 +3,7 @@ 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: { @@ -31,7 +32,9 @@ export const EntityRFPill = React.memo(({ data }: { data: any }) => { return ( <div - className={styles.entity} + className={cn(styles.entity, { + [styles.highlighted]: data.suggestedForConnection, + })} style={{ background: theme.palette.queryBuilder.entity.background, color: theme.palette.queryBuilder.text, 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 index c0b913a4537276fa249d66b21fb846fc6fd18635..aff84b2e5d8b9caba6dd9fdd34a99b54614946d4 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.module.scss @@ -7,12 +7,17 @@ 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; @@ -86,6 +91,7 @@ $height: 10px; .arrowLeft { + z-index: 2; width: 0; height: 0; border-top: $height solid transparent; diff --git a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx index 34c0d57eb52dbddd0412474b2c765f321a0c1c8f..f1d4d8592cf367e719ac3db963948b5c06145ef1 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/customFlowPills/relationpill/relationpill.tsx @@ -1,6 +1,7 @@ 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'; @@ -54,7 +55,9 @@ export default function RelationRFPill({ data }: { data: any }) { }} /> <div - className={styles.contentWrapper} + className={cn(styles.contentWrapper, { + [styles.highlighted]: data.suggestedForConnection, + })} style={{ color: theme.palette.queryBuilder.text, background: theme.palette.queryBuilder.relation.background, diff --git a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx index 22a655d556152fb7281b0bb22908bb8c0f78e0a9..3870f76de645a2d47757c5e0a40e2c8bea89503b 100644 --- a/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx +++ b/apps/web-graphpolaris/src/components/querybuilder/querybuilder.tsx @@ -1,14 +1,15 @@ import { createReactFlowElements, - DragAttributePill, - DragAttributesAlong, + dragPill, + dragPillStarted, + dragPillStopped, } from '@graphpolaris/querybuilder/usecases'; import { setQuerybuilderNodes, useAppDispatch, useQuerybuilderNodes, } from '@graphpolaris/shared/data-access/store'; -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; import ReactFlow, { ReactFlowProvider, Background, @@ -38,6 +39,7 @@ const onLoad = (reactFlowInstance: any) => { const QueryBuilder = (props: {}) => { const nodes = useQuerybuilderNodes(); const dispatch = useAppDispatch(); + const isDraggingPill = useRef(false); const elements = useMemo(() => createReactFlowElements(nodes), [nodes]); @@ -45,27 +47,35 @@ const QueryBuilder = (props: {}) => { 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; - nodes.setNodeAttribute(node.id, 'x', node.position.x); - nodes.setNodeAttribute(node.id, 'y', node.position.y); - - switch (nodes.getNodeAttribute(node.id, 'type')) { - case 'attribute': - DragAttributePill(node.id, nodes, dx, dy); - break; - case 'entity': - DragAttributesAlong(node.id, nodes, dx, dy); - break; - case 'relation': - DragAttributesAlong(node.id, nodes, dx, dy); - break; + // 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())); }; @@ -81,6 +91,7 @@ const QueryBuilder = (props: {}) => { connectionLineComponent={ConnectionDragLine} onLoad={onLoad} onNodeDrag={onNodeDrag} + onNodeDragStop={onNodeDragStop} className={styles.reactflow} > <Background gap={10} size={0.7} /> diff --git a/deployment.yml b/deployment.yml new file mode 100644 index 0000000000000000000000000000000000000000..fc945c18719db08baaf0e539fd9a16bcecb04609 --- /dev/null +++ b/deployment.yml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend-webserver-deployment + labels: + app: frontend-webserver +spec: + replicas: 1 + selector: + matchLabels: + app: frontend-webserver + template: + metadata: + labels: + app: frontend-webserver + spec: + containers: + - name: container + image: datastropheregistry.azurecr.io/frontend:latest + ports: + - containerPort: 3000 + imagePullSecrets: + - name: docker-regcred + +--- +kind: Service +apiVersion: v1 +metadata: + name: frontend-webserver +spec: + selector: + app: frontend-webserver + ports: + - port: 3000 + targetPort: 80 diff --git a/libs/querybuilder/usecases/src/index.ts b/libs/querybuilder/usecases/src/index.ts index fa59d4c1fd04854d57f7c8765d39de8a765054b9..32e0c81f2ecfe335cb61852728753c148b9c6c8b 100644 --- a/libs/querybuilder/usecases/src/index.ts +++ b/libs/querybuilder/usecases/src/index.ts @@ -2,6 +2,5 @@ export * from './lib/attribute/getAttributeBoolOperators'; export * from './lib/attribute/checkInput'; export * from './lib/createReactFlowElements'; export * from './lib/pillHandles'; -export * from './lib/dragging/dragAttribute'; -export * from './lib/dragging/dragAttributesAlong'; +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 index 008127598cfee48f3f71793ba23a5e3062607621..e7b359d8f0fc77f1281fa57a7c200f13acfb820e 100644 --- a/libs/querybuilder/usecases/src/lib/addPill.ts +++ b/libs/querybuilder/usecases/src/lib/addPill.ts @@ -56,7 +56,7 @@ function calcWidthHeightOfPill(attributes: Attributes): { const widthOfPillWithoutText = 42.1164; // WARNING: depends on styling w += widthOfPillWithoutText; - h = 21; + h = 20; break; } case 'relation': { diff --git a/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts index f48dd787c36ab5b8ceb4c2cdf24f9a8ba1c9d5a6..a1c29ab1f05d62105afaf80faaa405c30ce60535 100644 --- a/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts +++ b/libs/querybuilder/usecases/src/lib/attribute/checkInput.ts @@ -11,7 +11,7 @@ function toBoolean(s: string): string { return 'false'; } -/** Checks if the provided value is the same as the datatype of the attribute. */ +/** 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) { diff --git a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts index eea786d7fe5c6cd7f3888545af8b621e6b69fbfc..eba3bf06477f9bd53ac9e1c87ea13126ecd58e02 100644 --- a/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts +++ b/libs/querybuilder/usecases/src/lib/createReactFlowElements.ts @@ -1,6 +1,6 @@ -import Graph from 'graphology'; +import Graph, { MultiGraph } from 'graphology'; import { Attributes } from 'graphology-types'; -import { Elements, Node, Edge } from 'react-flow-renderer'; +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> { @@ -8,6 +8,7 @@ export function createReactFlowElements(graph: Graph): 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': @@ -41,21 +42,29 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { 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 }; + data = { + ...data, + name: attributes.name, + suggestedForConnection: attributes.suggestedForConnection, // Highlights the pill, with shadow or something + }; const RFNode: Node = { id: node, type: attributes.type, - position: { x: attributes?.x || 0, y: attributes?.y || 0 }, + 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; @@ -73,3 +82,49 @@ export function createReactFlowElements(graph: Graph): Elements<Node | Edge> { 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 index 332ca29cd3283c97d3876b9ca176068d97815e3e..df89ab57dcc526b42c151e868a50add049efbe7d 100644 --- a/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts +++ b/libs/querybuilder/usecases/src/lib/dragging/dragAttribute.ts @@ -1,12 +1,30 @@ -import Graph from 'graphology'; +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: Graph, + nodes: MultiGraph, dx: number, dy: number ) { - // if the attribute is still connected to an entity or relation pill, disconnect - const es = nodes.outEdges(id); - es.forEach((e) => nodes.dropEdge(e)); + // 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/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/shared/data-access/api/src/lib/database.ts b/libs/shared/data-access/api/src/lib/database.ts index 8d6700de6f55dde670227d1cedf1080c44637e28..da706c42a71c1cedc8a777afd5870480b3ec17eb 100644 --- a/libs/shared/data-access/api/src/lib/database.ts +++ b/libs/shared/data-access/api/src/lib/database.ts @@ -14,7 +14,7 @@ export type AddDatabaseRequest = { export function AddDatabase(request: AddDatabaseRequest): Promise<void> { return new Promise((resolve, reject) => { - fetch('https://datastrophe.science.uu.nl/user/database', { + fetch('https://api.graphpolaris.com/user/database', { method: 'POST', credentials: 'same-origin', headers: new Headers({ @@ -34,7 +34,7 @@ export function AddDatabase(request: AddDatabaseRequest): Promise<void> { export function GetAllDatabases(): Promise<Array<string>> { return new Promise<Array<string>>((resolve, reject) => { - fetch('https://datastrophe.science.uu.nl/user/database', { + fetch('https://api.graphpolaris.com/user/database', { method: 'GET', credentials: 'same-origin', headers: new Headers({ @@ -57,7 +57,7 @@ export function GetAllDatabases(): Promise<Array<string>> { export function DeleteDatabase(name: string): Promise<void> { return new Promise((resolve, reject) => { - fetch('https://datastrophe.science.uu.nl/user/database/' + name, { + fetch('https://api.graphpolaris.com/user/database/' + name, { method: 'DELETE', credentials: 'same-origin', headers: new Headers({ diff --git a/libs/shared/data-access/api/src/lib/user.ts b/libs/shared/data-access/api/src/lib/user.ts index 457ce20f6af6c8be31727f2b9757134827b4fc70..b3d7b3fc9e6b5411369a4a5b11e3ed8e7e009065 100644 --- a/libs/shared/data-access/api/src/lib/user.ts +++ b/libs/shared/data-access/api/src/lib/user.ts @@ -10,7 +10,7 @@ export type User = { export function GetUserInfo(): Promise<User> { return new Promise<User>((resolve, reject) => { - fetch('https://datastrophe.science.uu.nl/user/', { + fetch('https://api.graphpolaris.com/user/', { method: 'GET', credentials: 'same-origin', headers: new Headers({ diff --git a/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts b/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts index fc92922419ad609e85767e55dd35b06b2a581012..bc6001fd2aed8d24d60abf15411167af64629e81 100644 --- a/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts +++ b/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts @@ -1,5 +1,3 @@ -import { Cookies } from 'react-cookie'; - export class AuthorizationHandler { private static _instance: AuthorizationHandler; private accessToken = ''; @@ -52,7 +50,7 @@ export class AuthorizationHandler { */ 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'; + let url = 'https://api.graphpolaris.com/auth/refresh'; if (this.accessToken != '') { url += '?access_token=' + this.accessToken; } @@ -104,7 +102,7 @@ export class AuthorizationHandler { * initialiseRefreshToken attempts to initialise a refresh token */ private async initialiseRefreshToken() { - fetch('https://datastrophe.science.uu.nl/auth/refresh', { + fetch('https://api.graphpolaris.com/auth/refresh', { method: 'POST', credentials: 'include', }) 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/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/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..ef2e21a890fc402b46472b5c18fd5b95c3a53a2e --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } + +} diff --git a/package.json b/package.json index 3f7093bb5c59beccce637d66aefbbab914cb7188..9d07a7788766fc39c5af14ad5142bf853f2776fe 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@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", diff --git a/yarn.lock b/yarn.lock index 39285815c8ccda88216897b21a703f99843949e0..ad3e6305a7719eb8dc8d0e2a9ad48f4ff0bc32e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6427,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"