diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index 895b0f754d87a18cabf99dcf7ad417ad48071844..b952c255ebfe626756e5fee398be14f7acf5b111 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -3,12 +3,14 @@ import type { RootState } from './store'; import Graph, { MultiGraph } from 'graphology'; import { Attributes, SerializedGraph } from 'graphology-types'; import { QueryMultiGraph, QueryMultiGraphology as QueryGraphology } from '../../querybuilder/model/graphology/utils'; +import { AllLayoutAlgorithms } from '../../graph-layout'; const defaultGraph = () => ({ nodes: [], edges: [], attributes: {}, options: {} }); export type QueryBuilderSettings = { limit: number; depth: { min: number; max: number }; + layout: AllLayoutAlgorithms | 'manual'; }; // Define the initial state using that type @@ -20,6 +22,7 @@ export const initialState: { settings: { limit: 500, depth: { min: 0, max: 1 }, + layout: 'manual', }, // schemaLayout: 'Graphology_noverlap', }; diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index 3fdb6b89d10cacbbaf944b20c612ff1126d71e5f..1ff1733fe36afa2c442e448f56ab4ff7d223a0db 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import { AllLayoutAlgorithms, CytoscapeLayoutAlgorithms } from '@graphpolaris/shared/lib/graph-layout'; +import { AllLayoutAlgorithms, Layouts } from '@graphpolaris/shared/lib/graph-layout'; import { SchemaUtils } from '../../schema/schema-utils'; import { SchemaFromBackend, SchemaGraph, SchemaGraphology } from '../../schema'; @@ -20,7 +20,7 @@ type schemaSliceI = { export const initialState: schemaSliceI = { graph: new SchemaGraphology().export(), // layoutName: 'Cytoscape_fcose', - layoutName: CytoscapeLayoutAlgorithms.KLAY as AllLayoutAlgorithms, + layoutName: Layouts.KLAY, settings: { connectionType: 'connection', }, diff --git a/libs/shared/lib/graph-layout/cytoscape-layouts.ts b/libs/shared/lib/graph-layout/cytoscape-layouts.ts index 50267d6b9e316002349de9bf42d419713a622fec..4112c1f40284a26882faf2e3c28b099bcfae37c0 100644 --- a/libs/shared/lib/graph-layout/cytoscape-layouts.ts +++ b/libs/shared/lib/graph-layout/cytoscape-layouts.ts @@ -4,33 +4,23 @@ 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'; +import dagre from 'cytoscape-dagre'; import Graph from 'graphology'; import { Attributes } from 'graphology-types'; import { Layout } from './layout'; -import { ILayoutFactory, LayoutAlgorithm } from './layout-creator-usecase'; +import { ILayoutFactory } from './layout-creator-usecase'; +import { CytoscapeLayoutAlgorithms, LayoutAlgorithm } from './types'; cytoscape.use(klay); cytoscape.use(elk); export type CytoscapeProvider = 'Cytoscape'; -export enum CytoscapeLayoutAlgorithms { - KLAY = 'Cytoscape_klay', - DAGRE = 'Cytoscape_dagre', - ELK = 'Cytoscape_elk', - FCOSE = 'Cytoscape_fcose', - COSE_BILKENT = 'Cytoscape_cose-bilkent', - CISE = 'Cytoscape_cise', -} - type CytoNode = { data: { id: string; diff --git a/libs/shared/lib/graph-layout/graphology-layouts.ts b/libs/shared/lib/graph-layout/graphology-layouts.ts index 93c26cbb9013ef1a111c29ad4ac0ba7953155c70..c4b2d14243f0138137608c20edfff76bf2c919ab 100644 --- a/libs/shared/lib/graph-layout/graphology-layouts.ts +++ b/libs/shared/lib/graph-layout/graphology-layouts.ts @@ -5,16 +5,11 @@ 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'; +import { ILayoutFactory } from './layout-creator-usecase'; +import { GraphologyLayoutAlgorithms, LayoutAlgorithm } from './types'; 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/ diff --git a/libs/shared/lib/graph-layout/index.ts b/libs/shared/lib/graph-layout/index.ts index a0c02112a1e50e60d28927bbf8f18a3a194fa9df..af8bf5bcf5bd4c055cd1bd32053e188cc2b41bdb 100644 --- a/libs/shared/lib/graph-layout/index.ts +++ b/libs/shared/lib/graph-layout/index.ts @@ -1,3 +1,4 @@ +export * from './types'; export * from './layout'; export * from './graphology-layouts'; export * from './cytoscape-layouts'; diff --git a/libs/shared/lib/graph-layout/layout-creator-usecase.ts b/libs/shared/lib/graph-layout/layout-creator-usecase.ts index 725af76dc59c54f3323321d60cc9abfac8f4b2ee..fe9f27479bb2e70ec89508d7272e255d7a3a0524 100644 --- a/libs/shared/lib/graph-layout/layout-creator-usecase.ts +++ b/libs/shared/lib/graph-layout/layout-creator-usecase.ts @@ -1,17 +1,6 @@ -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; +import { CytoscapeFactory } from './cytoscape-layouts'; +import { GraphologyFactory } from './graphology-layouts'; +import { AllLayoutAlgorithms, AlgorithmToLayoutProvider, GraphologyLayoutAlgorithms, CytoscapeLayoutAlgorithms } from './types'; export interface ILayoutFactory<Algorithm extends AllLayoutAlgorithms> { createLayout: (Algorithm: Algorithm) => AlgorithmToLayoutProvider<Algorithm> | null; diff --git a/libs/shared/lib/graph-layout/layout.ts b/libs/shared/lib/graph-layout/layout.ts index e99ef69bbaf54a4b8cdff419e9332b24bee5a62b..fae32336ff0e472c93cf9956222c6fd8e2ec8362 100644 --- a/libs/shared/lib/graph-layout/layout.ts +++ b/libs/shared/lib/graph-layout/layout.ts @@ -1,6 +1,6 @@ /* eslint-disable no-prototype-builtins */ import Graph from 'graphology'; -import { Providers, LayoutAlgorithm } from './layout-creator-usecase'; +import { Providers, LayoutAlgorithm } from './types'; /** * This is our Product diff --git a/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx deleted file mode 100644 index 7b8e1891f06cf4e3fefeb1e2e840736781ee3545..0000000000000000000000000000000000000000 --- a/libs/shared/lib/querybuilder/panel/querySettingsDialog.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { PropsWithChildren, useEffect, useRef } from 'react'; -import { Dialog, DialogProps } from '../../components/Dialog'; -import React from 'react'; -import CloseIcon from '@mui/icons-material/Close'; -import { useAppDispatch, useQuerybuilderSettings } from '../../data-access'; -import { QueryBuilderSettings, setQuerybuilderSettings } from '../../data-access/store/querybuilderSlice'; -import { addWarning } from '../../data-access/store/configSlice'; -import { FormBody, FormCard, FormDiv, FormHBar, FormTitle } from '../../components/forms'; -import { NodeAttribute, QueryGraphNodes, toHandleData } from '../model'; -import { OnConnectStartParams, XYPosition } from 'reactflow'; - -type QuerySettingsDialogProps = DialogProps; - -export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, QuerySettingsDialogProps>((props, ref) => { - const qb = useQuerybuilderSettings(); - const dispatch = useAppDispatch(); - const [state, setState] = React.useState<QueryBuilderSettings>(qb); - - useEffect(() => { - setState(qb); - }, [qb, props.open]); - - function submit() { - if (state.depth.min < 0) { - dispatch(addWarning('The minimum depth cannot be smaller than 0')); - } else if (state.depth.max > 99) { - dispatch(addWarning('The maximum depth cannot be larger than 99')); - } else if (state.depth.min >= state.depth.max) { - dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); - } else { - dispatch(setQuerybuilderSettings(state)); - props.onClose(); - } - } - - return ( - <> - {props.open && ( - <FormDiv ref={ref} className="" hAnchor="right"> - <FormCard> - <FormBody - onSubmit={(e) => { - e.preventDefault(); - submit(); - }} - > - <FormTitle title="Query Settings" onClose={props.onClose} /> - <FormHBar /> - <div className="form-control px-5"> - <label className="label"> - <span className="label-text">Limit - Max number of results</span> - </label> - <input - type="number" - className="input input-sm input-bordered" - placeholder="500" - value={state.limit} - onChange={(e) => setState({ ...state, limit: parseInt(e.target.value) })} - /> - </div> - <FormHBar /> - <div className="form-control px-5 flex flex-row gap-3"> - <div className=""> - <label className="label"> - <span className="label-text">Min Depth Default</span> - </label> - <input - type="number" - className="input input-sm input-bordered w-full" - placeholder="0" - min={0} - max={state.depth.max - 1} - value={state.depth.min} - onChange={(e) => setState({ ...state, depth: { min: parseInt(e.target.value), max: state.depth.max } })} - onKeyDown={(e) => { - if (e.key === 'Enter') { - submit(); - } - }} - /> - </div> - <div className=""> - <label className="label"> - <span className="label-text">Max Depth Default</span> - </label> - <input - type="number" - className="input input-sm input-bordered w-full" - placeholder="0" - min={state.depth.min + 1} - max={99} - value={state.depth.max} - onChange={(e) => setState({ ...state, depth: { max: parseInt(e.target.value), min: state.depth.min } })} - onKeyDown={(e) => { - if (e.key === 'Enter') { - submit(); - } - }} - /> - </div> - </div> - <FormHBar /> - <div className="card-actions mt-1 w-full px-5 flex flex-row"> - <button - className="btn btn-secondary flex-grow" - onClick={(e) => { - e.preventDefault(); - props.onClose(); - }} - > - Cancel - </button> - <button className="btn btn-primary flex-grow">Apply</button> - </div> - </FormBody> - </FormCard> - </FormDiv> - )} - </> - ); -}); diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index e4d86f65c5e1814c5b254f6bdeb75107485e7bab..c54b5baccfacf298ed019e341e36ab9114b42180 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useConfig, useQuerybuilderGraph, useQuerybuilderSettings, useSchemaGraph } from '@graphpolaris/shared/lib/data-access/store'; import ReactFlow, { Background, @@ -19,7 +19,6 @@ import ReactFlow, { useReactFlow, } from 'reactflow'; import styles from './querybuilder.module.scss'; - import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { Cached as CachedIcon, Delete as DeleteIcon, ImportExport as ExportIcon, Settings as SettingsIcon } from '@mui/icons-material'; import { useDispatch } from 'react-redux'; @@ -38,12 +37,14 @@ import LogicPill from '../pills/customFlowPills/logicpill/logicpill'; import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill'; import DifferenceIcon from '@mui/icons-material/Difference'; import LightbulbIcon from '@mui/icons-material/Lightbulb'; +import CameraAltIcon from '@mui/icons-material/CameraAlt'; import { Dialog } from '../../components/Dialog'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/queryBuilderLogicPillsPanel'; import { QueryBuilderMLPanel } from './querysidepanel/queryBuilderMLPanel'; import { Popup } from '../../components/Popup'; -import { QuerySettingsDialog } from './querySettingsDialog'; +import { QuerySettingsDialog } from './querysidepanel/querySettingsDialog'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; +import { LayoutFactory } from '../../graph-layout'; export type QueryBuilderProps = { onRunQuery?: () => void; @@ -392,6 +393,16 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { dispatch(setQuerybuilderGraphology(graphologyGraph)); }; + function applyLayout() { + if (queryBuilderSettings.layout !== 'manual') { + const factory = new LayoutFactory(); + factory.createLayout(queryBuilderSettings.layout).layout(graphologyGraph); + dispatch(setQuerybuilderGraphology(graphologyGraph)); + } + } + + useEffect(() => { applyLayout() }, [queryBuilderSettings]); + return ( <div ref={reactFlowWrapper} className="h-full w-full"> <QuerySettingsDialog open={toggleSettings === 'settings'} onClose={() => setToggleSettings(undefined)} /> @@ -454,6 +465,16 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { onClick={(event) => { event.stopPropagation(); }} + > + <CameraAltIcon /> + </ControlButton> + <ControlButton + className={styles.buttons} + title={'Apply Layout'} + onClick={(event) => { + event.stopPropagation(); + applyLayout(); + }} > <ExportIcon /> </ControlButton> diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx index 2d2c4de5d18dba9aec85c3de63e981c829f819c5..8f77275cb18c3abde492b5ddbe68305c45d79910 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx @@ -104,7 +104,7 @@ export default function LogicPill(node: SchemaReactflowLogicNode) { ); } return ( - <> + <div key={i}> <div className="w-full flex">{inputTextBox}</div> <Handle type={'target'} @@ -114,7 +114,7 @@ export default function LogicPill(node: SchemaReactflowLogicNode) { style={{ top: `${((i + 0.8) / (node.data.logic.inputs.length + 0.6)) * 100}%` }} className={styleHandleMap[input.type] + ''} ></Handle> - </> + </div> ); })} {!!node.data.logic.output && ( diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts index 1109d58a9c55450ab31085a3b35f67c90ec27074..04b325bf18f4273106962a0e9e092fcc5944e77c 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts @@ -6,6 +6,7 @@ import { NumberAggregationTypes, NumberFilterTypes, NumberFunctionTypes } from ' import { Query2BackendQuery, calculateQueryLogic } from './query2backend'; import { SerializedNode } from 'graphology-types'; import { MathAggregations } from '../model/logic/numberAggregations'; +import { QueryBuilderSettings } from '../../data-access/store/querybuilderSlice'; const defaultQuery = { databaseName: 'database', @@ -14,9 +15,10 @@ const defaultQuery = { return: ['*'], }; -const defaultSettings = { +const defaultSettings: QueryBuilderSettings = { limit: 500, depth: { min: 0, max: 1 }, + layout: 'manual', }; describe('QueryUtils Entity and Relations', () => { diff --git a/libs/shared/package.json b/libs/shared/package.json index e87add37344d86afa63c9cf81dc7ea2da39224c6..f654e515f26e5652a13126c8e6c4f238acf217dd 100644 --- a/libs/shared/package.json +++ b/libs/shared/package.json @@ -79,6 +79,9 @@ "@testing-library/react": "14.0.0", "@testing-library/react-hooks": "8.0.1", "@types/color": "^3.0.3", + "@types/cytoscape-dagre": "^2.3.1", + "@types/cytoscape-fcose": "^2.2.2", + "@types/cytoscape-klay": "^3.1.2", "@types/d3": "^7.4.0", "@types/lodash-es": "^4.17.9", "@types/node": "18.13.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e34849fcbb4d82edfcc1ac5b6865948c500ff0c8..3449e4b30f71f5a687c787f1a1875b3e05e307d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -383,6 +383,15 @@ importers: '@types/color': specifier: ^3.0.3 version: 3.0.3 + '@types/cytoscape-dagre': + specifier: ^2.3.1 + version: 2.3.1 + '@types/cytoscape-fcose': + specifier: ^2.2.2 + version: 2.2.2 + '@types/cytoscape-klay': + specifier: ^3.1.2 + version: 3.1.2 '@types/d3': specifier: ^7.4.0 version: 7.4.0 @@ -8278,9 +8287,26 @@ packages: resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==} dev: false + /@types/cytoscape-dagre@2.3.1: + resolution: {integrity: sha512-4tLRpQxICUC+YooRTTs3pfTkctsOgu0/ID4A3u2knGDQykh3Tz+Go8MgUifPP1xV7VzGBogSdOqz3l/P9NTUZQ==} + dependencies: + '@types/cytoscape': 3.19.9 + dev: true + + /@types/cytoscape-fcose@2.2.2: + resolution: {integrity: sha512-C576Xjaga9/he4Isef5HiBbN8KWZRWRh1V2fvFsKezmwhPlE6op25CtX5xYgSq0CO45j/FyNmRwlEritT93bOA==} + dependencies: + '@types/cytoscape': 3.19.9 + dev: true + + /@types/cytoscape-klay@3.1.2: + resolution: {integrity: sha512-EIvsBqBv2XinDHJ/0Tqe5v3fDS9t3KDOimYUuFxmcViN0qbtkswHfxk0Gq2JLoc2AU5ckKmr4VsoLf4ibGrHFA==} + dependencies: + '@types/cytoscape': 3.19.9 + dev: true + /@types/cytoscape@3.19.9: resolution: {integrity: sha512-oqCx0ZGiBO0UESbjgq052vjDAy2X53lZpMrWqiweMpvVwKw/2IiYDdzPFK6+f4tMfdv9YKEM9raO5bAZc3UYBg==} - dev: false /@types/d3-array@3.0.4: resolution: {integrity: sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ==} @@ -16190,7 +16216,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.21 - ts-node: 10.9.1(@types/node@17.0.12)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@18.13.0)(typescript@4.9.5) yaml: 1.10.2 dev: true @@ -20217,8 +20243,8 @@ packages: tinybench: 2.4.0 tinypool: 0.4.0 tinyspy: 1.1.1 - vite: 4.2.1(@types/node@17.0.12)(sass@1.59.3) - vite-node: 0.29.4(@types/node@17.0.12)(sass@1.64.2) + vite: 4.2.1(@types/node@17.0.12)(sass@1.64.2) + vite-node: 0.29.4(@types/node@17.0.12)(sass@1.59.3) why-is-node-running: 2.2.2 transitivePeerDependencies: - less