From 5e27a027371d6306f11b28cb4e4245870ec3c5ac Mon Sep 17 00:00:00 2001 From: Niels de Jong Date: Fri, 27 Oct 2023 14:22:19 +0200 Subject: [PATCH] Feature/new dashboard load UI (#657) * Added sidebar prototype * Return of the sidebar * Add database selector to sidebar * Iterating on the dashboard sidebar interface * Fixed usage of hardcoded color * Updated dashboard loading mechanism, iterating * Updated dashboard loading mechanism, iterating * Updated file structure for dashboard sidebar * Improved modal/menu handling for dashboard load * Import/export dashboards * New dashboard sharing interface * Removed old save/load modal * Finalized v1 of the new multi-dashboard UI * removing useless imports * Removed old isLoaded in state of dashboard load screen. Updated warning buttons --------- Co-authored-by: Alfred Rubin --- src/application/Application.tsx | 4 +- src/application/ApplicationActions.ts | 6 + src/application/ApplicationReducer.ts | 32 +- src/application/ApplicationSelectors.ts | 4 + src/application/ApplicationThunks.ts | 13 +- src/config/ApplicationConfig.ts | 2 +- src/dashboard/Dashboard.tsx | 57 +- src/dashboard/DashboardActions.ts | 6 + src/dashboard/DashboardReducer.ts | 21 +- src/dashboard/DashboardSelectors.ts | 2 + src/dashboard/DashboardThunks.ts | 183 ++++--- src/dashboard/header/DashboardHeader.tsx | 2 +- .../header/DashboardHeaderPageTitle.tsx | 6 +- src/dashboard/header/DashboardTitle.tsx | 21 +- src/dashboard/sidebar/DashboardSidebar.tsx | 493 ++++++++++++++++++ .../sidebar/DashboardSidebarListItem.tsx | 73 +++ .../menu/DashboardSidebarCreateMenu.tsx | 38 ++ .../menu/DashboardSidebarDashboardMenu.tsx | 52 ++ .../menu/DashboardSidebarDatabaseMenu.tsx | 50 ++ .../modal/DashboardSidebarCreateModal.tsx | 43 ++ .../modal/DashboardSidebarDeleteModal.tsx | 43 ++ .../modal/DashboardSidebarExportModal.tsx | 79 +++ .../modal/DashboardSidebarImportModal.tsx | 73 +++ .../modal/DashboardSidebarInfoModal.tsx | 48 ++ .../modal/DashboardSidebarLoadModal.tsx | 44 ++ .../modal/DashboardSidebarSaveModal.tsx | 39 ++ .../modal/DashboardSidebarShareModal.tsx | 92 ++++ .../sidebar/modal/legacy/LegacyShareModal.tsx | 212 ++++++++ src/modal/ExportModal.tsx | 33 ++ src/modal/LoadModal.tsx | 224 -------- src/modal/SaveModal.tsx | 227 -------- src/modal/ShareModal.tsx | 349 ------------- src/modal/UpgradeOldDashboardModal.tsx | 3 +- src/page/Page.tsx | 5 +- src/page/PageReducer.ts | 6 +- yarn.lock | 76 +-- 36 files changed, 1712 insertions(+), 949 deletions(-) create mode 100644 src/dashboard/sidebar/DashboardSidebar.tsx create mode 100644 src/dashboard/sidebar/DashboardSidebarListItem.tsx create mode 100644 src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx create mode 100644 src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx create mode 100644 src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx create mode 100644 src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx create mode 100644 src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx create mode 100644 src/modal/ExportModal.tsx delete mode 100644 src/modal/LoadModal.tsx delete mode 100644 src/modal/SaveModal.tsx delete mode 100644 src/modal/ShareModal.tsx diff --git a/src/application/Application.tsx b/src/application/Application.tsx index cd8d9759a..1dbec13d1 100644 --- a/src/application/Application.tsx +++ b/src/application/Application.tsx @@ -214,9 +214,9 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setConnected(false)); dispatch(createConnectionFromDesktopIntegrationThunk()); }, - loadDashboard: (text) => { + loadDashboard: (uuid, text) => { dispatch(clearNotification()); - dispatch(loadDashboardThunk(text)); + dispatch(loadDashboardThunk(uuid, text)); }, resetDashboard: () => dispatch(resetDashboardState()), clearOldDashboard: () => dispatch(setOldDashboard(null)), diff --git a/src/application/ApplicationActions.ts b/src/application/ApplicationActions.ts index 4f85a17c5..cf49b4715 100644 --- a/src/application/ApplicationActions.ts +++ b/src/application/ApplicationActions.ts @@ -20,6 +20,12 @@ export const setConnected = (connected: boolean) => ({ payload: { connected }, }); +export const SET_DRAFT = 'APPLICATION/SET_DRAFT'; +export const setDraft = (draft: boolean) => ({ + type: SET_DRAFT, + payload: { draft }, +}); + export const SET_CONNECTION_MODAL_OPEN = 'APPLICATION/SET_CONNECTION_MODAL_OPEN'; export const setConnectionModalOpen = (open: boolean) => ({ type: SET_CONNECTION_MODAL_OPEN, diff --git a/src/application/ApplicationReducer.ts b/src/application/ApplicationReducer.ts index 5e01776c7..dc53aa7ad 100644 --- a/src/application/ApplicationReducer.ts +++ b/src/application/ApplicationReducer.ts @@ -2,7 +2,10 @@ * Reducers define changes to the application state when a given action is taken. */ +import { HARD_RESET_CARD_SETTINGS, UPDATE_ALL_SELECTIONS, UPDATE_FIELDS, UPDATE_SCHEMA } from '../card/CardActions'; import { DEFAULT_NEO4J_URL } from '../config/ApplicationConfig'; +import { SET_DASHBOARD, SET_DASHBOARD_UUID } from '../dashboard/DashboardActions'; +import { UPDATE_DASHBOARD_SETTING } from '../settings/SettingsActions'; import { CLEAR_DESKTOP_CONNECTION_PROPERTIES, CLEAR_NOTIFICATION, @@ -15,6 +18,7 @@ import { SET_CONNECTION_PROPERTIES, SET_DASHBOARD_TO_LOAD_AFTER_CONNECTING, SET_DESKTOP_CONNECTION_PROPERTIES, + SET_DRAFT, SET_OLD_DASHBOARD, SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING, SET_REPORT_HELP_MODAL_OPEN, @@ -36,6 +40,7 @@ const initialState = { notificationMessage: null, connectionModalOpen: false, welcomeScreenOpen: true, + draft: false, aboutModalOpen: false, connection: { protocol: 'neo4j', @@ -55,6 +60,25 @@ const initialState = { export const applicationReducer = (state = initialState, action: { type: any; payload: any }) => { const { type, payload } = action; + // This is a special application-level flag used to determine whether the dashboard needs to be saved to the database. + if (action.type.startsWith('DASHBOARD/') || action.type.startsWith('PAGE/') || action.type.startsWith('CARD/')) { + // if anything changes EXCEPT for the selected page, we flag that we are drafting a dashboard. + const NON_TRANSFORMATIVE_ACTIONS = [ + UPDATE_DASHBOARD_SETTING, + UPDATE_SCHEMA, + HARD_RESET_CARD_SETTINGS, + SET_DASHBOARD, + UPDATE_ALL_SELECTIONS, + UPDATE_FIELDS, + SET_DASHBOARD_UUID, + ]; + if (!state.draft && !NON_TRANSFORMATIVE_ACTIONS.includes(type)) { + state = update(state, { draft: true }); + return state; + } + } + + // Ignore any non-application actions. if (!action.type.startsWith('APPLICATION/')) { return state; } @@ -75,6 +99,11 @@ export const applicationReducer = (state = initialState, action: { type: any; pa state = update(state, { connected: connected }); return state; } + case SET_DRAFT: { + const { draft } = payload; + state = update(state, { draft: draft }); + return state; + } case SET_CONNECTION_MODAL_OPEN: { const { open } = payload; state = update(state, { connectionModalOpen: open }); @@ -82,9 +111,6 @@ export const applicationReducer = (state = initialState, action: { type: any; pa } case SET_ABOUT_MODAL_OPEN: { const { open } = payload; - if (!open) { - console.log(''); - } state = update(state, { aboutModalOpen: open }); return state; } diff --git a/src/application/ApplicationSelectors.ts b/src/application/ApplicationSelectors.ts index e99758185..e3dc20601 100644 --- a/src/application/ApplicationSelectors.ts +++ b/src/application/ApplicationSelectors.ts @@ -21,6 +21,10 @@ export const getNotificationTitle = (state: any) => { return state.application.notificationTitle; }; +export const dashboardIsDraft = (state: any) => { + return state.application.draft; +}; + export const applicationIsConnected = (state: any) => { return state.application.connected; }; diff --git a/src/application/ApplicationThunks.ts b/src/application/ApplicationThunks.ts index 38590733b..27e1b8e8c 100644 --- a/src/application/ApplicationThunks.ts +++ b/src/application/ApplicationThunks.ts @@ -4,8 +4,9 @@ import { DEFAULT_SCREEN, Screens } from '../config/ApplicationConfig'; import { setDashboard } from '../dashboard/DashboardActions'; import { NEODASH_VERSION } from '../dashboard/DashboardReducer'; import { + assignDashboardUuidIfNotPresentThunk, loadDashboardFromNeo4jByNameThunk, - loadDashboardFromNeo4jByUUIDThunk, + loadDashboardFromNeo4jThunk, loadDashboardThunk, upgradeDashboardVersion, } from '../dashboard/DashboardThunks'; @@ -40,6 +41,7 @@ import { setReportHelpModalOpen, } from './ApplicationActions'; import { version } from '../modal/AboutModal'; +import { createUUID } from '../utils/uuid'; /** * Application Thunks (https://redux.js.org/usage/writing-logic-thunks) handle complex state manipulations. @@ -67,9 +69,12 @@ export const createConnectionThunk = if (records && records[0] && records[0].error) { dispatch(createNotificationThunk('Unable to establish connection', records[0].error)); } else if (records && records[0] && records[0].keys[0] == 'connected') { + // Connected to Neo4j. Set state accordingly. dispatch(setConnectionProperties(protocol, url, port, database, username, password)); dispatch(setConnectionModalOpen(false)); dispatch(setConnected(true)); + // An old dashboard (pre-2.3.5) may not always have a UUID. We catch this case here. + dispatch(assignDashboardUuidIfNotPresentThunk()); dispatch(updateSessionParameterThunk('session_uri', `${protocol}://${url}:${port}`)); dispatch(updateSessionParameterThunk('session_database', database)); dispatch(updateSessionParameterThunk('session_username', username)); @@ -83,11 +88,11 @@ export const createConnectionThunk = ) { fetch(application.dashboardToLoadAfterConnecting) .then((response) => response.text()) - .then((data) => dispatch(loadDashboardThunk(data))); + .then((data) => dispatch(loadDashboardThunk(createUUID(), data))); dispatch(setDashboardToLoadAfterConnecting(null)); } else if (application.dashboardToLoadAfterConnecting) { const setDashboardAfterLoadingFromDatabase = (value) => { - dispatch(loadDashboardThunk(value)); + dispatch(loadDashboardThunk(createUUID(), value)); }; // If we specify a dashboard by name, load the latest version of it. @@ -103,7 +108,7 @@ export const createConnectionThunk = ); } else { dispatch( - loadDashboardFromNeo4jByUUIDThunk( + loadDashboardFromNeo4jThunk( driver, application.standaloneDashboardDatabase, application.dashboardToLoadAfterConnecting, diff --git a/src/config/ApplicationConfig.ts b/src/config/ApplicationConfig.ts index 20869f3f9..7589a102c 100644 --- a/src/config/ApplicationConfig.ts +++ b/src/config/ApplicationConfig.ts @@ -9,7 +9,7 @@ const styleConfig = await StyleConfig.getInstance(); export const DEFAULT_SCREEN = Screens.WELCOME_SCREEN; // WELCOME_SCREEN export const DEFAULT_NEO4J_URL = 'localhost'; // localhost -export const DEFAULT_DASHBOARD_TITLE = 'My dashboard'; // '' +export const DEFAULT_DASHBOARD_TITLE = 'New dashboard'; export const DASHBOARD_HEADER_COLOR = styleConfig?.style?.DASHBOARD_HEADER_COLOR || '#0B297D'; // '#0B297D' diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 04fc8fa39..e564c8043 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -11,6 +11,7 @@ import { forceRefreshPage } from '../page/PageActions'; import { getPageNumber } from '../settings/SettingsSelectors'; import { createNotificationThunk } from '../page/PageThunks'; import { version } from '../modal/AboutModal'; +import NeoDashboardSidebar from './sidebar/DashboardSidebar'; const Dashboard = ({ pagenumber, @@ -43,8 +44,12 @@ const Dashboard = ({ connection={connection} onConnectionUpdate={onConnectionUpdate} /> + {/* Navigation Bar */} -
+
{/* Main Page */} -
- {/* Main Content */} -
-
-
- {/* The main content of the page */} - {applicationSettings.standalonePassword ? ( -
- Warning: NeoDash is running with a plaintext password in config.json. +
+ +
+
+ {/* Main Content */} +
+
+
+ {/* The main content of the page */} + +
+ {applicationSettings.standalonePassword ? ( +
+ Warning: NeoDash is running with a plaintext password in config.json. +
+ ) : ( + <> + )} + + + +
- ) : ( - <> - )} - - - -
+
+
- +
); diff --git a/src/dashboard/DashboardActions.ts b/src/dashboard/DashboardActions.ts index 00dffbb7a..879f18fbe 100644 --- a/src/dashboard/DashboardActions.ts +++ b/src/dashboard/DashboardActions.ts @@ -10,6 +10,12 @@ export const setDashboard = (dashboard: any) => ({ payload: { dashboard }, }); +export const SET_DASHBOARD_UUID = 'DASHBOARD/SET_DASHBOARD_UUID'; +export const setDashboardUuid = (uuid: any) => ({ + type: SET_DASHBOARD_UUID, + payload: { uuid }, +}); + export const SET_DASHBOARD_TITLE = 'DASHBOARD/SET_DASHBOARD_TITLE'; export const setDashboardTitle = (title: any) => ({ type: SET_DASHBOARD_TITLE, diff --git a/src/dashboard/DashboardReducer.ts b/src/dashboard/DashboardReducer.ts index 98ea60eda..4ede3bc0c 100644 --- a/src/dashboard/DashboardReducer.ts +++ b/src/dashboard/DashboardReducer.ts @@ -4,7 +4,7 @@ import { DEFAULT_DASHBOARD_TITLE } from '../config/ApplicationConfig'; import { extensionsReducer, INITIAL_EXTENSIONS_STATE } from '../extensions/state/ExtensionReducer'; -import { FIRST_PAGE_INITIAL_STATE, pageReducer, PAGE_INITIAL_STATE } from '../page/PageReducer'; +import { PAGE_EXAMPLE_STATE, pageReducer, PAGE_EMPTY_STATE } from '../page/PageReducer'; import { settingsReducer, SETTINGS_INITIAL_STATE } from '../settings/SettingsReducer'; import { @@ -15,6 +15,7 @@ import { SET_DASHBOARD, MOVE_PAGE, SET_EXTENSION_ENABLED, + SET_DASHBOARD_UUID, } from './DashboardActions'; export const NEODASH_VERSION = '2.3'; @@ -22,9 +23,17 @@ export const NEODASH_VERSION = '2.3'; export const initialState = { title: DEFAULT_DASHBOARD_TITLE, version: NEODASH_VERSION, + settings: SETTINGS_INITIAL_STATE, + pages: [PAGE_EXAMPLE_STATE], + parameters: {}, + extensions: INITIAL_EXTENSIONS_STATE, +}; +export const emptyDashboardState = { + title: DEFAULT_DASHBOARD_TITLE, + version: NEODASH_VERSION, settings: SETTINGS_INITIAL_STATE, - pages: [FIRST_PAGE_INITIAL_STATE], + pages: [PAGE_EMPTY_STATE], parameters: {}, extensions: INITIAL_EXTENSIONS_STATE, }; @@ -68,12 +77,16 @@ export const dashboardReducer = (state = initialState, action: { type: any; payl // Global dashboard updates are handled here. switch (type) { case RESET_DASHBOARD_STATE: { - return { ...initialState }; + return { ...emptyDashboardState }; } case SET_DASHBOARD: { const { dashboard } = payload; return { ...dashboard }; } + case SET_DASHBOARD_UUID: { + const { uuid } = payload; + return { uuid: uuid, ...state }; + } case SET_DASHBOARD_TITLE: { const { title } = payload; return { ...state, title: title }; @@ -86,7 +99,7 @@ export const dashboardReducer = (state = initialState, action: { type: any; payl return { ...state, extensions: extensions }; } case CREATE_PAGE: { - return { ...state, pages: [...state.pages, PAGE_INITIAL_STATE] }; + return { ...state, pages: [...state.pages, PAGE_EMPTY_STATE] }; } case REMOVE_PAGE: { // Removes the card at a given index on a selected page number. diff --git a/src/dashboard/DashboardSelectors.ts b/src/dashboard/DashboardSelectors.ts index 71405434a..ef2e972c6 100644 --- a/src/dashboard/DashboardSelectors.ts +++ b/src/dashboard/DashboardSelectors.ts @@ -1,3 +1,5 @@ +export const getDashboardUuid = (state: any) => state.dashboard.uuid; + export const getDashboardTitle = (state: any) => state.dashboard.title; export const getDashboardSettings = (state: any) => state.dashboard.settings; diff --git a/src/dashboard/DashboardThunks.ts b/src/dashboard/DashboardThunks.ts index da3526805..57b7cc606 100644 --- a/src/dashboard/DashboardThunks.ts +++ b/src/dashboard/DashboardThunks.ts @@ -1,8 +1,8 @@ import { createNotificationThunk } from '../page/PageThunks'; import { updateDashboardSetting } from '../settings/SettingsActions'; -import { addPage, movePage, removePage, resetDashboardState, setDashboard } from './DashboardActions'; -import { runCypherQuery } from '../report/ReportQueryRunner'; -import { setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions'; +import { addPage, movePage, removePage, resetDashboardState, setDashboard, setDashboardUuid } from './DashboardActions'; +import { QueryStatus, runCypherQuery } from '../report/ReportQueryRunner'; +import { setDraft, setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions'; import { updateGlobalParametersThunk, updateParametersToNeo4jTypeThunk } from '../settings/SettingsThunks'; import { createUUID } from '../utils/uuid'; @@ -51,7 +51,7 @@ export const movePageThunk = (oldIndex: number, newIndex: number) => (dispatch: } }; -export const loadDashboardThunk = (text) => (dispatch: any, getState: any) => { +export const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) => { try { if (text.length == 0) { throw 'No dashboard file specified. Did you select a file?'; @@ -80,50 +80,50 @@ export const loadDashboardThunk = (text) => (dispatch: any, getState: any) => { const upgradedDashboard = upgradeDashboardVersion(dashboard, '1.1', '2.0'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.0. You might need to refresh this page.' ) ); - return; } if (dashboard.version == '2.0') { const upgradedDashboard = upgradeDashboardVersion(dashboard, '2.0', '2.1'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.1. You might need to refresh this page.' ) ); - return; } if (dashboard.version == '2.1') { const upgradedDashboard = upgradeDashboardVersion(dashboard, '2.1', '2.2'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.2. You might need to refresh this page.' ) ); - return; } if (dashboard.version == '2.2') { const upgradedDashboard = upgradeDashboardVersion(dashboard, '2.2', '2.3'); dispatch(setDashboard(upgradedDashboard)); dispatch(setWelcomeScreenOpen(false)); + dispatch(setDraft(true)); dispatch( createNotificationThunk( 'Successfully upgraded dashboard', 'Your old dashboard was migrated to version 2.3. You might need to refresh this page and reactivate extensions.' ) ); - return; } if (dashboard.version != '2.3') { @@ -148,57 +148,106 @@ export const loadDashboardThunk = (text) => (dispatch: any, getState: any) => { dispatch(updateGlobalParametersThunk(application.parametersToLoadAfterConnecting)); dispatch(setParametersToLoadAfterConnecting(null)); dispatch(updateParametersToNeo4jTypeThunk()); + + // Pre-2.3.4 dashboards might now always have a UUID. Set it if not present. + if (!dashboard.uuid) { + dispatch(setDashboardUuid(uuid)); + } } catch (e) { + console.log(e); dispatch(createNotificationThunk('Unable to load dashboard', e)); } }; -export const saveDashboardToNeo4jThunk = - (driver, database, dashboard, date, user, overwrite = false) => - (dispatch: any) => { - try { - const uuid = createUUID(); - const { title, version } = dashboard; - - // Generate a cypher query to save the dashboard. - const query = overwrite - ? 'OPTIONAL MATCH (n:_Neodash_Dashboard{title:$title}) DELETE n WITH 1 as X LIMIT 1 CREATE (n:_Neodash_Dashboard) SET n.uuid = $uuid, n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid' - : 'CREATE (n:_Neodash_Dashboard) SET n.uuid = $uuid, n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid'; - - const parameters = { - uuid: uuid, - title: title, - version: version, - user: user, - content: JSON.stringify(dashboard, null, 2), - date: date, - }; - runCypherQuery( - driver, - database, - query, - parameters, - 1, - () => {}, - (records) => { - if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { - dispatch(createNotificationThunk('🎉 Success!', 'Your current dashboard was saved to Neo4j.')); - } else { - dispatch( - createNotificationThunk( - 'Unable to save dashboard', - `Do you have write access to the '${database}' database?` - ) - ); - } - } - ); - } catch (e) { - dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e)); +export const saveDashboardToNeo4jThunk = (driver, database, dashboard, date, user, onSuccess) => (dispatch: any) => { + try { + let { uuid } = dashboard; + + // Dashboards pre-2.3.4 may not always have a UUID. If this is the case, generate one just before we save. + if (!dashboard.uuid) { + uuid = createUUID(); + dashboard.uuid = uuid; + dispatch(setDashboardUuid(uuid)); + createUUID(); } - }; -export const loadDashboardFromNeo4jByUUIDThunk = (driver, database, uuid, callback) => (dispatch: any) => { + const { title, version } = dashboard; + + // Generate a cypher query to save the dashboard. + const query = + 'MERGE (n:_Neodash_Dashboard {uuid: $uuid }) SET n.title = $title, n.version = $version, n.user = $user, n.content = $content, n.date = datetime($date) RETURN $uuid as uuid'; + + const parameters = { + uuid: uuid, + title: title, + version: version, + user: user, + content: JSON.stringify(dashboard, null, 2), + date: date, + }; + runCypherQuery( + driver, + database, + query, + parameters, + 1, + () => {}, + (records) => { + if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { + dispatch(createNotificationThunk('🎉 Success!', 'Your current dashboard was saved to Neo4j.')); + + onSuccess(uuid); + } else { + console.log(records); + dispatch( + createNotificationThunk( + 'Unable to save dashboard', + `Do you have write access to the '${database}' database?` + ) + ); + } + } + ); + } catch (e) { + dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e)); + } +}; + +export const deleteDashboardFromNeo4jThunk = (driver, database, uuid, onSuccess) => (dispatch: any) => { + try { + // Generate a cypher query to save the dashboard. + const query = 'MATCH (n:_Neodash_Dashboard {uuid: $uuid }) DETACH DELETE n RETURN $uuid as uuid'; + + const parameters = { + uuid: uuid, + }; + runCypherQuery( + driver, + database, + query, + parameters, + 1, + () => {}, + (records) => { + if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { + onSuccess(uuid); + } else { + console.log(records); + dispatch( + createNotificationThunk( + 'Unable to delete dashboard', + `Do you have write access to the '${database}' database?` + ) + ); + } + } + ); + } catch (e) { + dispatch(createNotificationThunk('Unable to delete dashboard from Neo4j', e)); + } +}; + +export const loadDashboardFromNeo4jThunk = (driver, database, uuid, callback) => (dispatch: any) => { try { const query = 'MATCH (n:_Neodash_Dashboard) WHERE n.uuid = $uuid RETURN n.content as dashboard'; runCypherQuery( @@ -207,17 +256,27 @@ export const loadDashboardFromNeo4jByUUIDThunk = (driver, database, uuid, callba query, { uuid: uuid }, 1, - () => {}, + (status) => { + if (status == QueryStatus.NO_DATA) { + dispatch( + createNotificationThunk( + `Unable to load dashboard from database '${database}'.`, + `A dashboard with UUID '${uuid}' does not exist.` + ) + ); + } + }, (records) => { if (!records[0]._fields) { dispatch( createNotificationThunk( `Unable to load dashboard from database '${database}'.`, - `A dashboard with UUID '${uuid}' could not be found.` + `A dashboard with UUID '${uuid}' could not be loaded.` ) ); + } else { + callback(records[0]._fields[0]); } - callback(records[0]._fields[0]); } ); } catch (e) { @@ -265,7 +324,7 @@ export const loadDashboardListFromNeo4jThunk = (driver, database, callback) => ( runCypherQuery( driver, database, - 'MATCH (n:_Neodash_Dashboard) RETURN n.uuid as id, n.title as title, toString(n.date) as date, n.user as author, n.version as version ORDER BY date DESC', + 'MATCH (n:_Neodash_Dashboard) RETURN n.uuid as uuid, n.title as title, toString(n.date) as date, n.user as author, n.version as version ORDER BY date DESC', {}, 1000, () => {}, @@ -274,13 +333,14 @@ export const loadDashboardListFromNeo4jThunk = (driver, database, callback) => ( callback([]); return; } - const result = records.map((r) => { + const result = records.map((r, index) => { return { - id: r._fields[0], + uuid: r._fields[0], title: r._fields[1], date: r._fields[2], author: r._fields[3], version: r._fields[4], + index: index, }; }); callback(result); @@ -312,6 +372,13 @@ export const loadDatabaseListFromNeo4jThunk = (driver, callback) => (dispatch: a } }; +export const assignDashboardUuidIfNotPresentThunk = () => (dispatch: any, getState: any) => { + const { uuid } = getState().dashboard; + if (!uuid) { + dispatch(setDashboardUuid(createUUID())); + } +}; + export function upgradeDashboardVersion(dashboard: any, origin: string, target: string) { if (origin == '2.2' && target == '2.3') { dashboard.pages.forEach((p) => { diff --git a/src/dashboard/header/DashboardHeader.tsx b/src/dashboard/header/DashboardHeader.tsx index a218bc4be..c67d37c2c 100644 --- a/src/dashboard/header/DashboardHeader.tsx +++ b/src/dashboard/header/DashboardHeader.tsx @@ -47,7 +47,7 @@ export const NeoDashboardHeader = ({ }, [isDarkMode]); const content = ( -
+
diff --git a/src/dashboard/header/DashboardHeaderPageTitle.tsx b/src/dashboard/header/DashboardHeaderPageTitle.tsx index 8bdbcaef6..86eb241bb 100644 --- a/src/dashboard/header/DashboardHeaderPageTitle.tsx +++ b/src/dashboard/header/DashboardHeaderPageTitle.tsx @@ -61,7 +61,11 @@ export const DashboardHeaderPageTitle = ({ title, tabIndex, removePage, setPageT
{!editing ? ( - title + title ? ( + title + ) : ( + '(no title)' + ) ) : ( ) : (
- {dashboardTitle} + {dashboardTitle ? dashboardTitle : '(no title)'} {editable ? ( + + {/* Saving, loading, extensions, sharing is only enabled when the dashboard is editable. */} - {editable ? ( - <> - - - - {renderExtensionsButtons()} - - ) : ( - <> - )} + {editable ? <>{renderExtensionsButtons()} : <>}
diff --git a/src/dashboard/sidebar/DashboardSidebar.tsx b/src/dashboard/sidebar/DashboardSidebar.tsx new file mode 100644 index 000000000..1647ee831 --- /dev/null +++ b/src/dashboard/sidebar/DashboardSidebar.tsx @@ -0,0 +1,493 @@ +import React, { useContext, useState } from 'react'; +import { connect } from 'react-redux'; +import { getDashboardIsEditable, getPageNumber } from '../../settings/SettingsSelectors'; +import { getDashboardSettings, getDashboardTitle } from '../DashboardSelectors'; +import { Button, SideNavigation, SideNavigationGroupHeader, SideNavigationList, TextInput } from '@neo4j-ndl/react'; +import { removeReportThunk } from '../../page/PageThunks'; +import { PlusIconOutline, MagnifyingGlassIconOutline, CircleStackIconOutline } from '@neo4j-ndl/react/icons'; +import Tooltip from '@mui/material/Tooltip'; +import { DashboardSidebarListItem } from './DashboardSidebarListItem'; +import { + applicationGetConnection, + applicationGetConnectionDatabase, + applicationIsStandalone, + dashboardIsDraft, +} from '../../application/ApplicationSelectors'; +import { setDraft } from '../../application/ApplicationActions'; +import NeoDashboardSidebarLoadModal from './modal/DashboardSidebarLoadModal'; +import { resetDashboardState } from '../DashboardActions'; +import NeoDashboardSidebarCreateModal from './modal/DashboardSidebarCreateModal'; +import NeoDashboardSidebarDatabaseMenu from './menu/DashboardSidebarDatabaseMenu'; +import NeoDashboardSidebarDashboardMenu from './menu/DashboardSidebarDashboardMenu'; +import { + deleteDashboardFromNeo4jThunk, + loadDashboardFromNeo4jThunk, + loadDashboardListFromNeo4jThunk, + loadDashboardThunk, + loadDatabaseListFromNeo4jThunk, + saveDashboardToNeo4jThunk, +} from '../DashboardThunks'; +import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; +import NeoDashboardSidebarSaveModal from './modal/DashboardSidebarSaveModal'; +import { getDashboardJson } from '../../modal/ModalSelectors'; +import NeoDashboardSidebarCreateMenu from './menu/DashboardSidebarCreateMenu'; +import NeoDashboardSidebarImportModal from './modal/DashboardSidebarImportModal'; +import { createUUID } from '../../utils/uuid'; +import NeoDashboardSidebarExportModal from './modal/DashboardSidebarExportModal'; +import NeoDashboardSidebarDeleteModal from './modal/DashboardSidebarDeleteModal'; +import NeoDashboardSidebarInfoModal from './modal/DashboardSidebarInfoModal'; +import NeoDashboardSidebarShareModal from './modal/DashboardSidebarShareModal'; +import LegacyShareModal from './modal/legacy/LegacyShareModal'; + +enum Menu { + DASHBOARD, + DATABASE, + CREATE, + NONE, +} + +enum Modal { + CREATE, + IMPORT, + EXPORT, + DELETE, + SHARE, + SHARE_LEGACY, + INFO, + LOAD, + SAVE, + NONE, +} + +/** + * A component responsible for rendering the sidebar on the left of the screen. + */ +export const NeoDashboardSidebar = ({ + database, + connection, + title, + readonly, + draft, + setDraft, + dashboard, + resetLocalDashboard, + loadDashboard, + loadDatabaseListFromNeo4j, + loadDashboardListFromNeo4j, + loadDashboardFromNeo4j, + saveDashboardToNeo4j, + deleteDashboardFromNeo4j, +}) => { + const { driver } = useContext(Neo4jContext); + const [expanded, setOnExpanded] = useState(false); + const [selectedDashboardIndex, setSelectedDashboardIndex] = React.useState(-1); + const [dashboardDatabase, setDashboardDatabase] = React.useState(database ? database : 'neo4j'); + const [databases, setDatabases] = useState([]); + const [inspectedIndex, setInspectedIndex] = useState(-1); + const [searchText, setSearchText] = useState(''); + const [menuAnchor, setMenuAnchor] = useState(null); + const [menuOpen, setMenuOpen] = useState(Menu.NONE); + const [modalOpen, setModalOpen] = useState(Modal.NONE); + const [dashboards, setDashboards] = React.useState([]); + const [cachedDashboard, setCachedDashboard] = React.useState(''); + + const getDashboardListFromNeo4j = () => { + // Retrieves list of all dashboards stored in a given database. + loadDashboardListFromNeo4j(driver, dashboardDatabase, (list) => { + setDashboards(list); + + // Update the UI to reflect the currently selected dashboard. + if (dashboard && dashboard.uuid) { + const index = list.findIndex((element) => element.uuid == dashboard.uuid); + setSelectedDashboardIndex(index); + if (index == -1) { + // If we can't find the currently dashboard in the database, we are drafting a new one. + setDraft(true); + } + } + }); + }; + + function createDashboard() { + // Creates new dashboard in draft state (not yet saved to Neo4j) + resetLocalDashboard(); + setDraft(true); + } + + function deleteDashboard(uuid) { + // Creates new dashboard in draft state (not yet saved to Neo4j) + deleteDashboardFromNeo4j(driver, dashboardDatabase, uuid, () => { + if (uuid == dashboard.uuid) { + setSelectedDashboardIndex[0]; + resetLocalDashboard(); + setDraft(true); + } + setTimeout(() => { + getDashboardListFromNeo4j(); + }, 100); + }); + } + + return ( +
+ { + saveDashboardToNeo4j( + driver, + dashboardDatabase, + dashboard, + new Date().toISOString(), + connection.username, + () => { + // After saving successfully, refresh the list after a small delay. + // The new dashboard will always be on top (the latest), so we select index 0. + setDashboards([]); + setTimeout(() => { + getDashboardListFromNeo4j(); + setSelectedDashboardIndex(0); + setDraft(false); + }, 100); + } + ); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.LOAD); + const { uuid } = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, uuid, (file) => { + loadDashboard(uuid, file); + setSelectedDashboardIndex(inspectedIndex); + setDraft(false); + }); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + }} + onLegacyShareClicked={() => setModalOpen(Modal.SHARE_LEGACY)} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + setModalOpen(Modal.NONE)} /> + + { + setModalOpen(Modal.NONE); + createDashboard(); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + if (dashboards[inspectedIndex]) { + deleteDashboard(dashboards[inspectedIndex].uuid); + } + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + setDraft(true); + loadDashboard(createUUID(), text); + }} + handleClose={() => setModalOpen(Modal.NONE)} + /> + + { + setModalOpen(Modal.NONE); + setCachedDashboard(''); + }} + /> + + { + setModalOpen(Modal.NONE); + setCachedDashboard(''); + }} + /> + + { + setOnExpanded(open); + if (open) { + getDashboardListFromNeo4j(); + } + // Wait until the sidebar has fully opened. Then trigger a resize event to align the grid layout. + const timeout = setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 300); + }} + > + + { + setDashboardDatabase(newDatabase); + // We changed the active dashboard database, reload the list in the sidebar. + loadDashboardListFromNeo4j(driver, newDatabase, (list) => { + setDashboards(list); + setDraft(true); + }); + }} + open={menuOpen == Menu.DATABASE} + anchorEl={menuAnchor} + handleClose={() => { + setMenuOpen(Menu.NONE); + setMenuAnchor(null); + }} + /> + { + setMenuOpen(Menu.NONE); + const d = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (text) => { + setCachedDashboard(JSON.parse(text)); + }); + setModalOpen(Modal.INFO); + }} + handleLoadClicked={() => { + setMenuOpen(Menu.NONE); + if (draft) { + setModalOpen(Modal.LOAD); + } else { + const d = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => { + loadDashboard(d.uuid, file); + setSelectedDashboardIndex(inspectedIndex); + }); + } + }} + handleExportClicked={() => { + setMenuOpen(Menu.NONE); + const d = dashboards[inspectedIndex]; + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (text) => { + setCachedDashboard(JSON.parse(text)); + }); + setModalOpen(Modal.EXPORT); + }} + handleShareClicked={() => { + setMenuOpen(Menu.NONE); + setModalOpen(Modal.SHARE); + }} + handleDeleteClicked={() => { + setMenuOpen(Menu.NONE); + setModalOpen(Modal.DELETE); + }} + handleClose={() => { + setMenuOpen(Menu.NONE); + setMenuAnchor(null); + }} + /> + + { + setMenuOpen(Menu.NONE); + if (draft) { + setModalOpen(Modal.CREATE); + } else { + createDashboard(); + } + }} + handleImportClicked={() => { + setMenuOpen(Menu.NONE); + if (draft) { + setModalOpen(Modal.IMPORT); + } else { + setModalOpen(Modal.IMPORT); + } + }} + handleClose={() => { + setMenuOpen(Menu.NONE); + setMenuAnchor(null); + }} + /> + + +
+ + Dashboards + + {/* Only let users create dashboards and change database when running in editor mode. */} + {readonly == false ? ( + <> + + + + + + + + + ) : ( + <> + )} +
+
+
+ + + } + className='n-w-full n-mr-2' + placeholder='Search...' + aria-label='Search' + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + /> + + {draft && !readonly ? ( + {}} + onSave={() => setModalOpen(Modal.SAVE)} + onSettingsOpen={() => {}} + /> + ) : ( + <> + )} + {dashboards + .filter((d) => d.title.toLowerCase().includes(searchText.toLowerCase())) + .map((d) => { + // index stored in list + return ( + { + if (draft) { + setInspectedIndex(d.index); + setModalOpen(Modal.LOAD); + } else { + loadDashboardFromNeo4j(driver, dashboardDatabase, d.uuid, (file) => { + loadDashboard(d.uuid, file); + setSelectedDashboardIndex(d.index); + }); + } + }} + onSave={() => {}} + onSettingsOpen={(event) => { + setInspectedIndex(d.index); + setMenuOpen(Menu.DASHBOARD); + setMenuAnchor(event.currentTarget); + }} + /> + ); + })} + +
+
+ ); +}; + +const mapStateToProps = (state) => ({ + readonly: applicationIsStandalone(state), + connection: applicationGetConnection(state), + pagenumber: getPageNumber(state), + title: getDashboardTitle(state), + editable: getDashboardIsEditable(state), + draft: dashboardIsDraft(state), + dashboard: getDashboardJson(state), + dashboardSettings: getDashboardSettings(state), + database: applicationGetConnectionDatabase(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + onRemovePressed: (id) => dispatch(removeReportThunk(id)), + resetLocalDashboard: () => dispatch(resetDashboardState()), + setDraft: (draft) => dispatch(setDraft(draft)), + loadDashboard: (uuid, text) => dispatch(loadDashboardThunk(uuid, text)), + loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), + loadDashboardFromNeo4j: (driver, database, uuid, callback) => + dispatch(loadDashboardFromNeo4jThunk(driver, database, uuid, callback)), + loadDashboardListFromNeo4j: (driver, database, callback) => + dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)), + saveDashboardToNeo4j: (driver: any, database: string, dashboard: any, date: any, user: any, onSuccess) => { + dispatch(saveDashboardToNeo4jThunk(driver, database, dashboard, date, user, onSuccess)); + }, + deleteDashboardFromNeo4j: (driver: any, database: string, uuid: string, onSuccess) => { + dispatch(deleteDashboardFromNeo4jThunk(driver, database, uuid, onSuccess)); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoDashboardSidebar); diff --git a/src/dashboard/sidebar/DashboardSidebarListItem.tsx b/src/dashboard/sidebar/DashboardSidebarListItem.tsx new file mode 100644 index 000000000..436371ae3 --- /dev/null +++ b/src/dashboard/sidebar/DashboardSidebarListItem.tsx @@ -0,0 +1,73 @@ +import { Button, IconButton, SideNavigationGroupHeader } from '@neo4j-ndl/react'; +import React from 'react'; +import { + CloudArrowDownIconOutline, + CloudArrowUpIconOutline, + EllipsisVerticalIconOutline, +} from '@neo4j-ndl/react/icons'; +import Tooltip from '@mui/material/Tooltip'; + +export const DashboardSidebarListItem = ({ title, selected, readonly, saved, onSelect, onSave, onSettingsOpen }) => { + return ( + +
+ + {readonly !== true ? ( + { + saved == false ? onSave() : onSettingsOpen(event); + }} + > + {saved == true ? ( + + + + ) : ( + + + + )} + + ) : ( + <> + )} +
+
+ ); +}; diff --git a/src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx b/src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx new file mode 100644 index 000000000..d7e06238a --- /dev/null +++ b/src/dashboard/sidebar/menu/DashboardSidebarCreateMenu.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react'; +import { DocumentTextIconOutline, PlusCircleIconOutline } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarCreateMenu = ({ + anchorEl, + open, + handleNewClicked, + handleImportClicked, + handleClose, +}) => { + return ( + + + + + + + ); +}; + +export default NeoDashboardSidebarCreateMenu; diff --git a/src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx b/src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx new file mode 100644 index 000000000..aa9956c11 --- /dev/null +++ b/src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Menu, MenuItem, MenuItems } from '@neo4j-ndl/react'; +import { + CloudArrowUpIconOutline, + DocumentDuplicateIconOutline, + DocumentTextIconOutline, + InformationCircleIconOutline, + ShareIconOutline, + TrashIconOutline, +} from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarDashboardMenu = ({ + anchorEl, + open, + handleInfoClicked, + handleLoadClicked, + handleExportClicked, + handleShareClicked, + handleDeleteClicked, + handleClose, +}) => { + return ( + + + } title='Info' /> + } title='Load' /> + {/* {}} icon={} title='Clone' /> */} + } title='Export' /> + } title='Share' /> + } title='Delete' /> + + + ); +}; + +export default NeoDashboardSidebarDashboardMenu; diff --git a/src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx b/src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx new file mode 100644 index 000000000..d3c783e17 --- /dev/null +++ b/src/dashboard/sidebar/menu/DashboardSidebarDatabaseMenu.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Button, Dialog, Menu, MenuItem, MenuItems } from '@neo4j-ndl/react'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarDatabaseMenu = ({ anchorEl, open, handleClose, databases, selected, setSelected }) => { + return ( + + + {databases.map((d) => { + return ( + { + setSelected(d); + }} + title={d} + style={ + d == selected + ? { + borderWidth: '1px', + borderStyle: 'solid', + color: 'rgb(var(--palette-primary-bg-strong))', + borderColor: 'rgb(var(--palette-primary-bg-strong))', + borderRadius: '8px', + } + : {} + } + /> + ); + })} + + + ); +}; + +export default NeoDashboardSidebarDatabaseMenu; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx new file mode 100644 index 000000000..4f998a09c --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarCreateModal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { BackspaceIconOutline, ExclamationTriangleIconOutline } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarCreateModal = ({ open, onConfirm, handleClose }) => { + return ( + + Discard Draft? + + Creating a new dashboard will delete your current draft. Save the draft first to ensure your dashboard is + stored. + + + + + + + ); +}; + +export default NeoDashboardSidebarCreateModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx new file mode 100644 index 000000000..023ac2ce6 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarDeleteModal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { BackspaceIconOutline, TrashIconSolid } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarDeleteModal = ({ open, title, onConfirm, handleClose }) => { + return ( + + Delete Dashboard '{title}'? + + Are you sure you want to delete this dashboard?
This action cannot be undone. +
+ + + + +
+ ); +}; + +export default NeoDashboardSidebarDeleteModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx new file mode 100644 index 000000000..b458bba55 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarExportModal.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { DocumentArrowDownIconOutline } from '@neo4j-ndl/react/icons'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { valueIsArray, valueIsObject } from '../../../chart/ChartUtils'; +import { TextareaAutosize } from '@mui/material'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarExportModal = ({ open, dashboard, handleClose }) => { + /** + * Removes the specified set of keys from the nested dictionary. + */ + const filterNestedDict = (value: any, removedKeys: any[]) => { + if (value == undefined) { + return value; + } + + if (valueIsArray(value)) { + return value.map((v) => filterNestedDict(v, removedKeys)); + } + + if (valueIsObject(value)) { + const newValue = {}; + Object.keys(value).forEach((k) => { + if (removedKeys.indexOf(k) != -1) { + newValue[k] = undefined; + } else { + newValue[k] = filterNestedDict(value[k], removedKeys); + } + }); + return newValue; + } + return value; + }; + + const filteredDashboard = filterNestedDict(dashboard, [ + 'fields', + 'settingsOpen', + 'advancedSettingsOpen', + 'collapseTimeout', + 'apiKey', // Added for query-translator extension + ]); + + const dashboardString = JSON.stringify(filteredDashboard, null, 2); + const downloadDashboard = () => { + const element = document.createElement('a'); + const file = new Blob([dashboardString], { type: 'text/plain' }); + element.href = URL.createObjectURL(file); + element.download = 'dashboard.json'; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + }; + + return ( + + Export Dashboard + + Export your dashboard as a JSON file, or copy-paste the file from here. +
+ +
+
+ +
+
+ ); +}; + +export default NeoDashboardSidebarExportModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx new file mode 100644 index 000000000..32eaed726 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarImportModal.tsx @@ -0,0 +1,73 @@ +import React, { useRef } from 'react'; +import { PlayIconSolid, DocumentPlusIconOutline } from '@neo4j-ndl/react/icons'; +import { Button, Checkbox, Dialog, Dropdown } from '@neo4j-ndl/react'; +import TextareaAutosize from '@mui/material/TextareaAutosize'; + +export const NeoDashboardSidebarImportModal = ({ open, onImport, handleClose }) => { + const [text, setText] = React.useState(''); + const loadFromFile = useRef(null); + + const reader = new FileReader(); + reader.onload = (e) => { + setText(e.target.result); + }; + + return ( + + Import Dashboard + + Import your dashboard from a JSON file, or copy-paste the save file here. +
+ Importing will discard your current draft, if any. +

+
+ setText(e.target.value)} + value={text} + aria-label='' + placeholder='Paste a dashboard JSON file here...' + /> + + + + +
+ ); +}; + +export default NeoDashboardSidebarImportModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx new file mode 100644 index 000000000..457adc3f1 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarInfoModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Dialog } from '@neo4j-ndl/react'; +import { DataGrid } from '@mui/x-data-grid'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarInfoModal = ({ open, dashboard, handleClose }) => { + const columns = [ + { field: 'field', headerName: 'Field', width: 150 }, + { field: 'value', headerName: 'Value', width: 600 }, + ]; + + const rows = dashboard + ? [ + { id: 0, field: 'ID', value: dashboard.uuid }, + { id: 1, field: 'Title', value: dashboard.title }, + { id: 2, field: 'Last Modified', value: dashboard.date }, + { id: 3, field: 'Author', value: dashboard.author }, + { id: 4, field: 'Version', value: dashboard.version }, + ] + : []; + + return ( + + About '{dashboard && dashboard.title}' + +
+ <>, + ColumnSortedAscendingIcon: () => <>, + }} + /> +
+
+
+ ); +}; + +export default NeoDashboardSidebarInfoModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx new file mode 100644 index 000000000..17da9b9bb --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarLoadModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Button, Dialog } from '@neo4j-ndl/react'; +import { BackspaceIconOutline, ExclamationTriangleIconOutline, TrashIconOutline } from '@neo4j-ndl/react/icons'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarLoadModal = ({ open, onConfirm, handleClose }) => { + return ( + + Discard Draft? + + Switching your active dashboard will delete your current draft. +
+ Save the draft first to ensure your dashboard is stored. +
+ + + + +
+ ); +}; + +export default NeoDashboardSidebarLoadModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx new file mode 100644 index 000000000..a307c5570 --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarSaveModal.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { DatabaseAddCircleIcon, BackspaceIconOutline } from '@neo4j-ndl/react/icons'; +import { Button, Dialog } from '@neo4j-ndl/react'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarSaveModal = ({ open, onConfirm, handleClose }) => { + return ( + + Save to Neo4j + + This will save your current draft as a node to your Neo4j database. +
+ Ensure you have write permissions to the database to use this feature. +
+ + + + +
+ ); +}; + +export default NeoDashboardSidebarSaveModal; diff --git a/src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx b/src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx new file mode 100644 index 000000000..95dd9acde --- /dev/null +++ b/src/dashboard/sidebar/modal/DashboardSidebarShareModal.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Checkbox, Dialog, TextLink } from '@neo4j-ndl/react'; + +/** + * Configures setting the current Neo4j database connection for the dashboard. + */ +export const NeoDashboardSidebarShareModal = ({ + uuid, + dashboardDatabase, + connection, + open, + onLegacyShareClicked, + handleClose, +}) => { + const shareBaseURL = 'http://neodash.graphapp.io'; + const shareBaseURLAlternative = 'https://neodash.graphapp.io'; + const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin; + const [selfHosted, setSelfHosted] = React.useState(false); + const [standalone, setStandalone] = React.useState(false); + const [includeCredentials, setIncludeCredentials] = React.useState(false); + + function getShareURL() { + const prefix = selfHosted ? shareLocalURL : shareBaseURL; + const id = encodeURIComponent(uuid); + const db = encodeURIComponent(dashboardDatabase); + const suffix1 = includeCredentials + ? `&credentials=${encodeURIComponent( + `${connection.protocol}://${connection.username}:${connection.password}@${connection.database}:${connection.url}:${connection.port}` + )}` + : ''; + const suffix2 = standalone ? `&standalone=Yes` : ''; + return `${prefix}/?share&type=database&id=${id}&dashboardDatabase=${db}${suffix1}${suffix2}`; + } + + return ( + + Share Dashboard + + This screen lets you create a one-off, direct link for your dashboard. Click{' '} + here to use legacy file-sharing instead. + + + {shareLocalURL !== shareBaseURL && shareLocalURL !== shareBaseURLAlternative ? ( + { + setSelfHosted(!selfHosted); + }} + /> + ) : ( + <> + )} + { + setStandalone(!standalone); + }} + /> + + { + setIncludeCredentials(!includeCredentials); + }} + /> + +
+ Your Temporary Link: +
+ + + {' '} + {getShareURL()}{' '} + +
+ {includeCredentials ? Caution: this link embeds your current database credentials. : <>} +
+
+
+ ); +}; + +export default NeoDashboardSidebarShareModal; diff --git a/src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx b/src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx new file mode 100644 index 000000000..f1fa344ee --- /dev/null +++ b/src/dashboard/sidebar/modal/legacy/LegacyShareModal.tsx @@ -0,0 +1,212 @@ +import React, { useContext } from 'react'; + +import { connect } from 'react-redux'; +import { DataGrid } from '@mui/x-data-grid'; +import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; +import NeoSetting from '../../../../component/field/Setting'; +import { applicationGetConnection } from '../../../../application/ApplicationSelectors'; +import { SELECTION_TYPES } from '../../../../config/CardConfig'; +import { MenuItem, Button, Dialog, Dropdown, TextLink } from '@neo4j-ndl/react'; +import { + ShareIconOutline, + PlayIconSolid, + DocumentCheckIconOutline, + DatabaseAddCircleIcon, +} from '@neo4j-ndl/react/icons'; + +const shareBaseURL = 'http://neodash.graphapp.io'; +const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin; + +export const NeoShareModal = ({ open, handleClose, connection }) => { + const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false); + const [loadFromFileModalOpen, setLoadFromFileModalOpen] = React.useState(false); + const [rows, setRows] = React.useState([]); + const { driver } = useContext(Neo4jContext); + + // One of [null, database, file] + const shareType = 'url'; + const [shareID, setShareID] = React.useState(null); + const [shareName, setShareName] = React.useState(null); + const [shareConnectionDetails, setShareConnectionDetails] = React.useState('No'); + const [shareStandalone, setShareStandalone] = React.useState('No'); + const [selfHosted, setSelfHosted] = React.useState('No'); + const [shareLink, setShareLink] = React.useState(null); + + const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); + + const columns = [ + { field: 'uuid', hide: true, headerName: 'ID', width: 150 }, + { field: 'date', headerName: 'Date', width: 200 }, + { field: 'title', headerName: 'Title', width: 370 }, + { field: 'author', headerName: 'Author', width: 160 }, + { + field: 'load', + headerName: ' ', + renderCell: (c) => { + return ( + + ); + }, + width: 130, + }, + ]; + + return ( + + + + Share Dashboard File + + + This window lets you create a temporary share link for your dashboard. Keep in mind that share links are not + intended as a way to publish your dashboard for users, see the  + + documentation + {' '} + for more on publishing. +
+
+
+ To share a dashboard file directly, make it accessible{' '} + + online + + .
Then, paste the direct link here: + { + setShareLink(null); + setShareID(e); + }} + /> + {shareID ? ( + <> +
+ { + if ((e == 'No') & (shareStandalone == 'Yes')) { + return; + } + setShareLink(null); + setShareConnectionDetails(e); + }} + /> + {shareLocalURL != shareBaseURL ? ( + { + setShareLink(null); + setShareStandalone(e); + if (e == 'Yes') { + setShareConnectionDetails('Yes'); + } + }} + /> + ) : ( + <> + )} + { + setShareLink(null); + setSelfHosted(e); + }} + /> + + + ) : ( + <> + )} + {shareLink ? ( + <> +
+ Use the generated link to view the dashboard: +
+ + {shareLink} + +
+ + ) : ( + <> + )} +
+
+ ); +}; + +const mapStateToProps = (state) => ({ + connection: applicationGetConnection(state), +}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoShareModal); diff --git a/src/modal/ExportModal.tsx b/src/modal/ExportModal.tsx new file mode 100644 index 000000000..877f34558 --- /dev/null +++ b/src/modal/ExportModal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { MenuItem } from '@neo4j-ndl/react'; +import NeoDashboardSidebarExportModal from '../dashboard/sidebar/modal/DashboardSidebarExportModal'; +import { getDashboardJson } from './ModalSelectors'; +import { DocumentTextIconOutline } from '@neo4j-ndl/react/icons'; +/** + * A modal to save a dashboard as a JSON text string. + * The button to open the modal is intended to use in a drawer at the side of the page. + */ +export const NeoExportModal = ({ dashboard }) => { + const [open, setOpen] = React.useState(false); + return ( +
+ setOpen(true)} icon={} title='Export' /> + { + setOpen(false); + }} + /> +
+ ); +}; + +const mapStateToProps = (state) => ({ + dashboard: getDashboardJson(state), +}); + +const mapDispatchToProps = () => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(NeoExportModal); diff --git a/src/modal/LoadModal.tsx b/src/modal/LoadModal.tsx deleted file mode 100644 index 1331612ae..000000000 --- a/src/modal/LoadModal.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useContext, useRef } from 'react'; -import { TextareaAutosize } from '@mui/material'; -import { connect } from 'react-redux'; -import { - loadDashboardFromNeo4jByUUIDThunk, - loadDashboardListFromNeo4jThunk, - loadDashboardThunk, - loadDatabaseListFromNeo4jThunk, -} from '../dashboard/DashboardThunks'; -import { DataGrid } from '@mui/x-data-grid'; -import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; -import { MenuItem, Button, Dialog, Dropdown } from '@neo4j-ndl/react'; -import { - CloudArrowUpIconOutline, - PlayIconSolid, - DatabaseAddCircleIcon, - DocumentPlusIconOutline, -} from '@neo4j-ndl/react/icons'; - -/** - * A modal to save a dashboard as a JSON text string. - * The button to open the modal is intended to use in a drawer at the side of the page. - */ - -export const NeoLoadModal = ({ - loadDashboard, - loadDatabaseListFromNeo4j, - loadDashboardFromNeo4j, - loadDashboardListFromNeo4j, -}) => { - const [loadModalOpen, setLoadModalOpen] = React.useState(false); - const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false); - const [text, setText] = React.useState(''); - const [rows, setRows] = React.useState([]); - const { driver } = useContext(Neo4jContext); - const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); - const [databases, setDatabases] = React.useState(['neo4j']); - const loadFromFile = useRef(null); - - const handleClickOpen = () => { - setLoadModalOpen(true); - }; - - const handleClose = () => { - setLoadModalOpen(false); - }; - - const handleCloseAndLoad = () => { - setLoadModalOpen(false); - loadDashboard(text); - setText(''); - }; - - function handleDashboardLoadedFromNeo4j(result) { - setText(result); - setLoadFromNeo4jModalOpen(false); - } - - const reader = new FileReader(); - reader.onload = (e) => { - setText(e.target.result); - }; - - const uploadDashboard = (e) => { - e.preventDefault(); - reader.readAsText(e.target.files[0]); - }; - - const columns = [ - { field: 'id', hide: true, headerName: 'ID', width: 150 }, - { field: 'date', headerName: 'Date', width: 200 }, - { field: 'title', headerName: 'Title', width: 300 }, - { field: 'author', headerName: 'Author', width: 160 }, - { field: 'version', headerName: 'Version', width: 85 }, - { - field: 'load', - headerName: 'Select', - renderCell: (c) => { - return ( - - ); - }, - width: 130, - }, - ]; - - return ( - <> - } /> - - - - - Load Dashboard - - -
- - - - -
- - setText(e.target.value)} - value={text} - aria-label='' - placeholder='Select a dashboard first, then preview it here...' - /> -
-
- { - setLoadFromNeo4jModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Select from Neo4j - If dashboards are saved in your current database, choose a dashboard below. - -
- <>, - ColumnSortedAscendingIcon: () => <>, - }} - /> -
- { - setRows([]); - setDashboardDatabase(newValue.value); - loadDashboardListFromNeo4j(driver, newValue.value, (result) => { - setRows(result); - }); - }, - options: databases.map((database) => ({ label: database, value: database })), - value: { label: dashboardDatabase, value: dashboardDatabase }, - menuPlacement: 'auto', - }} - style={{ width: '150px', marginTop: '-65px' }} - > -
-
- - ); -}; - -const mapStateToProps = () => ({}); - -const mapDispatchToProps = (dispatch) => ({ - loadDashboard: (text) => dispatch(loadDashboardThunk(text)), - loadDashboardFromNeo4j: (driver, database, uuid, callback) => - dispatch(loadDashboardFromNeo4jByUUIDThunk(driver, database, uuid, callback)), - loadDashboardListFromNeo4j: (driver, database, callback) => - dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)), - loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NeoLoadModal); diff --git a/src/modal/SaveModal.tsx b/src/modal/SaveModal.tsx deleted file mode 100644 index 41383e830..000000000 --- a/src/modal/SaveModal.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import React, { useContext, useEffect } from 'react'; -import { FormControl, TextareaAutosize, Tooltip } from '@mui/material'; -import { connect } from 'react-redux'; -import { getDashboardJson } from './ModalSelectors'; -import { valueIsArray, valueIsObject } from '../chart/ChartUtils'; -import { applicationGetConnection } from '../application/ApplicationSelectors'; -import { loadDatabaseListFromNeo4jThunk, saveDashboardToNeo4jThunk } from '../dashboard/DashboardThunks'; -import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; -import { - CloudArrowDownIconOutline, - DatabaseAddCircleIcon, - DocumentArrowDownIconOutline, - BackspaceIconOutline, -} from '@neo4j-ndl/react/icons'; -import { Button, Checkbox, Dialog, Dropdown, MenuItem } from '@neo4j-ndl/react'; - -/** - * Removes the specified set of keys from the nested dictionary. - */ -const filterNestedDict = (value: any, removedKeys: any[]) => { - if (value == undefined) { - return value; - } - - if (valueIsArray(value)) { - return value.map((v) => filterNestedDict(v, removedKeys)); - } - - if (valueIsObject(value)) { - const newValue = {}; - Object.keys(value).forEach((k) => { - if (removedKeys.indexOf(k) != -1) { - newValue[k] = undefined; - } else { - newValue[k] = filterNestedDict(value[k], removedKeys); - } - }); - return newValue; - } - return value; -}; - -/** - * A modal to save a dashboard as a JSON text string. - * The button to open the modal is intended to use in a drawer at the side of the page. - */ - -export const NeoSaveModal = ({ dashboard, connection, saveDashboardToNeo4j, loadDatabaseListFromNeo4j }) => { - const [saveModalOpen, setSaveModalOpen] = React.useState(false); - const [saveToNeo4jModalOpen, setSaveToNeo4jModalOpen] = React.useState(false); - const [overwriteExistingDashboard, setOverwriteExistingDashboard] = React.useState(false); - const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); - const [databases, setDatabases] = React.useState(['neo4j']); - - const { driver } = useContext(Neo4jContext); - - useEffect(() => { - loadDatabaseListFromNeo4j(driver, (result) => { - setDatabases(result); - }); - }, []); - - const handleClickOpen = () => { - setSaveModalOpen(true); - }; - - const handleClose = () => { - setSaveModalOpen(false); - }; - - const filteredDashboard = filterNestedDict(dashboard, [ - 'fields', - 'settingsOpen', - 'advancedSettingsOpen', - 'collapseTimeout', - 'apiKey', // Added for query-translator extension - ]); - - const dashboardString = JSON.stringify(filteredDashboard, null, 2); - const downloadDashboard = () => { - const element = document.createElement('a'); - const file = new Blob([dashboardString], { type: 'text/plain' }); - element.href = URL.createObjectURL(file); - element.download = 'dashboard.json'; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); - }; - - return ( - <> - } /> - - - - - Save Dashboard - - -
- - -
- -
-
- - { - setSaveToNeo4jModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Save to Neo4j - - This will save your current dashboard as a node to your active Neo4j database. -
- Ensure you have write permissions to the database to use this feature. - - { - newValue && setDashboardDatabase(newValue.value); - }, - options: databases.map((database) => ({ label: database, value: database })), - value: { label: dashboardDatabase, value: dashboardDatabase }, - menuPlacement: 'auto', - }} - style={{ width: '150px', display: 'inline-block' }} - > - - - setOverwriteExistingDashboard(!overwriteExistingDashboard)} - label='Overwrite' - /> - - -
- - - - -
- - ); -}; - -const mapStateToProps = (state) => ({ - dashboard: getDashboardJson(state), - connection: applicationGetConnection(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - saveDashboardToNeo4j: (driver: any, database: string, dashboard: any, date: any, user: any, overwrite: boolean) => { - dispatch(saveDashboardToNeo4jThunk(driver, database, dashboard, date, user, overwrite)); - }, - loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NeoSaveModal); diff --git a/src/modal/ShareModal.tsx b/src/modal/ShareModal.tsx deleted file mode 100644 index ff0392411..000000000 --- a/src/modal/ShareModal.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import React, { useContext } from 'react'; - -import { connect } from 'react-redux'; -import { DataGrid } from '@mui/x-data-grid'; -import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context'; -import NeoSetting from '../component/field/Setting'; -import { loadDashboardListFromNeo4jThunk, loadDatabaseListFromNeo4jThunk } from '../dashboard/DashboardThunks'; -import { applicationGetConnection } from '../application/ApplicationSelectors'; -import { SELECTION_TYPES } from '../config/CardConfig'; -import { MenuItem, Button, Dialog, Dropdown, TextLink } from '@neo4j-ndl/react'; -import { - ShareIconOutline, - PlayIconSolid, - DocumentCheckIconOutline, - DatabaseAddCircleIcon, -} from '@neo4j-ndl/react/icons'; - -const shareBaseURL = 'http://neodash.graphapp.io'; -const shareLocalURL = window.location.origin.startsWith('file') ? shareBaseURL : window.location.origin; - -export const NeoShareModal = ({ connection, loadDashboardListFromNeo4j, loadDatabaseListFromNeo4j }) => { - const [shareModalOpen, setShareModalOpen] = React.useState(false); - const [loadFromNeo4jModalOpen, setLoadFromNeo4jModalOpen] = React.useState(false); - const [loadFromFileModalOpen, setLoadFromFileModalOpen] = React.useState(false); - const [rows, setRows] = React.useState([]); - const { driver } = useContext(Neo4jContext); - - // One of [null, database, file] - const [shareType, setShareType] = React.useState('database'); - const [shareID, setShareID] = React.useState(null); - const [shareName, setShareName] = React.useState(null); - const [shareFileURL, setShareFileURL] = React.useState(''); - const [shareConnectionDetails, setShareConnectionDetails] = React.useState('No'); - const [shareStandalone, setShareStandalone] = React.useState('No'); - const [selfHosted, setSelfHosted] = React.useState('No'); - - const [shareLink, setShareLink] = React.useState(null); - - const [dashboardDatabase, setDashboardDatabase] = React.useState('neo4j'); - const [databases, setDatabases] = React.useState(['neo4j']); - - const handleClickOpen = () => { - setShareID(null); - setShareLink(null); - setShareModalOpen(true); - loadDatabaseListFromNeo4j(driver, (result) => { - setDatabases(result); - }); - }; - - const handleClose = () => { - setShareModalOpen(false); - }; - - const columns = [ - { field: 'id', hide: true, headerName: 'ID', width: 150 }, - { field: 'date', headerName: 'Date', width: 200 }, - { field: 'title', headerName: 'Title', width: 370 }, - { field: 'author', headerName: 'Author', width: 160 }, - { - field: 'load', - headerName: ' ', - renderCell: (c) => { - return ( - - ); - }, - width: 130, - }, - ]; - - return ( - <> - } /> - - - - - Share Dashboard - - - This window lets you create a temporary share link for your dashboard. Keep in mind that share links are not - intended as a way to publish your dashboard for users, see the  - - documentation - {' '} - for more on publishing. -
-
-
- Step 1: Select a dashboard to share. -
-
-
- - -
- {shareID ? `Selected dashboard: ${shareName}` : ''} -
- {shareID ? ( - <> - {' '} -
- Step 2: Configure sharing settings. -
-
- { - if ((e == 'No') & (shareStandalone == 'Yes')) { - return; - } - setShareLink(null); - setShareConnectionDetails(e); - }} - /> - {shareLocalURL != shareBaseURL ? ( - { - setShareLink(null); - setShareStandalone(e); - if (e == 'Yes') { - setShareConnectionDetails('Yes'); - } - }} - /> - ) : ( - <> - )} - { - setShareLink(null); - setSelfHosted(e); - }} - /> - -
- - ) : ( - <> - )} - {shareLink ? ( - <> -
- Step 3: Use the generated link to view the dashboard: -
- - {shareLink} - -
- - ) : ( - <> - )} -
-
- { - setLoadFromNeo4jModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Select From Neo4j - - Choose a dashboard to share below. -
- <>, - ColumnSortedAscendingIcon: () => <>, - }} - /> -
- { - setRows([]); - setDashboardDatabase(newValue.value); - loadDashboardListFromNeo4j(driver, newValue.value, (result) => { - setRows(result); - }); - }, - options: databases.map((database) => ({ label: database, value: database })), - value: { label: dashboardDatabase, value: dashboardDatabase }, - menuPlacement: 'auto', - }} - style={{ width: '150px' }} - > -
-
- { - setLoadFromFileModalOpen(false); - }} - aria-labelledby='form-dialog-title' - > - Select from URL - - To share a dashboard file directly, make it accessible{' '} - - online - - . Then, paste the direct link here: - { - setShareFileURL(e); - }} - /> -
- -
-
-
- - ); -}; - -const mapStateToProps = (state) => ({ - connection: applicationGetConnection(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - loadDashboardListFromNeo4j: (driver, database, callback) => - dispatch(loadDashboardListFromNeo4jThunk(driver, database, callback)), - loadDatabaseListFromNeo4j: (driver, callback) => dispatch(loadDatabaseListFromNeo4jThunk(driver, callback)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NeoShareModal); diff --git a/src/modal/UpgradeOldDashboardModal.tsx b/src/modal/UpgradeOldDashboardModal.tsx index c86dbb545..7ad40d1f6 100644 --- a/src/modal/UpgradeOldDashboardModal.tsx +++ b/src/modal/UpgradeOldDashboardModal.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { TextareaAutosize } from '@mui/material'; import { Button, Dialog } from '@neo4j-ndl/react'; import { TrashIconOutline, PlayIconSolid } from '@neo4j-ndl/react/icons'; +import { createUUID } from '../utils/uuid'; export const NeoUpgradeOldDashboardModal = ({ open, text, clearOldDashboard, loadDashboard }) => { return ( @@ -29,7 +30,7 @@ export const NeoUpgradeOldDashboardModal = ({ open, text, clearOldDashboard, loa
); - return !isLoaded ? loadingMessage : content; + return content; }; const mapStateToProps = (state) => ({ - isLoaded: true, pagenumber: getPageNumber(state), editable: getDashboardIsEditable(state), dashboardSettings: getDashboardSettings(state), diff --git a/src/page/PageReducer.ts b/src/page/PageReducer.ts index c3a4096d3..18996110d 100644 --- a/src/page/PageReducer.ts +++ b/src/page/PageReducer.ts @@ -11,7 +11,7 @@ import { createUUID } from '../utils/uuid'; const update = (state, mutations) => Object.assign({}, state, mutations); // TODO : Alfredo: this should source the card config defined inside the reducer and then define the first page initial state -export const FIRST_PAGE_INITIAL_STATE = { +export const PAGE_EXAMPLE_STATE = { title: 'Main Page', reports: [ { @@ -42,7 +42,7 @@ export const FIRST_PAGE_INITIAL_STATE = { ], }; -export const PAGE_INITIAL_STATE = { +export const PAGE_EMPTY_STATE = { title: 'New page', reports: [], }; @@ -52,7 +52,7 @@ export const PAGE_INITIAL_STATE = { * This reducer handles updates to a single page of the dashboard. * TODO - pagenumbers can be cut from here with new reducer architecture. */ -export const pageReducer = (state = PAGE_INITIAL_STATE, action: { type: any; payload: any }) => { +export const pageReducer = (state = PAGE_EMPTY_STATE, action: { type: any; payload: any }) => { const { type, payload } = action; if (!action.type.startsWith('PAGE/')) { diff --git a/yarn.lock b/yarn.lock index 892cd3918..67b736bdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1701,7 +1701,7 @@ js-yaml "4.1.0" nyc "15.1.0" -"@cypress/request@^2.88.10": +"@cypress/request@2.88.12": version "2.88.12" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== @@ -2506,25 +2506,25 @@ "@babel/runtime" "^7.20.13" "@neo4j-cypher/codemirror" "1.0.2" -"@neo4j-ndl/base@1.10.1", "@neo4j-ndl/base@^1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@neo4j-ndl/base/-/base-1.10.1.tgz#18b3b35b9a52d0f5f0ee978c435bc96717ff16a9" - integrity sha512-ytz82vN1qMDCZButP4Wm0bLTStz6BWXWWRXMY0iP2Wfw/OAcI3WF2fBVL902FtzCBq0MR/GHHwjgMVpy9g7XeA== +"@neo4j-ndl/base@1.10.3", "@neo4j-ndl/base@^1.10.3": + version "1.10.3" + resolved "https://registry.yarnpkg.com/@neo4j-ndl/base/-/base-1.10.3.tgz#99557e3bede274510fc465a781d2e52e0cca47ea" + integrity sha512-NTFUz8j9+yx9AN1/TR3yzs7Nt/K+p1yqo2RHqf3UCtuD4ZyMUgYW1gmPQS0Du2S43gvQJkpVenXYJFgjNcFEMA== -"@neo4j-ndl/react@1.10.2": - version "1.10.2" - resolved "https://registry.yarnpkg.com/@neo4j-ndl/react/-/react-1.10.2.tgz#aaf61a06f3c63212f275b2a67ecf588a03287c84" - integrity sha512-t4OPV+qA5EqO4qb3FQBfdNnEZAhIO4CiW1qKN4cPeOvXCAyquPBkSM2+8l/rI1+Ra8o4r5FJ1LKln1BhLQPHPw== +"@neo4j-ndl/react@1.10.8": + version "1.10.8" + resolved "https://registry.yarnpkg.com/@neo4j-ndl/react/-/react-1.10.8.tgz#ab2ad2719cbdbe8a286ec54b18afc8fae6e5da33" + integrity sha512-EVUjwyxup/uNFItJl634z4JA9EVKJ5rvdncu9FOWHs85cw3VNqIfnFuApuuslveo7AknAwkCyeqQmOtjlPVSRQ== dependencies: "@floating-ui/react" "^0.24.2" "@heroicons/react" "2.0.13" "@neo4j-cypher/react-codemirror" "^1.0.1" - "@neo4j-ndl/base" "^1.10.1" + "@neo4j-ndl/base" "^1.10.3" "@tanstack/react-table" "^8.9.3" classnames "^2.3.1" date-fns "^2.30.0" detect-browser "^5.3.0" - re-resizable "^6.9.9" + re-resizable "^6.9.11" react-aria "^3.25.0" react-datepicker "^4.14.1" react-dropzone "^14.0.0" @@ -4535,10 +4535,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== -"@types/node@^14.14.31": - version "14.18.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.54.tgz#fc304bd66419030141fa997dc5a9e0e374029ae8" - integrity sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw== +"@types/node@^16.18.39": + version "16.18.59" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.59.tgz#4cdbd631be6d9be266a96fb17b5d0d7ad6bbe26c" + integrity sha512-PJ1w2cNeKUEdey4LiPra0ZuxZFOGvetswE8qHRriV/sUkL5Al4tTmPV9D2+Y/TPIxTHHgxTfRjZVKWhPw/ORhQ== "@types/parse-json@^4.0.0": version "4.0.0" @@ -5924,10 +5924,10 @@ commander@^4.0.0, commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== commander@^9.4.1: version "9.5.0" @@ -6194,14 +6194,14 @@ csstype@^3.1.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== -cypress@^10.11.0: - version "10.11.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.11.0.tgz#e9fbdd7638bae3d8fb7619fd75a6330d11ebb4e8" - integrity sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA== +cypress@^12.17.4: + version "12.17.4" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" + integrity sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ== dependencies: - "@cypress/request" "^2.88.10" + "@cypress/request" "2.88.12" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -6213,10 +6213,10 @@ cypress@^10.11.0: check-more-types "^2.24.0" cli-cursor "^3.1.0" cli-table3 "~0.6.1" - commander "^5.1.0" + commander "^6.2.1" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" eventemitter2 "6.4.7" execa "4.1.0" @@ -6231,12 +6231,13 @@ cypress@^10.11.0: listr2 "^3.8.3" lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.6" + minimist "^1.2.8" ospath "^1.2.2" pretty-bytes "^5.6.0" + process "^0.11.10" proxy-from-env "1.0.0" request-progress "^3.0.0" - semver "^7.3.2" + semver "^7.5.3" supports-color "^8.1.1" tmp "~0.2.1" untildify "^4.0.0" @@ -10175,7 +10176,7 @@ minimatch@^7.4.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -11269,6 +11270,11 @@ process-utils@^4.0.0: memoizee "^0.4.14" type "^2.1.0" +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -11389,10 +11395,10 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -re-resizable@^6.9.9: - version "6.9.9" - resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216" - integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA== +re-resizable@^6.9.11: + version "6.9.11" + resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475" + integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ== react-aria@^3.25.0: version "3.25.0" @@ -12210,7 +12216,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.7, semver@^7.3.8, semver@^7.5.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==