From 89b8cee87d6de58fb02c8dca4eb266ea4f3fbd0b Mon Sep 17 00:00:00 2001 From: Leonardo Christino <leomilho@gmail.com> Date: Tue, 23 Apr 2024 16:50:19 +0200 Subject: [PATCH] fix: schema tooltips working and with copy --- apps/web/src/app/{app.tsx => App.tsx} | 54 +---- apps/web/src/app/app.spec.tsx | 2 +- apps/web/src/app/app.stories.tsx | 2 +- .../DatabaseManagement/forms/settings.tsx | 2 +- apps/web/src/main.tsx | 2 +- apps/web/tsconfig.json | 2 +- libs/shared/lib/components/index.ts | 3 +- .../lib/components/{ => layout}/Dialog.tsx | 0 libs/shared/lib/components/layout/Panel.tsx | 26 +++ .../lib/components/{ => layout}/Resizable.tsx | 0 libs/shared/lib/components/layout/index.ts | 3 + .../lib/querybuilder/panel/querybuilder.tsx | 2 +- .../panel/querysidepanel/queryMLDialog.tsx | 2 +- .../querysidepanel/querySettingsDialog.tsx | 2 +- .../schema/panel/{schema.tsx => Schema.tsx} | 139 +++++++---- .../{schemaDialog.tsx => SchemaDialog.tsx} | 2 +- libs/shared/lib/schema/panel/index.ts | 2 +- .../lib/schema/panel/schema.stories.tsx | 2 +- libs/shared/lib/sidebar/index.tsx | 6 +- libs/shared/lib/sidebar/search/SearchBar.tsx | 216 ++++++++++++++++++ libs/shared/lib/sidebar/search/searchbar.tsx | 182 --------------- 21 files changed, 360 insertions(+), 291 deletions(-) rename apps/web/src/app/{app.tsx => App.tsx} (58%) rename libs/shared/lib/components/{ => layout}/Dialog.tsx (100%) create mode 100644 libs/shared/lib/components/layout/Panel.tsx rename libs/shared/lib/components/{ => layout}/Resizable.tsx (100%) create mode 100644 libs/shared/lib/components/layout/index.ts rename libs/shared/lib/schema/panel/{schema.tsx => Schema.tsx} (53%) rename libs/shared/lib/schema/panel/{schemaDialog.tsx => SchemaDialog.tsx} (96%) create mode 100644 libs/shared/lib/sidebar/search/SearchBar.tsx delete mode 100644 libs/shared/lib/sidebar/search/searchbar.tsx diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/App.tsx similarity index 58% rename from apps/web/src/app/app.tsx rename to apps/web/src/app/App.tsx index 15401c5fd..23d49ba3c 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/App.tsx @@ -5,12 +5,13 @@ import { useML, useQuerybuilderGraph, useQuerybuilderSettings, + useSchemaGraph, useSessionCache, } from '@graphpolaris/shared/lib/data-access'; import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; import { Query2BackendQuery, QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder'; import { Navbar } from '../components/navbar/navbar'; -import { Resizable } from '@graphpolaris/shared/lib/components/Resizable'; +import { Resizable } from '@graphpolaris/shared/lib/components/layout'; import { DashboardAlerts } from '@graphpolaris/shared/lib/data-access/authorization/dashboardAlerts'; import { EventBus } from '@graphpolaris/shared/lib/data-access/api/eventBus'; import { Onboarding } from '../components/onboarding/onboarding'; @@ -21,11 +22,8 @@ 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 { Tooltip, TooltipTrigger, Button, TooltipContent, TooltipProvider } from '@graphpolaris/shared'; -import { ControlContainer } from '@graphpolaris/shared/lib/components/controls'; -import { Searchbar } from '@graphpolaris/shared/lib/sidebar/search/searchbar'; +import { SearchBar } from '@graphpolaris/shared/lib/sidebar/search/SearchBar'; import { Schema } from '@graphpolaris/shared/lib/schema/panel'; -import { Fullscreen, FitScreen, Remove, Schema as SchemaIcon, Search as SearchIcon } from '@mui/icons-material'; export type App = { load?: string; @@ -39,6 +37,7 @@ export function App(props: App) { const dispatch = useAppDispatch(); const queryBuilderSettings = useQuerybuilderSettings(); const manager = VisualizationManager(); + const schema = useSchemaGraph(); const runQuery = () => { if (session?.currentSaveState && query) { @@ -79,51 +78,12 @@ export function App(props: App) { <Navbar /> </aside> <main className="grow flex flex-row h-screen pt-12"> - <Sidebar onTab={(tab) => setTab(tab)} /> + <Sidebar onTab={(tab) => setTab(tab)} tab={tab} /> <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}> {tab !== undefined ? ( <div className="flex flex-col border w-full h-full bg-light"> - <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">{tab}</h1> - </div> - <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> - <ControlContainer> - <TooltipProvider delayDuration={100}> - <Tooltip> - <TooltipTrigger asChild> - <Button type="secondary" variant="ghost" size="xs" iconComponent={<Remove />} onClick={() => {}} /> - </TooltipTrigger> - <TooltipContent side={'top'}> - <p>Hide</p> - </TooltipContent> - </Tooltip> - {tab === 'Schema' && ( - <Tooltip> - <TooltipTrigger asChild> - <Button type="secondary" variant="ghost" size="xs" iconComponent={<FitScreen />} onClick={() => {}} /> - </TooltipTrigger> - <TooltipContent side={'top'}> - <p>Fit to screen</p> - </TooltipContent> - </Tooltip> - )} - {tab === 'Search' && ( - <Tooltip> - <TooltipTrigger asChild> - <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={() => {}} /> - </TooltipTrigger> - <TooltipContent side={'top'}> - <p>Mock icon</p> - </TooltipContent> - </Tooltip> - )} - </TooltipProvider> - </ControlContainer> - </div> - </div> - {tab === 'Search' && <Searchbar />} - {tab === 'Schema' && <Schema auth={authCheck} />} + {tab === 'Search' && <SearchBar onRemove={() => setTab(undefined)} />} + {tab === 'Schema' && <Schema auth={authCheck} onRemove={() => setTab(undefined)} />} </div> ) : null} diff --git a/apps/web/src/app/app.spec.tsx b/apps/web/src/app/app.spec.tsx index 5bb3582e5..45f220412 100644 --- a/apps/web/src/app/app.spec.tsx +++ b/apps/web/src/app/app.spec.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import { assert, describe, expect, it, test } from 'vitest'; -import App from './app'; +import App from './App'; describe('App', () => { it('should render successfully', () => { diff --git a/apps/web/src/app/app.stories.tsx b/apps/web/src/app/app.stories.tsx index 74396173e..13e418060 100644 --- a/apps/web/src/app/app.stories.tsx +++ b/apps/web/src/app/app.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { App } from './app'; +import { App } from './App'; import { Provider } from 'react-redux'; import { store } from '@graphpolaris/shared/lib/data-access/store'; import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index 233899f09..f9bb74087 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -8,7 +8,7 @@ import { useAuthorizationCache, } from '@graphpolaris/shared/lib/data-access'; import { ErrorOutline } from '@mui/icons-material'; -import { Dialog } from '@graphpolaris/shared/lib/components/Dialog'; +import { Dialog } from '@graphpolaris/shared/lib/components/layout'; import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { useImmer } from 'use-immer'; import { addSaveState, testedSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 8c0776767..11ca09fff 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { store } from '@graphpolaris/shared/lib/data-access/store'; -import App from './app/app'; +import App from './app/App'; import { createRoot } from 'react-dom/client'; import './main.css'; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 5b58d4e9d..493b19469 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -45,7 +45,7 @@ "postcss.config.js", // excludes PostCSS configuration file "tsconfig.tsbuildinfo" // excludes TypeScript build info file ], - "include": ["vite.config.ts", "src/**/*"], + "include": ["vite.config.ts", "src/**/*", "../../libs/shared/lib/components/layout/Panel.tsx"], "files": ["vite.config.ts"], "references": [] } diff --git a/libs/shared/lib/components/index.ts b/libs/shared/lib/components/index.ts index 46862ade6..3a41c2845 100644 --- a/libs/shared/lib/components/index.ts +++ b/libs/shared/lib/components/index.ts @@ -10,9 +10,8 @@ export * from './info'; export * from './inputs'; export * from './pagination'; export * from './tooltip'; -export * from './Dialog'; export * from './Legend'; export * from './LoadingSpinner'; export * from './Popup'; -export * from './Resizable'; +export * from './layout'; export * from './pills'; diff --git a/libs/shared/lib/components/Dialog.tsx b/libs/shared/lib/components/layout/Dialog.tsx similarity index 100% rename from libs/shared/lib/components/Dialog.tsx rename to libs/shared/lib/components/layout/Dialog.tsx diff --git a/libs/shared/lib/components/layout/Panel.tsx b/libs/shared/lib/components/layout/Panel.tsx new file mode 100644 index 000000000..ad77898ff --- /dev/null +++ b/libs/shared/lib/components/layout/Panel.tsx @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from 'react'; +import { ControlContainer } from '../controls'; + +export type Panel = { + title: string; + tooltips: React.ReactNode; + children: React.ReactNode; +}; + +export function Panel(props: Panel) { + return ( + <div className="flex flex-col border w-full h-full bg-light"> + <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">{props.title}</h1> + </div> + <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> + <ControlContainer>{props.tooltips}</ControlContainer> + </div> + </div> + {props.children} + </div> + ); +} + +export default Panel; diff --git a/libs/shared/lib/components/Resizable.tsx b/libs/shared/lib/components/layout/Resizable.tsx similarity index 100% rename from libs/shared/lib/components/Resizable.tsx rename to libs/shared/lib/components/layout/Resizable.tsx diff --git a/libs/shared/lib/components/layout/index.ts b/libs/shared/lib/components/layout/index.ts new file mode 100644 index 000000000..93c40e523 --- /dev/null +++ b/libs/shared/lib/components/layout/index.ts @@ -0,0 +1,3 @@ +export * from './Dialog'; +export * from './Panel'; +export * from './Resizable'; diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index e8224d3cc..3e118b5dc 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -24,7 +24,7 @@ import ReactFlow, { isNode, useReactFlow, } from 'reactflow'; -import { Dialog } from '../../components/Dialog'; +import { Dialog } from '../../components/layout/Dialog'; import { Button } from '../../components/buttons'; import { ControlContainer } from '../../components/controls'; import { addError } from '../../data-access/store/configSlice'; diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx index 0c409aa29..ff724e1ca 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { DialogProps } from '@graphpolaris/shared/lib/components/Dialog'; +import { DialogProps } from '@graphpolaris/shared/lib/components/layout'; import { useAppDispatch, useML } from '@graphpolaris/shared/lib/data-access'; import { setCentralityEnabled, diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx index b5103ac70..81e7567c1 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { DialogProps } from '../../../components/Dialog'; +import { DialogProps } from '../../../components/layout/Dialog'; import React from 'react'; import { useAppDispatch, useQuerybuilderSettings } from '../../../data-access'; import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-access/store/querybuilderSlice'; diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/Schema.tsx similarity index 53% rename from libs/shared/lib/schema/panel/schema.tsx rename to libs/shared/lib/schema/panel/Schema.tsx index 04fe939b2..2e29acd26 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/Schema.tsx @@ -9,15 +9,17 @@ 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 { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; +import { SchemaDialog } from './SchemaDialog'; +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'; interface Props { content?: string; auth?: boolean; + onRemove?: () => void; } const onInit = (reactFlowInstance: ReactFlowInstance) => { @@ -108,50 +110,99 @@ export const Schema = (props: Props) => { }, [searchResults]); return ( - <div className="schema-panel w-full h-full flex flex-col justify-between" ref={reactFlowRef}> - {nodes.length === 0 ? ( - <p className="m-3 text-xl font-bold">No Elements</p> - ) : ( - <ReactFlowProvider> - <ReactFlow - snapGrid={[10, 10]} - snapToGrid - onlyRenderVisibleElements={false} - nodesDraggable={true} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - connectionLineComponent={ConnectionDragLine} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - nodes={nodes} - edges={edges} - onInit={(reactFlowInstance) => { - reactFlowInstanceRef.current = reactFlowInstance; - onInit(reactFlowInstance); - }} - proOptions={{ hideAttribution: true }} - ></ReactFlow> - </ReactFlowProvider> - )} - <div> - <div - className="w-full py-0 px-2 bg-secondary-50 cursor-pointer border-y flex items-center gap-1" - onClick={() => setExpanded(!expanded)} - > - <Button - size="xs" - variant="ghost" - iconComponent={expanded ? <KeyboardArrowDown /> : <KeyboardArrowRight />} + <Panel + title="Schema" + tooltips={ + <TooltipProvider delayDuration={10}> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Remove />} + onClick={() => { + if (props.onRemove) props.onRemove(); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Hide</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<ContentCopy />} + onClick={() => { + // Copy the schema to the clipboard + navigator.clipboard.writeText(JSON.stringify(schemaGraph, null, 2)); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Copy Schema to Clipboard</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<FitScreen />} onClick={() => {}} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Fit to screen</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + } + > + <div className="schema-panel w-full h-full flex flex-col justify-between" ref={reactFlowRef}> + {nodes.length === 0 ? ( + <p className="m-3 text-xl font-bold">No Elements</p> + ) : ( + <ReactFlowProvider> + <ReactFlow + snapGrid={[10, 10]} + snapToGrid + onlyRenderVisibleElements={false} + nodesDraggable={true} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + connectionLineComponent={ConnectionDragLine} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + nodes={nodes} + edges={edges} + onInit={(reactFlowInstance) => { + reactFlowInstanceRef.current = reactFlowInstance; + onInit(reactFlowInstance); + }} + proOptions={{ hideAttribution: true }} + ></ReactFlow> + </ReactFlowProvider> + )} + <div> + <div + className="w-full py-0 px-2 bg-secondary-50 cursor-pointer border-y flex items-center gap-1" onClick={() => setExpanded(!expanded)} - /> - <span className="text-xs font-semibold text-secondary-600 truncate">Schema settings</span> - </div> - {expanded && ( - <div className="h-full w-full overflow-y-auto"> - <SchemaDialog open={toggleSchemaSettings} onClose={() => setToggleSchemaSettings(false)} /> + > + <Button + size="xs" + variant="ghost" + iconComponent={expanded ? <KeyboardArrowDown /> : <KeyboardArrowRight />} + onClick={() => setExpanded(!expanded)} + /> + <span className="text-xs font-semibold text-secondary-600 truncate">Schema settings</span> </div> - )} + {expanded && ( + <div className="h-full w-full overflow-y-auto"> + <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 similarity index 96% rename from libs/shared/lib/schema/panel/schemaDialog.tsx rename to libs/shared/lib/schema/panel/SchemaDialog.tsx index 3d6771997..492e7be6e 100644 --- a/libs/shared/lib/schema/panel/schemaDialog.tsx +++ b/libs/shared/lib/schema/panel/SchemaDialog.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Dialog, DialogProps } from '../../components/Dialog'; +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'; diff --git a/libs/shared/lib/schema/panel/index.ts b/libs/shared/lib/schema/panel/index.ts index e27a6e2f5..5adcc326e 100644 --- a/libs/shared/lib/schema/panel/index.ts +++ b/libs/shared/lib/schema/panel/index.ts @@ -1 +1 @@ -export * from './schema'; +export * from './Schema'; diff --git a/libs/shared/lib/schema/panel/schema.stories.tsx b/libs/shared/lib/schema/panel/schema.stories.tsx index 051acd2f7..17679da8a 100644 --- a/libs/shared/lib/schema/panel/schema.stories.tsx +++ b/libs/shared/lib/schema/panel/schema.stories.tsx @@ -6,7 +6,7 @@ import { schemaSlice, setSchema } from '@graphpolaris/shared/lib/data-access/sto import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import { Schema } from './schema'; +import { Schema } from './Schema'; import { movieSchemaRaw, diff --git a/libs/shared/lib/sidebar/index.tsx b/libs/shared/lib/sidebar/index.tsx index 6b2a03c0c..aa1fb99bd 100644 --- a/libs/shared/lib/sidebar/index.tsx +++ b/libs/shared/lib/sidebar/index.tsx @@ -15,9 +15,7 @@ const tabs: Array<{ { name: 'Schema', icon: <SchemaIcon /> }, ]; -export function Sidebar({ onTab }: { onTab: (tab: SideNavTab) => void }) { - const [tab, setTab] = useState<SideNavTab>('Schema'); - +export function Sidebar({ onTab, tab }: { onTab: (tab: SideNavTab) => void; tab: SideNavTab }) { return ( <div className="side-bar w-fit h-full flex shrink"> <TooltipProvider delayDuration={100}> @@ -33,10 +31,8 @@ export function Sidebar({ onTab }: { onTab: (tab: SideNavTab) => void }) { onClick={() => { if (tab === t.name) { onTab(undefined); - setTab(undefined); } else { onTab(t.name); - setTab(t.name); } }} additionalClasses={tab === t.name ? 'bg-secondary-100' : ''} diff --git a/libs/shared/lib/sidebar/search/SearchBar.tsx b/libs/shared/lib/sidebar/search/SearchBar.tsx new file mode 100644 index 000000000..04e24282f --- /dev/null +++ b/libs/shared/lib/sidebar/search/SearchBar.tsx @@ -0,0 +1,216 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { + AppDispatch, + useAppDispatch, + useGraphQueryResult, + useQuerybuilderGraph, + useRecentSearches, + useSchemaGraph, + useSearchResult, +} from '../../data-access'; +import { QueryMultiGraph } from '../../querybuilder'; +import { + CATEGORY_KEYS, + addRecentSearch, + addSearchResultData, + addSearchResultQueryBuilder, + addSearchResultSchema, +} from '../../data-access/store/searchResultSlice'; +import { filterData } from './similarity'; +import { Button, Panel, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components'; +import { Remove, Fullscreen } from '@mui/icons-material'; + +const SIMILARITY_THRESHOLD = 0.7; + +const CATEGORY_ACTIONS: { + [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void; +} = { + data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { + dispatch(addSearchResultData(payload)); + }, + schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { + dispatch(addSearchResultSchema(payload)); + }, + querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { + dispatch(addSearchResultQueryBuilder(payload)); + }, +}; + +const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[]; + +export function SearchBar(props: { onRemove?: () => void }) { + const inputRef = useRef<HTMLInputElement>(null); + const [search, setSearch] = useState<string>(''); + const searchbarRef = useRef<HTMLDivElement>(null); + const dispatch = useAppDispatch(); + const results = useSearchResult(); + const recentSearches = useRecentSearches(); + const schema = useSchemaGraph(); + const graphData = useGraphQueryResult(); + const querybuilderData = useQuerybuilderGraph(); + + const dataSources: { + [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; + } = { + data: graphData, + schema: schema, + querybuilder: querybuilderData as QueryMultiGraph, + }; + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + if (search !== '') { + dispatch(addRecentSearch(search)); + } + } + }; + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [search]); + + useEffect(() => { + handleSearch(); + }, [search]); + + const handleSearch = () => { + let query = search.toLowerCase(); + const categories = search.match(/@[^ ]+/g); + + if (categories) { + categories.map((category) => { + query = query.replace(category, '').trim(); + const cat = category.substring(1); + + if (cat in CATEGORY_ACTIONS) { + const categoryAction = CATEGORY_ACTIONS[cat as CATEGORY_KEYS]; + const data = dataSources[cat]; + + const payload = { + nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), + edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), + }; + categoryAction(payload, dispatch); + } + }); + } else { + for (const category of SEARCH_CATEGORIES) { + const categoryAction = CATEGORY_ACTIONS[category]; + const data = dataSources[category]; + + const payload = { + nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), + edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), + }; + + categoryAction(payload, dispatch); + } + } + }; + + return ( + <Panel + title="Search" + tooltips={ + <TooltipProvider delayDuration={10}> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Remove />} + onClick={() => { + if (props.onRemove) props.onRemove(); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Hide</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={() => {}} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Mock icon</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + } + > + <div className="flex flex-col w-full p-2"> + <div className="w-full"> + <input + type="text" + ref={inputRef} + value={search} + onChange={(e) => setSearch(e.target.value)} + id="input-group-search" + className="block w-full p-2 ps-2 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0" + placeholder="Search database" + ></input> + </div> + <div> + {recentSearches.length !== 0 && ( + <div className="px-3 pb-3"> + <p className="text-sm">Recent searches</p> + {recentSearches.slice(0, 3).map((term) => ( + <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}> + {term} + </p> + ))} + </div> + )} + {search !== '' && ( + <div className="z-10 w-full overflow-y-auto scroll h-full px-2 pb-2"> + {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( + <div className="ml-1 text-sm"> + <p className="text-secondary-500">Found no matches...</p> + </div> + ) : ( + SEARCH_CATEGORIES.map((category, index) => { + if (results[category].nodes.length > 0 || results[category].edges.length > 0) { + return ( + <div key={index}> + <div className="flex justify-between p-2 text-lg"> + <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p> + <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> + </div> + <div className="h-[1px] w-full bg-secondary-200"></div> + {Object.values(Object.values(results[category])) + .flat() + .map((item, index) => ( + <div + key={index} + className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2" + title={JSON.stringify(item)} + onClick={() => { + CATEGORY_ACTIONS[category]( + { + nodes: results[category].nodes.includes(item) ? [item] : [], + edges: results[category].edges.includes(item) ? [item] : [], + }, + dispatch, + ); + }} + > + <div className="font-bold text-sm"> + {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} + </div> + <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> + </div> + ))} + </div> + ); + } else return <></>; + }) + )} + </div> + )} + </div> + </div> + </Panel> + ); +} diff --git a/libs/shared/lib/sidebar/search/searchbar.tsx b/libs/shared/lib/sidebar/search/searchbar.tsx deleted file mode 100644 index 837085911..000000000 --- a/libs/shared/lib/sidebar/search/searchbar.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { - AppDispatch, - useAppDispatch, - useGraphQueryResult, - useQuerybuilderGraph, - useRecentSearches, - useSchemaGraph, - useSearchResult, -} from '../../data-access'; -import { QueryMultiGraph } from '../../querybuilder'; -import { - CATEGORY_KEYS, - addRecentSearch, - addSearchResultData, - addSearchResultQueryBuilder, - addSearchResultSchema, -} from '../../data-access/store/searchResultSlice'; -import { filterData } from './similarity'; - -const SIMILARITY_THRESHOLD = 0.7; - -const CATEGORY_ACTIONS: { - [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void; -} = { - data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { - dispatch(addSearchResultData(payload)); - }, - schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { - dispatch(addSearchResultSchema(payload)); - }, - querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { - dispatch(addSearchResultQueryBuilder(payload)); - }, -}; - -const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[]; - -export function Searchbar() { - const inputRef = useRef<HTMLInputElement>(null); - const [search, setSearch] = useState<string>(''); - const searchbarRef = useRef<HTMLDivElement>(null); - const dispatch = useAppDispatch(); - const results = useSearchResult(); - const recentSearches = useRecentSearches(); - const schema = useSchemaGraph(); - const graphData = useGraphQueryResult(); - const querybuilderData = useQuerybuilderGraph(); - - const dataSources: { - [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; - } = { - data: graphData, - schema: schema, - querybuilder: querybuilderData as QueryMultiGraph, - }; - - useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - if (search !== '') { - dispatch(addRecentSearch(search)); - } - } - }; - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [search]); - - useEffect(() => { - handleSearch(); - }, [search]); - - const handleSearch = () => { - let query = search.toLowerCase(); - const categories = search.match(/@[^ ]+/g); - - if (categories) { - categories.map((category) => { - query = query.replace(category, '').trim(); - const cat = category.substring(1); - - if (cat in CATEGORY_ACTIONS) { - const categoryAction = CATEGORY_ACTIONS[cat as CATEGORY_KEYS]; - const data = dataSources[cat]; - - const payload = { - nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), - edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), - }; - categoryAction(payload, dispatch); - } - }); - } else { - for (const category of SEARCH_CATEGORIES) { - const categoryAction = CATEGORY_ACTIONS[category]; - const data = dataSources[category]; - - const payload = { - nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), - edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), - }; - - categoryAction(payload, dispatch); - } - } - }; - - return ( - <div className="flex flex-col w-full p-2"> - <div className="w-full"> - <input - type="text" - ref={inputRef} - value={search} - onChange={(e) => setSearch(e.target.value)} - id="input-group-search" - className="block w-full p-2 ps-2 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0" - placeholder="Search database" - ></input> - </div> - <div> - {recentSearches.length !== 0 && ( - <div className="px-3 pb-3"> - <p className="text-sm">Recent searches</p> - {recentSearches.slice(0, 3).map((term) => ( - <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}> - {term} - </p> - ))} - </div> - )} - {search !== '' && ( - <div className="z-10 w-full overflow-y-auto scroll h-full px-2 pb-2"> - {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( - <div className="ml-1 text-sm"> - <p className="text-secondary-500">Found no matches...</p> - </div> - ) : ( - SEARCH_CATEGORIES.map((category, index) => { - if (results[category].nodes.length > 0 || results[category].edges.length > 0) { - return ( - <div key={index}> - <div className="flex justify-between p-2 text-lg"> - <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p> - <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> - </div> - <div className="h-[1px] w-full bg-secondary-200"></div> - {Object.values(Object.values(results[category])) - .flat() - .map((item, index) => ( - <div - key={index} - className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2" - title={JSON.stringify(item)} - onClick={() => { - CATEGORY_ACTIONS[category]( - { - nodes: results[category].nodes.includes(item) ? [item] : [], - edges: results[category].edges.includes(item) ? [item] : [], - }, - dispatch, - ); - }} - > - <div className="font-bold text-sm"> - {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} - </div> - <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> - </div> - ))} - </div> - ); - } else return <></>; - }) - )} - </div> - )} - </div> - </div> - ); -} -- GitLab