From 60ce4ec24dcbeca6d854efcda12e20e0000559d6 Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Wed, 5 Jun 2024 12:01:18 +0200 Subject: [PATCH] feat(inspector): inspector panel refactor (cherry picked from commit f307ae4a77ef00d9ca1f41cf1f685e321170458d) --- apps/web/src/app/App.tsx | 46 +++--- .../components/charts/Axis/axis.stories.tsx | 2 +- libs/shared/lib/components/forms/index.tsx | 51 ++++-- libs/shared/lib/components/inputs/index.tsx | 52 +++++-- .../lib/components/inputs/inputs.module.scss | 23 +++ .../components/inputs/inputs.module.scss.d.ts | 1 + libs/shared/lib/components/layout/Panel.tsx | 12 +- .../lib/components/layout/Resizable.tsx | 4 + libs/shared/lib/components/tabs/Tab.tsx | 29 ++++ libs/shared/lib/components/tabs/index.ts | 1 + libs/shared/lib/data-access/store/hooks.ts | 3 +- .../lib/data-access/store/interactionSlice.ts | 12 +- libs/shared/lib/index.ts | 1 + .../lib/inspector/ConnectionInspector.tsx | 26 ++++ libs/shared/lib/inspector/InspectorPanel.tsx | 76 +++++++++ libs/shared/lib/inspector/InspectorTab.tsx | 6 + libs/shared/lib/inspector/index.ts | 2 + .../lib/querybuilder/panel/QueryBuilder.tsx | 10 +- .../panel/querysidepanel/QuerySettings.tsx | 102 ++++++++++++ .../querysidepanel/querySettingsDialog.tsx | 145 ------------------ .../pills/pilldropdown/PillDropdown.tsx | 2 +- libs/shared/lib/schema/panel/Schema.tsx | 10 +- libs/shared/lib/schema/panel/SchemaDialog.tsx | 71 --------- .../lib/schema/panel/SchemaSettings.tsx | 49 ++++++ .../{panel.tsx => VisualizationPanel.tsx} | 13 +- libs/shared/lib/vis/components/bar.tsx | 13 +- .../config/ActiveVisualizationConfig.tsx | 44 +++--- .../vis/components/config/SelectionConfig.tsx | 9 +- .../lib/vis/components/config/index.tsx | 2 +- .../lib/vis/components/config/panel.tsx | 53 +------ libs/shared/lib/vis/index.ts | 2 +- .../matrixvis/matrix.stories.tsx | 2 +- 32 files changed, 506 insertions(+), 368 deletions(-) create mode 100644 libs/shared/lib/components/tabs/Tab.tsx create mode 100644 libs/shared/lib/components/tabs/index.ts create mode 100644 libs/shared/lib/inspector/ConnectionInspector.tsx create mode 100644 libs/shared/lib/inspector/InspectorPanel.tsx create mode 100644 libs/shared/lib/inspector/InspectorTab.tsx create mode 100644 libs/shared/lib/inspector/index.ts create mode 100644 libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx delete mode 100644 libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx delete mode 100644 libs/shared/lib/schema/panel/SchemaDialog.tsx create mode 100644 libs/shared/lib/schema/panel/SchemaSettings.tsx rename libs/shared/lib/vis/components/{panel.tsx => VisualizationPanel.tsx} (66%) diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx index bde42e867..cb7388cd4 100644 --- a/apps/web/src/app/App.tsx +++ b/apps/web/src/app/App.tsx @@ -20,8 +20,7 @@ import { URLParams, setParam } from '@graphpolaris/shared/lib/data-access/api/ur import { VisualizationPanel } from '@graphpolaris/shared/lib/vis'; import { QueryBuilder } from '@graphpolaris/shared/lib/querybuilder'; import { SideNavTab, Sidebar } from '@graphpolaris/shared/lib/sidebar'; -import { VisualizationManager } from '@graphpolaris/shared/lib/vis/manager'; -import { ConfigPanel } from '@graphpolaris/shared/lib/vis/components/config'; +import { InspectorPanel } from '@graphpolaris/shared/lib/inspector'; import { SearchBar } from '@graphpolaris/shared/lib/sidebar/search/SearchBar'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; @@ -36,7 +35,6 @@ export function App(props: App) { const session = useSessionCache(); const dispatch = useAppDispatch(); const queryBuilderSettings = useQuerybuilderSettings(); - const manager = VisualizationManager(); const schema = useSchemaGraph(); const runQuery = () => { @@ -58,7 +56,7 @@ export function App(props: App) { const [authCheck, setAuthCheck] = useState(false); const [tab, setTab] = useState<SideNavTab>('Schema'); - const [visFullSize, setVisFullSize] = useState<boolean>(false); + // const [visFullSize, setVisFullSize] = useState<boolean>(false); return ( <div className="h-screen w-screen overflow-clip"> @@ -79,30 +77,30 @@ export function App(props: App) { </aside> <main className="grow flex flex-row h-screen pt-12"> <Sidebar onTab={(tab) => setTab(tab)} tab={tab} /> - <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}> - {tab !== undefined ? ( - <div className="flex flex-col w-full h-full"> - {tab === 'Search' && <SearchBar onRemove={() => setTab(undefined)} />} - {tab === 'Schema' && <Schema auth={authCheck} onRemove={() => setTab(undefined)} />} - </div> - ) : null} + <Resizable divisorSize={3} horizontal={true} defaultProportion={0.85} maxProportion={0.85}> + <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}> + {tab !== undefined ? ( + <div className="flex flex-col w-full h-full"> + {tab === 'Search' && <SearchBar onRemove={() => setTab(undefined)} />} + {tab === 'Schema' && <Schema auth={authCheck} onRemove={() => setTab(undefined)} />} + </div> + ) : null} - <Resizable divisorSize={3} horizontal={false}> - <VisualizationPanel - manager={manager} - fullSize={() => { - setVisFullSize(!visFullSize); - tab === undefined && setTab('Schema'); - tab !== undefined && setTab(undefined); - }} - /> + <Resizable divisorSize={3} horizontal={false}> + <VisualizationPanel + fullSize={() => { + // setVisFullSize(!visFullSize); + // tab === undefined && setTab('Schema'); + // tab !== undefined && setTab(undefined); + }} + /> - <QueryBuilder onRunQuery={runQuery} /> + <QueryBuilder onRunQuery={runQuery} /> + </Resizable> </Resizable> + {/* <ConfigPanel /> */} + <InspectorPanel /> </Resizable> - <div className="info-panel flex h-full w-60 ml-[3px] shrink-0 overflow-auto bg-light border"> - <ConfigPanel manager={manager} /> - </div> </main> </div> </div> diff --git a/libs/shared/lib/components/charts/Axis/axis.stories.tsx b/libs/shared/lib/components/charts/Axis/axis.stories.tsx index 00c13d2fb..918d14c1d 100644 --- a/libs/shared/lib/components/charts/Axis/axis.stories.tsx +++ b/libs/shared/lib/components/charts/Axis/axis.stories.tsx @@ -9,7 +9,7 @@ const Component: Meta<AxisComponentProps> = { component: AxisComponent, decorators: [ (Story) => ( - <div className="w-full h-full flex flex-row justify-center"> + <div className="w-full h-full flex flex-row justify-center flex-grow"> <svg className="border border-secondary-300" width="300" height="200"> <Story /> </svg> diff --git a/libs/shared/lib/components/forms/index.tsx b/libs/shared/lib/components/forms/index.tsx index 643126cac..a4bb74327 100644 --- a/libs/shared/lib/components/forms/index.tsx +++ b/libs/shared/lib/components/forms/index.tsx @@ -79,27 +79,48 @@ export const FormBody = ({ {children} </form> ); -export const FormTitle = ({ children, title, onClose }: PropsWithChildren<{ title: string; onClose: () => void }>) => { +export const FormTitle = ({ children, title, onClose }: PropsWithChildren<{ title: string; onClose?: () => void }>) => { return ( <div className="card-title p-5 py-0 mt-2 flex w-full"> <h2 className="w-full">{title}</h2> - <Button rounded variant="ghost" iconComponent={<Close />} onClick={() => onClose()} /> + {onClose && <Button rounded variant="ghost" iconComponent={<Close />} onClick={() => onClose()} />} </div> ); }; export const FormHBar = () => <div className="divider m-0"></div>; export const FormControl = ({ children }: PropsWithChildren) => <div className="form-control px-5">{children}</div>; -export const FormActions = (props: { onClose: () => void }) => ( - <div className="grid grid-cols-2 px-5 gap-2 mb-2"> - <Button - type="secondary" - variant="outline" - label="Cancel" - onClick={(e) => { - e.preventDefault(); - props.onClose(); - }} - /> - <Button type="primary" label="Apply" onClick={() => {}} /> - </div> +export const FormActions = (props: { onClose?: () => void; onApply?: () => void }) => ( + <> + {props.onClose && ( + <div className="grid grid-cols-2 px-5 gap-2 mb-2"> + <Button + type="secondary" + variant="outline" + label="Cancel" + onClick={(e) => { + e.preventDefault(); + if (props.onClose) props.onClose(); + }} + /> + <Button + type="primary" + label="Apply" + onClick={() => { + if (props.onApply) props.onApply(); + }} + className="flex-grow" + /> + </div> + )} + {!props.onClose && ( + <Button + type="primary" + label="Apply" + onClick={() => { + if (props.onApply) props.onApply(); + }} + className="w-full" + /> + )} + </> ); diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index 02cf813b7..cb8c073e7 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -26,6 +26,7 @@ type TextProps = { visible?: boolean; disabled?: boolean; tooltip?: string; + inline?: boolean; info?: string; className?: string; validate?: (value: any) => boolean; @@ -35,6 +36,7 @@ type TextProps = { type NumberProps = { label: string; type: 'number'; + size?: 'xs' | 'sm' | 'md' | 'xl'; placeholder?: string; value: number; required?: boolean; @@ -43,8 +45,12 @@ type NumberProps = { disabled?: boolean; tooltip?: string; info?: string; + inline?: boolean; validate?: (value: any) => boolean; onChange?: (value: number) => void; + onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void; + max?: number; + min?: number; }; type CheckboxProps = { @@ -84,6 +90,7 @@ type DropdownProps = { tooltip?: string; onChange?: (value: string | number) => void; required?: boolean; + inline?: boolean; info?: string; disabled?: boolean; }; @@ -151,16 +158,21 @@ export const TextInput = ({ validate, disabled = false, onChange, + inline = false, tooltip, info, className, }: TextProps) => { const [isValid, setIsValid] = React.useState<boolean>(true); + if (!tooltip && inline) tooltip = label; return ( - <div data-tip={tooltip || null} className={'form-control w-full' + (tooltip ? ' tooltip' : '')}> + <div + data-tip={tooltip || null} + className={'form-control w-full' + (inline ? ' grid grid-cols-2 items-center' : '') + (tooltip ? ' tooltip' : '')} + > {label && ( - <label className="label"> + <label className="label p-0"> <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} > @@ -197,7 +209,9 @@ export const TextInput = ({ export const NumberInput = ({ label, placeholder, + size = 'md', value = 0, + inline = false, required = false, visible = true, errorText, @@ -206,13 +220,22 @@ export const NumberInput = ({ onChange, tooltip, info, + onKeyDown, + max, + min, }: NumberProps) => { const [isValid, setIsValid] = React.useState<boolean>(true); + if (!tooltip && inline) tooltip = label; return ( - <div data-tip={tooltip || null} className={'form-control w-full' + (tooltip ? ' tooltip' : '')}> - <label className="label"> - <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}> + <div + data-tip={tooltip || null} + className={styles['input'] + ' form-control w-full' + (inline ? ' grid grid-cols-2 items-center' : '') + (tooltip ? ' tooltip' : '')} + > + <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"}`} + > {label} </span> {required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>} @@ -221,7 +244,7 @@ export const NumberInput = ({ <input type="number" placeholder={placeholder} - className={`px-3 py-2 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-300 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ isValid ? '' : 'input-error' }`} value={value.toString()} @@ -235,6 +258,9 @@ export const NumberInput = ({ }} required={required} disabled={disabled} + onKeyDown={onKeyDown} + max={max} + min={min} /> </div> ); @@ -243,7 +269,7 @@ export const NumberInput = ({ export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioProps) => { return ( <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> - <label className="label"> + <label className="label p-0"> <span className="label-text">{label}</span> </label> {options.map((option, index) => ( @@ -270,7 +296,7 @@ export const CheckboxInput = ({ label, value, options, onChange, tooltip }: Chec return ( <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> {label && ( - <label className="label"> + <label className="label p-0"> <span className="label-text">{label}</span> </label> )} @@ -334,12 +360,14 @@ export const DropDownInput = ({ required = false, tooltip, size = 'sm', + inline = false, disabled = false, info, }: DropdownProps) => { const dropdownRef = React.useRef<HTMLDivElement>(null); const [isDropdownOpen, setIsDropdownOpen] = React.useState<boolean>(false); + if (!tooltip && inline) tooltip = label; React.useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -353,18 +381,18 @@ export const DropDownInput = ({ }, [isDropdownOpen]); return ( - <div data-tip={tooltip || null} className={'w-full' + (tooltip ? ' tooltip' : '')}> + <div data-tip={tooltip || null} className={'w-full' + (inline ? ' grid grid-cols-2 items-center' : '') + (tooltip ? ' tooltip' : '')}> {label && ( - <label className="label"> + <label className="label p-0"> <span - className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} + className={`text-sm text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} > {label} </span> {info && <Info tooltip={info} />} </label> )} - <DropdownContainer className="w-full" ref={dropdownRef}> + <DropdownContainer className="w-full right-0 left-auto" ref={dropdownRef}> <DropdownButton title={overrideRender || value} size={size} diff --git a/libs/shared/lib/components/inputs/inputs.module.scss b/libs/shared/lib/components/inputs/inputs.module.scss index 32f8e811d..f1733a091 100644 --- a/libs/shared/lib/components/inputs/inputs.module.scss +++ b/libs/shared/lib/components/inputs/inputs.module.scss @@ -21,3 +21,26 @@ } } } + +.input { + input[class~='xs'] { + @apply py-0; + @apply px-0; + } + input[class~='sm'] { + @apply py-1; + @apply px-1; + } + input[class~='md'] { + @apply py-2; + @apply px-3; + } + input[class~='md'] { + @apply py-2; + @apply px-3; + } + input[class~='xl'] { + @apply py-3; + @apply px-5; + } +} diff --git a/libs/shared/lib/components/inputs/inputs.module.scss.d.ts b/libs/shared/lib/components/inputs/inputs.module.scss.d.ts index c7907e61e..8c532fb8c 100644 --- a/libs/shared/lib/components/inputs/inputs.module.scss.d.ts +++ b/libs/shared/lib/components/inputs/inputs.module.scss.d.ts @@ -1,4 +1,5 @@ declare const classNames: { readonly slider: 'slider'; + readonly input: 'input'; }; export = classNames; diff --git a/libs/shared/lib/components/layout/Panel.tsx b/libs/shared/lib/components/layout/Panel.tsx index 191afbf50..34556eac9 100644 --- a/libs/shared/lib/components/layout/Panel.tsx +++ b/libs/shared/lib/components/layout/Panel.tsx @@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react'; import { ControlContainer } from '..'; export type Panel = { - title: string; - tooltips: React.ReactNode; + title: string | React.ReactNode; + tooltips?: React.ReactNode; children: React.ReactNode; }; @@ -14,9 +14,11 @@ export function Panel(props: Panel) { <div className="flex items-center"> <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">{props.title}</h1> </div> - <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> - <ControlContainer>{props.tooltips}</ControlContainer> - </div> + {props.tooltips && ( + <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> + <ControlContainer>{props.tooltips}</ControlContainer> + </div> + )} </div> {props.children} </div> diff --git a/libs/shared/lib/components/layout/Resizable.tsx b/libs/shared/lib/components/layout/Resizable.tsx index 9b88169e6..061620796 100644 --- a/libs/shared/lib/components/layout/Resizable.tsx +++ b/libs/shared/lib/components/layout/Resizable.tsx @@ -9,6 +9,7 @@ type Props = { defaultProportion?: number; classNameLeft?: string; classNameRight?: string; + maxProportion?: number; }; function convertRemToPixels(rem: number) { @@ -24,6 +25,7 @@ export const Resizable = ({ defaultProportion, classNameLeft, classNameRight, + maxProportion, ...props }: Props) => { const ref = useRef<HTMLDivElement>(null); @@ -81,11 +83,13 @@ export const Resizable = ({ if (horizontal) { const newFirstSize = Math.max(minSizeX, relativeX); + if (maxProportion && newFirstSize / rect.width > maxProportion) return; setFirstSize(newFirstSize); setSecondSize(Math.max(minSizeX, rect.width - relativeX)); setCurrentProportion(newFirstSize / rect.width); } else { const newFirstSize = Math.max(minSizeY, relativeY); + if (maxProportion && newFirstSize / rect.height > maxProportion) return; setFirstSize(newFirstSize); setSecondSize(Math.max(minSizeY, rect.height - relativeY)); setCurrentProportion(newFirstSize / rect.height); diff --git a/libs/shared/lib/components/tabs/Tab.tsx b/libs/shared/lib/components/tabs/Tab.tsx new file mode 100644 index 000000000..855da1029 --- /dev/null +++ b/libs/shared/lib/components/tabs/Tab.tsx @@ -0,0 +1,29 @@ +import React, { MouseEventHandler } from 'react'; + +export const Tabs = (props: { children: React.ReactNode }) => { + return ( + <div className="flex items-stretch divide-x divide-secondary-200 border-x border-secondary-200 overflow-x-auto -my-px"> + {props.children} + </div> + ); +}; + +export const Tab = ( + props: React.ButtonHTMLAttributes<HTMLDivElement> & { + active: boolean; + children: React.ReactNode; + text: string; + key?: string; + }, +) => { + return ( + <div + key={props.key} + className={`flex items-center pl-2 pr-1 gap-1 cursor-pointer relative border-secondary-200 before:content-[''] before:absolute before:left-0 before:bottom-0 before:h-[2px] before:w-full ${props.active ? 'before:bg-primary-500' : 'before:bg-transparent hover:before:bg-secondary-300 hover:bg-secondary-200'}`} + {...props} + > + <p className={`text-xs text-secondary-500 font-semibold ${props.active && 'text-secondary-950'}`}>{props.text}</p> + {props.children} + </div> + ); +}; diff --git a/libs/shared/lib/components/tabs/index.ts b/libs/shared/lib/components/tabs/index.ts new file mode 100644 index 000000000..2e2986cb5 --- /dev/null +++ b/libs/shared/lib/components/tabs/index.ts @@ -0,0 +1 @@ +export * from './Tab'; diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 247785872..3cf9e53fa 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -29,7 +29,7 @@ import { AllLayoutAlgorithms } from '../../graph-layout'; import { QueryGraphEdgeHandle, QueryMultiGraph } from '../../querybuilder'; import { SchemaGraph } from '../../schema'; import { GraphMetadata } from '../statistics'; -import { SelectionStateI, selectionState } from './interactionSlice'; +import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -73,3 +73,4 @@ export const useVisualization: () => VisState = () => useAppSelector(visualizati // Interaction Slices export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState); +export const useFocus: () => FocusStateI | undefined = () => useAppSelector(focusState); diff --git a/libs/shared/lib/data-access/store/interactionSlice.ts b/libs/shared/lib/data-access/store/interactionSlice.ts index 4a7434768..145cd7a32 100644 --- a/libs/shared/lib/data-access/store/interactionSlice.ts +++ b/libs/shared/lib/data-access/store/interactionSlice.ts @@ -17,15 +17,21 @@ export type SelectionStateI = content: Edge[]; }; +export type FocusStateI = { + focusType: 'schema' | 'query' | 'visualization'; +}; + // Define the initial state using that type export type InteractionsType = { hover?: HoverStateI; selection?: SelectionStateI; + focus?: FocusStateI; }; export const initialState: InteractionsType = { hover: undefined, selection: undefined, + focus: undefined, }; export const interactionSlice = createSlice({ @@ -50,12 +56,16 @@ export const interactionSlice = createSlice({ }; } }, + resultSetFocus: (state, action: PayloadAction<FocusStateI | undefined>) => { + state.focus = action.payload; + }, }, }); -export const { addHover, unSelect, resultSetSelection } = interactionSlice.actions; +export const { addHover, unSelect, resultSetSelection, resultSetFocus } = interactionSlice.actions; export const interactionState = (state: RootState) => state.interaction; export const selectionState = (state: RootState) => state.interaction.selection; +export const focusState = (state: RootState) => state.interaction.focus; export default interactionSlice.reducer; diff --git a/libs/shared/lib/index.ts b/libs/shared/lib/index.ts index 82d44d1ea..eae28d420 100644 --- a/libs/shared/lib/index.ts +++ b/libs/shared/lib/index.ts @@ -4,3 +4,4 @@ export * from './graph-layout'; export * from './querybuilder'; export * from './schema'; export * from './vis'; +export * from './inspector'; diff --git a/libs/shared/lib/inspector/ConnectionInspector.tsx b/libs/shared/lib/inspector/ConnectionInspector.tsx new file mode 100644 index 000000000..16988b41f --- /dev/null +++ b/libs/shared/lib/inspector/ConnectionInspector.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useSessionCache } from '../data-access'; + +export function ConnectionInspector() { + const session = useSessionCache(); + + return ( + <div> + {session && session.currentSaveState && ( + <div className="flex flex-col p-4 border-b"> + <span className="text-sm font-bold">Connection details</span> + <span className="text-xs font-semibold">Name</span> + <span className="text-xs">{session.saveStates[session.currentSaveState].name}</span> + <span className="text-xs font-semibold">Database</span> + <span className="text-xs">{session.saveStates[session.currentSaveState].db.internalDatabaseName}</span> + <span className="text-xs font-semibold">Protocol</span> + <span className="text-xs">{session.saveStates[session.currentSaveState].db.protocol}</span> + <span className="text-xs font-semibold">Hostname</span> + <span className="text-xs">{session.saveStates[session.currentSaveState].db.url}</span> + <span className="text-xs font-semibold">Port</span> + <span className="text-xs">{session.saveStates[session.currentSaveState].db.port}</span> + </div> + )} + </div> + ); +} diff --git a/libs/shared/lib/inspector/InspectorPanel.tsx b/libs/shared/lib/inspector/InspectorPanel.tsx new file mode 100644 index 000000000..703af6cee --- /dev/null +++ b/libs/shared/lib/inspector/InspectorPanel.tsx @@ -0,0 +1,76 @@ +import React, { useMemo } from 'react'; +import { Button, Panel } from '../components'; +import { useFocus, useSelection } 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 { SelectionConfig } from '../vis/components/config/SelectionConfig'; +import { SchemaDialog } from '../schema/panel/SchemaSettings'; +import { QuerySettings } from '../querybuilder/panel/querysidepanel/QuerySettings'; + +export function InspectorPanel(props: { children?: React.ReactNode }) { + const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; + const selection = useSelection(); + const focus = useFocus(); + const dispatch = useDispatch(); + + 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 />; + return null; + }, [focus, selection]); + + return ( + <Panel + title={ + <div className="flex flex-row gap-0.5 items-center align-middle"> + <Button variant="ghost" size="2xs" className="hover:underline p-0" onClick={() => dispatch(resultSetFocus(undefined))}> + GP + </Button> + {focus && ( + <> + <span className="pb-0.5">{'>'}</span> + <Button variant="ghost" size="2xs" className="hover:underline p-0"> + {focus.focusType} + </Button> + </> + )} + {selection && ( + <> + <span className="pb-0.5">{'>'}</span> + <Button variant="ghost" size="2xs" className="hover:underline p-0"> + Selection + </Button> + </> + )} + </div> + } + > + {inspector} + + <div className="flex flex-col w-full"> + {buildInfo === 'dev' && ( + <div className="mt-auto p-2 bg-light"> + <Button + type="primary" + variant="outline" + size="xs" + label="Report an issue" + onClick={() => + window.open( + 'https://app.asana.com/-/login?u=https%3A%2F%2Fform.asana.com%2F%3Fk%3D2QEC88Dl7ETs2wYYWjkMXg%26d%3D1206648675960041&error=01', + '_blank', + ) + } + className="block w-full" + /> + </div> + )} + </div> + </Panel> + ); +} diff --git a/libs/shared/lib/inspector/InspectorTab.tsx b/libs/shared/lib/inspector/InspectorTab.tsx new file mode 100644 index 000000000..b2f11a384 --- /dev/null +++ b/libs/shared/lib/inspector/InspectorTab.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { Button } from '../components'; + +export function InspectorTab(props: { children: React.ReactNode }) { + return <div className="flex flex-col w-full"></div>; +} diff --git a/libs/shared/lib/inspector/index.ts b/libs/shared/lib/inspector/index.ts new file mode 100644 index 000000000..83f3c61a6 --- /dev/null +++ b/libs/shared/lib/inspector/index.ts @@ -0,0 +1,2 @@ +export * from './InspectorPanel'; +export * from './InspectorTab'; diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx index 5c3b9a459..f5b3872b7 100644 --- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx @@ -38,10 +38,10 @@ import styles from './querybuilder.module.scss'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/queryBuilderLogicPillsPanel'; import { QueryBuilderRelatedNodesPanel } from './querysidepanel/queryBuilderRelatedNodesPanel'; import { QueryMLDialog } from './querysidepanel/queryMLDialog'; -import { QuerySettingsDialog } from './querysidepanel/querySettingsDialog'; import { ConnectingNodeDataI } from './utils/connectorDrop'; import { CameraAlt, Cached, Difference, ImportExport, Lightbulb, Settings, Fullscreen, Delete } from '@mui/icons-material'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; +import { resultSetFocus } from '../../data-access/store/interactionSlice'; export type QueryBuilderProps = { onRunQuery?: () => void; @@ -428,12 +428,15 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { }; useEffect(() => { - applyLayout(); + try { + applyLayout(); + } catch (e) { + console.error(e); + } }, [queryBuilderSettings]); return ( <div ref={reactFlowWrapper} className="h-full w-full flex flex-col"> - <QuerySettingsDialog open={toggleSettings === 'settings'} onClose={() => setToggleSettings(undefined)} /> <QueryMLDialog open={toggleSettings === 'ml'} onClose={() => setToggleSettings(undefined)} /> <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"> @@ -623,6 +626,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { nodeTypes={nodeTypes} edgeTypes={edgeTypes} connectionLineComponent={ConnectionDragLine} + onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'query' }))} // connectionMode={ConnectionMode.Loose} onInit={(reactFlowInstance) => { reactFlowInstanceRef.current = reactFlowInstance; diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx new file mode 100644 index 000000000..d3b36d1d9 --- /dev/null +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/QuerySettings.tsx @@ -0,0 +1,102 @@ +import { useEffect } from 'react'; +import React from 'react'; +import { useAppDispatch, useQuerybuilderSettings } from '../../../data-access'; +import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-access/store/querybuilderSlice'; +import { addWarning } from '../../../data-access/store/configSlice'; +import { FormActions, FormBody, FormCard, FormControl, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; +import { Layouts } from '@graphpolaris/shared/lib/graph-layout'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; + +export const QuerySettings = React.forwardRef<HTMLDivElement, {}>((props, ref) => { + const qb = useQuerybuilderSettings(); + const dispatch = useAppDispatch(); + const [state, setState] = React.useState<QueryBuilderSettings>(qb); + + useEffect(() => { + setState(qb); + }, [qb]); + + function submit() { + if (state.depth.min < 0) { + dispatch(addWarning('The minimum depth cannot be smaller than 0')); + } else if (state.depth.max > 99) { + dispatch(addWarning('The maximum depth cannot be larger than 99')); + } else if (state.depth.min > state.depth.max) { + dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); + } else { + dispatch(setQuerybuilderSettings(state)); + } + } + + return ( + <div className="flex flex-col w-full gap-2 px-4 py-2"> + <span className="text-xs font-bold">Query Settings</span> + <Input + type="boolean" + value={state.autocompleteRelation} + label="Autocomplete Edges" + tooltip="When enabled, if you drag a relationship to the query, the query builder will automatically add the entity nodes it is connected to." + onChange={(value: boolean) => { + setState({ ...state, autocompleteRelation: value as any }); + }} + /> + + <Input + type="number" + tooltip="The maximum number of results to return" + label="Limit" + inline + size="sm" + value={state.limit} + onChange={(e) => setState({ ...state, limit: e })} + /> + <Input + type="number" + label="Min Depth Default" + size="sm" + inline + value={state.depth.min} + onChange={(e) => setState({ ...state, depth: { min: e, max: state.depth.max } })} + placeholder="0" + min={0} + max={state.depth.max} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submit(); + } + }} + /> + <Input + type="number" + label="Max Depth Default" + size="sm" + inline + value={state.depth.max} + onChange={(e) => setState({ ...state, depth: { max: e, min: state.depth.min } })} + placeholder="0" + min={state.depth.min} + max={99} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submit(); + } + }} + /> + + <Input + type="dropdown" + inline + label="Default Layout" + value={state.layout} + onChange={(e) => setState({ ...state, layout: e as any })} + options={['manual', ...Object.entries(Layouts).map(([k, v]) => v)]} + /> + + <FormActions + onApply={() => { + submit(); + }} + /> + </div> + ); +}); diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx deleted file mode 100644 index 81e7567c1..000000000 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useEffect } from 'react'; -import { DialogProps } from '../../../components/layout/Dialog'; -import React from 'react'; -import { useAppDispatch, useQuerybuilderSettings } from '../../../data-access'; -import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-access/store/querybuilderSlice'; -import { addWarning } from '../../../data-access/store/configSlice'; -import { FormActions, FormBody, FormCard, FormControl, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; -import { Layouts } from '@graphpolaris/shared/lib/graph-layout'; -import { Input } from '@graphpolaris/shared/lib/components/inputs'; - -type QuerySettingsDialogProps = DialogProps; - -export const QuerySettingsDialog = React.forwardRef<HTMLDivElement, QuerySettingsDialogProps>((props, ref) => { - const qb = useQuerybuilderSettings(); - const dispatch = useAppDispatch(); - const [state, setState] = React.useState<QueryBuilderSettings>(qb); - - useEffect(() => { - setState(qb); - }, [qb, props.open]); - - function submit() { - if (state.depth.min < 0) { - dispatch(addWarning('The minimum depth cannot be smaller than 0')); - } else if (state.depth.max > 99) { - dispatch(addWarning('The maximum depth cannot be larger than 99')); - } else if (state.depth.min > state.depth.max) { - dispatch(addWarning('The minimum depth cannot be larger than the maximum depth')); - } else { - dispatch(setQuerybuilderSettings(state)); - props.onClose(); - } - } - - return ( - <> - {props.open && ( - <FormDiv hAnchor="right" ref={ref}> - <FormCard> - <FormBody - onSubmit={(e) => { - e.preventDefault(); - submit(); - }} - > - <FormTitle title="Query Settings" onClose={props.onClose} /> - <FormHBar /> - <FormControl> - <Input - type="boolean" - value={state.autocompleteRelation} - label="Autocomplete Edges" - tooltip="When enabled, if you drag a relationship to the query, the query builder will automatically add the entity nodes it is connected to." - onChange={(value: boolean) => { - setState({ ...state, autocompleteRelation: value as any }); - }} - /> - </FormControl> - - <FormHBar /> - <div className="form-control px-5"> - <label className="label"> - <span className="label-text">Limit - Max number of results</span> - </label> - <input - type="number" - className="input input-sm input-bordered" - placeholder="500" - value={state.limit} - onChange={(e) => setState({ ...state, limit: parseInt(e.target.value) })} - /> - </div> - <FormHBar /> - <div className="form-control px-5 flex flex-row gap-3"> - <div className=""> - <label className="label"> - <span className="label-text">Min Depth Default</span> - </label> - <input - type="number" - className="input input-sm input-bordered w-full" - placeholder="0" - min={0} - max={state.depth.max} - value={state.depth.min} - onChange={(e) => setState({ ...state, depth: { min: parseInt(e.target.value), max: state.depth.max } })} - onKeyDown={(e) => { - if (e.key === 'Enter') { - submit(); - } - }} - /> - </div> - <div className=""> - <label className="label"> - <span className="label-text">Max Depth Default</span> - </label> - <input - type="number" - className="input input-sm input-bordered w-full" - placeholder="0" - min={state.depth.min} - max={99} - value={state.depth.max} - onChange={(e) => setState({ ...state, depth: { max: parseInt(e.target.value), min: state.depth.min } })} - onKeyDown={(e) => { - if (e.key === 'Enter') { - submit(); - } - }} - /> - </div> - </div> - <FormHBar /> - <div className="form-control px-5 "> - <label className="label"> - <span className="label-text">Layout Type</span> - </label> - <select - className="select select-primary select-sm " - value={state.layout} - onChange={(e) => { - setState({ ...state, layout: e.target.value as any }); - }} - > - <option className="option" value={'manual'}> - Manual - </option> - {Object.entries(Layouts).map(([k, v]) => ( - <option className="option" value={v} key={v}> - {k} - </option> - ))} - </select> - </div> - <FormHBar /> - - <FormActions onClose={props.onClose} /> - </FormBody> - </FormCard> - </FormDiv> - )} - </> - ); -}); diff --git a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx index d96f52f5e..bb72abda7 100644 --- a/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx +++ b/libs/shared/lib/querybuilder/pills/pilldropdown/PillDropdown.tsx @@ -46,7 +46,7 @@ export const PillDropdown = (props: PillDropdownProps) => { return ( <div - className={'border-[1px] border-secondary-200 divide-y divide-secondary-200'} + className={'border-[1px] border-secondary-200 divide-y divide-secondary-200 !z-50'} onMouseEnter={(e) => { if (props.onMouseEnterDropdown) props.onMouseEnterDropdown(e); }} diff --git a/libs/shared/lib/schema/panel/Schema.tsx b/libs/shared/lib/schema/panel/Schema.tsx index 2e29acd26..bb9a9c409 100644 --- a/libs/shared/lib/schema/panel/Schema.tsx +++ b/libs/shared/lib/schema/panel/Schema.tsx @@ -9,12 +9,14 @@ import { NodeEdge } from '../pills/edges/node-edge'; import { SelfEdge } from '../pills/edges/self-edge'; import { SchemaEntityPill } from '../pills/nodes/entity/SchemaEntityPill'; import { SchemaRelationPill } from '../pills/nodes/relation/SchemaRelationPill'; -import { SchemaDialog } from './SchemaDialog'; +import { SchemaDialog } from './SchemaSettings'; import { ContentCopy, FitScreen, Fullscreen, KeyboardArrowDown, KeyboardArrowRight, Remove } from '@mui/icons-material'; import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout'; import { ConnectionLine, ConnectionDragLine } from '../../querybuilder'; import { schemaExpandRelation, schemaGraphology2Reactflow } from '../schema-utils'; import { Panel, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components'; +import { resultSetFocus } from '../../data-access/store/interactionSlice'; +import { useDispatch } from 'react-redux'; interface Props { content?: string; @@ -42,6 +44,7 @@ const edgeTypes = { export const Schema = (props: Props) => { const settings = useSchemaSettings(); const searchResults = useSearchResultSchema(); + const dispatch = useDispatch(); const [toggleSchemaSettings, setToggleSchemaSettings] = useState(false); const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]); const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]); @@ -173,6 +176,7 @@ export const Schema = (props: Props) => { connectionLineComponent={ConnectionDragLine} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} + onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'schema' }))} nodes={nodes} edges={edges} onInit={(reactFlowInstance) => { @@ -183,7 +187,7 @@ export const Schema = (props: Props) => { ></ReactFlow> </ReactFlowProvider> )} - <div> + {/* <div> <div className="w-full py-0 px-2 bg-secondary-50 cursor-pointer border-y flex items-center gap-1" onClick={() => setExpanded(!expanded)} @@ -201,7 +205,7 @@ export const Schema = (props: Props) => { <SchemaDialog open={toggleSchemaSettings} onClose={() => setToggleSchemaSettings(false)} /> </div> )} - </div> + </div> */} </div> </Panel> ); diff --git a/libs/shared/lib/schema/panel/SchemaDialog.tsx b/libs/shared/lib/schema/panel/SchemaDialog.tsx deleted file mode 100644 index 492e7be6e..000000000 --- a/libs/shared/lib/schema/panel/SchemaDialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Dialog, DialogProps } from '../../components/layout/Dialog'; -import React from 'react'; -import { useAppDispatch, useSchemaSettings } from '../../data-access'; -import { SchemaConnectionTypes, SchemaSettings, schemaConnectionTypeArray, setSchemaSettings } from '../../data-access/store/schemaSlice'; -import { FormActions, FormBody, FormCard, FormControl, FormHBar, FormTitle, FormDiv } from '../../components/forms'; -import { Layouts } from '../../graph-layout'; -import { Input } from '../../components/inputs'; - -export const SchemaDialog = (props: DialogProps) => { - const settings = useSchemaSettings(); - const dispatch = useAppDispatch(); - const [state, setState] = React.useState<SchemaSettings>(settings); - - useEffect(() => { - setState(settings); - }, [settings, props.open]); - - function submit() { - dispatch(setSchemaSettings(state)); - props.onClose(); - } - - return ( - <form - className="w-full" - onSubmit={(e) => { - e.preventDefault(); - submit(); - }} - > - <FormControl> - <Input - type="dropdown" - label="Type of Connection" - value={state.connectionType} - options={schemaConnectionTypeArray} - onChange={(value: string | number) => { - setState({ ...state, connectionType: value as SchemaConnectionTypes }); - }} - /> - </FormControl> - <FormHBar /> - <FormControl> - <Input - type="boolean" - value={state.animatedEdges} - label="Animated Edges" - onChange={(value: boolean) => { - setState({ ...state, animatedEdges: value as any }); - }} - /> - </FormControl> - <FormHBar /> - <FormControl> - <Input - type="dropdown" - label="Layout Type" - value={state.layoutName} - options={Object.values(Layouts)} - onChange={(value: string | number) => { - setState({ ...state, layoutName: value as any }); - }} - /> - </FormControl> - <FormHBar /> - - <FormActions onClose={props.onClose} /> - </form> - ); -}; diff --git a/libs/shared/lib/schema/panel/SchemaSettings.tsx b/libs/shared/lib/schema/panel/SchemaSettings.tsx new file mode 100644 index 000000000..270cb50d4 --- /dev/null +++ b/libs/shared/lib/schema/panel/SchemaSettings.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; +import { Dialog, DialogProps } from '../../components/layout/Dialog'; +import React from 'react'; +import { useAppDispatch, useSchemaSettings } from '../../data-access'; +import { SchemaConnectionTypes, SchemaSettings, schemaConnectionTypeArray, setSchemaSettings } from '../../data-access/store/schemaSlice'; +import { FormActions, FormBody, FormCard, FormControl, FormHBar, FormTitle, FormDiv } from '../../components/forms'; +import { Layouts } from '../../graph-layout'; +import { Input } from '../../components/inputs'; + +export const SchemaDialog = () => { + const settings = useSchemaSettings(); + const dispatch = useAppDispatch(); + + return ( + <div className="flex flex-col w-full gap-2 px-4 py-2"> + <span className="text-xs font-bold">Schema Settings</span> + <Input + type="boolean" + value={settings.animatedEdges} + label="Animated Edges" + onChange={(value: boolean) => { + dispatch(setSchemaSettings({ ...settings, animatedEdges: value as any })); + }} + /> + <Input + type="dropdown" + label="Type of Connection" + inline + size="sm" + value={settings.connectionType} + options={schemaConnectionTypeArray} + onChange={(value: string | number) => { + dispatch(setSchemaSettings({ ...settings, connectionType: value as SchemaConnectionTypes })); + }} + /> + <Input + type="dropdown" + label="Layout Type" + inline + size="sm" + value={settings.layoutName} + options={Object.values(Layouts)} + onChange={(value: string | number) => { + dispatch(setSchemaSettings({ ...settings, layoutName: value as any })); + }} + /> + </div> + ); +}; diff --git a/libs/shared/lib/vis/components/panel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx similarity index 66% rename from libs/shared/lib/vis/components/panel.tsx rename to libs/shared/lib/vis/components/VisualizationPanel.tsx index 38a37bab4..61bcfb78e 100644 --- a/libs/shared/lib/vis/components/panel.tsx +++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx @@ -1,12 +1,16 @@ import React, { useMemo } from 'react'; import { useGraphQueryResult, useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; import VisualizationBar from './bar'; -import { VisualizationManagerType } from '../manager'; +import { VisualizationManager, VisualizationManagerType } from '../manager'; import { Recommender, NoData, Querying } from '../views'; +import { resultSetFocus } from '../../data-access/store/interactionSlice'; +import { useDispatch } from 'react-redux'; -export const VisualizationPanel = ({ manager, fullSize }: { manager: VisualizationManagerType; fullSize: () => void }) => { +export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { + const manager = VisualizationManager(); const query = useQuerybuilderGraph(); const graphQueryResult = useGraphQueryResult(); + const dispatch = useDispatch(); const renderContent = useMemo(() => { if (graphQueryResult.queryingBackend) { @@ -20,7 +24,10 @@ export const VisualizationPanel = ({ manager, fullSize }: { manager: Visualizati }, [graphQueryResult, manager]); return ( - <div className="vis-panel h-full w-full flex flex-col border bg-light"> + <div + className="vis-panel h-full w-full flex flex-col border bg-light" + onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'visualization' }))} + > <VisualizationBar manager={manager} fullSize={fullSize} /> <div className="grow overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}> {renderContent} diff --git a/libs/shared/lib/vis/components/bar.tsx b/libs/shared/lib/vis/components/bar.tsx index 269d25f57..76cca2479 100644 --- a/libs/shared/lib/vis/components/bar.tsx +++ b/libs/shared/lib/vis/components/bar.tsx @@ -6,6 +6,7 @@ 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'; type Props = { manager: VisualizationManagerType; @@ -64,20 +65,20 @@ export default function VisualizationBar({ manager, fullSize }: Props) { </DropdownMenu.Portal> </DropdownMenu.Root> </div> - <div className="flex items-stretch divide-x divide-secondary-200 border-x border-secondary-200 overflow-x-auto -my-px"> + <Tabs> {manager.tabs.map((visId: string) => { const isActive = manager.activeVisualization === visId; return ( - <div + <Tab key={visId} - className={`flex items-center pl-2 pr-1 gap-1 cursor-pointer relative border-secondary-200 before:content-[''] before:absolute before:left-0 before:bottom-0 before:h-[2px] before:w-full ${isActive && 'before:bg-primary-500'} ${!isActive && 'before:bg-transparent hover:before:bg-secondary-300 hover:bg-secondary-200'}`} + active={isActive} + text={visId} onClick={() => manager.changeActive(visId)} onDragStart={(e) => handleDragStart(e, visId)} onDragOver={(e) => handleDragOver(e)} onDrop={(e) => handleDrop(e, visId)} draggable > - <p className={`text-xs text-secondary-500 font-semibold ${isActive && 'text-secondary-950'}`}>{visId}</p> <Button type="secondary" variant="ghost" @@ -89,10 +90,10 @@ export default function VisualizationBar({ manager, fullSize }: Props) { manager.deleteVisualization(visId); }} /> - </div> + </Tab> ); })} - </div> + </Tabs> <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> <ControlContainer> <TooltipProvider delayDuration={0}> diff --git a/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx b/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx index fa36ceed1..88ed5de60 100644 --- a/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx +++ b/libs/shared/lib/vis/components/config/ActiveVisualizationConfig.tsx @@ -1,5 +1,5 @@ import { Delete } from '@mui/icons-material'; -import { Button, Input } from '../../..'; +import { Button, Input, Panel } from '../../..'; import { VisualizationManagerType, VISUALIZATION_TYPES } from '../../manager'; import { SettingsHeader } from './components'; @@ -10,30 +10,28 @@ type Props = { export const ActiveVisualizationConfig = ({ manager }: Props) => { return ( <> - <div className="border-b py-2"> - <div className="flex justify-between items-center px-4 py-2"> - <span className="text-xs font-bold">Visualization</span> - <Button - type="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 rouded text-xs w-36" value={manager.activeVisualization} onChange={() => {}} /> + <div className="flex justify-between items-center px-4 py-2"> + <span className="text-xs font-bold">Visualization</span> + <Button + type="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" /> diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx index d790d651f..08dcadd77 100644 --- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx +++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx @@ -1,13 +1,16 @@ import { SelectionStateI, unSelect } from '@graphpolaris/shared/lib/data-access/store/interactionSlice'; import { Delete } from '@mui/icons-material'; import { useDispatch } from 'react-redux'; -import { Button, EntityPill } from '../../..'; +import { Button, EntityPill, useSelection } from '../../..'; import { VISUALIZATION_TYPES } from '../../manager'; import { SettingsHeader } from './components'; -export const SelectionConfig = (props: { selection: SelectionStateI }) => { +export const SelectionConfig = () => { + const selection = useSelection(); const dispatch = useDispatch(); + if (!selection) return null; + return ( <div className="border-b py-2"> <div className="flex justify-between items-center px-4 py-2"> @@ -22,7 +25,7 @@ export const SelectionConfig = (props: { selection: SelectionStateI }) => { }} /> </div> - {props.selection.content.map((item, index) => ( + {selection.content.map((item, index) => ( <> <div key={index + 'id'} className="flex justify-between items-center px-4 py-1 gap-1"> <span className="text-xs font-normal">ID</span> diff --git a/libs/shared/lib/vis/components/config/index.tsx b/libs/shared/lib/vis/components/config/index.tsx index 2f98579c7..4bf7db1e4 100644 --- a/libs/shared/lib/vis/components/config/index.tsx +++ b/libs/shared/lib/vis/components/config/index.tsx @@ -1,2 +1,2 @@ -export { ConfigPanel } from './panel'; +export { VisualizationConfigPanel as ConfigPanel } from './panel'; 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 index 5aae132cd..4f0bd9e03 100644 --- a/libs/shared/lib/vis/components/config/panel.tsx +++ b/libs/shared/lib/vis/components/config/panel.tsx @@ -1,61 +1,18 @@ import React from 'react'; import { Button } from '../../../components'; -import { VisualizationManagerType } from '../../manager'; +import { VisualizationManager, VisualizationManagerType } from '../../manager'; import { useSelection, useSessionCache } from '../../../data-access'; import { SelectionConfig } from './SelectionConfig'; import { ActiveVisualizationConfig } from './ActiveVisualizationConfig'; -type Props = { - manager: VisualizationManagerType; -}; +type Props = {}; -export function ConfigPanel({ manager }: Props) { - const session = useSessionCache(); - const selection = useSelection(); - - const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; +export function VisualizationConfigPanel({}: Props) { + const manager = VisualizationManager(); return ( <div className="flex flex-col w-full"> - {!!selection && <SelectionConfig selection={selection} />} - {!selection && manager.activeVisualization && <ActiveVisualizationConfig manager={manager} />} - {!selection && !manager.activeVisualization && ( - <div> - {session && session.currentSaveState && ( - <div className="flex flex-col p-4 border-b"> - <span className="text-sm font-bold">Connection details</span> - <span className="text-xs font-semibold">Name</span> - <span className="text-xs">{session.saveStates[session.currentSaveState].name}</span> - <span className="text-xs font-semibold">Database</span> - <span className="text-xs">{session.saveStates[session.currentSaveState].db.internalDatabaseName}</span> - <span className="text-xs font-semibold">Protocol</span> - <span className="text-xs">{session.saveStates[session.currentSaveState].db.protocol}</span> - <span className="text-xs font-semibold">Hostname</span> - <span className="text-xs">{session.saveStates[session.currentSaveState].db.url}</span> - <span className="text-xs font-semibold">Port</span> - <span className="text-xs">{session.saveStates[session.currentSaveState].db.port}</span> - </div> - )} - </div> - )} - - {buildInfo === 'dev' && ( - <div className="mt-auto p-2 bg-light"> - <Button - type="primary" - variant="outline" - size="xs" - label="Report an issue" - onClick={() => - window.open( - 'https://app.asana.com/-/login?u=https%3A%2F%2Fform.asana.com%2F%3Fk%3D2QEC88Dl7ETs2wYYWjkMXg%26d%3D1206648675960041&error=01', - '_blank', - ) - } - className="block w-full" - /> - </div> - )} + <ActiveVisualizationConfig manager={manager}></ActiveVisualizationConfig> </div> ); } diff --git a/libs/shared/lib/vis/index.ts b/libs/shared/lib/vis/index.ts index ac473812c..b08eef1b8 100644 --- a/libs/shared/lib/vis/index.ts +++ b/libs/shared/lib/vis/index.ts @@ -1 +1 @@ -export * from './components/panel'; +export * from './components/VisualizationPanel'; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx b/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx index 5fdecb241..1a961e293 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx @@ -3,7 +3,7 @@ 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/panel'; +import { VisualizationPanel } from '../../components/VisualizationPanel'; import { setNewGraphQueryResult, -- GitLab