From 4db0ee945d9f4b409825c92551ab21c8a8277306 Mon Sep 17 00:00:00 2001 From: "Behrisch, M. (Michael)" <m.behrisch@uu.nl> Date: Tue, 6 Feb 2024 17:13:42 +0000 Subject: [PATCH] fix(schema): update style and layout of schema Reduces edge overlap, adds an autocomplete option for dragged relations to query, and updates certain styles. --- .gitignore | 5 +- .../components/inputs/checkbox.stories.tsx | 8 +- .../components/inputs/dropdown.stories.tsx | 8 +- libs/shared/lib/components/inputs/index.tsx | 45 ++--- .../lib/components/inputs/radio.stories.tsx | 12 +- .../lib/components/inputs/slider.stories.tsx | 8 +- .../lib/components/inputs/text.stories.tsx | 10 +- .../data-access/store/querybuilderSlice.ts | 2 + .../lib/data-access/store/schemaSlice.ts | 4 +- .../lib/graph-layout/cytoscape-layouts.ts | 4 + .../lib/querybuilder/panel/querybuilder.tsx | 70 ++++++-- .../querysidepanel/querySettingsDialog.tsx | 16 +- ...ies.tsx => schemaquerybuilder.stories.tsx} | 2 +- .../pills/customFlowLines/connection.tsx | 7 +- .../customFlowPills/entitypill/entitypill.tsx | 6 +- .../relationpill/relation-handles.tsx | 19 ++- .../relationpill/relationpill.tsx | 155 +++++++++--------- .../query-utils/query2backend.spec.ts | 1 + libs/shared/lib/schema/model/reactflow.tsx | 1 + libs/shared/lib/schema/panel/schema.tsx | 30 ++-- libs/shared/lib/schema/panel/schemaDialog.tsx | 46 ++---- .../lib/schema/pills/edges/node-edge.tsx | 2 +- .../pills/nodes/entity/SchemaEntityPopup.tsx | 81 ++++++--- .../nodes/entity/entity-node.stories.tsx | 44 ++++- .../schema/pills/nodes/entity/entity-node.tsx | 6 +- .../relation/SchemaRelationshipPopup.tsx | 89 +++++++--- .../nodes/relation/relation-node.stories.tsx | 45 ++++- .../pills/nodes/relation/relation-node.tsx | 15 +- .../schema/schema-utils/schema-usecases.ts | 91 +--------- 29 files changed, 478 insertions(+), 354 deletions(-) rename libs/shared/lib/querybuilder/panel/{shemaquerybuilder.stories.tsx => schemaquerybuilder.stories.tsx} (97%) diff --git a/.gitignore b/.gitignore index 6f9727aa4..cf4d02514 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,7 @@ certs/*.pem node_modules tsconfig.tsbuildinfo -vite.config.ts.* \ No newline at end of file +vite.config.ts.* + +# autogenerated scss type files +*.scss.d.ts diff --git a/libs/shared/lib/components/inputs/checkbox.stories.tsx b/libs/shared/lib/components/inputs/checkbox.stories.tsx index 4ef5a1b18..e28212597 100644 --- a/libs/shared/lib/components/inputs/checkbox.stories.tsx +++ b/libs/shared/lib/components/inputs/checkbox.stories.tsx @@ -1,10 +1,10 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { Checkbox } from '.'; +import { CheckboxInput } from '.'; -const Component: Meta<typeof Checkbox> = { +const Component: Meta<typeof CheckboxInput> = { title: 'Components/Inputs', - component: Checkbox, + component: CheckboxInput, argTypes: { onChange: {} }, decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], }; @@ -12,7 +12,7 @@ const Component: Meta<typeof Checkbox> = { export default Component; type Story = StoryObj<typeof Component>; -export const CheckboxInput: Story = { +export const CheckboxInputStory: Story = { args: { type: 'checkbox', label: 'Checkbox component', diff --git a/libs/shared/lib/components/inputs/dropdown.stories.tsx b/libs/shared/lib/components/inputs/dropdown.stories.tsx index 0c7aa3d5d..319132e5a 100644 --- a/libs/shared/lib/components/inputs/dropdown.stories.tsx +++ b/libs/shared/lib/components/inputs/dropdown.stories.tsx @@ -1,10 +1,10 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { DropDown } from '.'; +import { DropDownInput } from '.'; -const Component: Meta<typeof DropDown> = { +const Component: Meta<typeof DropDownInput> = { title: 'Components/Inputs', - component: DropDown, + component: DropDownInput, argTypes: { onChange: {} }, decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], }; @@ -12,7 +12,7 @@ const Component: Meta<typeof DropDown> = { export default Component; type Story = StoryObj<typeof Component>; -export const DropdownInput: Story = { +export const DropdownInputStory: Story = { args: { type: 'dropdown', label: 'Select component', diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index 4454d2e73..76b72f2ba 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -11,6 +11,7 @@ type SliderProps = { step: number; showValue?: boolean; unit?: string; + tooltip?: string; onChange?: (value: number) => void; }; @@ -23,6 +24,7 @@ type TextProps = { errorText?: string; visible?: boolean; disabled?: boolean; + tooltip?: string; validate?: (value: any) => boolean; onChange?: (value: string) => void; }; @@ -32,6 +34,7 @@ type CheckboxProps = { type: 'checkbox'; options: Array<string>; value: Array<string>; + tooltip?: string; onChange?: (value: Array<string>) => void; }; @@ -40,6 +43,7 @@ type BooleanProps = { type: 'boolean'; options?: any; value: boolean; + tooltip?: string; onChange?: (value: boolean) => void; }; @@ -48,6 +52,7 @@ type RadioProps = { type: 'radio'; options: Array<string>; value: string; + tooltip?: string; onChange?: (value: string) => void; }; @@ -56,6 +61,7 @@ type DropdownProps = { value: string | number; type: 'dropdown'; options: any; + tooltip?: string; onChange?: (value: string | number) => void; required?: boolean; }; @@ -65,25 +71,25 @@ export type InputProps = TextProps | SliderProps | CheckboxProps | DropdownProps const Input = (props: InputProps) => { switch (props.type) { case 'slider': - return <Slider {...(props as SliderProps)} />; + return <SliderInput {...(props as SliderProps)} />; case 'text' || 'password': - return <Text {...(props as TextProps)} />; + return <TextInput {...(props as TextProps)} />; case 'checkbox': - return <Checkbox {...(props as CheckboxProps)} />; + return <CheckboxInput {...(props as CheckboxProps)} />; case 'dropdown': - return <DropDown {...(props as DropdownProps)} />; + return <DropDownInput {...(props as DropdownProps)} />; case 'radio': - return <Radio {...(props as RadioProps)} />; + return <RadioInput {...(props as RadioProps)} />; case 'boolean': - return <Boolean {...(props as BooleanProps)} />; + return <BooleanInput {...(props as BooleanProps)} />; default: return null; } }; -export const Slider = ({ label, value, min, max, step, unit, showValue = true, onChange }: SliderProps) => { +export const SliderInput = ({ label, value, min, max, step, unit, showValue = true, onChange, tooltip }: SliderProps) => { return ( - <div className={styles['slider']}> + <div data-tip={tooltip} className={'tooltip ' + styles['slider']}> <label className="label flex flex-row justify-between items-end"> <span className="label-text">{label}</span> {showValue ? ( @@ -111,7 +117,7 @@ export const Slider = ({ label, value, min, max, step, unit, showValue = true, o ); }; -export const Text = ({ +export const TextInput = ({ label, placeholder, value = '', @@ -121,11 +127,12 @@ export const Text = ({ validate, disabled = false, onChange, + tooltip, }: TextProps) => { const [isValid, setIsValid] = React.useState<boolean>(true); return ( - <div className="form-control w-full"> + <div data-tip={tooltip || null} className="tooltip form-control w-full"> <label className="label"> <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}> {label} @@ -154,9 +161,9 @@ export const Text = ({ ); }; -export const Radio = ({ label, value, options, onChange }: RadioProps) => { +export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioProps) => { return ( - <div> + <div data-tip={tooltip || null} className="tooltip"> <label className="label"> <span className="label-text">{label}</span> </label> @@ -180,9 +187,9 @@ export const Radio = ({ label, value, options, onChange }: RadioProps) => { ); }; -export const Checkbox = ({ label, value, options, onChange }: CheckboxProps) => { +export const CheckboxInput = ({ label, value, options, onChange, tooltip }: CheckboxProps) => { return ( - <div> + <div data-tip={tooltip || null} className="tooltip"> {label && ( <label className="label"> <span className="label-text">{label}</span> @@ -209,10 +216,10 @@ export const Checkbox = ({ label, value, options, onChange }: CheckboxProps) => ); }; -export const Boolean = ({ label, value, onChange }: BooleanProps) => { +export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) => { return ( - <div> - <label className="label cursor-pointer w-fit gap-2 px-0 py-1"> + <div data-tip={tooltip || null} className="tooltip"> + <label className={`label cursor-pointer w-fit gap-2 px-0 py-1`}> <span className="label-text">{label}</span> <input type="checkbox" @@ -229,7 +236,7 @@ export const Boolean = ({ label, value, onChange }: BooleanProps) => { ); }; -export const DropDown = ({ label, value, options, onChange, required = false }: DropdownProps) => { +export const DropDownInput = ({ label, value, options, onChange, required = false, tooltip }: DropdownProps) => { const dropdownRef = React.useRef<HTMLDivElement>(null); const [isDropdownOpen, setIsDropdownOpen] = React.useState<boolean>(false); @@ -246,7 +253,7 @@ export const DropDown = ({ label, value, options, onChange, required = false }: }, [isDropdownOpen]); return ( - <div className="w-full"> + <div data-tip={tooltip || null} className="tooltip w-full"> {label && ( <label className="label"> <span diff --git a/libs/shared/lib/components/inputs/radio.stories.tsx b/libs/shared/lib/components/inputs/radio.stories.tsx index e1d3be1c3..563e01816 100644 --- a/libs/shared/lib/components/inputs/radio.stories.tsx +++ b/libs/shared/lib/components/inputs/radio.stories.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { Radio } from '.'; +import { RadioInput } from '.'; -const Component: Meta<typeof Radio> = { +const Component: Meta<typeof RadioInput> = { title: 'Components/Inputs', - component: Radio, + component: RadioInput, argTypes: { onChange: { action: 'changed' } }, decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], }; @@ -12,12 +12,12 @@ const Component: Meta<typeof Radio> = { export default Component; type Story = StoryObj<typeof Component>; -export const RadioInput: Story = (args: any) => { +export const RadioInputStory: Story = (args: any) => { const [value, setValue] = useState<string>(''); - return <Radio {...args} value={value} onChange={setValue} />; + return <RadioInput {...args} value={value} onChange={setValue} />; }; -RadioInput.args = { +RadioInputStory.args = { type: 'radio', label: 'Radio component', options: ['Option 1', 'Option 2'], diff --git a/libs/shared/lib/components/inputs/slider.stories.tsx b/libs/shared/lib/components/inputs/slider.stories.tsx index 12c65ffb8..6598690f4 100644 --- a/libs/shared/lib/components/inputs/slider.stories.tsx +++ b/libs/shared/lib/components/inputs/slider.stories.tsx @@ -1,10 +1,10 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { Slider } from '.'; +import { SliderInput } from '.'; -const Component: Meta<typeof Slider> = { +const Component: Meta<typeof SliderInput> = { title: 'Components/Inputs', - component: Slider, + component: SliderInput, argTypes: { onChange: {}, unit: {}, showValue: { control: 'boolean' } }, decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], }; @@ -12,7 +12,7 @@ const Component: Meta<typeof Slider> = { export default Component; type Story = StoryObj<typeof Component>; -export const SliderInput: Story = { +export const SliderInputStory: Story = { args: { type: 'slider', label: 'Slider component', diff --git a/libs/shared/lib/components/inputs/text.stories.tsx b/libs/shared/lib/components/inputs/text.stories.tsx index c76d075cd..f383435de 100644 --- a/libs/shared/lib/components/inputs/text.stories.tsx +++ b/libs/shared/lib/components/inputs/text.stories.tsx @@ -1,10 +1,10 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { Text } from '.'; +import { TextInput } from '.'; -const Component: Meta<typeof Text> = { +const Component: Meta<typeof TextInput> = { title: 'Components/Inputs', - component: Text, + component: TextInput, argTypes: { onChange: {} }, decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], }; @@ -12,7 +12,7 @@ const Component: Meta<typeof Text> = { export default Component; type Story = StoryObj<typeof Component>; -export const TextInput: Story = { +export const TextInputStory: Story = { args: { type: 'text', label: 'Text input', @@ -22,7 +22,7 @@ export const TextInput: Story = { }, }; -export const RequiredTextInput: Story = { +export const RequiredTextInputStory: Story = { args: { type: 'text', label: 'Text input', diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts index 98e282a8c..9c5fe1fc6 100644 --- a/libs/shared/lib/data-access/store/querybuilderSlice.ts +++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts @@ -11,6 +11,7 @@ export type QueryBuilderSettings = { limit: number; depth: { min: number; max: number }; layout: AllLayoutAlgorithms | 'manual'; + autocompleteRelation: boolean; }; export type QueryBuilderState = { @@ -27,6 +28,7 @@ export const initialState: QueryBuilderState = { limit: 500, depth: { min: 1, max: 1 }, layout: 'manual', + autocompleteRelation: true, }, // schemaLayout: 'Graphology_noverlap', }; diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index 691cb7088..85097c6d0 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -9,6 +9,7 @@ import { SchemaFromBackend, SchemaGraph, SchemaGraphology } from '../../schema'; export type SchemaSettings = { connectionType: 'connection' | 'bezier' | 'straight' | 'step'; layoutName: AllLayoutAlgorithms; + animatedEdges: boolean; }; type schemaSliceI = { @@ -22,7 +23,8 @@ export const initialState: schemaSliceI = { // layoutName: 'Cytoscape_fcose', settings: { connectionType: 'connection', - layoutName: Layouts.KLAY, + layoutName: Layouts.DAGRE, + animatedEdges: false, }, }; export const schemaSlice = createSlice({ diff --git a/libs/shared/lib/graph-layout/cytoscape-layouts.ts b/libs/shared/lib/graph-layout/cytoscape-layouts.ts index 4112c1f40..42d5d5e0c 100644 --- a/libs/shared/lib/graph-layout/cytoscape-layouts.ts +++ b/libs/shared/lib/graph-layout/cytoscape-layouts.ts @@ -438,6 +438,10 @@ class CytoscapeDagre extends Cytoscape { const layout = cy.layout({ name: 'dagre', + animate: false, + // acyclicer: 'greedy', + ranker: 'longest-path', + spacingFactor: 0.7, ready: function () { // console.log('Layout.start'); }, // on layoutready diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index e6d3b474e..0bab30e6b 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -1,12 +1,13 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useConfig, - useQuerybuilder, useQuerybuilderGraph, useQuerybuilderSettings, useSchemaGraph, useSearchResultQB, } from '@graphpolaris/shared/lib/data-access/store'; +import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; import ReactFlow, { Background, Connection, @@ -22,24 +23,22 @@ import ReactFlow, { isNode, useReactFlow, } from 'reactflow'; -import styles from './querybuilder.module.scss'; -import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; -import { useDispatch } from 'react-redux'; +import { Dialog } from '../../components/Dialog'; +import { Button } from '../../components/buttons'; +import ControlContainer from '../../components/controls'; +import { addError } from '../../data-access/store/configSlice'; +import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; +import { LayoutFactory } from '../../graph-layout'; import { AllLogicMap, QueryElementTypes, createReactFlowElements, isLogicHandle, toHandleData } from '../model'; import { ConnectionDragLine, ConnectionLine, EntityFlowElement, RelationPill } from '../pills'; import LogicPill from '../pills/customFlowPills/logicpill/logicpill'; import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill'; -import { Dialog } from '../../components/Dialog'; +import styles from './querybuilder.module.scss'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/queryBuilderLogicPillsPanel'; +import { QueryBuilderRelatedNodesPanel } from './querysidepanel/queryBuilderRelatedNodesPanel'; import { QueryMLDialog } from './querysidepanel/queryMLDialog'; import { QuerySettingsDialog } from './querysidepanel/querySettingsDialog'; -import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; -import { LayoutFactory } from '../../graph-layout'; import { ConnectingNodeDataI } from './utils/connectorDrop'; -import { QueryBuilderRelatedNodesPanel } from './querysidepanel/queryBuilderRelatedNodesPanel'; -import { addError } from '../../data-access/store/configSlice'; -import ControlContainer from '../../components/controls'; -import { Button } from '../../components/buttons'; export type QueryBuilderProps = { onRunQuery?: () => void; @@ -211,6 +210,53 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { // sendQuery(); } + if (queryBuilderSettings.autocompleteRelation == true) { + let fromNodeID: any = null; + let toNodeID: any = null; + schema.nodes().forEach((node: string) => { + if (node === dragData.from) fromNodeID = node; + if (node === dragData.to) toNodeID = node; + + if (fromNodeID && toNodeID) return; + }); + + if (fromNodeID && toNodeID) { + const fromNode = graphologyGraph.addPill2Graphology( + { + type: QueryElementTypes.Entity, + x: position.x - 180, + y: position.y, + name: fromNodeID, + schemaKey: fromNodeID, + }, + schema.getNodeAttribute(fromNodeID, 'attributes') + ); + + const toNode = graphologyGraph.addPill2Graphology( + { + type: QueryElementTypes.Entity, + x: position.x + 250, + y: position.y, + name: toNodeID, + schemaKey: toNodeID, + }, + schema.getNodeAttribute(toNodeID, 'attributes') + ); + + graphologyGraph.addEdge2Graphology(fromNode, relation, { + type: 'connection', + sourceHandleData: toHandleData(fromNodeID), + targetHandleData: toHandleData(dragData.collection), + }); + + graphologyGraph.addEdge2Graphology(relation, toNode, { + type: 'connection', + sourceHandleData: toHandleData(dragData.collection), + targetHandleData: toHandleData(toNodeID), + }); + } + } + dispatch(setQuerybuilderGraphology(graphologyGraph)); break; default: diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx index cae5133bc..bddfd287a 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx @@ -4,8 +4,9 @@ import React from 'react'; import { useAppDispatch, useQuerybuilderSettings } from '../../../data-access'; import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-access/store/querybuilderSlice'; import { addWarning } from '../../../data-access/store/configSlice'; -import { FormActions, FormBody, FormCard, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; +import { FormActions, FormBody, FormCard, FormControl, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; import { Layouts } from '@graphpolaris/shared/lib/graph-layout'; +import Input from '@graphpolaris/shared/lib/components/inputs'; type QuerySettingsDialogProps = DialogProps; @@ -43,6 +44,19 @@ export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, QuerySetting }} > <FormTitle title="Query Settings" onClose={props.onClose} /> + <FormHBar /> + <FormControl> + <Input + type="boolean" + value={state.autocompleteRelation} + label="Autocomplete Edges" + tooltip="When enabled, if you drag a relationship to the query, the query builder will automatically add the entity nodes it is connected to." + onChange={(value: boolean) => { + setState({ ...state, autocompleteRelation: value as any }); + }} + /> + </FormControl> + <FormHBar /> <div className="form-control px-5"> <label className="label"> diff --git a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx b/libs/shared/lib/querybuilder/panel/schemaquerybuilder.stories.tsx similarity index 97% rename from libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx rename to libs/shared/lib/querybuilder/panel/schemaquerybuilder.stories.tsx index bb9dc3aac..63b141e88 100644 --- a/libs/shared/lib/querybuilder/panel/shemaquerybuilder.stories.tsx +++ b/libs/shared/lib/querybuilder/panel/schemaquerybuilder.stories.tsx @@ -32,7 +32,7 @@ const SchemaAndQueryBuilder = () => { const Component: Meta = { component: SchemaAndQueryBuilder, - title: 'Panel', + title: 'Integration/Schema and QueryBuilder', decorators: [ // using the real store here (story) => ( diff --git a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx index 9bded7d29..3504f6d57 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowLines/connection.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { EdgeProps, getSmoothStepPath, Position, Background, MarkerType, BaseEdge } from 'reactflow'; +import { EdgeProps, Position, getSmoothStepPath } from 'reactflow'; import './connection.scss'; @@ -53,10 +52,10 @@ export function ConnectionLine({ id, sourceX, sourceY, targetX, targetY, style, targetX: targetX, targetY: targetY, targetPosition, + offset: Math.abs(targetX - sourceX) / 10, + borderRadius: Math.abs(targetX - sourceX) / 10, }); - // console.log(source, target, path); - return ( <g stroke="#2e2e2e"> <path id={id} fill="none" strokeWidth={1} style={style} d={path[0]} /> diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx index 56777be2c..7f2cac788 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx @@ -18,7 +18,7 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => const graph = useQuerybuilderGraph(); const attributeEdges = useMemo( () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), - [graph], + [graph] ); const [hovered, setHovered] = useState(false); @@ -54,13 +54,13 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => handle={data.leftRelationHandleId} type="target" position={Position.Left} - className={'!top-8 !left-2 !bg-accent-700 !rounded-none w-2 h-2'} + className={'!top-8 !left-2 !bg-accent-700 !rounded-none'} /> <FilterHandle handle={data.rightRelationHandleId} type="source" position={Position.Right} - className={'!top-8 !right-2 !bg-accent-700 !rounded-none w-2 h-2'} + className={'!top-8 !right-2 !bg-accent-700 !rounded-none'} /> <div className="text-center py-1">{data.name}</div> {data?.attributes && ( diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx index 56fcf6848..61ea6fc06 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relation-handles.tsx @@ -13,18 +13,21 @@ const getArrow = { }; export type RelationshipHandleArrowType = 'right' | 'left' | 'both'; -type Props = { handle: QueryGraphEdgeHandle; type: HandleType; point: RelationshipHandleArrowType; onDoubleClick?: () => void }; +type Props = { + handle: QueryGraphEdgeHandle; + type: HandleType; + point: RelationshipHandleArrowType; + onDoubleClick?: () => void; +}; export const LeftHandle = (props: Props) => { - const offset = 15; - return ( <FilterHandle handle={props.handle} type={props.type} position={Position.Left} - className="!top-4" - style={{ transform: `translate(${offset}px, -3px)` }} + className="!top- !left-2" + // style={{ transform: `translate(${offsetX}px, ${offsetY}px)` }} onDoubleClickCapture={(e) => { e.preventDefault(); e.stopPropagation(); @@ -39,15 +42,13 @@ export const LeftHandle = (props: Props) => { }; export const RightHandle = (props: Props) => { - const offset = -13; - return ( <FilterHandle handle={props.handle} type={props.type} position={Position.Right} - className="!top-4" - style={{ transform: `translate(${offset}px, -3px)` }} + className="!top-8 !right-2" + // style={{ transform: `translate(${offsetX}px, ${offsetY}px)` }} onDoubleClickCapture={(e) => { e.preventDefault(); e.stopPropagation(); diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx index b1ec0d735..7280803b6 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/relationpill.tsx @@ -18,11 +18,11 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { const dispatch = useAppDispatch(); const graphologyNodeAttributes = useMemo<RelationNodeAttributes | undefined>( () => (graphologyGraph.hasNode(node.id) ? { ...(graphologyGraph.getNodeAttributes(node.id) as RelationNodeAttributes) } : undefined), - [node.id], + [node.id] ); const attributeEdges = useMemo( () => graph.edges.filter((edge) => edge.source === node.id && !!edge?.attributes?.sourceHandleData.attributeType), - [graph], + [graph] ); const [hovered, setHovered] = useState(false); const [handleBeingDragged, setHandleBeingDragged] = useState(-1); @@ -85,84 +85,85 @@ export const RelationPill = memo((node: SchemaReactflowRelationNode) => { }; return ( - <div - className={`rounded-sm shadow min-w-[200px] text-[13px] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} - > - <div className={`py-1 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> - <span> - {data.rightEntityHandleId && ( - <RightHandle handle={data.rightEntityHandleId} type="source" point={direction} onDoubleClick={onChangeDirection} /> - )} - </span> - <div className="px-10"> - <span>{data?.name}</span> + <div className="p-3 bg-transparent" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> + <div className={`rounded-sm shadow min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`}> + <div className={`py-1 ${data.selected ? 'bg-secondary-400' : 'bg-secondary-50'}`}> + <div className="px-10"> + <div className="text-center py-1"> + {data?.name} + <span> + <span> [</span> + <input + className={ + 'bg-inherit text-center appearance-none mx-0.1 rounded-sm ' + + (depth.min < 0 || depth.min > depth.max ? ' bg-danger-400 ' : '') + } + style={{ maxWidth: calcWidth(depth.min) }} + type="number" + min={0} + placeholder={'?'} + value={depth.min} + onChange={(e) => { + setDepth({ ...depth, min: parseInt(e.target.value) }); + }} + onBlur={(e) => { + onNodeUpdated(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onNodeUpdated(); + } + }} + ></input> + <span>..</span> + <input + className={ + 'bg-inherit text-center appearance-none mx-0.1 rounded-sm ' + + (depth.max > 99 || depth.min > depth.max ? ' bg-danger-400 ' : '') + } + style={{ maxWidth: calcWidth(depth.max) }} + type="number" + min={1} + placeholder={'?'} + value={depth.max} + onChange={(e) => { + setDepth({ ...depth, max: parseInt(e.target.value) }); + }} + onBlur={(e) => { + onNodeUpdated(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onNodeUpdated(); + } + }} + ></input> + <span>]</span> + </span> + </div> + </div> <span> - <span>[</span> - <input - className={ - 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + - (depth.min < 0 || depth.min > depth.max ? ' bg-danger-400 ' : '') - } - style={{ maxWidth: calcWidth(depth.min) }} - type="number" - min={0} - placeholder={'?'} - value={depth.min} - onChange={(e) => { - setDepth({ ...depth, min: parseInt(e.target.value) }); - }} - onBlur={(e) => { - onNodeUpdated(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNodeUpdated(); - } - }} - ></input> - <span>..</span> - <input - className={ - 'bg-inherit text-center appearance-none mx-0.5 rounded-sm ' + - (depth.max > 99 || depth.min > depth.max ? ' bg-danger-400 ' : '') - } - style={{ maxWidth: calcWidth(depth.max) }} - type="number" - min={1} - placeholder={'?'} - value={depth.max} - onChange={(e) => { - setDepth({ ...depth, max: parseInt(e.target.value) }); - }} - onBlur={(e) => { - onNodeUpdated(); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onNodeUpdated(); - } - }} - ></input> - <span>]</span> + {data.leftEntityHandleId && ( + <LeftHandle handle={data.leftEntityHandleId} type="target" point={'left'} onDoubleClick={onChangeDirection} /> + )} </span> - </div> - <span> - {data.leftEntityHandleId && ( - <LeftHandle handle={data.leftEntityHandleId} type="target" point={'left'} onDoubleClick={onChangeDirection} /> + + <span> + {data.rightEntityHandleId && ( + <RightHandle handle={data.rightEntityHandleId} type="source" point={direction} onDoubleClick={onChangeDirection} /> + )} + </span> + {data?.attributes && ( + <PillDropdown + node={node} + attributes={data.attributes} + attributeEdges={attributeEdges.map((edge) => edge?.attributes)} + hovered={hovered} + handleBeingDraggedIdx={handleBeingDragged} + onHandleMouseDown={onHandleMouseDown} + /> )} - </span> - {data?.attributes && ( - <PillDropdown - node={node} - attributes={data.attributes} - attributeEdges={attributeEdges.map((edge) => edge?.attributes)} - hovered={hovered} - handleBeingDraggedIdx={handleBeingDragged} - onHandleMouseDown={onHandleMouseDown} - /> - )} + </div> </div> </div> ); diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts index b57c1eaa9..6438a1e19 100644 --- a/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts +++ b/libs/shared/lib/querybuilder/query-utils/query2backend.spec.ts @@ -20,6 +20,7 @@ const defaultSettings: QueryBuilderSettings = { limit: 500, depth: { min: 0, max: 1 }, layout: 'manual', + autocompleteRelation: true, }; describe('QueryUtils Entity and Relations', () => { diff --git a/libs/shared/lib/schema/model/reactflow.tsx b/libs/shared/lib/schema/model/reactflow.tsx index cecfd06a2..f6fbbb3db 100644 --- a/libs/shared/lib/schema/model/reactflow.tsx +++ b/libs/shared/lib/schema/model/reactflow.tsx @@ -36,6 +36,7 @@ export interface SchemaReactflowData { summedNullAmount: number; label: string; type: string; + hovered: boolean; } export interface SchemaReactflowEntity extends SchemaReactflowData { diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index 73b9a8fd7..531bd450a 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -1,24 +1,24 @@ -import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '@graphpolaris/shared/lib/graph-layout'; -import { schemaGraphology2Reactflow, schemaExpandRelation } from '@graphpolaris/shared/lib/schema/schema-utils'; import { useSchemaGraph, useSchemaSettings, useSearchResultSchema, useSessionCache } from '@graphpolaris/shared/lib/data-access/store'; +import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '@graphpolaris/shared/lib/graph-layout'; +import { schemaExpandRelation, schemaGraphology2Reactflow } from '@graphpolaris/shared/lib/schema/schema-utils'; import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge'; import { useEffect, useMemo, useRef, useState } from 'react'; -import ReactFlow, { Node, Edge, ReactFlowProvider, useNodesState, useEdgesState, ReactFlowInstance } from 'reactflow'; +import ReactFlow, { Edge, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow'; import 'reactflow/dist/style.css'; -import { ConnectionDragLine, ConnectionLine } from '@graphpolaris/shared/lib/querybuilder/pills'; -import { EntityNode } from '../pills/nodes/entity/entity-node'; -import { RelationNode } from '../pills/nodes/relation/relation-node'; -import NodeEdge from '../pills/edges/node-edge'; -import SelfEdge from '../pills/edges/self-edge'; -import { SchemaDialog } from './schemaDialog'; import { wsSchemaRequest } from '@graphpolaris/shared/lib/data-access/api/wsSchema'; -import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; +import { ConnectionDragLine, ConnectionLine } from '@graphpolaris/shared/lib/querybuilder/pills'; import { Button } from '../../components/buttons'; import ControlContainer from '../../components/controls'; import { wsGetStates } from '../../data-access'; +import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; +import NodeEdge from '../pills/edges/node-edge'; +import SelfEdge from '../pills/edges/self-edge'; +import { EntityNode } from '../pills/nodes/entity/entity-node'; +import { RelationNode } from '../pills/nodes/relation/relation-node'; +import { SchemaDialog } from './schemaDialog'; interface Props { content?: string; @@ -47,8 +47,8 @@ export const Schema = (props: Props) => { const settings = useSchemaSettings(); const searchResults = useSearchResultSchema(); const [toggleSchemaSettings, setToggleSchemaSettings] = useState(false); - const [nodes, setNodes, onNodeChanged] = useNodesState([] as Node[]); - const [edges, setEdges, onEdgeChanged] = useEdgesState([] as Edge[]); + const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]); + const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]); const [firstUserConnection, setFirstUserConnection] = useState<boolean>(true); const [auth, setAuth] = useState(props.auth); @@ -89,7 +89,7 @@ export const Schema = (props: Props) => { updateLayout(); const expandedSchema = schemaExpandRelation(schemaGraphology); layout.current?.layout(expandedSchema); - const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType); + const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType, settings.animatedEdges); setNodes(schemaFlow.nodes); setEdges(schemaFlow.edges); }, [schemaGraph, settings]); @@ -146,8 +146,8 @@ export const Schema = (props: Props) => { nodeTypes={nodeTypes} edgeTypes={edgeTypes} connectionLineComponent={ConnectionDragLine} - onNodesChange={onNodeChanged} - onEdgesChange={onEdgeChanged} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} nodes={nodes} edges={edges} onInit={(reactFlowInstance) => { diff --git a/libs/shared/lib/schema/panel/schemaDialog.tsx b/libs/shared/lib/schema/panel/schemaDialog.tsx index e8fbeaca8..644be97cd 100644 --- a/libs/shared/lib/schema/panel/schemaDialog.tsx +++ b/libs/shared/lib/schema/panel/schemaDialog.tsx @@ -11,8 +11,6 @@ export const SchemaDialog = (props: DialogProps) => { const settings = useSchemaSettings(); const dispatch = useAppDispatch(); const [state, setState] = React.useState<SchemaSettings>(settings); - const [display, setDisplay] = useState<string[]>([]); - const [opacity, setOpacity] = useState<number>(0); useEffect(() => { setState(settings); @@ -34,38 +32,7 @@ export const SchemaDialog = (props: DialogProps) => { submit(); }} > - <FormTitle title="Quick Settings" onClose={props.onClose} /> - <FormHBar /> - <FormControl> - <Input - type="checkbox" - value={display} - options={['Points', 'Line', 'Box']} - onChange={(value: string[]) => { - setDisplay(value); - }} - /> - </FormControl> - <FormHBar /> - <FormControl> - <Input - type="slider" - label="Opacity" - unit={'%'} - value={opacity} - min={0} - max={100} - step={1} - onChange={(value: number) => setOpacity(value)} - /> - </FormControl> - <FormHBar /> - <FormControl> - <label className="label"> - <span className="label-text">Histogram</span> - </label> - ... - </FormControl> + <FormTitle title="Schema Settings" onClose={props.onClose} /> <FormHBar /> <FormControl> <Input @@ -79,6 +46,17 @@ export const SchemaDialog = (props: DialogProps) => { /> </FormControl> <FormHBar /> + <FormControl> + <Input + type="boolean" + value={state.animatedEdges} + label="Animated Edges" + onChange={(value: boolean) => { + setState({ ...state, animatedEdges: value as any }); + }} + /> + </FormControl> + <FormHBar /> <FormControl> <Input type="dropdown" diff --git a/libs/shared/lib/schema/pills/edges/node-edge.tsx b/libs/shared/lib/schema/pills/edges/node-edge.tsx index 52114f9a0..6e230d231 100644 --- a/libs/shared/lib/schema/pills/edges/node-edge.tsx +++ b/libs/shared/lib/schema/pills/edges/node-edge.tsx @@ -60,7 +60,7 @@ export default function NodeEdge({ type="smoothstep" id={id} fill="none" - strokeWidth={0.5} + strokeWidth={1.5} style={style} // The d is used to create the path for the edge. d={`M${sourceX},${sourceY}h ${data.d} L ${targetX + data.d},${targetY} h ${-data.d}`} diff --git a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx index 3e9b26c70..c0f5c9247 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx @@ -9,8 +9,9 @@ * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ -import React from 'react'; +import { FormBody, FormCard, FormControl, FormHBar, FormTitle } from '@graphpolaris/shared/lib/components/forms'; import { SchemaReactflowEntity } from '@graphpolaris/shared/lib/schema/model'; +import { FormEvent } from 'react'; export type SchemaEntityPopupProps = { data: SchemaReactflowEntity; @@ -22,30 +23,62 @@ export type SchemaEntityPopupProps = { * @param data Input data of type NodeQualityDataForEntities, which is for the popup. */ export const SchemaEntityPopup = (props: SchemaEntityPopupProps) => { + function submit() { + // dispatch(setSchemaSettings(state)); + props.onClose(); + } + return ( - <div className="card card-bordered rounded-none text-[0.9rem] min-w-[10rem]"> - <div className="card-body p-0"> - <span className="px-2.5 pt-2"> - <span>Nodes</span> - <span className="float-right">TBD</span> - </span> - <div className="h-[1px] w-full bg-secondary-200"></div> - <div className="px-2.5 text-[0.8rem]"> - <p> - Null Values: <span className="float-right">TBD</span> - </p> - <p> - Not connected: <span className="float-right">TBD</span> - </p> - </div> - <div className="h-[1px] w-full bg-secondary-200"></div> - <button - className="btn btn-outline btn-accent border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" - onClick={() => props.onClose()} + // <FormDiv hAnchor="left"> + <> + <FormCard> + <FormBody + onSubmit={(e: FormEvent<HTMLFormElement>) => { + e.preventDefault(); + submit(); + }} > - Close - </button> - </div> - </div> + <FormTitle + title="Node Statistics" + // title={props.data.name} + onClose={props.onClose} + /> + <FormHBar /> + + <span className="px-5 pt-2"> + <span>Name</span> + <span className="float-right break-all text-wrap text-pretty font-light font-mono">{props.data.name}</span> + </span> + + <FormHBar /> + + <span className="px-5 pt-2"> + <span>Attributes</span> + <span className="float-right font-light font-mono">{props.data.attributes.length}</span> + </span> + + {props.data.attributes.map((attribute: any) => { + return ( + <div key={attribute.name} className="px-5 pt-1"> + <span>{attribute.name}</span> + <span className="float-right font-light font-mono">{attribute.type}</span> + </div> + ); + })} + <FormHBar /> + + <FormControl> + <button + className="btn btn-outline btn-accent border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" + onClick={() => { + submit(); + }} + > + Close + </button> + </FormControl> + </FormBody> + </FormCard> + </> ); }; diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx index 23db8c916..9bf1fe02b 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.stories.tsx @@ -46,7 +46,49 @@ export const Default = { const schema = SchemaUtils.schemaBackend2Graphology({ nodes: [ { - name: 'Thijs', + name: 'NodeDefault', + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + }, + ], + edges: [], + }); + + dispatch(setSchema(schema.export())); + }, +}; + +export const TooLongTextLabel = { + play: async () => { + const dispatch = Mockstore.dispatch; + const schema = SchemaUtils.schemaBackend2Graphology({ + nodes: [ + { + name: 'NodeDefaultNodeDefaultNodeDefaultNodeDefaultNodeDefaultNodeDefault', + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + }, + ], + edges: [], + }); + + dispatch(setSchema(schema.export())); + }, +}; + +export const ShortTextLabel = { + play: async () => { + const dispatch = Mockstore.dispatch; + const schema = SchemaUtils.schemaBackend2Graphology({ + nodes: [ + { + name: 'N', attributes: [ { name: 'city', type: 'string' }, { name: 'vip', type: 'bool' }, diff --git a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx index f533cc908..4e343b817 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/entity-node.tsx @@ -57,12 +57,12 @@ export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaRe return ( <> {openPopup && ( - <Popup open={openPopup} hAnchor="left" className="-top-10" offset="-9rem"> + <Popup open={openPopup} hAnchor="left" className="-top-8" offset="-20rem"> <SchemaEntityPopup data={data} onClose={() => setOpenPopup(false)} /> </Popup> )} <div - className={`rounded-sm transition-all duration-150 shadow shadow-dark/10 hover:shadow-md hover:shadow-dark/20 min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#FFA952] to-[#D66700]`} + className={`rounded-sm transition-all duration-150 shadow shadow-dark/10 hover:shadow-md hover:shadow-dark/20 min-w-[4rem] max-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#FFA952] to-[#D66700]`} onDragStart={(event) => onDragStart(event)} onDragStartCapture={(event) => onDragStart(event)} onMouseDownCapture={(event) => { @@ -88,7 +88,7 @@ export const EntityNode = React.memo(({ id, selected, data }: NodeProps<SchemaRe className={styles.handleTriangleRight} type="source" ></Handle> - <div className="p-2 py-1 text-center"> + <div style={{ pointerEvents: 'none' }} className="p-2 py-1 text-center truncate"> <span className="">{id}</span> </div> </div> diff --git a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx index d87169926..81767e432 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationshipPopup.tsx @@ -9,10 +9,9 @@ * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ -import { FormBody, FormDiv, FormCard, FormHBar } from '@graphpolaris/shared/lib/components/forms'; +import { FormBody, FormCard, FormControl, FormHBar, FormTitle } from '@graphpolaris/shared/lib/components/forms'; import { SchemaReactflowRelation } from '@graphpolaris/shared/lib/schema/model'; -import React from 'react'; -import { NodeProps } from 'reactflow'; +import { FormEvent } from 'react'; export type SchemaRelationshipPopupProps = { data: SchemaReactflowRelation; @@ -24,30 +23,68 @@ export type SchemaRelationshipPopupProps = { * @param data Input data of type NodeQualityDataForEntities, which is for the popup. */ export const SchemaRelationshipPopup = (props: SchemaRelationshipPopupProps) => { + function submit() { + // dispatch(setSchemaSettings(state)); + } + + console.log('data relation popup', props); return ( - <div className="card card-bordered rounded-none text-[0.9rem] min-w-[10rem]"> - <div className="card-body p-0"> - <span className="px-2.5 pt-2"> - <span>Relationships</span> - <span className="float-right">TBD</span> - </span> - <div className="h-[1px] w-full bg-secondary-200"></div> - <div className="px-2.5 text-[0.8rem]"> - <p> - Null Values: <span className="float-right">TBD</span> - </p> - <p> - Not connected: <span className="float-right">TBD</span> - </p> - </div> - <div className="h-[1px] w-full bg-secondary-200"></div> - <button - className="btn btn-outline btn-primary border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" - onClick={() => props.onClose()} + <> + <FormCard> + <FormBody + onSubmit={(e: FormEvent<HTMLFormElement>) => { + e.preventDefault(); + submit(); + }} > - Close - </button> - </div> - </div> + <FormTitle + title="Edge Statistics" + onClose={props.onClose} + /> + <FormHBar /> + + <span className="px-5"> + <span>Name</span> + <span className="float-right break-all text-wrap text-pretty font-light font-mono">{props.data.collection}</span> + </span> + <span className="px-5"> + <span>From</span> + <span className="float-right break-all text-wrap text-pretty font-light font-mono">{props.data.from}</span> + </span> + <span className="px-5"> + <span>To</span> + <span className="float-right break-all text-wrap text-pretty font-light font-mono">{props.data.to}</span> + </span> + + <FormHBar /> + + <span className="px-5 pt-2"> + <span>Attributes</span> + <span className="float-right font-light font-mono">{Object.keys(props.data.attributes).length}</span> + </span> + + {Object.values(props.data.attributes).map((attribute: any) => { + return ( + <div key={attribute.name} className="px-5"> + <span>{attribute.name}</span> + <span className="float-right font-light font-mono">{attribute.type}</span> + </div> + ); + })} + <FormHBar /> + + <FormControl> + <button + className="btn btn-outline btn-accent border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" + onClick={() => { + submit(); + }} + > + Close + </button> + </FormControl> + </FormBody> + </FormCard> + </> ); }; diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx index bb8bc3f1b..c62745747 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.stories.tsx @@ -46,7 +46,7 @@ export const Default = { const schema = SchemaUtils.schemaBackend2Graphology({ nodes: [ { - name: 'Thijs', + name: 'Node', attributes: [ { name: 'city', type: 'string' }, { name: 'vip', type: 'bool' }, @@ -56,11 +56,44 @@ export const Default = { ], edges: [ { - name: 'Thijs:Thijs', - label: 'Thijs:Thijs', - from: 'Thijs', - to: 'Thijs', - collection: 'flights', + name: 'Node:Node', + label: 'Node:Node', + from: 'Node', + to: 'Node', + collection: 'Flights', + attributes: [ + { name: 'arrivalTime', type: 'int' }, + { name: 'departureTime', type: 'int' }, + ], + }, + ], + }); + + dispatch(setSchema(schema.export())); + }, +}; + +export const TooLongLabel = { + play: async () => { + const dispatch = Mockstore.dispatch; + const schema = SchemaUtils.schemaBackend2Graphology({ + nodes: [ + { + name: 'Node', + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + }, + ], + edges: [ + { + name: 'Node:Node', + label: 'Node:Node', + from: 'Node', + to: 'Node', + collection: 'FlightsFlightsFlightsFlightsFlightsFlightsFlightsFlights', attributes: [ { name: 'arrivalTime', type: 'int' }, { name: 'departureTime', type: 'int' }, diff --git a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx index 6520c642e..5cb30ac7f 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/relation-node.tsx @@ -22,7 +22,7 @@ import { SchemaEdge } from '../../../model'; * Can be dragged and dropped to the query builder. * @param {NodeProps} param0 The data of an entity flow element. */ -export const RelationNode = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowRelationWithFunctions>) => { +export const RelationNode = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); /** @@ -37,7 +37,9 @@ export const RelationNode = React.memo(({ id, selected, data }: NodeProps<Schema to: data.to, collection: data.collection, label: data.label, - attributes: data.attributes.map((attribute) => attribute.attributes).flat(), // TODO this seems wrong + attributes: Array.from(data.attributes) + .map((attribute) => attribute.attributes) + .flat(), }; event.dataTransfer.setData('application/reactflow', JSON.stringify(eventData)); event.dataTransfer.effectAllowed = 'move'; @@ -60,7 +62,7 @@ export const RelationNode = React.memo(({ id, selected, data }: NodeProps<Schema return ( <> {openPopup && ( - <Popup open={openPopup} hAnchor="left" className="-top-10" offset="-9rem"> + <Popup open={openPopup} hAnchor="left" className="-top-8" offset="-20rem"> <SchemaRelationshipPopup data={data} onClose={() => setOpenPopup(false)} /> </Popup> )} @@ -76,10 +78,7 @@ export const RelationNode = React.memo(({ id, selected, data }: NodeProps<Schema draggable > <div - className={`rounded-sm transition-all duration-150 shadow shadow-dark/10 hover:shadow-md hover:shadow-dark/20 min-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`} - style={{ - backgroundColor: selected ? '#97a2b6' : '#f4f6f7', - }} + className={`rounded-sm transition-all duration-150 shadow shadow-dark/10 hover:shadow-md hover:shadow-dark/20 min-w-[4rem] max-w-[8rem] text-[0.8rem] bg-gradient-to-r pt-1 from-[#4893D4] to-[#1A476E]`} > <div className={`py-1 ${selected ? 'bg-secondary-300' : 'bg-secondary-100'}`}> <Handle @@ -89,7 +88,7 @@ export const RelationNode = React.memo(({ id, selected, data }: NodeProps<Schema position={Position.Top} type="target" ></Handle> - <div className="p-2 py-1 text-center"> + <div className="p-2 py-1 text-center truncate"> <span className="">{data.collection}</span> </div> diff --git a/libs/shared/lib/schema/schema-utils/schema-usecases.ts b/libs/shared/lib/schema/schema-utils/schema-usecases.ts index abbe835f0..f544f0ad7 100644 --- a/libs/shared/lib/schema/schema-utils/schema-usecases.ts +++ b/libs/shared/lib/schema/schema-utils/schema-usecases.ts @@ -7,7 +7,6 @@ import { QueryElementTypes } from '../../querybuilder'; import { SchemaGraph, SchemaGraphology } from '../model'; //TODO does not belong here; maybe should go into the GraphPolarisThemeProvider -const ANIMATEDEDGES = false; export function schemaExpandRelation(graph: SchemaGraphology): SchemaGraphology { const newGraph = graph.copy(); @@ -25,7 +24,7 @@ export function schemaExpandRelation(graph: SchemaGraphology): SchemaGraphology ...attributes, name: edge, label: edge, - attributes: [], + attributes: { ...attributes.attributes }, x: 0, y: 0, type: QueryElementTypes.Relation, @@ -49,7 +48,8 @@ export function schemaExpandRelation(graph: SchemaGraphology): SchemaGraphology // Takes the schema as an input and creates basic react flow elements for them. export function schemaGraphology2Reactflow( graph: Graph, - defaultEdgeType: string + defaultEdgeType: string, + animatedEdges = false ): { nodes: Array<Node<SchemaReactflowNodeWithFunctions | SchemaReactflowRelationWithFunctions>>; edges: Array<Edge>; @@ -60,7 +60,7 @@ export function schemaGraphology2Reactflow( }; initialElements.nodes = createReactFlowNodes(graph); - initialElements.edges = createReactFlowEdges(graph, defaultEdgeType); + initialElements.edges = createReactFlowEdges(graph, defaultEdgeType, animatedEdges); return initialElements; } @@ -82,7 +82,7 @@ export function createReactFlowNodes(graph: Graph): Array<Node> { return nodeElements; } -export function createReactFlowEdges(graph: Graph, defaultEdgeType: string): Array<Edge> { +export function createReactFlowEdges(graph: Graph, defaultEdgeType: string, animatedEdges = false): Array<Edge> { const edgeElements: Array<Edge> = []; graph.forEachEdge((edge, attributes, source, target): void => { @@ -96,7 +96,7 @@ export function createReactFlowEdges(graph: Graph, defaultEdgeType: string): Arr }, // label: edge, type: attributes?.type || defaultEdgeType, - animated: ANIMATEDEDGES, + animated: animatedEdges, markerEnd: MarkerType.ArrowClosed, // TODO: Check }; edgeElements.push(newEdge); @@ -104,82 +104,3 @@ export function createReactFlowEdges(graph: Graph, defaultEdgeType: string): Arr return edgeElements; } - -// export function createReactFlowRelationNodes(graph: Graph): Elements<Node> { -// const nodeElements: Elements<Node> = []; -// graph.forEachEdge((edge, attributes, source, target): void => { -// const newRelationNode: Node = { -// id: edge, -// data: { -// label: edge, -// name: edge, -// }, -// position: { x: attributes.x, y: attributes.y }, -// type: QueryElementTypes.Relation, -// }; -// nodeElements.push(newRelationNode); -// }); - -// return nodeElements; -// } - -// export function createReactFlowRelationEdges(graph: Graph): Elements<Edge> { -// const edgeElements: Elements<Edge> = []; -// graph.forEachEdge((edge, attributes, source, target): void => { -// const newEdgeIncoming: Edge = { -// //into relation node -// id: edge + '' + source, -// source: source, -// target: edge, -// // label: edge, -// type: 'smoothstep', -// animated: ANIMATEDEDGES, -// arrowHeadType: ArrowHeadType.ArrowClosed, -// }; -// edgeElements.push(newEdgeIncoming); - -// const newEdgeOutgoing: Edge = { -// //out of relation node -// id: edge + '' + target, -// source: edge, -// target: target, -// // label: edge, -// type: 'smoothstep', -// animated: ANIMATEDEDGES, -// arrowHeadType: ArrowHeadType.ArrowClosed, -// }; -// edgeElements.push(newEdgeOutgoing); -// }); - -// return edgeElements; -// } - -// 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; -// } -- GitLab