From 69a1349c20b55854c9c1882927865f459f347d1d Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Wed, 12 Jun 2024 20:11:41 +0200 Subject: [PATCH] fix: settings panel and visualization loading --- .../dbConnectionSelector.tsx | 2 - .../dropdowns/dropdowns.module.scss | 26 +-- .../dropdowns/dropdowns.module.scss.d.ts | 5 +- .../shared/lib/components/dropdowns/index.tsx | 31 +++- .../components/inputs/dropdown.stories.tsx | 6 +- libs/shared/lib/components/inputs/index.tsx | 18 +- libs/shared/lib/components/layout/Popover.tsx | 2 +- libs/shared/lib/data-access/api/eventBus.tsx | 13 +- .../shared/lib/data-access/broker/wsState.tsx | 3 +- libs/shared/lib/data-access/store/hooks.ts | 4 +- .../data-access/store/visualizationSlice.ts | 98 ++++++++--- libs/shared/lib/inspector/InspectorPanel.tsx | 16 +- libs/shared/lib/vis/common/types.ts | 28 ++- .../lib/vis/components/VisualizationPanel.tsx | 119 ++++++++++--- .../vis/components/VisualizationTabBar.tsx | 120 +++++++++++++ libs/shared/lib/vis/components/bar.tsx | 115 ------------- .../config/ActiveVisualizationConfig.tsx | 43 ----- .../vis/components/config/SelectionConfig.tsx | 1 - .../config/VisualizationSettings.tsx | 113 +++++++++++++ .../lib/vis/components/config/components.tsx | 2 +- .../lib/vis/components/config/index.tsx | 2 +- .../lib/vis/components/config/panel.tsx | 18 -- .../lib/vis/manager/VisualizationManager.tsx | 159 ------------------ libs/shared/lib/vis/manager/index.ts | 2 - libs/shared/lib/vis/manager/manager.types.ts | 12 -- .../lib/vis/views/{noData.tsx => NoData.tsx} | 0 .../vis/views/{querying.tsx => Querying.tsx} | 0 .../{recommender.tsx => Recommender.tsx} | 13 +- libs/shared/lib/vis/views/index.tsx | 6 +- .../vis/visualizations/mapvis/MapSettings.tsx | 63 +++++++ .../visualizations/mapvis/configuration.tsx | 65 ------- .../lib/vis/visualizations/mapvis/mapvis.tsx | 61 +++---- .../matrixvis/components/MatrixPixi.tsx | 13 +- .../matrixvis/matrix.stories.tsx | 1 - .../visualizations/matrixvis/matrixvis.tsx | 32 ++-- .../nodelinkvis/components/NLPixi.tsx | 7 +- .../nodelinkvis/nodelinkvis.tsx | 70 ++++---- .../vis/visualizations/paohvis/paohvis.tsx | 152 ++++++++--------- .../visualizations/rawjsonvis/rawjsonvis.tsx | 36 ++-- .../semanticsubstratesvis.tsx | 24 +-- .../tablevis/tablevis.stories.tsx | 6 +- .../vis/visualizations/tablevis/tablevis.tsx | 70 ++++---- 42 files changed, 788 insertions(+), 789 deletions(-) create mode 100644 libs/shared/lib/vis/components/VisualizationTabBar.tsx delete mode 100644 libs/shared/lib/vis/components/bar.tsx delete mode 100644 libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx create mode 100644 libs/shared/lib/vis/components/config/VisualizationSettings.tsx delete mode 100644 libs/shared/lib/vis/components/config/panel.tsx delete mode 100644 libs/shared/lib/vis/manager/VisualizationManager.tsx delete mode 100644 libs/shared/lib/vis/manager/index.ts delete mode 100644 libs/shared/lib/vis/manager/manager.types.ts rename libs/shared/lib/vis/views/{noData.tsx => NoData.tsx} (100%) rename libs/shared/lib/vis/views/{querying.tsx => Querying.tsx} (100%) rename libs/shared/lib/vis/views/{recommender.tsx => Recommender.tsx} (62%) create mode 100644 libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx delete mode 100644 libs/shared/lib/vis/visualizations/mapvis/configuration.tsx diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx index 2775725cf..488156b24 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx @@ -76,8 +76,6 @@ export default function DatabaseSelector({}) { <DropdownContainer open={dbSelectionMenuOpen} onOpenChange={(ret) => { - console.log('dbSelectionMenuOpen', dbSelectionMenuOpen, ret); - if (!ret) { if (session.saveStates && Object.keys(session.saveStates).length === 0) setSettingsMenuOpen('add'); else setDbSelectionMenuOpen(!dbSelectionMenuOpen); diff --git a/libs/shared/lib/components/dropdowns/dropdowns.module.scss b/libs/shared/lib/components/dropdowns/dropdowns.module.scss index 8d45e1520..c051a7c9f 100644 --- a/libs/shared/lib/components/dropdowns/dropdowns.module.scss +++ b/libs/shared/lib/components/dropdowns/dropdowns.module.scss @@ -1,19 +1,19 @@ .dropdown { } .dropdown-item { - //todo: color - //@apply cursor-pointer block px-4 py-2 text-sm bg-secondary-200; - @apply cursor-pointer block px-4 py-2 text-sm; + // //todo: color + // //@apply cursor-pointer block px-4 py-2 text-sm bg-secondary-200; + // @apply cursor-pointer block text-sm px-2 py-1 rounded hover:bg-secondary-200 border-0; } .dropdown-container { - width: inherit; - max-width: 100%; - @apply absolute z-10 mt-2 origin-top-right rounded-sm shadow-lg border border-secondary-200 truncate; - ul { - //todo: color - @apply divide-y; - //@apply divide-y divide-secondary-100; - li { - } - } + // width: inherit; + // max-width: 100%; + // @apply absolute z-10 mt-2 origin-top-right rounded-sm shadow-lg truncate; + // ul { + // //todo: color + // @apply divide-y; + // //@apply divide-y divide-secondary-100; + // li { + // } + // } } diff --git a/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts b/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts index 9b29e9805..5fc8829cc 100644 --- a/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts +++ b/libs/shared/lib/components/dropdowns/dropdowns.module.scss.d.ts @@ -1,5 +1,2 @@ -declare const classNames: { - readonly 'dropdown-item': 'dropdown-item'; - readonly 'dropdown-container': 'dropdown-container'; -}; +declare const classNames: {}; export = classNames; diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx index cc2171f84..cc1ffae20 100644 --- a/libs/shared/lib/components/dropdowns/index.tsx +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -2,21 +2,37 @@ import React, { useState, useEffect, useRef, ReactNode } from 'react'; import styles from './dropdowns.module.scss'; import Icon from '../icon'; import { ArrowDropDown } from '@mui/icons-material'; -import { PopoverContent, PopoverTrigger, Popover } from '../layout/Popover'; +import { PopoverContent, PopoverTrigger, Popover, PopoverOptions } from '../layout/Popover'; -export const DropdownContainer = Popover; +export const DropdownContainer = ({ children, ...props }: { children: React.ReactNode } & PopoverOptions) => { + return ( + <Popover placement="bottom-start" {...props}> + {children} + </Popover> + ); +}; type DropdownTriggerProps = { - title: string | ReactNode; + title?: string | ReactNode; size?: 'xs' | 'sm' | 'md' | 'xl'; disabled?: boolean; variant?: 'primary' | 'ghost' | 'outline'; className?: string; popover?: boolean; onClick?: () => void; + children?: ReactNode; }; -export function DropdownTrigger({ title, size, disabled, variant, className, onClick, popover = true }: DropdownTriggerProps) { +export function DropdownTrigger({ + title, + size, + disabled, + variant, + className, + onClick, + popover = true, + children = undefined, +}: DropdownTriggerProps) { const paddingClass = size === 'xs' ? 'py-0' : size === 'sm' ? 'px-1 py-1' : size === 'md' ? 'px-2 py-1' : 'px-4 py-2'; const textSizeClass = size === 'xs' ? 'text-xs' : size === 'sm' ? 'text-sm' : size === 'md' ? 'text-base' : 'text-lg'; @@ -27,7 +43,7 @@ export function DropdownTrigger({ title, size, disabled, variant, className, onC ? 'bg-transparent shadow-none' : 'border rounded bg-transparent'; - const inner = ( + const inner = children || ( <div className={`inline-flex w-full truncate justify-between items-center gap-x-1.5 ${variantClass} ${textSizeClass} ${paddingClass} text-secondary-900 shadow-sm hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400 pl-1 truncate${className ? ` ${className}` : ''}`} > @@ -57,7 +73,7 @@ export const DropdownItemContainer = React.forwardRef<HTMLDivElement, DropdownIt return ( <PopoverContent ref={ref} - className={`${styles['dropdown-container']} ${className} bg-light`} + className={`bg-light p-1 rounded border`} role="menu" aria-orientation="vertical" aria-labelledby="menu-button" @@ -85,7 +101,8 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel return ( <li ref={itemRef} - className={`${styles['dropdown-item']} ${className && className} hover:bg-primary-100 ${selected ? 'bg-primary text-white hover:text-black' : ''}`} + style={{ border: 0 }} + className={`cursor-pointer divide-y origin-top-right rounded-sm truncate block text-sm px-2 py-1 hover:bg-secondary-200 ${className && className} ${selected ? 'bg-secondary-400 text-white hover:text-black' : ''}`} onClick={() => { !disabled && onClick && onClick(value); }} diff --git a/libs/shared/lib/components/inputs/dropdown.stories.tsx b/libs/shared/lib/components/inputs/dropdown.stories.tsx index 319132e5a..166f8892f 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 { DropDownInput } from '.'; +import { DropdownInput } from '.'; -const Component: Meta<typeof DropDownInput> = { +const Component: Meta<typeof DropdownInput> = { title: 'Components/Inputs', - component: DropDownInput, + component: DropdownInput, argTypes: { onChange: {} }, decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], }; diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index b2160102c..637887824 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -85,19 +85,19 @@ type RadioProps = { type DropdownProps = { label?: string; - value: string | number | undefined; overrideRender?: React.ReactNode; type: 'dropdown'; - options: any; size?: 'xs' | 'sm' | 'md' | 'xl'; tooltip?: string; - onChange?: (value: string | number) => void; required?: boolean; inline?: boolean; buttonVariant?: 'primary' | 'outline' | 'ghost'; info?: string; disabled?: boolean; className?: string; + value?: number | string; + options: number[] | string[]; + onChange?: (value: number | string) => void; }; export type InputProps = TextProps | SliderProps | CheckboxProps | DropdownProps | RadioProps | BooleanProps | NumberProps; @@ -111,7 +111,7 @@ export const Input = (props: InputProps) => { case 'checkbox': return <CheckboxInput {...(props as CheckboxProps)} />; case 'dropdown': - return <DropDownInput {...(props as DropdownProps)} />; + return <DropdownInput {...(props as DropdownProps)} />; case 'radio': return <RadioInput {...(props as RadioProps)} />; case 'boolean': @@ -195,7 +195,7 @@ export const TextInput = ({ type={visible ? 'text' : 'password'} placeholder={placeholder} className={ - `${size} bg-light border border-secondary-300 placeholder-secondary-400 focus:outline-none block w-full focus:ring-1 ${ + `${size} bg-light border border-secondary-200 placeholder-secondary-400 focus:outline-none block w-full focus:ring-1 ${ isValid ? '' : 'input-error' }` + (className ? ` ${className}` : '') } @@ -253,7 +253,7 @@ export const NumberInput = ({ <input type="number" placeholder={placeholder} - className={`${size} bg-light border border-secondary-300 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ + className={`${size} bg-light border border-secondary-200 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ isValid ? '' : 'input-error' }`} value={value.toString()} @@ -371,7 +371,7 @@ export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) ); }; -export const DropDownInput = ({ +export const DropdownInput = ({ label, value, overrideRender, @@ -396,7 +396,7 @@ export const DropDownInput = ({ {label && ( <label className="label p-0"> <span - className={`text-sm text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + className={`text-${size} text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} > {label} </span> @@ -413,7 +413,7 @@ export const DropDownInput = ({ variant={buttonVariant} title={overrideRender || value} size={size} - className="" + className="cursor-pointer" disabled={disabled} onClick={() => { setIsDropdownOpen(!isDropdownOpen); diff --git a/libs/shared/lib/components/layout/Popover.tsx b/libs/shared/lib/components/layout/Popover.tsx index 70af69867..7a2543de1 100644 --- a/libs/shared/lib/components/layout/Popover.tsx +++ b/libs/shared/lib/components/layout/Popover.tsx @@ -16,7 +16,7 @@ import { useId, } from '@floating-ui/react'; -interface PopoverOptions { +export interface PopoverOptions { initialOpen?: boolean; placement?: Placement; modal?: boolean; diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx index 01c72fb6a..838c1bb93 100644 --- a/libs/shared/lib/data-access/api/eventBus.tsx +++ b/libs/shared/lib/data-access/api/eventBus.tsx @@ -46,7 +46,7 @@ import { setFetchingSaveStates, } from '../store/sessionSlice'; import { URLParams, getParam, deleteParam } from './url'; -import { setVisualizationState } from '../store/visualizationSlice'; +import { VisState, setVisualizationState } from '../store/visualizationSlice'; import { isEqual } from 'lodash-es'; import { addSchemaAttributeDimensions } from '../store/schemaSlice'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; @@ -70,7 +70,16 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function } const state = saveStates[saveStateID]; if (state) { dispatch(setQuerybuilderNodes(state.queryBuilder)); - dispatch(setVisualizationState(state.visualization)); + dispatch( + setVisualizationState( + Object.keys(state.visualization).length !== 0 // should only occur in mock data + ? (state.visualization as VisState) + : { + activeVisualizationIndex: -1, + openVisualizationArray: [], + }, + ), + ); } } } diff --git a/libs/shared/lib/data-access/broker/wsState.tsx b/libs/shared/lib/data-access/broker/wsState.tsx index 3634da71a..bc302e7cb 100644 --- a/libs/shared/lib/data-access/broker/wsState.tsx +++ b/libs/shared/lib/data-access/broker/wsState.tsx @@ -2,6 +2,7 @@ import { QueryBuilderState } from '../store/querybuilderSlice'; import { URLParams, setParam } from '../api/url'; import { Broker } from './broker'; import { DateStringStatement } from '../../querybuilder/model/logic/general'; +import { VisState } from '../store/visualizationSlice'; export const databaseNameMapping: string[] = ['arangodb', 'neo4j']; export const databaseProtocolMapping: string[] = ['neo4j://', 'neo4j+s://', 'bolt://', 'bolt+s://']; @@ -36,7 +37,7 @@ export type SaveStateI = { db: DatabaseInfo; schema: any; queryBuilder: QueryBuilderState; - visualization: any; + visualization: VisState | {}; share_state: any; }; diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 3cf9e53fa..8e52b38f4 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -14,7 +14,7 @@ import { } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { SessionCacheI, sessionCacheState } from './sessionSlice'; import { UseIsAuthorizedState, authState } from './authSlice'; -import { visualizationState, VisState } from './visualizationSlice'; +import { visualizationState, VisState, visualizationActive } from './visualizationSlice'; import { ML, allMLEnabled, selectML } from './mlSlice'; import { searchResultState, @@ -30,6 +30,7 @@ import { QueryGraphEdgeHandle, QueryMultiGraph } from '../../querybuilder'; import { SchemaGraph } from '../../schema'; import { GraphMetadata } from '../statistics'; import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice'; +import { VisualizationSettingsType } from '../../vis/common'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -70,6 +71,7 @@ export const useRecentSearches: () => string[] = () => useAppSelector(recentSear // Visualization Slices export const useVisualization: () => VisState = () => useAppSelector(visualizationState); +export const useActiveVisualization: () => VisualizationSettingsType | undefined = () => useAppSelector(visualizationActive); // Interaction Slices export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState); diff --git a/libs/shared/lib/data-access/store/visualizationSlice.ts b/libs/shared/lib/data-access/store/visualizationSlice.ts index 217118a0e..1f206d932 100644 --- a/libs/shared/lib/data-access/store/visualizationSlice.ts +++ b/libs/shared/lib/data-access/store/visualizationSlice.ts @@ -1,53 +1,103 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import { VisualizationConfiguration } from '../../vis/common'; +import { VisualizationSettingsType } from '../../vis/common'; import { isEqual } from 'lodash-es'; -export type VisStateSettings = { - [id: string]: VisualizationConfiguration; -}; +export type VisStateSettings = VisualizationSettingsType[]; export type VisState = { - activeVisualization?: string; - openVisualizations: VisStateSettings; + activeVisualizationIndex: number; + openVisualizationArray: VisStateSettings; }; export const initialState: VisState = { - // activeVisualization: 'NodeLinkVis', - openVisualizations: {}, + activeVisualizationIndex: -1, + openVisualizationArray: [], }; export const visualizationSlice = createSlice({ name: 'visualization', initialState, reducers: { - removeVisualization: (state, action: PayloadAction<string>) => { - if (state.openVisualizations[action.payload]) { - delete state.openVisualizations[action.payload]; + removeVisualization: (state, action: PayloadAction<number | undefined>) => { + let index = action.payload || state.activeVisualizationIndex; + state.openVisualizationArray.splice(index, 1); + if (state.activeVisualizationIndex === index) { + if (state.openVisualizationArray.length === 0) state.activeVisualizationIndex = -1; + else { + state.activeVisualizationIndex = Math.max(state.openVisualizationArray.length - 1, 0); + } } }, - setActiveVisualization: (state, action: PayloadAction<string>) => { - state.activeVisualization = action.payload; + setActiveVisualization: (state, action: PayloadAction<number>) => { + state.activeVisualizationIndex = action.payload; }, setVisualizationState: (state, action: PayloadAction<VisState>) => { - if (action.payload.activeVisualization && !isEqual(action.payload, state)) { - state.activeVisualization = action.payload.activeVisualization; - state.openVisualizations = action.payload.openVisualizations || {}; + if (action.payload.activeVisualizationIndex !== undefined && !isEqual(action.payload, state)) { + state.openVisualizationArray = action.payload.openVisualizationArray || []; + state.activeVisualizationIndex = action.payload.activeVisualizationIndex; } }, - updateVisualization: (state, action: PayloadAction<{ id: string; settings: any }>) => { + updateVisualization: (state, action: PayloadAction<{ id: number; settings: VisualizationSettingsType }>) => { const { id, settings } = action.payload; - state.openVisualizations[id] = settings; + state.openVisualizationArray[id] = settings; + }, + addVisualization: (state, action: PayloadAction<VisualizationSettingsType>) => { + state.openVisualizationArray.push(action.payload); + state.activeVisualizationIndex = state.openVisualizationArray.length - 1; }, - reorderVisState: (state, action: PayloadAction<{ [id: string]: VisualizationConfiguration }>) => { - state.openVisualizations = action.payload; + updateActiveVisualization: (state, action: PayloadAction<VisualizationSettingsType>) => { + if (state.activeVisualizationIndex === -1) { + return; + } + + state.openVisualizationArray[state.activeVisualizationIndex] = action.payload; + }, + updateActiveVisualizationAttributes: (state, action: PayloadAction<Record<string, any>>) => { + if (state.activeVisualizationIndex === -1) { + return; + } + + state.openVisualizationArray[state.activeVisualizationIndex] = { + ...state.openVisualizationArray[state.activeVisualizationIndex], + ...action.payload, + }; + }, + reorderVisState: ( + state, + action: PayloadAction<{ + id: number; + newPosition: number; + }>, + ) => { + const { id, newPosition } = action.payload; + const settingsCopy = [...state.openVisualizationArray]; + settingsCopy.splice(newPosition, 0, settingsCopy.splice(id, 1)[0]); + state.openVisualizationArray = settingsCopy; + + if (state.activeVisualizationIndex === id) { + state.activeVisualizationIndex = newPosition; + } else if (state.activeVisualizationIndex === newPosition) { + state.activeVisualizationIndex = id; + } }, }, }); -export const { removeVisualization, setActiveVisualization, setVisualizationState, updateVisualization, reorderVisState } = - visualizationSlice.actions; +export const { + removeVisualization, + setActiveVisualization, + updateActiveVisualization, + setVisualizationState, + updateVisualization, + reorderVisState, + addVisualization, + updateActiveVisualizationAttributes, +} = visualizationSlice.actions; export const visualizationState = (state: RootState) => state.visualize; -export const visualizationAllSettings = (state: RootState) => state.visualize.openVisualizations; - +export const visualizationAllSettings = (state: RootState) => state.visualize.openVisualizationArray; +export const visualizationActive = (state: RootState) => + state.visualize.activeVisualizationIndex !== -1 + ? state.visualize.openVisualizationArray?.[state.visualize.activeVisualizationIndex] + : undefined; export default visualizationSlice.reducer; diff --git a/libs/shared/lib/inspector/InspectorPanel.tsx b/libs/shared/lib/inspector/InspectorPanel.tsx index 48c07f40b..9d570ef29 100644 --- a/libs/shared/lib/inspector/InspectorPanel.tsx +++ b/libs/shared/lib/inspector/InspectorPanel.tsx @@ -1,26 +1,30 @@ import React, { useMemo } from 'react'; import { Button, Panel } from '../components'; -import { useFocus, useSelection } from '../data-access'; +import { useFocus, useSelection, useVisualization } from '../data-access'; import { resultSetFocus } from '../data-access/store/interactionSlice'; import { useDispatch } from 'react-redux'; import { ConnectionInspector } from './ConnectionInspector'; -import { VisualizationConfigPanel } from '../vis/components/config/panel'; +import { VisualizationSettings } from '../vis/components/config/VisualizationSettings'; import { SelectionConfig } from '../vis/components/config/SelectionConfig'; import { SchemaDialog } from '../schema/panel/SchemaSettings'; import { QuerySettings } from '../querybuilder/panel/querysidepanel/QuerySettings'; +import { useActiveVisualization } from '@graphpolaris/shared/lib/data-access'; export function InspectorPanel(props: { children?: React.ReactNode }) { const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; const selection = useSelection(); const focus = useFocus(); const dispatch = useDispatch(); + const { activeVisualizationIndex } = useVisualization(); const inspector = useMemo(() => { if (selection) return <SelectionConfig />; - if (!focus) return <ConnectionInspector />; - if (focus.focusType === 'visualization') return <VisualizationConfigPanel />; - else if (focus.focusType === 'schema') return <SchemaDialog />; - else if (focus.focusType === 'query') return <QuerySettings />; + // if (!focus) return <ConnectionInspector />; + // if (activeVisualizationIndex !== -1) return <ConnectionInspector />; + return <VisualizationSettings />; + // if (focus.focusType === 'visualization') return <VisualizationConfigPanel />; + // else if (focus.focusType === 'schema') return <SchemaDialog />; + // else if (focus.focusType === 'query') return <QuerySettings />; return null; }, [focus, selection]); diff --git a/libs/shared/lib/vis/common/types.ts b/libs/shared/lib/vis/common/types.ts index 2d8f8bf3d..d3eda36aa 100644 --- a/libs/shared/lib/vis/common/types.ts +++ b/libs/shared/lib/vis/common/types.ts @@ -2,25 +2,29 @@ import { GraphQueryResult } from '../../data-access/store/graphQueryResultSlice' import { ML } from '../../data-access/store/mlSlice'; import { SchemaGraph } from '../../schema'; import type { AppDispatch } from '../../data-access'; -import { FC } from 'react'; -import { Visualizations } from '../manager'; +import { FC, ReactElement } from 'react'; import { GraphMetadata } from '../../data-access/statistics'; import { Node, Edge } from '../../data-access/store/graphQueryResultSlice'; +import { Visualizations } from '../components/VisualizationPanel'; -export type VisualizationConfiguration = { [id: string]: any }; +export type VisualizationSettingsType = { + id: string; + name: string; + [id: string]: any; +}; -export type VISComponentType = { +export type VISComponentType<T> = { displayName: keyof typeof Visualizations; - component: FC<VisualizationPropTypes>; - settings: FC<any>; - configuration: { [id: string]: any }; + component: React.FC<VisualizationPropTypes<T>>; + settingsComponent: FC<VisualizationSettingsPropTypes<T>>; + settings: T; }; -export type VisualizationPropTypes = { +export type VisualizationPropTypes<T = {}> = { data: GraphQueryResult; schema: SchemaGraph; ml: ML; - configuration: VisualizationConfiguration; + settings: T & VisualizationSettingsType; dispatch: AppDispatch; graphMetadata: GraphMetadata; updateSettings: (newSettings: any) => void; @@ -28,6 +32,12 @@ export type VisualizationPropTypes = { handleSelect: (selection?: { nodes?: Node[]; edges?: Edge[] }) => void; }; +export type VisualizationSettingsPropTypes<T = {}> = { + settings: T & VisualizationSettingsType; + graphMetadata: GraphMetadata; + updateSettings: (val: Partial<T>) => void; +}; + export type SchemaElements = { nodes: Node[]; edges: Edge[]; diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx index 61bcfb78e..bbe2ffaac 100644 --- a/libs/shared/lib/vis/components/VisualizationPanel.tsx +++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx @@ -1,36 +1,115 @@ -import React, { useMemo } from 'react'; -import { useGraphQueryResult, useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; -import VisualizationBar from './bar'; -import { VisualizationManager, VisualizationManagerType } from '../manager'; +import React, { Suspense, lazy, useEffect, useMemo, useState } from 'react'; +import { + Edge, + Node, + useActiveVisualization, + useAppDispatch, + useGraphQueryResult, + useGraphQueryResultMeta, + useML, + useQuerybuilderGraph, + useSchemaGraph, + useVisualization, +} from '@graphpolaris/shared/lib/data-access'; +import VisualizationTabBar from './VisualizationTabBar'; import { Recommender, NoData, Querying } from '../views'; -import { resultSetFocus } from '../../data-access/store/interactionSlice'; -import { useDispatch } from 'react-redux'; +import { resultSetFocus, resultSetSelection, unSelect } from '../../data-access/store/interactionSlice'; +import { + setActiveVisualization, + removeVisualization, + reorderVisState, + updateVisualization, + addVisualization, +} from '../../data-access/store/visualizationSlice'; +import { VisualizationSettingsType, VisualizationPropTypes, VISComponentType } from '../common'; + +type PromiseFunc = () => Promise<{ default: VISComponentType<any> }>; +export const Visualizations: Record<string, PromiseFunc> = { + TableVis: () => import('../visualizations/tablevis/tablevis'), + PaohVis: () => import('../visualizations/paohvis/paohvis'), + // PaohVis: () => Promise.resolve({ default: Paohviz }), + RawJSONVis: () => import('../visualizations/rawjsonvis/rawjsonvis'), + NodeLinkVis: () => import('../visualizations/nodelinkvis/nodelinkvis'), + // NodeLinkVis: () => Promise.resolve({ default: NodeLinkComponent }), + MatrixVis: () => import('../visualizations/matrixvis/matrixvis'), + SemanticSubstratesVis: () => import('../visualizations/semanticsubstratesvis/semanticsubstratesvis'), + MapVis: () => import('../visualizations/mapvis/mapvis'), +}; +export const VISUALIZATION_TYPES: string[] = Object.keys(Visualizations); export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { - const manager = VisualizationManager(); const query = useQuerybuilderGraph(); const graphQueryResult = useGraphQueryResult(); - const dispatch = useDispatch(); - - const renderContent = useMemo(() => { - if (graphQueryResult.queryingBackend) { - return <Querying />; - } else if (graphQueryResult.nodes.length === 0) { - return <NoData dataAvailable={query.nodes.length > 0} />; - } else if (manager.tabs.length === 0) { - return <Recommender onClick={(id: string) => manager.changeActive(id)} />; + const dispatch = useAppDispatch(); + const { activeVisualizationIndex, openVisualizationArray } = useVisualization(); + const activeVisualization = useActiveVisualization(); + const ml = useML(); + const schema = useSchemaGraph(); + const graphMetadata = useGraphQueryResultMeta(); + const [viz, setViz] = useState<{ component: React.FC<VisualizationPropTypes> } | undefined>(undefined); + + useEffect(() => { + loadVisualization(); + }, [activeVisualizationIndex]); + + const loadVisualization = async (add = false) => { + if (!activeVisualization) { + setViz(undefined); + return; + } + const componentModule = await Visualizations[activeVisualization.id](); + const component = componentModule.default; + + if (add) { + dispatch(addVisualization(component.settings)); + } + setViz({ component: component.component }); + }; + + const handleSelect = (selection?: { nodes?: Node[]; edges?: Edge[] }) => { + if (selection?.nodes && selection.nodes.length > 0) dispatch(resultSetSelection(selection.nodes)); + else dispatch(unSelect()); + }; + + const updateSettings = (newSettings: Record<string, any>) => { + if (activeVisualizationIndex) { + const updatedSettings = { ...openVisualizationArray[activeVisualizationIndex], ...newSettings }; + dispatch(updateVisualization({ id: activeVisualizationIndex, settings: updatedSettings })); } - return <div className="w-full h-full flex">{manager.renderComponent()}</div>; - }, [graphQueryResult, manager]); + }; return ( <div className="vis-panel h-full w-full flex flex-col border bg-light" onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'visualization' }))} > - <VisualizationBar manager={manager} fullSize={fullSize} /> + <VisualizationTabBar fullSize={fullSize} /> <div className="grow overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}> - {renderContent} + {graphQueryResult.queryingBackend ? ( + <Querying /> + ) : graphQueryResult.nodes.length === 0 ? ( + <NoData dataAvailable={query.nodes.length > 0} /> + ) : openVisualizationArray.length === 0 ? ( + <Recommender /> + ) : ( + <div className="w-full h-full flex"> + <Suspense fallback={<div>Loading...</div>}> + {!!viz && !!activeVisualization && ( + <viz.component + data={graphQueryResult} + schema={schema} + ml={ml} + settings={activeVisualization} + dispatch={dispatch} + handleSelect={handleSelect} + graphMetadata={graphMetadata} + updateSettings={updateSettings} + handleHover={() => {}} + /> + )} + </Suspense> + </div> + )} </div> </div> ); diff --git a/libs/shared/lib/vis/components/VisualizationTabBar.tsx b/libs/shared/lib/vis/components/VisualizationTabBar.tsx new file mode 100644 index 000000000..1c39bdc3f --- /dev/null +++ b/libs/shared/lib/vis/components/VisualizationTabBar.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, DropdownTrigger } from '../../components'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; +import { Add, Close, Fullscreen } from '@mui/icons-material'; +import { ControlContainer } from '../../components/controls'; +import { Tabs, Tab } from '../../components/tabs'; +import { useAppDispatch, useVisualization } from '../../data-access'; +import { addVisualization, removeVisualization, reorderVisState, setActiveVisualization } from '../../data-access/store/visualizationSlice'; +import { Visualizations } from './VisualizationPanel'; + +export default function VisualizationTabBar(props: { fullSize: () => void }) { + const { activeVisualizationIndex, openVisualizationArray } = useVisualization(); + const [open, setOpen] = useState(false); + const dispatch = useAppDispatch(); + + const handleDragStart = (e: React.DragEvent<HTMLDivElement>, i: number) => { + e.dataTransfer.setData('text/plain', i.toString()); + }; + + const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { + e.preventDefault(); + }; + + const handleDrop = (e: React.DragEvent<HTMLDivElement>, i: number) => { + e.preventDefault(); + const draggedVisIndex = e.dataTransfer.getData('text/plain'); + dispatch(reorderVisState({ id: Number(draggedVisIndex), newPosition: i })); + }; + + const onSelect = async (id?: number) => { + if (id === undefined) return; + dispatch(setActiveVisualization(id)); + }; + + const onDelete = (id: number) => { + dispatch(removeVisualization(id)); + }; + + return ( + <div className="sticky shrink-0 top-0 flex items-stretch justify-between h-7 bg-secondary-100 border-b border-secondary-200 max-w-full"> + <div className="flex items-center"> + <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">Visualization</h1> + </div> + <div className="items-center shrink-0 px-0.5"> + <TooltipProvider delayDuration={0}> + <Tooltip> + <TooltipTrigger> + <DropdownContainer open={open} onOpenChange={setOpen}> + <DropdownTrigger onClick={() => setOpen((v) => !v)}> + <Button as={'a'} variantType="secondary" variant="ghost" size="xs" iconComponent={<Add />} onClick={() => {}} /> + </DropdownTrigger> + <DropdownItemContainer> + {Object.keys(Visualizations).map((name, i) => ( + <DropdownItem + value={name} + key={name} + className="" + onClick={async (e) => { + // onSelect(i); + const component = await Visualizations[name](); + dispatch(addVisualization({ ...component.default.settings, name: name, id: name })); + setOpen(false); + }} + ></DropdownItem> + ))} + </DropdownItemContainer> + </DropdownContainer> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Add visualization</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </div> + <Tabs> + {openVisualizationArray.map((vis, i) => { + const isActive = activeVisualizationIndex === i; + return ( + <Tab + key={i} + activeTab={isActive} + text={vis.name} + onClick={() => onSelect(i)} + onDragStart={(e) => handleDragStart(e, i)} + onDragOver={(e) => handleDragOver(e)} + onDrop={(e) => handleDrop(e, i)} + draggable + > + <Button + variantType="secondary" + variant="ghost" + rounded + size="2xs" + iconComponent={<Close />} + onClick={(e) => { + e.stopPropagation(); + onDelete(i); + }} + /> + </Tab> + ); + })} + </Tabs> + <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> + <ControlContainer> + <TooltipProvider delayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button variantType="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={props.fullSize} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Full screen</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </ControlContainer> + </div> + </div> + ); +} diff --git a/libs/shared/lib/vis/components/bar.tsx b/libs/shared/lib/vis/components/bar.tsx deleted file mode 100644 index fa1c79de7..000000000 --- a/libs/shared/lib/vis/components/bar.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import { Button, DropdownItem } from '../../components'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; -import { Add, Close, Fullscreen } from '@mui/icons-material'; -import { ControlContainer } from '../../components/controls'; -import { Visualizations } from '../manager'; -import { VisualizationManagerType } from '../manager'; -import { Tabs, Tab } from '../../components/tabs'; -import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; - -type Props = { - manager: VisualizationManagerType; - fullSize: () => void; -}; - -export default function VisualizationBar({ manager, fullSize }: Props) { - const [open, setOpen] = React.useState(false); - - const handleDragStart = (e: React.DragEvent<HTMLDivElement>, visId: string) => { - e.dataTransfer.setData('text/plain', visId); - }; - - const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { - e.preventDefault(); - }; - - const handleDrop = (e: React.DragEvent<HTMLDivElement>, dropVisId: string) => { - e.preventDefault(); - const draggedVisId = e.dataTransfer.getData('text/plain'); - manager.reorderVisualizations({ draggedVisId, dropVisId }); - manager.changeActive(draggedVisId); - }; - - return ( - <div className="sticky shrink-0 top-0 flex items-stretch justify-between h-7 bg-secondary-100 border-b border-secondary-200 max-w-full"> - <div className="flex items-center"> - <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">Visualization</h1> - </div> - <div className="items-center shrink-0 px-0.5"> - <TooltipProvider delayDuration={0}> - <Tooltip> - <TooltipTrigger> - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger onClick={() => setOpen((v) => !v)}> - <Button as={'a'} variantType="secondary" variant="ghost" size="xs" iconComponent={<Add />} onClick={() => {}} /> - </PopoverTrigger> - <PopoverContent> - <div className="bg-light p-1 rounded border"> - {Object.keys(Visualizations).map((key) => ( - <DropdownItem - value={key} - key={key} - className="text-sm px-2 py-1 rounded cursor-pointer hover:bg-secondary-200" - onClick={(e) => { - manager.changeActive(key); - setOpen(false); - }} - ></DropdownItem> - ))} - </div> - </PopoverContent> - </Popover> - </TooltipTrigger> - <TooltipContent side={'top'}> - <p>Add visualization</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - <Tabs> - {manager.tabs.map((visId: string) => { - const isActive = manager.activeVisualization === visId; - return ( - <Tab - key={visId} - activeTab={isActive} - text={visId} - onClick={() => manager.changeActive(visId)} - onDragStart={(e) => handleDragStart(e, visId)} - onDragOver={(e) => handleDragOver(e)} - onDrop={(e) => handleDrop(e, visId)} - draggable - > - <Button - variantType="secondary" - variant="ghost" - rounded - size="2xs" - iconComponent={<Close />} - onClick={(e) => { - e.stopPropagation(); - manager.deleteVisualization(visId); - }} - /> - </Tab> - ); - })} - </Tabs> - <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> - <ControlContainer> - <TooltipProvider delayDuration={0}> - <Tooltip> - <TooltipTrigger asChild> - <Button variantType="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={fullSize} /> - </TooltipTrigger> - <TooltipContent side={'top'}> - <p>Full screen</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </ControlContainer> - </div> - </div> - ); -} diff --git a/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx b/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx deleted file mode 100644 index 38fe19fa9..000000000 --- a/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Delete } from '@mui/icons-material'; -import { Button, Input, Panel } from '../../..'; -import { VisualizationManagerType, VISUALIZATION_TYPES } from '../../manager'; -import { SettingsHeader } from './components'; - -type Props = { - manager: VisualizationManagerType; -}; - -export const ActiveVisualizationConfig = ({ manager }: Props) => { - return ( - <> - <div className="flex justify-between items-center px-4 py-2"> - <span className="text-xs font-bold">Visualization</span> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent={<Delete />} - onClick={() => { - if (manager.activeVisualization) manager.deleteVisualization(manager.activeVisualization); - }} - /> - </div> - <div className="flex justify-between items-center px-4 py-1"> - <span className="text-xs font-normal">Type</span> - <div className="w-36"> - <Input type="dropdown" size="xs" options={VISUALIZATION_TYPES} value={manager.activeVisualization} onChange={() => {}} /> - </div> - </div> - <div className="flex justify-between items-center px-4 py-1"> - <span className="text-xs font-normal">Name</span> - <input type="text" className="border rounded text-xs w-36" value={manager.activeVisualization} onChange={() => {}} /> - </div> - {manager.activeVisualization && ( - <div className="border-b p-4 w-full"> - <SettingsHeader name="Configuration" /> - {manager.renderSettings()} - </div> - )} - </> - ); -}; diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx index 496bad2c7..7d41cf4a1 100644 --- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx +++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx @@ -2,7 +2,6 @@ import { SelectionStateI, unSelect } from '@graphpolaris/shared/lib/data-access/ import { Delete } from '@mui/icons-material'; import { useDispatch } from 'react-redux'; import { Button, EntityPill, useSelection } from '../../..'; -import { VISUALIZATION_TYPES } from '../../manager'; import { SettingsHeader } from './components'; export const SelectionConfig = () => { diff --git a/libs/shared/lib/vis/components/config/VisualizationSettings.tsx b/libs/shared/lib/vis/components/config/VisualizationSettings.tsx new file mode 100644 index 000000000..9fdb7269d --- /dev/null +++ b/libs/shared/lib/vis/components/config/VisualizationSettings.tsx @@ -0,0 +1,113 @@ +import React, { Suspense, useEffect, useState } from 'react'; +import { Delete } from '@mui/icons-material'; +import { + Button, + Input, + VISUALIZATION_TYPES, + Visualizations, + useAppDispatch, + useGraphQueryResultMeta, + useVisualization, + useActiveVisualization, +} from '../../..'; +import { SettingsHeader } from './components'; +import { + removeVisualization, + updateActiveVisualization, + updateVisualization, + updateActiveVisualizationAttributes, +} from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import { VisualizationSettingsPropTypes, VisualizationSettingsType } from '../../common'; + +type Props = {}; + +export function VisualizationSettings({}: Props) { + // const manager = VisualizationManager(); + // const activeVisualization = useActiveVisualization(); + const { activeVisualizationIndex, openVisualizationArray } = useVisualization(); + const activeVisualization = openVisualizationArray[activeVisualizationIndex]; + const graphMetadata = useGraphQueryResultMeta(); + + const dispatch = useAppDispatch(); + + const [component, setComponent] = useState< + | undefined + | { + component: React.FC<VisualizationSettingsPropTypes>; + } + >(undefined); + + useEffect(() => { + loadVisualization(activeVisualization); + }, [activeVisualization]); + + const loadVisualization = async (vis?: VisualizationSettingsType) => { + if (!vis) { + setComponent(undefined); + return; + } + + const componentModule = await Visualizations[vis.id](); + const component = componentModule.default; + + setComponent({ + component: component.settingsComponent, + }); + }; + + const updateSettings = (newSettings: Record<string, any>) => { + if (activeVisualization) { + dispatch(updateActiveVisualizationAttributes(newSettings)); + } + }; + + return ( + <div className="flex flex-col w-full"> + <div className="text-sm px-4 py-2 flex flex-col gap-1"> + <div className="flex justify-between items-center"> + <span className="font-bold">Visualization</span> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent={<Delete />} + onClick={() => { + if (activeVisualization) removeVisualization(); + }} + /> + </div> + <Input + type="dropdown" + size="xs" + options={VISUALIZATION_TYPES} + value={activeVisualization?.id} + onChange={(val) => { + updateSettings({ id: val }); + }} + label="Type" + inline + /> + <Input + type="text" + size="sm" + value={activeVisualization?.name || ''} + onChange={(val) => { + updateSettings({ name: val }); + }} + label="Name" + inline + /> + {activeVisualization && ( + <> + <SettingsHeader name="Visualization Settings" /> + <Suspense fallback={<div>Loading...</div>}> + {component && component.component && activeVisualization && ( + <component.component settings={activeVisualization} graphMetadata={graphMetadata} updateSettings={updateSettings} /> + )} + </Suspense> + </> + )} + </div> + </div> + ); +} diff --git a/libs/shared/lib/vis/components/config/components.tsx b/libs/shared/lib/vis/components/config/components.tsx index 842f508ce..6f704987c 100644 --- a/libs/shared/lib/vis/components/config/components.tsx +++ b/libs/shared/lib/vis/components/config/components.tsx @@ -17,7 +17,7 @@ type SettingsHeaderProps = { export function SettingsHeader({ name, icon, onClickIcon }: SettingsHeaderProps) { return ( <div className="flex justify-between items-center"> - <span className="text-xs font-bold">{name}</span> + <span className="text-sm font-bold">{name}</span> {icon && icon} </div> ); diff --git a/libs/shared/lib/vis/components/config/index.tsx b/libs/shared/lib/vis/components/config/index.tsx index 4bf7db1e4..028738a4c 100644 --- a/libs/shared/lib/vis/components/config/index.tsx +++ b/libs/shared/lib/vis/components/config/index.tsx @@ -1,2 +1,2 @@ -export { VisualizationConfigPanel as ConfigPanel } from './panel'; +export { VisualizationSettings as VisualizationConfigPanel } from './VisualizationSettings'; export { SettingsContainer, SettingsHeader } from './components'; diff --git a/libs/shared/lib/vis/components/config/panel.tsx b/libs/shared/lib/vis/components/config/panel.tsx deleted file mode 100644 index 4f0bd9e03..000000000 --- a/libs/shared/lib/vis/components/config/panel.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Button } from '../../../components'; -import { VisualizationManager, VisualizationManagerType } from '../../manager'; -import { useSelection, useSessionCache } from '../../../data-access'; -import { SelectionConfig } from './SelectionConfig'; -import { ActiveVisualizationConfig } from './ActiveVisualizationConfig'; - -type Props = {}; - -export function VisualizationConfigPanel({}: Props) { - const manager = VisualizationManager(); - - return ( - <div className="flex flex-col w-full"> - <ActiveVisualizationConfig manager={manager}></ActiveVisualizationConfig> - </div> - ); -} diff --git a/libs/shared/lib/vis/manager/VisualizationManager.tsx b/libs/shared/lib/vis/manager/VisualizationManager.tsx deleted file mode 100644 index 94866c583..000000000 --- a/libs/shared/lib/vis/manager/VisualizationManager.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useState, useMemo, useEffect, Suspense } from 'react'; -import { VISComponentType, VisualizationConfiguration } from '../common'; -import { - removeVisualization, - reorderVisState, - setActiveVisualization, - updateVisualization, -} from '../../data-access/store/visualizationSlice'; -import { - useAppDispatch, - useGraphQueryResult, - useGraphQueryResultMeta, - useML, - useSchemaGraph, - useSessionCache, - useVisualization, -} from '../../data-access'; -import { VisualizationManagerType } from '.'; -import { Node, Edge } from '../../data-access/store/graphQueryResultSlice'; -import { SelectionStateI, resultSetSelection, unSelect } from '../../data-access/store/interactionSlice'; -import { NodeLinkComponent } from '../visualizations'; - -export const Visualizations: Record<string, Function> = { - TableVis: () => import('../visualizations/tablevis/tablevis'), - PaohVis: () => import('../visualizations/paohvis/paohvis'), - // PaohVis: () => Promise.resolve({ default: PaohVisComponent }), - RawJSONVis: () => import('../visualizations/rawjsonvis/rawjsonvis'), - // NodeLinkVis: () => import('../visualizations/nodelinkvis/nodelinkvis'), - NodeLinkVis: () => Promise.resolve({ default: NodeLinkComponent }), - MatrixVis: () => import('../visualizations/matrixvis/matrixvis'), - SemanticSubstratesVis: () => import('../visualizations/semanticsubstratesvis/semanticsubstratesvis'), - MapVis: () => import('../visualizations/mapvis/mapvis'), -}; - -export const VISUALIZATION_TYPES: string[] = Object.keys(Visualizations); - -export const VisualizationManager = (): VisualizationManagerType => { - const dispatch = useAppDispatch(); - const session = useSessionCache(); - const ml = useML(); - const schema = useSchemaGraph(); - const graphQueryResult = useGraphQueryResult(); - const graphMetadata = useGraphQueryResultMeta(); - const { activeVisualization, openVisualizations } = useVisualization(); - - const [configuration, setConfiguration] = useState<any>(); - const [visualization, setVisualization] = useState<VISComponentType>(); - const [selected, setSelected] = useState<any>(); - const activeType = useMemo( - () => (activeVisualization ? openVisualizations[activeVisualization]?.displayName : undefined), - [openVisualizations, activeVisualization], - ); - const tabs = useMemo(() => (Object.keys(openVisualizations).length ? Object.keys(openVisualizations) : []), [openVisualizations]); - - useEffect(() => { - loadVisualization(); - }, [activeVisualization]); - - const loadVisualization = async () => { - if (activeVisualization && Visualizations[activeVisualization]) { - const componentModule = await Visualizations[activeVisualization](); - const component = componentModule.default; - - if (!(activeVisualization in Object.keys(openVisualizations))) { - // Visualization doesn't yet exist so add its configuration - const configuration = component.configuration; - dispatch(updateVisualization({ id: activeVisualization, settings: configuration })); - setConfiguration(configuration); - } else { - setConfiguration(openVisualizations[activeVisualization]); - } - - setVisualization(component); - } - }; - - const changeActive = (id: string) => { - dispatch(setActiveVisualization(id)); - }; - - const deleteVisualization = (id: string) => { - dispatch(removeVisualization(id)); - if (Object.keys(openVisualizations).length > 0) { - const newActive = tabs.find((v: string) => id !== v); - changeActive(newActive || ''); - } - }; - - const reorderVisualizations = ({ draggedVisId, dropVisId }: { draggedVisId: string; dropVisId: string }) => { - const settingsCopy = { ...openVisualizations }; - const keys = Object.keys(settingsCopy); - const draggedIndex = keys.indexOf(draggedVisId); - const dropIndex = keys.indexOf(dropVisId); - - if (draggedIndex !== -1 && dropIndex !== -1) { - keys.splice(dropIndex, 0, draggedVisId); - const newSettings: { [id: string]: VisualizationConfiguration } = {}; - keys.forEach((key) => { - newSettings[key] = settingsCopy[key]; - }); - - dispatch(reorderVisState(newSettings)); - } - }; - - const handleSelect = (selection?: { nodes?: Node[]; edges?: Edge[] }) => { - if (selection?.nodes && selection.nodes.length > 0) dispatch(resultSetSelection(selection.nodes)); - else dispatch(unSelect()); - }; - - const updateSettings = (newSettings: Record<string, any>) => { - if (activeVisualization) { - const updatedSettings = { ...configuration, ...newSettings }; - setConfiguration(updatedSettings); - dispatch(updateVisualization({ id: activeVisualization, settings: updatedSettings })); - } - }; - - const renderSettings = () => { - return ( - visualization?.settings && - configuration && ( - <visualization.settings configuration={configuration} graphMetadata={graphMetadata} updateSettings={updateSettings} /> - ) - ); - }; - - // TODO: we should remove the renderable part of this useFunction into its own component, since this here is an anti-pattern - const renderComponent = () => { - return ( - <Suspense fallback={<div>Loading...</div>}> - {visualization?.component && configuration && ( - <visualization.component - data={graphQueryResult} - schema={schema} - ml={ml} - configuration={configuration} - dispatch={dispatch} - handleSelect={handleSelect} - graphMetadata={graphMetadata} - updateSettings={updateSettings} - handleHover={() => {}} - /> - )} - </Suspense> - ); - }; - - return { - renderComponent, - renderSettings, - activeVisualization, - activeType, - tabs, - changeActive, - reorderVisualizations, - deleteVisualization, - }; -}; diff --git a/libs/shared/lib/vis/manager/index.ts b/libs/shared/lib/vis/manager/index.ts deleted file mode 100644 index 96616a248..000000000 --- a/libs/shared/lib/vis/manager/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { VisualizationManager, Visualizations, VISUALIZATION_TYPES } from './VisualizationManager'; -export type { VisualizationManagerType } from './manager.types'; diff --git a/libs/shared/lib/vis/manager/manager.types.ts b/libs/shared/lib/vis/manager/manager.types.ts deleted file mode 100644 index fca458010..000000000 --- a/libs/shared/lib/vis/manager/manager.types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -export type VisualizationManagerType = { - renderComponent: () => React.ReactNode; - renderSettings: () => React.ReactNode; - activeVisualization: string | undefined; - activeType: string | undefined; - tabs: string[]; - changeActive: (id: string) => void; - reorderVisualizations: (args: { draggedVisId: string; dropVisId: string }) => void; - deleteVisualization: (id: string) => void; -}; diff --git a/libs/shared/lib/vis/views/noData.tsx b/libs/shared/lib/vis/views/NoData.tsx similarity index 100% rename from libs/shared/lib/vis/views/noData.tsx rename to libs/shared/lib/vis/views/NoData.tsx diff --git a/libs/shared/lib/vis/views/querying.tsx b/libs/shared/lib/vis/views/Querying.tsx similarity index 100% rename from libs/shared/lib/vis/views/querying.tsx rename to libs/shared/lib/vis/views/Querying.tsx diff --git a/libs/shared/lib/vis/views/recommender.tsx b/libs/shared/lib/vis/views/Recommender.tsx similarity index 62% rename from libs/shared/lib/vis/views/recommender.tsx rename to libs/shared/lib/vis/views/Recommender.tsx index 5f85f9a0d..a021353d0 100644 --- a/libs/shared/lib/vis/views/recommender.tsx +++ b/libs/shared/lib/vis/views/Recommender.tsx @@ -1,10 +1,12 @@ import React from 'react'; import Info from '../../components/info'; -import { Visualizations } from '../manager'; +import { addVisualization } from '../../data-access/store/visualizationSlice'; +import { useAppDispatch } from '../../data-access'; +import { Visualizations } from '../components/VisualizationPanel'; -type Props = { onClick: (id: string) => void }; +export function Recommender() { + const dispatch = useAppDispatch(); -export function Recommender({ onClick }: Props) { return ( <div className="p-4"> <span className="text-md">Select a visualization</span> @@ -13,9 +15,10 @@ export function Recommender({ onClick }: Props) { <div key={name} className="p-4 cursor-pointer border hover:bg-secondary-100" - onClick={(e) => { + onClick={async (e) => { e.preventDefault(); - onClick(name); + const component = await Visualizations[name](); + dispatch(addVisualization({ ...component.default.settings, name: name, id: name })); }} > <div className="flex items-center justify-between"> diff --git a/libs/shared/lib/vis/views/index.tsx b/libs/shared/lib/vis/views/index.tsx index de85438c8..d16d4d610 100644 --- a/libs/shared/lib/vis/views/index.tsx +++ b/libs/shared/lib/vis/views/index.tsx @@ -1,3 +1,3 @@ -export { NoData } from './noData'; -export { Recommender } from './recommender'; -export { Querying } from './querying'; +export { NoData } from './NoData'; +export { Recommender } from './Recommender'; +export { Querying } from './Querying'; diff --git a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx new file mode 100644 index 000000000..5e411a08c --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx @@ -0,0 +1,63 @@ +import React, { useMemo } from 'react'; +import { SettingsContainer } from '../../components/config'; +import { layerTypes } from './components/layers'; +import { Input } from '../../..'; +import { VisualizationSettingsPropTypes } from '../../common'; +import { MapProps } from './mapvis'; + +export const MapSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) => { + // const spatialAttributes = useMemo(() => { + // if (!settings.node) return []; + // return Object.entries(graphMetadata.nodes.types[settings.node].attributes) + // .filter((kv) => kv[1].dimension === 'spatial') + // .map((kv) => kv[0]); + // }, [settings.node]); + const spatialAttributes = useMemo(() => { + if (!settings.node) return []; + return Object.entries(graphMetadata.nodes.types[settings.node].attributes).map((kv) => kv[0]); + }, [settings.node]); + + return ( + <SettingsContainer> + <Input + label="Data layer" + type="dropdown" + inline + value={settings.layer} + options={Object.keys(layerTypes)} + onChange={(val) => updateSettings({ layer: val as string })} + /> + + <Input + label="Node Label" + type="dropdown" + inline + value={settings.node} + options={[...Object.keys(graphMetadata.nodes.types)]} + disabled={Object.keys(graphMetadata.nodes.types).length < 1} + onChange={(val) => { + updateSettings({ node: val as string }); + }} + /> + <Input + label="Latitude Location" + type="dropdown" + inline + value={settings.lat} + options={[...spatialAttributes]} + disabled={!settings.node || spatialAttributes.length < 1} + onChange={(val) => updateSettings({ lat: val as string })} + /> + + <Input + inline + label="Longitude Location accessor" + type="dropdown" + value={settings.lon} + options={[...spatialAttributes]} + disabled={!settings.node || spatialAttributes.length < 1} + onChange={(val) => updateSettings({ lon: val as string })} + /> + </SettingsContainer> + ); +}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/configuration.tsx b/libs/shared/lib/vis/visualizations/mapvis/configuration.tsx deleted file mode 100644 index b295e90df..000000000 --- a/libs/shared/lib/vis/visualizations/mapvis/configuration.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useMemo } from 'react'; -import { SettingsContainer } from '../../components/config'; -import { layerTypes } from './components/layers'; -import { Input } from '../../..'; -import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; -import { MapProps } from './mapvis'; - -export const MapSettings = ({ - configuration, - graphMetadata, - updateSettings, -}: { - configuration: MapProps; - graphMetadata: GraphMetadata; - updateSettings: (val: any) => void; -}) => { - // const spatialAttributes = useMemo(() => { - // if (!configuration.node) return []; - // return Object.entries(graphMetadata.nodes.types[configuration.node].attributes) - // .filter((kv) => kv[1].dimension === 'spatial') - // .map((kv) => kv[0]); - // }, [configuration.node]); - const spatialAttributes = useMemo(() => { - if (!configuration.node) return []; - return Object.entries(graphMetadata.nodes.types[configuration.node].attributes).map((kv) => kv[0]); - }, [configuration.node]); - - return ( - <SettingsContainer> - <span className="text-xs font-semibold">Data layer</span> - <Input - type="dropdown" - value={configuration.layer} - options={Object.keys(layerTypes)} - onChange={(val) => updateSettings({ layer: val })} - /> - - <span className="text-xs font-semibold">Node Label</span> - <Input - type="dropdown" - value={configuration.node} - options={[...Object.keys(graphMetadata.nodes.types)]} - disabled={Object.keys(graphMetadata.nodes.types).length < 1} - onChange={(val) => updateSettings({ node: val })} - /> - <span className="text-xs font-semibold">Location accessor (lat)</span> - <Input - type="dropdown" - value={configuration.lat} - options={[...spatialAttributes]} - disabled={!configuration.node || spatialAttributes.length < 1} - onChange={(val) => updateSettings({ lat: val })} - /> - - <span className="text-xs font-semibold">Location accessor (lon)</span> - <Input - type="dropdown" - value={configuration.lon} - options={[...spatialAttributes]} - disabled={!configuration.node || spatialAttributes.length < 1} - onChange={(val) => updateSettings({ lon: val })} - /> - </SettingsContainer> - ); -}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index 56d182548..c2d00d429 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -6,16 +6,16 @@ import { Coordinate, Layer } from './mapvis.types'; import { VISComponentType, VisualizationPropTypes } from '../../common'; import { layerTypes } from './components/layers'; import { createBaseMap } from './components/BaseMap'; -import { MapSettings } from './configuration'; +import { MapSettings } from './MapSettings'; export type MapProps = { - layer: undefined | string; + layer: string; node: undefined | string; - lat: undefined | string; - lon: undefined | string; + lat: string; + lon: string; }; -const configuration: MapProps = { +const settings: MapProps = { layer: 'node', node: undefined, lat: 'gp_latitude', @@ -34,7 +34,7 @@ const FLY_SPEED = 1000; const baseLayer = createBaseMap(); -export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: VisualizationPropTypes) => { +export const MapVis = ({ data, settings, updateSettings, graphMetadata }: VisualizationPropTypes<MapProps>) => { const [layer, setLayer] = React.useState<Layer | undefined>(undefined); const [viewport, setViewport] = React.useState<Record<string, any>>(INITIAL_VIEW_STATE); const [hoverObject, setHoverObject] = React.useState<Node | null>(null); @@ -72,34 +72,36 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V ); useEffect(() => { - if (configuration.layer) { - const layerType = layerTypes[configuration.layer] as any; - - setLayer({ - id: Date.now(), - name: 'New layer', - type: layerType, - config: { - ...layerType.layerOptions, - }, - visible: true, - }); - } - }, [configuration.layer]); + const layerType = settings.layer ? layerTypes?.[settings.layer] : layerTypes.node; + + setLayer({ + id: Date.now(), + name: 'New layer', + type: layerType, + config: { + ...layerType.layerOptions, + }, + visible: true, + }); + }, [settings.layer]); + + useEffect(() => { + console.log('configuration.node', settings.node); + }, [settings.node]); useEffect(() => { - if (configuration.node != undefined && !graphMetadata.nodes.labels.includes(configuration.node)) { + if (settings.node != undefined && !graphMetadata.nodes.labels.includes(settings.node)) { updateSettings({ node: undefined }); } - }, [graphMetadata.nodes.types, data, configuration]); + }, [graphMetadata.nodes.types, data, settings]); const dataLayer = useMemo(() => { - if (!layer || !configuration.node || !configuration.lat || !configuration.lon) return null; + if (!layer || !settings.node || !settings.lat || !settings.lon) return null; const coordinateLookup: { [id: string]: Coordinate } = data.nodes.reduce( (acc, node) => { - const latitude = node?.attributes?.[configuration.lat] as string | undefined; - const longitude = node?.attributes?.[configuration.lon] as string | undefined; + const latitude = settings.lat ? (node?.attributes?.[settings.lat] as string) : undefined; + const longitude = settings.lon ? (node?.attributes?.[settings.lon] as string) : undefined; if (!!latitude && !!longitude) { acc[node._id] = [parseFloat(longitude), parseFloat(latitude)]; @@ -109,6 +111,7 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V }, {} as { [id: string]: Coordinate }, ); + console.log('coordinateLookup', coordinateLookup); return new layer.type({ id: `${layer.id}`, @@ -121,7 +124,7 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V getNodeLocation: (id: string) => coordinateLookup[id], flyToBoundingBox: flyToBoundingBox, }); - }, [layer, data, selected, hoverObject, isSelecting, configuration.lat, configuration.lon, configuration.node]); + }, [layer, data, selected, hoverObject, isSelecting, settings.lat, settings.lon, settings.node]); const selectionLayer = useMemo( () => @@ -173,11 +176,11 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V ); }; -const MapComponent: VISComponentType = { +const MapComponent: VISComponentType<MapProps> = { displayName: 'MapVis', component: MapVis, - settings: MapSettings, - configuration: configuration, + settingsComponent: MapSettings, + settings: settings, }; export default MapComponent; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx index 3db2cb6d1..a53fd8beb 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx @@ -23,8 +23,9 @@ import { Actions, Interpolations } from 'pixi-actions'; import Color from 'color'; import { createColumn } from './ColumnGraphicsComponent'; import { ReorderingManager } from './ReorderingManager'; -import { VisualizationConfiguration } from '../../../common'; +import { VisualizationSettingsType } from '../../../common'; import { range, scaleLinear, scaleOrdinal, schemeCategory10 } from 'd3'; +import { MatrixVisProps } from '../matrixvis'; type Props = { // onClick: (node: NodeType, pos: IPointData) => void; @@ -34,7 +35,7 @@ type Props = { currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; graph?: GraphQueryResult; - configuration: VisualizationConfiguration; + settings: MatrixVisProps; }; const columnsContainer = new Container(); @@ -121,7 +122,7 @@ export const MatrixPixi = (props: Props) => { if (props.graph && ref.current && ref.current.children.length > 0) { setup(); } - }, [props.configuration]); + }, [props.settings]); // TODO implement search results // useEffect(() => { @@ -345,7 +346,7 @@ export const MatrixPixi = (props: Props) => { config.cellWidth = Math.max((size?.width || 1000) / props.graph.nodes.length, (size?.height || 1000) / props.graph.nodes.length); config.cellHeight = config.cellWidth; - setupVisualizationEncodingMapping(props.configuration); + setupVisualizationEncodingMapping(props.settings); setupColumns(props.graph.edges, columnOrder, rowOrder); setupColumnLegend(columnOrder); @@ -364,7 +365,7 @@ export const MatrixPixi = (props: Props) => { isSetup.current = true; }; - const setupVisualizationEncodingMapping = (configuration: VisualizationConfiguration) => { + const setupVisualizationEncodingMapping = (settings: MatrixVisProps) => { if (!props.graph) throw new Error('Graph is undefined; cannot setup matrix'); const visMapping = []; // TODO type @@ -376,7 +377,7 @@ export const MatrixPixi = (props: Props) => { const adjacenyScale = scaleLinear([colorNeutral, visualizationColors.GPSelected.colors[1][0]]); visMapping.push({ attribute: 'adjacency', - encoding: configuration.marks, + encoding: settings.marks, colorScale: adjacenyScale, renderFunction: function (i: number, color: ColorSource, gfxContext: Graphics) { gfxContext.beginFill(color, 1); diff --git a/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx b/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx index 1a961e293..05522042c 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx @@ -3,7 +3,6 @@ import { Meta } from '@storybook/react'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { big2ndChamberQueryResult, smallFlightsQueryResults, mockLargeQueryResults } from '../../../mock-data'; -import { VisualizationPanel } from '../../components/VisualizationPanel'; import { setNewGraphQueryResult, diff --git a/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx b/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx index c225b2994..3b9223034 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx @@ -3,7 +3,7 @@ import { useImmer } from 'use-immer'; import { GraphQueryResult } from '../../../data-access/store'; import { LinkType, NodeType } from './types'; import { MatrixPixi } from './components/MatrixPixi'; -import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; @@ -13,12 +13,12 @@ export interface MatrixVisProps { color: string; } -const configuration: MatrixVisProps = { +const settings: MatrixVisProps = { marks: 'rect', color: 'blue', }; -export const MatrixVis = React.memo(({ data, ml, configuration }: VisualizationPropTypes) => { +export const MatrixVis = React.memo(({ data, ml, settings }: VisualizationPropTypes<MatrixVisProps>) => { const ref = useRef<HTMLDivElement>(null); const [graph, setGraph] = useImmer<GraphQueryResult | undefined>(undefined); const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); @@ -33,47 +33,39 @@ export const MatrixVis = React.memo(({ data, ml, configuration }: VisualizationP return ( <> <div className="h-full w-full overflow-hidden" ref={ref}> - <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} configuration={configuration} /> + <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} settings={settings} /> </div> </> ); }); -const MatrixSettings = ({ - configuration, - graph, - updateSettings, -}: { - configuration: MatrixVisProps; - graph: GraphMetadata; - updateSettings: (val: any) => void; -}) => { +const MatrixSettings = ({ settings, updateSettings }: VisualizationSettingsPropTypes<MatrixVisProps>) => { return ( <SettingsContainer> <Input type="dropdown" label="Configure marks" - value={configuration.marks} + value={settings.marks} options={['rect', 'circle']} - onChange={(val) => updateSettings({ marks: val })} + onChange={(val) => updateSettings({ marks: val as string })} /> <Input type="dropdown" label="Color" - value={configuration.color} + value={settings.color} options={['blue', 'green']} - onChange={(val) => updateSettings({ color: val })} + onChange={(val) => updateSettings({ color: val as string })} /> </SettingsContainer> ); }; -export const MatrixVisComponent: VISComponentType = { +export const MatrixVisComponent: VISComponentType<MatrixVisProps> = { displayName: 'MatrixVis', component: MatrixVis, - settings: MatrixSettings, - configuration: configuration, + settingsComponent: MatrixSettings, + settings: settings, }; export default MatrixVisComponent; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 257e6c3e5..0b14261d0 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -7,15 +7,16 @@ import { NLPopup } from './NLPopup'; import { hslStringToHex, nodeColor } from './utils'; import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../../../../graph-layout'; import { MultiGraph } from 'graphology'; -import { VisualizationConfiguration } from '../../../common'; +import { VisualizationSettingsType } from '../../../common'; import { Viewport } from 'pixi-viewport'; +import { NodelinkVisProps } from '../nodelinkvis'; type Props = { onClick: (event?: { node: NodeType; pos: IPointData }) => void; // onHover: (data: { node: NodeType; pos: IPointData }) => void; // onUnHover: (data: { node: NodeType; pos: IPointData }) => void; highlightNodes: NodeType[]; - configuration: VisualizationConfiguration; + configuration: NodelinkVisProps; currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; graph?: GraphType; @@ -90,7 +91,7 @@ export const NLPixi = (props: Props) => { LAYOUT_ALGORITHM: (props.layoutAlgorithm as Layouts) || lastConfig.LAYOUT_ALGORITHM, }; }); - }, [props.layoutAlgorithm]); + }, [props.layoutAlgorithm, props.configuration]); const imperative = useRef<any>(null); diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx index 2bdf3c3c6..05406c258 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx @@ -8,13 +8,15 @@ import { Layouts } from '../../../graph-layout/types'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; -import { VISComponentType, VisualizationPropTypes } from '../../common'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; import { nodeColorHex } from './components/utils'; import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { IPointData } from 'pixi.js'; +import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; export interface NodelinkVisProps { + id: string; + name: string; layout: string; showPopUpOnHover: boolean; shapes: { @@ -31,7 +33,9 @@ export interface NodelinkVisProps { nodeList: string[]; } -const configuration: NodelinkVisProps = { +const settings: NodelinkVisProps = { + id: 'NodeLinkVis', + name: 'NodeLinkVis', layout: Layouts.FORCEATLAS2WEBWORKER as string, showPopUpOnHover: false, shapes: { @@ -45,7 +49,7 @@ const configuration: NodelinkVisProps = { nodeList: [], }; -export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration, handleSelect }: VisualizationPropTypes) => { +export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings, handleSelect }: VisualizationPropTypes<NodelinkVisProps>) => { const ref = useRef<HTMLDivElement>(null); const [graph, setGraph] = useImmer<GraphType | undefined>(undefined); const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); @@ -104,27 +108,19 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration, hand return ( <NLPixi graph={graph} - configuration={configuration} + configuration={settings} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} onClick={(event) => { onClickedNode(event, ml); }} - layoutAlgorithm={configuration.layout} - showPopupsOnHover={configuration.showPopUpOnHover} + layoutAlgorithm={settings.layout} + showPopupsOnHover={settings.showPopUpOnHover} /> ); }); -const NodelinkSettings = ({ - configuration, - graphMetadata, - updateSettings, -}: { - configuration: NodelinkVisProps; - graphMetadata: GraphMetadata; - updateSettings: (val: any) => void; -}) => { +const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<NodelinkVisProps>) => { useEffect(() => { if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) { updateSettings({ nodeList: graphMetadata.nodes.labels }); @@ -133,11 +129,11 @@ const NodelinkSettings = ({ return ( <SettingsContainer> - <div className="mb-4"> - <h1 className="text-sm font-bold">General</h1> - <div className="m-1 flex flex-col space-y-4"> - <h1>Nodes Labels:</h1> - {configuration.nodeList.map((item, index) => ( + <div className="mb-4 text-xs"> + <h1 className="font-bold">General</h1> + <div className="m-1 flex flex-col space-y-2 mb-2"> + <h4 className="font-semibold">Nodes Labels:</h4> + {settings.nodeList.map((item, index) => ( <div className="flex m-1 items-center" key={item}> <div className="w-3/4 mr-6"> <EntityPill title={item} /> @@ -151,36 +147,36 @@ const NodelinkSettings = ({ <Input type="dropdown" label="Layout" - value={configuration.layout} + value={settings.layout} options={Object.values(Layouts) as string[]} - onChange={(val) => updateSettings({ layout: val })} + onChange={(val) => updateSettings({ layout: val as string })} /> <Input type="boolean" label="Show pop-up on hover" - value={configuration.showPopUpOnHover} + value={settings.showPopUpOnHover} onChange={(val) => updateSettings({ showPopUpOnHover: val })} /> </div> <div className="mb-4"> - <h1 className="text-sm font-bold">Nodes</h1> + <h1 className="font-bold">Nodes</h1> <div> <span className="text-xs font-semibold">Shape</span> <Input type="boolean" label="Common shape?" - value={configuration.shapes.similar} - onChange={(val) => updateSettings({ shapes: { ...configuration.shapes, similar: val } })} + value={settings.shapes.similar} + onChange={(val) => updateSettings({ shapes: { ...settings.shapes, similar: val } })} /> - {configuration.shapes.similar ? ( + {settings.shapes.similar ? ( <Input type="dropdown" label="Shape" - value={configuration.shapes.shape} + value={settings.shapes.shape} options={['Circle', 'Square']} - onChange={(val) => updateSettings({ shapes: { ...configuration.shapes, shape: val } })} + onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })} /> ) : ( <span>Map shapes to labels (to be implemented)</span> @@ -193,20 +189,20 @@ const NodelinkSettings = ({ </div> <div> - <h1 className="text-sm font-bold">Edges</h1> + <h1 className="font-bold">Edges</h1> <div> <span className="text-xs font-semibold">Edge width</span> <Input type="boolean" label="Common width" - value={configuration.edges.width.similar} - onChange={(val) => updateSettings({ edges: { ...configuration.edges, width: { ...configuration.edges.width, similar: val } } })} + value={settings.edges.width.similar} + onChange={(val) => updateSettings({ edges: { ...settings.edges, width: { ...settings.edges.width, similar: val } } })} /> <Input type="slider" label="Width" - value={configuration.edges.width.width} - onChange={(val) => updateSettings({ edges: { ...configuration.edges, width: { ...configuration.edges.width, width: val } } })} + value={settings.edges.width.width} + onChange={(val) => updateSettings({ edges: { ...settings.edges, width: { ...settings.edges.width, width: val } } })} min={0.1} max={2} step={0.1} @@ -217,11 +213,11 @@ const NodelinkSettings = ({ ); }; -export const NodeLinkComponent: VISComponentType = { +export const NodeLinkComponent: VISComponentType<NodelinkVisProps> = { displayName: 'NodeLinkVis', component: NodeLinkVis, - settings: NodelinkSettings, - configuration: configuration, + settingsComponent: NodelinkSettings, + settings: settings, }; export default NodeLinkComponent; diff --git a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx index 9a848bd69..821eb6c80 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx @@ -11,7 +11,7 @@ import { select, selectAll } from 'd3'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; -import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; import { ArrowDropDown } from '@mui/icons-material'; @@ -32,7 +32,7 @@ export type PaohVisProps = { mergeData: boolean; }; -const configuration: PaohVisProps = { +const settings: PaohVisProps = { rowHeight: 20, rowNode: '', columnNode: '', @@ -47,9 +47,8 @@ const configuration: PaohVisProps = { const devEnv: string = 'dev_env'; //'dev_env','sb' -export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, updateSettings }: VisualizationPropTypes) => { +export const PaohVis = ({ data, graphMetadata, schema, settings, updateSettings }: VisualizationPropTypes<PaohVisProps>) => { // general - const configuration = conf as PaohVisProps; const [loading, setLoading] = useState(true); // row states @@ -129,15 +128,15 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda () => ({ rowHeight: 30, hyperEdgeRanges: 30, - rowsMaxPerPage: configuration.numRowsDisplay, - columnsMaxPerPage: configuration.numColumnsDisplay, + rowsMaxPerPage: settings.numRowsDisplay, + columnsMaxPerPage: settings.numColumnsDisplay, maxSizeTextColumns: 120, maxSizeTextRows: 120, maxSizeTextID: 70, marginText: 0.05, sizeIcons: 16, }), - [configuration], + [settings], ); // @@ -312,9 +311,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda })); // update rows - const sortedRowInformationSlicedFiltered = sortedRowInformationSliced.filter((row) => - configuration.attributeRowShow.includes(row.header), - ); + const sortedRowInformationSlicedFiltered = sortedRowInformationSliced.filter((row) => settings.attributeRowShow.includes(row.header)); setInformationRow(sortedRowInformationSlicedFiltered); setInformationRowAllData(sortedRowInformation); @@ -437,7 +434,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda // update rows const sortedColumnInformationSlicedFiltered = sortedColumnInformationSliced.filter((row) => - configuration.attributeColumnShow.includes(row.header), + settings.attributeColumnShow.includes(row.header), ); setInformationColumn(sortedColumnInformationSlicedFiltered); @@ -486,27 +483,27 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda useEffect(() => { if ( graphMetadata && - configuration.columnNode !== '' && - configuration.rowNode !== '' && - graphMetadata.nodes.types[configuration.rowNode] && - graphMetadata.nodes.types[configuration.columnNode] + settings.columnNode !== '' && + settings.rowNode !== '' && + graphMetadata.nodes.types[settings.rowNode] && + graphMetadata.nodes.types[settings.columnNode] ) { - const firstColumnLabels = Object.keys(graphMetadata.nodes.types[configuration.columnNode].attributes).slice(0, 2); - const firstRowLabels = Object.keys(graphMetadata.nodes.types[configuration.rowNode].attributes).slice(0, 2); + const firstColumnLabels = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes).slice(0, 2); + const firstRowLabels = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes).slice(0, 2); if (firstColumnLabels && firstRowLabels) { - if (configuration.attributeColumnShow.includes('_id')) { + if (settings.attributeColumnShow.includes('_id')) { updateSettings({ attributeColumnShow: [...firstColumnLabels, '# Connections'] }); } - if (configuration.attributeRowShow.includes('_id')) { + if (settings.attributeRowShow.includes('_id')) { updateSettings({ attributeRowShow: [...firstRowLabels, '# Connections'] }); } setTimeout(() => setLoading(false), 100); // wait for the settings to update first } } - }, [graphMetadata, configuration]); + }, [graphMetadata, settings]); useEffect(() => { - if (loading || configuration.rowNode === '' || configuration.columnNode === '') return; + if (loading || settings.rowNode === '' || settings.columnNode === '') return; // set new data // for dev env @@ -532,13 +529,13 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda } //const toNode = edgeSchema?.target as string; //devEnv - const columnNodeAttributes = Object.keys(graphMetadata.nodes.types[configuration.columnNode].attributes); - const newData = parseQueryResult(data, configuration as PaohVisProps, toNode, configuration.mergeData); + const columnNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes); + const newData = parseQueryResult(data, settings as PaohVisProps, toNode, settings.mergeData); // original data without slicing setNumRowsVisible(Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)); - const rowNodes = newData.nodes.filter((obj) => obj[devEnv === 'sb' ? '_id' : 'label'].includes(configuration.rowNode)); - const columnNodes = newData.nodes.filter((obj) => obj[devEnv === 'sb' ? '_id' : 'label'].includes(configuration.columnNode)); + const rowNodes = newData.nodes.filter((obj) => obj[devEnv === 'sb' ? '_id' : 'label'].includes(settings.rowNode)); + const columnNodes = newData.nodes.filter((obj) => obj[devEnv === 'sb' ? '_id' : 'label'].includes(settings.columnNode)); // to keep order of new attributes prevDisplayAttributesColumns.current = [...columnNodeAttributes]; @@ -582,7 +579,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda // columnInformation const informationColumnTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries( - graphMetadata.nodes.types[configuration.columnNode].attributes, + graphMetadata.nodes.types[settings.columnNode].attributes, ).map(([k, v]) => { const mappedData = columnNodes.map((node) => node.attributes[k]); return { @@ -634,7 +631,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda // select necessary variables to show const filteredInformationColumnTemporal = informationColumnTemporal.filter((row) => - configuration.attributeColumnShow.includes(row.header), + settings.attributeColumnShow.includes(row.header), ); // set data @@ -651,7 +648,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda // build const informationRowTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries( - graphMetadata.nodes.types[configuration.rowNode].attributes, + graphMetadata.nodes.types[settings.rowNode].attributes, ).map(([k, v]) => { const mappedData = rowNodes.map((node) => node.attributes[k]); @@ -693,7 +690,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda setIndicesRowsForColumnSort(originalIndices); // select necessary variables to show - const filteredInformationRowTemporal = informationRowTemporal.filter((row) => configuration.attributeRowShow.includes(row.header)); + const filteredInformationRowTemporal = informationRowTemporal.filter((row) => settings.attributeRowShow.includes(row.header)); // set data setInformationRow(filteredInformationRowTemporal); @@ -704,12 +701,12 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda setWidthTotalRowInformation(totalWidthRowInformation); } }, [ - configuration.rowNode, - configuration.columnNode, + settings.rowNode, + settings.columnNode, configPaohvis, - configuration.attributeRowShow, - configuration.attributeColumnShow, - configuration.mergeData, + settings.attributeRowShow, + settings.attributeColumnShow, + settings.mergeData, loading, ]); @@ -734,7 +731,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda let tableWidthWithExtraColumnLabelWidth = 0; dataModel.pageData.hyperEdgeRanges.forEach((hyperEdgeRange) => { - const columnWidth = 1 * configuration.rowHeight; + const columnWidth = 1 * settings.rowHeight; tableWidth += columnWidth; if (tableWidth > tableWidthWithExtraColumnLabelWidth) tableWidthWithExtraColumnLabelWidth = tableWidth; @@ -750,21 +747,21 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda }; }, [ dataModel, - configuration.rowHeight, + settings.rowHeight, widthTotalColumnInformation, widthTotalRowInformation, - configuration.attributeColumnShow, - configuration.attributeRowShow, + settings.attributeColumnShow, + settings.attributeRowShow, ]); const onWheel = (event: React.WheelEvent<SVGSVGElement>) => { if (event.deltaY !== 0) { - if (event.shiftKey) onPageChangeColumns(event.deltaY > 0 ? configuration.colJumpAmount : -configuration.colJumpAmount); - else onPageChangeRows(event.deltaY > 0 ? configuration.rowJumpAmount : -configuration.rowJumpAmount); + if (event.shiftKey) onPageChangeColumns(event.deltaY > 0 ? settings.colJumpAmount : -settings.colJumpAmount); + else onPageChangeRows(event.deltaY > 0 ? settings.rowJumpAmount : -settings.rowJumpAmount); } if (event.deltaX !== 0) { - onPageChangeColumns(event.deltaX > 0 ? configuration.colJumpAmount : -configuration.colJumpAmount); + onPageChangeColumns(event.deltaX > 0 ? settings.colJumpAmount : -settings.colJumpAmount); } }; @@ -796,9 +793,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda data: table.data.slice(startIndexColumn, endIndexColumn), })); - const filteredInformationColumnsTemporal = dataColumnsSortedSliced.filter((row) => - configuration.attributeColumnShow.includes(row.header), - ); + const filteredInformationColumnsTemporal = dataColumnsSortedSliced.filter((row) => settings.attributeColumnShow.includes(row.header)); setInformationColumn(filteredInformationColumnsTemporal); @@ -823,7 +818,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda data: table.data.slice(startIndexRow, endIndexRow), })); - const filteredInformationRowTemporal = dataRowsSortedSliced.filter((row) => configuration.attributeRowShow.includes(row.header)); + const filteredInformationRowTemporal = dataRowsSortedSliced.filter((row) => settings.attributeRowShow.includes(row.header)); setInformationRow(filteredInformationRowTemporal); @@ -846,13 +841,13 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda style={{ // width: computedSizesSvg.tableWidthWithExtraColumnLabelWidth, width: '100%', - height: computedSizesSvg.colWidth + (numRowsVisible + 1) * configuration.rowHeight, + height: computedSizesSvg.colWidth + (numRowsVisible + 1) * settings.rowHeight, }} onWheel={onWheel} > <RowLabels dataRows={informationRow} - rowHeight={configuration.rowHeight} + rowHeight={settings.rowHeight} yOffset={computedSizesSvg.colWidth} rowLabelColumnWidth={widthTotalRowInformation} classTopTextColumns={classTopTextColumns} @@ -867,7 +862,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda <HyperEdgeRangesBlock dataModel={dataModel} dataLinesHyperedges={lineHyperEdges} - rowHeight={configuration.rowHeight} + rowHeight={settings.rowHeight} yOffset={computedSizesSvg.colWidth} rowLabelColumnWidth={widthTotalRowInformation} classTopTextColumns={classTopTextColumns} @@ -887,15 +882,7 @@ export const PaohVis = ({ data, graphMetadata, schema, configuration: conf, upda ); }; -const PaohSettings = ({ - configuration, - graphMetadata, - updateSettings, -}: { - configuration: PaohVisProps; - graphMetadata: GraphMetadata; - updateSettings: (val: any) => void; -}) => { +const PaohSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<PaohVisProps>) => { const [areCollapsedAttrRows, setAreCollapsedAttrRows] = useState<boolean>(true); const [areCollapsedAttrColumns, setAreCollapsedAttrColumns] = useState<boolean>(true); @@ -908,8 +895,8 @@ const PaohSettings = ({ }; const rowNodeAttributes = useMemo(() => { - if (configuration.rowNode) { - const nodeType = graphMetadata.nodes.types[configuration.rowNode]; + if (settings.rowNode) { + const nodeType = graphMetadata.nodes.types[settings.rowNode]; if (nodeType && nodeType.attributes) { const attributes = Object.keys(nodeType.attributes); attributes.unshift('# Connections'); @@ -918,11 +905,11 @@ const PaohSettings = ({ } } return []; - }, [configuration.rowNode, graphMetadata]); + }, [settings.rowNode, graphMetadata]); const columnsNodeAttributes = useMemo(() => { - if (configuration.columnNode) { - const nodeType = graphMetadata.nodes.types[configuration.columnNode]; + if (settings.columnNode) { + const nodeType = graphMetadata.nodes.types[settings.columnNode]; if (nodeType && nodeType.attributes) { const attributes = Object.keys(nodeType.attributes); attributes.unshift('# Connections'); @@ -931,7 +918,7 @@ const PaohSettings = ({ } } return []; - }, [configuration.columnNode, graphMetadata]); + }, [settings.columnNode, graphMetadata]); useEffect(() => { if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 1) { @@ -946,10 +933,10 @@ const PaohSettings = ({ <span className="text-xs font-semibold">Node used in Row</span> <Input type="dropdown" - value={configuration.rowNode} + value={settings.rowNode} options={graphMetadata.nodes.labels} - onChange={(val) => updateSettings({ rowNode: val })} - overrideRender={<EntityPill title={configuration.rowNode} />} + onChange={(val) => updateSettings({ rowNode: val as string })} + overrideRender={<EntityPill title={settings.rowNode} />} /> </div> <Button @@ -966,7 +953,7 @@ const PaohSettings = ({ <div className=""> <Input type="checkbox" - value={configuration.attributeRowShow} + value={settings.attributeRowShow} options={rowNodeAttributes} onChange={(val: string[] | string) => { const updatedVal = Array.isArray(val) ? val : [val]; @@ -980,10 +967,10 @@ const PaohSettings = ({ <span className="text-xs font-semibold">Node used in Column</span> <Input type="dropdown" - value={configuration.columnNode} + value={settings.columnNode} options={graphMetadata.nodes.labels} - onChange={(val) => updateSettings({ columnNode: val })} - overrideRender={<EntityPill title={configuration.columnNode} />} + onChange={(val) => updateSettings({ columnNode: val as string })} + overrideRender={<EntityPill title={settings.columnNode} />} /> </div> @@ -1002,7 +989,7 @@ const PaohSettings = ({ <div className=""> <Input type="checkbox" - value={configuration.attributeColumnShow} + value={settings.attributeColumnShow} options={columnsNodeAttributes} onChange={(val: string[] | string) => { const updatedVal = Array.isArray(val) ? val : [val]; @@ -1012,43 +999,38 @@ const PaohSettings = ({ </div> )} - <Input type="number" label="Row height" value={configuration.rowHeight} onChange={(val) => updateSettings({ rowHeight: val })} /> + <Input type="number" label="Row height" value={settings.rowHeight} onChange={(val) => updateSettings({ rowHeight: val })} /> - <Input - type="number" - label="# Rows" - value={configuration.numRowsDisplay} - onChange={(val) => updateSettings({ numRowsDisplay: val })} - /> + <Input type="number" label="# Rows" value={settings.numRowsDisplay} onChange={(val) => updateSettings({ numRowsDisplay: val })} /> <Input type="number" label="# Columns" - value={configuration.numColumnsDisplay} + value={settings.numColumnsDisplay} onChange={(val) => updateSettings({ numColumnsDisplay: val })} /> <Input type="number" label="Row jump sensitivity" - value={configuration.rowJumpAmount} + value={settings.rowJumpAmount} onChange={(val) => updateSettings({ rowJumpAmount: val })} /> <Input type="number" label="Column jump sensitivity" - value={configuration.colJumpAmount} + value={settings.colJumpAmount} onChange={(val) => updateSettings({ colJumpAmount: val })} /> - <Input type="boolean" label="Merge Data" value={configuration.mergeData} onChange={(val) => updateSettings({ mergeData: val })} /> + <Input type="boolean" label="Merge Data" value={settings.mergeData} onChange={(val) => updateSettings({ mergeData: val })} /> </div> </SettingsContainer> ); }; -export const PaohVisComponent: VISComponentType = { +export const PaohVisComponent: VISComponentType<PaohVisProps> = { displayName: 'PaohVis', component: PaohVis, - settings: PaohSettings, - configuration: configuration, + settingsComponent: PaohSettings, + settings: settings, }; export default PaohVisComponent; diff --git a/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx b/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx index 7aa8e7549..3b0932a59 100644 --- a/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx +++ b/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx @@ -1,67 +1,59 @@ import React, { useEffect } from 'react'; import ReactJSONView from 'react-json-view'; -import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; export interface RawJSONVisProps { theme: string; - iconStyle: string; + iconStyle: 'circle' | 'triangle' | 'square' | undefined; } -const configuration: RawJSONVisProps = { +const settings: RawJSONVisProps = { theme: 'bright:inverted', iconStyle: 'circle', }; -export const RawJSONVis = React.memo(({ data, configuration }: VisualizationPropTypes) => { +export const RawJSONVis = React.memo(({ data, settings }: VisualizationPropTypes) => { return ( <ReactJSONView src={data} collapsed={1} quotesOnKeys={false} style={{ padding: '20px', flexGrow: 1 }} - theme={configuration.theme} - iconStyle={configuration.iconStyle} + theme={settings.theme || 'bright:inverted'} + iconStyle={settings.iconStyle} enableClipboard={true} /> ); }); -const RawJSONSettings = ({ - configuration, - graph, - updateSettings, -}: { - configuration: RawJSONVisProps; - graph: GraphMetadata; - updateSettings: (val: any) => void; -}) => { +const RawJSONSettings = ({ settings, updateSettings }: VisualizationSettingsPropTypes<RawJSONVisProps>) => { return ( <SettingsContainer> <Input type="dropdown" label="Select a theme" - value={configuration.theme} + value={settings.theme} options={['bright:inverted', 'monokai', 'ocean']} - onChange={(val) => updateSettings({ theme: val })} + onChange={(val) => updateSettings({ theme: val as any })} /> <Input type="dropdown" label="Icon style" - value={configuration.iconStyle} + value={settings.iconStyle} options={['circle', 'square', 'triangle']} - onChange={(val) => updateSettings({ iconStyle: val })} + onChange={(val) => updateSettings({ iconStyle: val as any })} /> </SettingsContainer> ); }; -export const RawJSONComponent: VISComponentType = { +export const RawJSONComponent: VISComponentType<RawJSONVisProps> = { displayName: 'RawJSONVis', component: RawJSONVis, - settings: RawJSONSettings, - configuration: configuration, + settingsComponent: RawJSONSettings, + settings: settings, }; export default RawJSONComponent; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx index cddb8a206..40e03c4ee 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx @@ -4,7 +4,7 @@ import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { visualizationColors } from 'config/src/colors'; -import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { findConnectionsNodes, getRegionData, setExtension, filterArray, getUniqueValues } from './components/utils'; import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; @@ -31,13 +31,13 @@ export type SemSubstrProps = { showColor: boolean; }; -const configuration: SemSubstrProps = { +const settings: SemSubstrProps = { showColor: true, }; const displayName = 'SemSubstrVis'; -export const VisSemanticSubstrates = ({ data, configuration }: VisualizationPropTypes) => { +export const VisSemanticSubstrates = ({ data }: VisualizationPropTypes<SemSubstrProps>) => { const nodes = data.nodes; const edges = data.edges; @@ -422,27 +422,19 @@ export const VisSemanticSubstrates = ({ data, configuration }: VisualizationProp ); }; -const SemSubstrSettings = ({ - configuration, - graph, - updateSettings, -}: { - configuration: SemSubstrProps; - graph: GraphMetadata; - updateSettings: (val: any) => void; -}) => { +const SemSubstrSettings = ({ settings, updateSettings }: VisualizationSettingsPropTypes<SemSubstrProps>) => { return ( <SettingsContainer> - <Input type="boolean" label="Show color" value={configuration.showColor} onChange={(val) => updateSettings({ showColor: val })} /> + <Input type="boolean" label="Show color" value={settings.showColor} onChange={(val) => updateSettings({ showColor: val })} /> </SettingsContainer> ); }; -export const SemSubstrVisComponent: VISComponentType = { +export const SemSubstrVisComponent: VISComponentType<SemSubstrProps> = { displayName: displayName, component: VisSemanticSubstrates, - settings: SemSubstrSettings, - configuration: configuration, + settingsComponent: SemSubstrSettings, + settings: settings, }; export default SemSubstrVisComponent; diff --git a/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx b/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx index 26488d4a2..8b7f81534 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx @@ -45,7 +45,7 @@ export const TestWithAirport = { args: { data: bigMockQueryResults, schema: simpleSchemaAirportRaw, - configuration: TableComponent.configuration, + configuration: TableComponent.settings, }, }; @@ -53,7 +53,7 @@ export const TestWithBig2ndChamber = { args: { data: big2ndChamberQueryResult, schema: big2ndChamberSchemaRaw, - configuration: TableComponent.configuration, + configuration: TableComponent.settings, }, }; @@ -61,7 +61,7 @@ export const TestWithTypesMock = { args: { data: typesMockQueryResults, schema: typesMockSchemaRaw, - configuration: TableComponent.configuration, + configuration: TableComponent.settings, }, }; diff --git a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx index d2070b3f0..f92d97beb 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx @@ -1,14 +1,15 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Table, AugmentedNodeAttributes } from './components/Table'; import { SchemaAttribute } from '../../../schema'; -import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; -import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { ArrowDropDown } from '@mui/icons-material'; export type TableProps = { + id: string; + name: string; showBarplot: boolean; itemsPerPage: number; displayAttributes: string[]; @@ -16,7 +17,9 @@ export type TableProps = { maxBarsCount: number; }; -const configuration: TableProps = { +const settings: TableProps = { + id: 'TableVis', + name: 'TableVis', itemsPerPage: 10, showBarplot: true, displayAttributes: [], @@ -24,19 +27,19 @@ const configuration: TableProps = { maxBarsCount: 10, }; -export const TableVis = ({ data, schema, configuration, updateSettings, graphMetadata }: VisualizationPropTypes) => { +export const TableVis = ({ data, schema, settings, updateSettings, graphMetadata }: VisualizationPropTypes<TableProps>) => { const ref = useRef<HTMLDivElement>(null); useEffect(() => { - if (!graphMetadata.nodes.labels.includes(configuration.displayEntity)) { + if (!graphMetadata.nodes.labels.includes(settings.displayEntity)) { updateSettings({ displayEntity: graphMetadata.nodes.labels[0] }); } - }, [graphMetadata.nodes.labels, data, configuration]); + }, [graphMetadata.nodes.labels, data, settings]); const attributesArray = useMemo<AugmentedNodeAttributes[]>( () => data.nodes - .filter((node) => node.label === configuration.displayEntity) + .filter((node) => node.label === settings.displayEntity) .map((node) => { const types: SchemaAttribute[] = schema.nodes.find((n) => n.key === node.label)?.attributes?.attributes ?? @@ -48,7 +51,7 @@ export const TableVis = ({ data, schema, configuration, updateSettings, graphMet type: Object.fromEntries(types.map((t) => [t.name, t.type])), }; }), - [data.nodes, configuration.displayEntity], + [data.nodes, settings.displayEntity], ); return ( @@ -56,26 +59,18 @@ export const TableVis = ({ data, schema, configuration, updateSettings, graphMet {attributesArray.length > 0 && ( <Table data={attributesArray} - itemsPerPage={configuration.itemsPerPage} - showBarPlot={configuration.showBarplot} - showAttributes={configuration.displayAttributes} - selectedEntity={configuration.displayEntity} - maxBarsCount={configuration.maxBarsCount} + itemsPerPage={settings.itemsPerPage} + showBarPlot={settings.showBarplot} + showAttributes={settings.displayAttributes} + selectedEntity={settings.displayEntity} + maxBarsCount={settings.maxBarsCount} /> )} </div> ); }; -const TableSettings = ({ - configuration, - graphMetadata, - updateSettings, -}: { - configuration: TableProps; - graphMetadata: GraphMetadata; - updateSettings: (val: any) => void; -}) => { +const TableSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<TableProps>) => { useEffect(() => { if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) { updateSettings({ displayEntity: graphMetadata.nodes.labels[0] }); @@ -87,14 +82,14 @@ const TableSettings = ({ setAreCollapsedAttr(!areCollapsedAttr); }; const selectedNodeAttributes = useMemo(() => { - if (configuration.displayEntity) { - const nodeType = graphMetadata.nodes.types[configuration.displayEntity]; + if (settings.displayEntity) { + const nodeType = graphMetadata.nodes.types[settings.displayEntity]; if (nodeType && nodeType.attributes) { return Object.keys(nodeType.attributes); } } return []; - }, [configuration.displayEntity, graphMetadata]); + }, [settings.displayEntity, graphMetadata]); useEffect(() => { if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) { @@ -107,27 +102,22 @@ const TableSettings = ({ <Input type="dropdown" label="Select entity" - value={configuration.displayEntity} - onChange={(val) => updateSettings({ displayEntity: val })} + value={settings.displayEntity} + onChange={(val) => updateSettings({ displayEntity: val as string })} options={graphMetadata.nodes.labels} /> - <Input - type="boolean" - label="Show barplot" - value={configuration.showBarplot} - onChange={(val) => updateSettings({ showBarplot: val })} - /> + <Input type="boolean" label="Show barplot" value={settings.showBarplot} onChange={(val) => updateSettings({ showBarplot: val })} /> <Input type="dropdown" label="Items per page" - value={configuration.itemsPerPage} - onChange={(val) => updateSettings({ itemsPerPage: val })} + value={settings.itemsPerPage} + onChange={(val) => updateSettings({ itemsPerPage: val as number })} options={[10, 25, 50, 100]} /> <Input type="number" label="Max Bars in Bar Plots" - value={configuration.maxBarsCount} + value={settings.maxBarsCount} onChange={(val) => updateSettings({ maxBarsCount: val })} /> <div> @@ -146,7 +136,7 @@ const TableSettings = ({ {!areCollapsedAttr && ( <Input type="checkbox" - value={configuration.displayAttributes} + value={settings.displayAttributes} options={selectedNodeAttributes} onChange={(val: string[] | string) => { const updatedVal = Array.isArray(val) ? val : [val]; @@ -160,11 +150,11 @@ const TableSettings = ({ ); }; -export const TableComponent: VISComponentType = { +export const TableComponent: VISComponentType<TableProps> = { displayName: 'TableVis', component: TableVis, - settings: TableSettings, - configuration: configuration, + settingsComponent: TableSettings, + settings: settings, }; export default TableComponent; -- GitLab