From c70c98c85c0eab8a57c9f351a916f69ab22f153d Mon Sep 17 00:00:00 2001 From: "Vink, S.A. (Sjoerd)" <s.a.vink@uu.nl> Date: Tue, 20 Feb 2024 12:55:00 +0000 Subject: [PATCH] fix(dbSelector): improve db selector ui with descriptions --- .../dbConnectionSelector.tsx | 150 ++++++++++-------- .../forms/mockSaveStates.tsx | 8 + .../DatabaseManagement/forms/settings.tsx | 18 ++- libs/shared/lib/vis/index.tsx | 16 +- 4 files changed, 114 insertions(+), 78 deletions(-) diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx index 5c0e1be3b..b8106764e 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx @@ -8,7 +8,7 @@ import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice import { DropdownButton, DropdownContainer, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { clearSchema } from '@graphpolaris/shared/lib/data-access/store/schemaSlice'; -import { DatabaseStatus, SaveStateI, wsDeleteState } from '@graphpolaris/shared/lib/data-access/api/wsState'; +import { DatabaseStatus, SaveStateI, nilUUID, wsDeleteState } from '@graphpolaris/shared/lib/data-access/api/wsState'; export default function DatabaseSelector({}) { const dispatch = useAppDispatch(); @@ -23,6 +23,15 @@ export default function DatabaseSelector({}) { const [selectedSaveState, setSelectedSaveState] = useState<SaveStateI | null>(null); // const [addDbConnectionFormOpen, setAddDbConnectionFormOpen] = useState<boolean>(false); + useEffect(() => { + if ( + (session.saveStates && Object.keys(session.saveStates).length === 0 && settingsMenuOpen !== 'create') || + session.currentSaveState === nilUUID + ) { + setSettingsMenuOpen('create'); + } + }, [session, settingsMenuOpen]); + useEffect(() => { const handleClickOutside = ({ target }: MouseEvent) => { if (dbSelectionMenuRef.current && !dbSelectionMenuRef.current.contains(target as Node)) { @@ -62,6 +71,10 @@ export default function DatabaseSelector({}) { <SettingsForm open={settingsMenuOpen} saveState={settingsMenuOpen === 'update' ? selectedSaveState : null} + disableCancel={ + (session.saveStates && Object.keys(session.saveStates).length === 0) || + session.currentSaveState === '00000000-0000-0000-0000-000000000000' + } onClose={() => { setSettingsMenuOpen(undefined); }} @@ -77,7 +90,7 @@ export default function DatabaseSelector({}) { <LoadingSpinner /> <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p> </> - ) : session.currentSaveState && session.currentSaveState in session.saveStates ? ( + ) : session.currentSaveState && session.currentSaveState in session.saveStates && session.currentSaveState !== nilUUID ? ( <> <div className={`h-2 w-2 rounded-full ${ @@ -91,7 +104,7 @@ export default function DatabaseSelector({}) { <LoadingSpinner /> <p className="ml-2">Retrieving databases</p> </> - ) : Object.keys(session.saveStates).length === 0 ? ( + ) : Object.keys(session.saveStates).length === 0 || session.currentSaveState === nilUUID ? ( <> <p className="ml-2">Add your first Database</p> </> @@ -133,71 +146,74 @@ export default function DatabaseSelector({}) { </> )} </li> - {Object.values(session.saveStates).map((save) => ( - <li - key={save.id} - className="flex justify-between items-center px-4 py-2 hover:bg-primary-100 gap-2 cursor-pointer" - onClick={(e) => { - if (save.id !== session.currentSaveState) { - e.preventDefault(); - setDbSelectionMenuOpen(false); - setConnecting(true); - dispatch(selectSaveState(save.id)); - dispatch(clearQB()); - dispatch(clearSchema()); - } else { - setDbSelectionMenuOpen(false); - } - }} - onMouseEnter={() => setHovered(save.id)} - onMouseLeave={() => setHovered(null)} - title={`Connect to ${save.name}`} - > - <div - className={`h-[8px] w-[8px] rounded-full shrink-0 ${ - session.testedSaveState[save.id] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500' - }`} - /> - <div className="w-full shrink min-w-0 flex flex-col"> - <p className="truncate w-full shrink-0 min-w-0">{save.name}</p> - <p className="bg-light text-2xs text-secondary-500 truncate w-fit shrink-0 min-w-0 max-w-full h-full border border-secondary-200 rounded-sm p-0.5"> - {save.db.protocol} - {save.db.url} - </p> - </div> - {hovered === save.id && ( - <div className="flex items-center ml-2"> - <div - className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - setSettingsMenuOpen('update'); - setSelectedSaveState(save); - }} - > - <Settings /> - </div> - <div - className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - if (session.currentSaveState === save.id) { - dispatch(clearQB()); - dispatch(clearSchema()); - } - wsDeleteState(save.id); - dispatch(deleteSaveState(save.id)); - }} - title="Delete database connection" - > - <Delete /> - </div> + {Object.values(session.saveStates) + .filter((save) => save.id !== nilUUID) + .map((save) => ( + <li + key={save.id} + className="flex justify-between items-center px-4 py-2 hover:bg-primary-100 gap-2 cursor-pointer" + onClick={(e) => { + if (save.id !== session.currentSaveState) { + e.preventDefault(); + setDbSelectionMenuOpen(false); + setConnecting(true); + dispatch(selectSaveState(save.id)); + dispatch(clearQB()); + dispatch(clearSchema()); + } else { + setDbSelectionMenuOpen(false); + } + }} + onMouseEnter={() => setHovered(save.id)} + onMouseLeave={() => setHovered(null)} + title={`Connect to ${save.name}`} + > + <div + className={`h-[8px] w-[8px] rounded-full shrink-0 ${ + session.testedSaveState[save.id] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500' + }`} + /> + <div className="w-full shrink min-w-0 flex flex-col"> + <p className="truncate w-full shrink-0 min-w-0">{save.name}</p> + <p className="bg-light text-2xs text-secondary-500 truncate w-fit shrink-0 min-w-0 max-w-full h-full border border-secondary-200 rounded-sm p-0.5"> + {save.db.protocol} + {save.db.url} + </p> </div> - )} - </li> - ))} + {hovered === save.id && ( + <div className="flex items-center ml-2"> + <div + className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setSettingsMenuOpen('update'); + setSelectedSaveState(save); + }} + > + <Settings /> + </div> + <div + className="text-secondary-700 hover:text-secondary-400 transition-colors duration-300" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDbSelectionMenuOpen(false); + if (session.currentSaveState === save.id) { + dispatch(clearQB()); + dispatch(clearSchema()); + } + wsDeleteState(save.id); + dispatch(deleteSaveState(save.id)); + }} + title="Delete database connection" + > + <Delete /> + </div> + </div> + )} + </li> + ))} </DropdownItemContainer> )} </DropdownContainer> diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx index 61f4a3d69..f25e3ddf8 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/mockSaveStates.tsx @@ -5,6 +5,7 @@ import { initialState as qbInitialState } from '@graphpolaris/shared/lib/data-ac export type SaveStateSampleI = SaveStateI & { subtitle: string; + description: string; }; export const sampleSaveStates: Array<SaveStateSampleI> = [ @@ -12,6 +13,7 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ id: nilUUID, name: 'Recommendations', subtitle: 'Hosted by Neo4j', + description: 'Network of movies, actors, directors and reviews by people', db: { username: 'recommendations', password: 'recommendations', @@ -31,6 +33,7 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ id: nilUUID, name: 'Movies', subtitle: 'Hosted by Neo4j', + description: 'Movies and people related to those movies as actors, directors and producers', db: { username: 'movies', password: 'movies', @@ -50,6 +53,7 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ id: nilUUID, name: 'Northwind', subtitle: 'Hosted by Neo4j', + description: 'Retail-system with products, orders, customers, suppliers and employees', db: { username: 'northwind', password: 'northwind', @@ -69,6 +73,7 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ id: nilUUID, name: 'Fincen', subtitle: 'Hosted by Neo4j', + description: 'FinCEN files investigation for banks and countries', db: { username: 'fincen', password: 'fincen', @@ -88,6 +93,7 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ id: nilUUID, name: 'Slack', subtitle: 'Hosted by Neo4j', + description: 'Communication network consisting of several types of users and messages', db: { username: 'slack', password: 'slack', @@ -107,6 +113,7 @@ export const sampleSaveStates: Array<SaveStateSampleI> = [ id: nilUUID, name: 'Game of Thrones', subtitle: 'Hosted by Neo4j', + description: 'Character interactions and actors in the Game of Thrones movie', db: { username: 'gameofthrones', password: 'gameofthrones', @@ -136,6 +143,7 @@ export const SampleDatabaseSelector = (props: { onClick: (data: SaveStateI) => v <div className="card-body"> <h2 className="card-title">{sample.name}</h2> <p className="font-light text-secondary-400">{sample.subtitle}</p> + <p className="font-light text-xs text-secondary-400">{sample.description}</p> </div> </div> ))} diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index 7c366e604..b6e2ae560 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -28,7 +28,12 @@ type Connection = { verified: boolean | null; }; -export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update'; saveState: SaveStateI | null }) => { +export const SettingsForm = (props: { + onClose(): void; + open: 'create' | 'update'; + saveState: SaveStateI | null; + disableCancel?: boolean; +}) => { const dispatch = useAppDispatch(); const ref = useRef<HTMLDialogElement>(null); const auth = useAuthorizationCache(); @@ -142,9 +147,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update' return ( <Dialog open={!!props.open} onClose={props.onClose} className="lg:min-w-[50rem]"> <div className="flex justify-between align-center"> - <h1 className="text-xl font-bold"> - {formTitle} {formData.name} Database - </h1> + <h1 className="text-xl font-bold">{formTitle} Database</h1> <div> {sampleDataPanel === true ? ( <Button variant="outline" label="Go back" onClick={() => setSampleDataPanel(false)} /> @@ -167,7 +170,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update' onClick={(data) => { setFormData({ ...data, user_id: auth.userID || '' }); setHasError(false); - setSampleDataPanel(false); + handleSubmit(); }} /> ) : ( @@ -190,7 +193,9 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update' </div> )} - <div className="grid md:grid-cols-2 gap-3 card-actions w-full justify-stretch items-center"> + <div + className={`grid md:grid-cols-2 gap-3 card-actions w-full justify-stretch items-center ${sampleDataPanel === true && 'hidden'}`} + > <Button type="primary" label={connection.updating ? formTitle.slice(0, -1) + 'ing...' : formTitle} @@ -203,6 +208,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'create' | 'update' <Button variant="outline" label="Cancel" + disabled={props.disableCancel} onClick={(event) => { event.preventDefault(); closeDialog(); diff --git a/libs/shared/lib/vis/index.tsx b/libs/shared/lib/vis/index.tsx index ca5a61cde..6cbdbd9c0 100644 --- a/libs/shared/lib/vis/index.tsx +++ b/libs/shared/lib/vis/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useAppDispatch } from '@graphpolaris/shared/lib/data-access'; import { addVisualization } from '../data-access/store/visualizationSlice'; import { @@ -9,7 +9,7 @@ import { useVisualizationState, } from '@graphpolaris/shared/lib/data-access/store/hooks'; import { localConfigPropTypes, globalConfigPropTypes, VISComponentType } from './Types'; -import {MatrixVisComponent, NodeLinkComponent, PaohVisComponent, RawJSONComponent, TableComponent } from './visualizations'; +import { MatrixVisComponent, NodeLinkComponent, PaohVisComponent, RawJSONComponent, TableComponent } from './visualizations'; // export * from './rawjsonvis'; // export * from './nodelink/nodelinkvis'; @@ -38,9 +38,15 @@ export const createVisualizationComponent = () => { const VisualizationComponent: VISComponentType = Visualizations[vis.activeVisualization]; - React.useEffect(() => { - dispatch(addVisualization({ id: VisualizationComponent.displayName, settings: VisualizationComponent.localConfigSchema })); - }, [vis.activeVisualization]); + useEffect(() => { + if (VisualizationComponent) { + dispatch(addVisualization({ id: VisualizationComponent.displayName, settings: VisualizationComponent.localConfigSchema })); + } + }, [dispatch, VisualizationComponent]); + + if (!VisualizationComponent) { + return <div className="w-full h-full flex items-center justify-center">Visualization not found</div>; + } const globalConfig: globalConfigPropTypes = settings.general && -- GitLab