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