From 30834c211859e6ddb93eb1ae3808286c8b69cfb3 Mon Sep 17 00:00:00 2001 From: Michael Behrisch <m.behrisch@uu.nl> Date: Thu, 10 Feb 2022 23:11:40 +0100 Subject: [PATCH] fix(store): :bug: fixes some problems with data loading and parsing --- .vscode/launch.json | 17 + .vscode/settings.json | 6 +- .../src/components/schema/schema.tsx | 40 ++- libs/schema/schema-usecases/src/index.ts | 2 +- .../src/lib/mockdata/moviesSchema.json | 96 ++++++ .../src/lib/mockdata/northwindSchema.json | 284 ++++++++++++++++ .../src/lib/mockdata/simple.json | 101 ++++++ .../src/lib/mockdata/twitterSchema.json | 313 ++++++++++++++++++ .../src/lib/schema-schema-usecases.spec.ts | 7 - .../src/lib/schema-schema-usecases.ts | 221 ------------- .../src/lib/schema-usecases.spec.ts | 72 ++++ .../src/lib/schema-usecases.ts | 212 ++++++++++++ libs/schema/schema-usecases/tsconfig.json | 3 +- .../store/src/lib/schemaSlice.spec.ts | 94 ++++++ .../data-access/store/src/lib/schemaSlice.ts | 64 ++-- package.json | 4 +- yarn.lock | 26 +- 17 files changed, 1271 insertions(+), 291 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 libs/schema/schema-usecases/src/lib/mockdata/moviesSchema.json create mode 100644 libs/schema/schema-usecases/src/lib/mockdata/northwindSchema.json create mode 100644 libs/schema/schema-usecases/src/lib/mockdata/simple.json create mode 100644 libs/schema/schema-usecases/src/lib/mockdata/twitterSchema.json delete mode 100644 libs/schema/schema-usecases/src/lib/schema-schema-usecases.spec.ts delete mode 100644 libs/schema/schema-usecases/src/lib/schema-schema-usecases.ts create mode 100644 libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts create mode 100644 libs/schema/schema-usecases/src/lib/schema-usecases.ts create mode 100644 libs/shared/data-access/store/src/lib/schemaSlice.spec.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..663259f3f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "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 + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 033495ac5..84265c462 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,8 @@ "vis-nl", "vis-paoh", "vis-schema", - "storybook" - ] + "storybook", + "store" + ], + "jest.jestCommandLine": "nx affected:test" } diff --git a/apps/web-graphpolaris/src/components/schema/schema.tsx b/apps/web-graphpolaris/src/components/schema/schema.tsx index e005a8543..2114a9722 100644 --- a/apps/web-graphpolaris/src/components/schema/schema.tsx +++ b/apps/web-graphpolaris/src/components/schema/schema.tsx @@ -1,35 +1,49 @@ 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 { useEffect, useState } from 'react'; interface Props { content: string; } -const Div = styled.div` - background-color: red; - font: 'Arial'; - width: 300px; - height: 600px; -`; +const initialElements = [ + { + id: '1', + type: 'input', + data: { label: 'Input Node' }, + position: { x: 250, y: 25 }, + }, + { + id: '2', + data: { label: 'Another Node' }, + position: { x: 100, y: 125 }, + }, +]; const Schema = (props: Props) => { const dbschema = useSchema(); + // const [dbschema, setSchema] = useState(useSchema()); + + // const [flowElements, setFlowElements] = useState(initialElements); // In case the schema is updated // useEffect(() => { + // const flowElements = createReactFlowElements(dbschema); // console.log('update schema useEffect'); // }, [dbschema]); - console.log(dbschema); + // console.log(dbschema); return ( - <Div> - <p>hey</p> - <ReactFlow elements={createReactFlowElements(dbschema)} /> - <p>hoi</p> - </Div> + <div + style={{ + width: '100%', + height: '100%', + }} + > + <ReactFlow elements={createReactFlowElements(dbschema)} /> + </div> ); }; diff --git a/libs/schema/schema-usecases/src/index.ts b/libs/schema/schema-usecases/src/index.ts index 5d9846859..1aa537795 100644 --- a/libs/schema/schema-usecases/src/index.ts +++ b/libs/schema/schema-usecases/src/index.ts @@ -1 +1 @@ -export * from './lib/schema-schema-usecases'; +export * from './lib/schema-usecases'; diff --git a/libs/schema/schema-usecases/src/lib/mockdata/moviesSchema.json b/libs/schema/schema-usecases/src/lib/mockdata/moviesSchema.json new file mode 100644 index 000000000..00b4bb2ec --- /dev/null +++ b/libs/schema/schema-usecases/src/lib/mockdata/moviesSchema.json @@ -0,0 +1,96 @@ +{ + "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": [] + } + ] +} \ No newline at end of file diff --git a/libs/schema/schema-usecases/src/lib/mockdata/northwindSchema.json b/libs/schema/schema-usecases/src/lib/mockdata/northwindSchema.json new file mode 100644 index 000000000..d3f92c47d --- /dev/null +++ b/libs/schema/schema-usecases/src/lib/mockdata/northwindSchema.json @@ -0,0 +1,284 @@ +{ + "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": [] + } + ] +} \ No newline at end of file diff --git a/libs/schema/schema-usecases/src/lib/mockdata/simple.json b/libs/schema/schema-usecases/src/lib/mockdata/simple.json new file mode 100644 index 000000000..826dbe483 --- /dev/null +++ b/libs/schema/schema-usecases/src/lib/mockdata/simple.json @@ -0,0 +1,101 @@ +{ + "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" }] + } + ] +} diff --git a/libs/schema/schema-usecases/src/lib/mockdata/twitterSchema.json b/libs/schema/schema-usecases/src/lib/mockdata/twitterSchema.json new file mode 100644 index 000000000..2ec7e5fb9 --- /dev/null +++ b/libs/schema/schema-usecases/src/lib/mockdata/twitterSchema.json @@ -0,0 +1,313 @@ +{ + "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": [] + } + ] +} \ No newline at end of file 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 f2130094e..000000000 --- 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 e12f733f0..000000000 --- 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/schema-usecases/src/lib/schema-usecases.spec.ts b/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts new file mode 100644 index 000000000..de75d0433 --- /dev/null +++ b/libs/schema/schema-usecases/src/lib/schema-usecases.spec.ts @@ -0,0 +1,72 @@ +import { SchemaFromBackend } from '@graphpolaris/shared/data-access/store'; +import { MultiGraph } from 'graphology'; +import { parse } from 'path/posix'; +import { parseSchemaFromBackend } from '..'; +import * as simpleSchema from './mockdata/simple.json'; +import * as moviewSchema from './mockdata/moviesSchema.json'; +import * as northwindSchema from './mockdata/northwindSchema.json'; +import * as twitterSchema from './mockdata/twitterSchema.json'; +import { Attributes } from 'graphology-types'; + +describe('SchemaUsecases', () => { + test.each([ + { data: simpleSchema }, + { data: moviewSchema }, + { data: northwindSchema }, + { data: twitterSchema }, + ])('parseSchemaFromBackend parsing should work', ({ data }) => { + // console.log('testinput', input); + + const parsed = parseSchemaFromBackend(data as SchemaFromBackend); + expect(parsed).toBeDefined(); + + let parsedNodeAttributes: Attributes[] = []; + parsed.forEachNode((node, attr) => { + // console.log('Node', node, attr); + parsedNodeAttributes.push(attr.attributes); + }); + + let 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); + + let inputNodeAttributes: Attributes = []; + data.nodes.forEach((node) => { + inputNodeAttributes.push(node.attributes as Attributes); + }); + + let 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 = parseSchemaFromBackend(simpleSchema as SchemaFromBackend); + const reload = MultiGraph.from(parsed.export()); + + expect(parsed).toStrictEqual(reload); + }); + + test.each([ + // import * as simpleSchema from './mockdata/simple.json'; + // import * as moviewSchema from './mockdata/moviesSchema.json'; + // import * as northwindSchema from './mockdata/northwindSchema.json'; + // import * as twitterSchema from './mockdata/twitterSchema.json'; + { data: simpleSchema }, + { data: moviewSchema }, + { data: northwindSchema }, + { data: twitterSchema }, + ])('should load my test json $data', ({ data }) => { + expect(data).toBeDefined(); + expect(data.nodes).toBeDefined(); + }); +}); diff --git a/libs/schema/schema-usecases/src/lib/schema-usecases.ts b/libs/schema/schema-usecases/src/lib/schema-usecases.ts new file mode 100644 index 000000000..ea3fdc0fa --- /dev/null +++ b/libs/schema/schema-usecases/src/lib/schema-usecases.ts @@ -0,0 +1,212 @@ +import Graph, { MultiGraph } 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'; +import { Attributes } from 'graphology-types'; + +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 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: 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/schema-usecases/tsconfig.json b/libs/schema/schema-usecases/tsconfig.json index 355f7fda9..6ebadfb9d 100644 --- a/libs/schema/schema-usecases/tsconfig.json +++ b/libs/schema/schema-usecases/tsconfig.json @@ -14,6 +14,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true } } 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 000000000..3a6b17a52 --- /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 b44303d1c..eb8fdb562 100644 --- a/libs/shared/data-access/store/src/lib/schemaSlice.ts +++ b/libs/shared/data-access/store/src/lib/schemaSlice.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import Graph from 'graphology'; +import Graph, { MultiGraph } from 'graphology'; /*************** schema format from the backend *************** */ // TODO: should probably not live here @@ -34,7 +34,7 @@ export type Edge = { // Define the initial state using that type export const initialState = { - graphologySerialized: new Graph().export(), + graphologySerialized: new MultiGraph().export(), }; export const schemaSlice = createSlice({ @@ -43,6 +43,8 @@ export const schemaSlice = createSlice({ initialState, reducers: { setSchema: (state, action: PayloadAction<Graph>) => { + console.log('setSchema', action); + state.graphologySerialized = action.payload.export(); }, @@ -52,38 +54,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, - }); - }); - - // 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); - }); + // 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); + // }); state.graphologySerialized = schema.export(); }, diff --git a/package.json b/package.json index d8191b152..efd91bb4a 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "@types/styled-components": "^5.1.21", "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-dom": "17.0.2", "react-flow-renderer": "^9.7.4", diff --git a/yarn.lock b/yarn.lock index 4b4bf132a..1f11be6a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9634,18 +9634,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" @@ -12848,10 +12848,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" -- GitLab