From b56215ea16bd6371dd3ca76fb7026dd57ee6b2f6 Mon Sep 17 00:00:00 2001 From: alfredorubin96 <103421036+alfredorubin96@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:50:50 +0100 Subject: [PATCH] Feature/8raven (#705) * Title not editable in standalone mode * standalone load v1 (working) * load from database refinements with new conf var * config.json reset to defaults * first implementation - needs few refinements * index on feature/log: ceda481c first implementation - needs few refinements * added Tooltip for Save button in standalone mode * polished * fix on error notification * minor refinement in documentation * hide logout button in standalone mode * man merge d137081cfde4b2d5dcf1192b8a9be675e953bd0b * man chg 0484e44ed345e8a9008fcebc8753ddf881f038f1 * added configuration to allow multiple data DBs * fix on config-entrypoint and reorder parameters * bugfix on config-entrypoint.sh * fix to update standaloneDB for standaloneMultiDB * added useffect in card.tsx to save DB * query modified in saveDashboardThunks * updated config-entrypoint * added config parameter to set cusom Header * documentation * fix dirt in style.config * fix dirt in config.json * Update ApplicationConfig.ts fix dirt * moving logging logic to its own reducer * fixing new selector and small refactorings * cleaning code and testing standalone * adding database list check * changing version to 3.18 to address address Cve-2023-4863 and cve-2023-38039 * removing unused imports * working on final release * fixed dashboards sidebar error when the db doesn't contain any dashboard and tested standalone * removing useless import * removing change in runCypherQuery and reusing the status of the queryResult correctly to trigger db change * removing change in runCypherQuery and reusing the status of the queryResult correctly to trigger db change --------- Co-authored-by: BlackRaven Co-authored-by: BlackRaven <35220904+8Rav3n@users.noreply.github.com> Co-authored-by: Alfred Rubin --- Dockerfile | 4 +- .../pages/developer-guide/configuration.adoc | 69 +++- .../developer-guide/standalone-mode.adoc | 5 + .../developer-guide/state-management.adoc | 7 + public/config.json | 9 +- scripts/config-entrypoint.sh | 15 +- src/application/ApplicationActions.ts | 16 +- src/application/ApplicationReducer.ts | 42 ++ src/application/ApplicationSelectors.ts | 16 + src/application/ApplicationThunks.ts | 65 ++- src/application/logging/LoggingActions.ts | 19 + src/application/logging/LoggingReducer.ts | 39 ++ src/application/logging/LoggingSelectors.ts | 6 + src/application/logging/LoggingThunk.ts | 77 ++++ src/dashboard/Dashboard.tsx | 12 +- src/dashboard/DashboardThunks.ts | 387 +++++++++++++----- src/dashboard/header/DashboardHeader.tsx | 15 +- .../header/DashboardHeaderLogoutButton.tsx | 10 +- src/dashboard/header/DashboardTitle.tsx | 21 +- src/dashboard/sidebar/DashboardSidebar.tsx | 63 +-- src/modal/ConnectionModal.tsx | 48 ++- 21 files changed, 773 insertions(+), 172 deletions(-) create mode 100644 src/application/logging/LoggingActions.ts create mode 100644 src/application/logging/LoggingReducer.ts create mode 100644 src/application/logging/LoggingSelectors.ts create mode 100644 src/application/logging/LoggingThunk.ts diff --git a/Dockerfile b/Dockerfile index ebe0f16ed..0733ff216 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # build stage -FROM node:lts-alpine AS build-stage +FROM node:lts-alpine3.18 AS build-stage RUN yarn global add typescript jest WORKDIR /usr/local/src/neodash @@ -16,7 +16,7 @@ COPY ./ /usr/local/src/neodash RUN yarn run build-minimal # production stage -FROM nginx:alpine AS neodash +FROM nginx:alpine3.18 AS neodash RUN apk upgrade ENV NGINX_PORT=5005 diff --git a/docs/modules/ROOT/pages/developer-guide/configuration.adoc b/docs/modules/ROOT/pages/developer-guide/configuration.adoc index a56946281..355c0c19a 100644 --- a/docs/modules/ROOT/pages/developer-guide/configuration.adoc +++ b/docs/modules/ROOT/pages/developer-guide/configuration.adoc @@ -25,7 +25,14 @@ will look like this: "standaloneDatabase": "neo4j", "standaloneDashboardName": "My Dashboard", "standaloneDashboardDatabase": "dashboards", - "standaloneDashboardURL": "" + "standaloneDashboardURL": "", + "standaloneAllowLoad": false, + "standaloneLoadFromOtherDatabases": false, + "standaloneMultiDatabase": false, + "standaloneDatabaseList": "neo4j" + "loggingMode": "0", + "loggingDatabase": "logs", + "customHeader": "", } .... @@ -87,6 +94,52 @@ use multiple databases. inside Neo4j and would like to run a standalone mode deployment with a dashboard from a URL, set this parameter to the complete URL pointing to the dashboard JSON. + +|standaloneAllowLoad |boolean |false |If set to yes the "Load Dashboard" +button will be enabled in standalone mode, allowing users to load +additional dashboards from Neo4J. This parameter is false by default +_unless you are using Neo4j Enterprise Edition_, which lets you use multiple +databases. +*NOTE*: when Load is enabled in standalone mode, only Database is available +as a source, not file. + +|standaloneLoadFromOtherDatabases |boolean |false |If _standaloneAllowLoad_ is +set to true, this parmeter enables or not users to load dashboards from +other databases than the one deifned in _standaloneDashboardDatabase_. If +_standaloneAllowLoad_ is set to false this parameters has no effect. + +|standaloneMultiDatabase |boolean |false |If this parameter set to true, the +standalone configuration will ignore the _standaloneDatabase_ parameter and +allow users to choose which database to connect to in the login screen, among +the ones provided in _standaloneDatabaseList_, with a dropdown list. This +parameter is false by default _unless you are using Neo4j Enterprise Edition_, +which lets you use multiple databases. + +|standaloneDatabaseList |string |neo4j |If _standaloneMultiDatabase_ is +set to true, this parmeter must contain a comma separated list of database +names that will be displayed as options in the Database dropdown at user +login (e.g. 'neo4j,database1,database2' will populate the database dropdown +with the values 'neo4j','database1' and 'database2' in the connection screen). +If _standaloneMultiDatabase_ is set to false this parameters has no effect. + +|loggingMode |string |none |Determines whether neodash should create any +user activity logs. possible values include: `0` (no log is created), +`1` (user login are tracked), `2` (tracks when a specific dashboard is +accessed/loaded or saved by a user*). + +⚠️ Logs are created in Neo4J DB using the current user credentials +(or standaloneUsername if configured); write access to the log database +must be granted to enble any user to create logs. + +⚠️ * Load/Save from/to file are not logged (only from/to Database) + +|loggingDatabase |string |neo4j |When loggingMode is set to anything +else than '0', the database to use for logging. Log records (nodes) +will be created in this database. + +|customHeader |string |none |When set the dashboard header will display +the prameter value as a fixed string, otherwise it will display the host +and port of current connection. |=== == Configuring SSO @@ -129,7 +182,7 @@ be enabled by changing the `standalone` config parameter: * If standalone mode is `false`, all other configuration parameters are ignored. NeoDash will run in Editor mode, and require a manual sign-in. * If standalone mode is `true`, NeoDash will read all configuration -parameters. A *fixed dashboard* will be auto-loaded, and no changes to +parameters. A *predefined dashboard* will be auto-loaded, and no changes to the dashboard can be made. There are two types of valid standalone deployments: ** A standalone deployment that *reads the fixed dashboard from Neo4j*. @@ -137,3 +190,15 @@ The `standaloneDashboardName` and `standaloneDashboardDatabase` config parameters are used to define these. ** A standalone deployment that *reads the fixed dashboard from a URL*. The `standaloneDashboardURL` config parameter is used to define this. + +* Standalone mode can also be configured to allow users load a different +dashboard after the predefined one is loaded (a `Load Dashboard` button +will be displayed on the right side of dashboard title). +The `standaloneAllowLoad` and `standaloneLoadFromOtherDatabases` are used +to define this. +* When allowing users to load dashboards dyamically in standalone mode, +they may also need to connect to different databases, depending on the +specific dashboard bing loaded. this can be enabled setting +`standaloneMultiDatabase` to true and providing a comma separated list +of the allowed database names in the`standaloneDatabaseList` parameter. + diff --git a/docs/modules/ROOT/pages/developer-guide/standalone-mode.adoc b/docs/modules/ROOT/pages/developer-guide/standalone-mode.adoc index b40bda419..f752cbad1 100644 --- a/docs/modules/ROOT/pages/developer-guide/standalone-mode.adoc +++ b/docs/modules/ROOT/pages/developer-guide/standalone-mode.adoc @@ -48,6 +48,11 @@ docker run -it --rm -p 5005:5005 \ -e standaloneDatabase="neo4j" \ -e standaloneDashboardName="My Dashboard" \ -e standaloneDashboardDatabase="dashboards" \ + -e standaloneDashboardURL="dashboards" \ + -e standaloneAllowLoad=false \ + -e standaloneLoadFromOtherDatabases=false \ + -e standaloneMultiDatabase=false \ + -e standaloneDatabaseList="neo4j" \ neo4jlabs/neodash .... diff --git a/docs/modules/ROOT/pages/developer-guide/state-management.adoc b/docs/modules/ROOT/pages/developer-guide/state-management.adoc index 548d7d8c8..bdece9b9e 100644 --- a/docs/modules/ROOT/pages/developer-guide/state-management.adoc +++ b/docs/modules/ROOT/pages/developer-guide/state-management.adoc @@ -135,6 +135,13 @@ standalone mode. "standaloneDatabase": "neo4j", "standaloneDashboardName": "My Dashboard", "standaloneDashboardDatabase": "dashboards", + "standaloneDashboardURL": "dashboards", + "loggingMode": "0", + "loggingDatabase": "logging", + "standaloneAllowLoad": false, + "standaloneLoadFromOtherDatabases ": false, + "standaloneMultiDatabase": false, + "standaloneDatabaseList": "neo4j", "notificationIsDismissable": null } .... diff --git a/public/config.json b/public/config.json index 17754d56b..4e8d475a2 100644 --- a/public/config.json +++ b/public/config.json @@ -9,5 +9,12 @@ "standaloneDatabase": "neo4j", "standaloneDashboardName": "My Dashboard", "standaloneDashboardDatabase": "dashboards", - "standaloneDashboardURL": "" + "standaloneDashboardURL": "", + "standaloneAllowLoad": false, + "standaloneLoadFromOtherDatabases": false, + "standaloneMultiDatabase": false, + "standaloneDatabaseList": "neo4j", + "loggingMode": "0", + "loggingDatabase": "logs", + "customHeader": "" } diff --git a/scripts/config-entrypoint.sh b/scripts/config-entrypoint.sh index 2eb04129c..65a7f9d4f 100644 --- a/scripts/config-entrypoint.sh +++ b/scripts/config-entrypoint.sh @@ -7,7 +7,7 @@ echo " \ \"ssoEnabled\": ${ssoEnabled:=false}, \ \"ssoProviders\": ${ssoProviders:=[]}, \ \"ssoDiscoveryUrl\": \"${ssoDiscoveryUrl:='https://example.com'}\", \ - \"standalone\": "${standalone:=false}", \ + \"standalone\": ${standalone:=false}, \ \"standaloneProtocol\": \"${standaloneProtocol:='neo4j+s'}\", \ \"standaloneHost\": \"${standaloneHost:='test.databases.neo4j.io'}\", \ \"standalonePort\": ${standalonePort:=7687}, \ @@ -16,5 +16,14 @@ echo " \ \"standalonePassword\": \"${standalonePassword:=}\", \ \"standaloneDashboardName\": \"${standaloneDashboardName:='My Dashboard'}\", \ \"standaloneDashboardDatabase\": \"${standaloneDashboardDatabase:='neo4j'}\", \ - \"standaloneDashboardURL\": \"${standaloneDashboardURL:=}\" \ - }" > /usr/share/nginx/html/config.json + \"standaloneDashboardURL\": \"${standaloneDashboardURL:=}\", \ + \"standaloneAllowLoad\": ${standaloneAllowLoad:=false}, \ + \"standaloneLoadFromOtherDatabases\": ${standaloneLoadFromOtherDatabases:=false}, \ + \"standaloneMultiDatabase\": ${standaloneMultiDatabase:=false}, \ + \"standaloneDatabaseList\": \"${standaloneDatabaseList:='neo4j'}\", \ + \"loggingMode\": \"${loggingMode:='0'}\", \ + \"loggingDatabase\": \"${loggingDatabase:='logs'}\", \ + \"customHeader\": \"${customHeader:=}\" \ + }" > /usr/share/nginx/html/config.json + +echo "${styleConfigJson:={\}}" > /usr/share/nginx/html/style.config.json diff --git a/src/application/ApplicationActions.ts b/src/application/ApplicationActions.ts index cf49b4715..084d4d31b 100644 --- a/src/application/ApplicationActions.ts +++ b/src/application/ApplicationActions.ts @@ -149,7 +149,11 @@ export const setStandaloneEnabled = ( standaloneDashboardDatabase: string, standaloneDashboardURL: string, standaloneUsername: string, - standalonePassword: string + standalonePassword: string, + standaloneAllowLoad: boolean, + standaloneLoadFromOtherDatabases: boolean, + standaloneMultiDatabase: boolean, + standaloneDatabaseList: string ) => ({ type: SET_STANDALONE_ENABLED, payload: { @@ -163,6 +167,10 @@ export const setStandaloneEnabled = ( standaloneDashboardURL, standaloneUsername, standalonePassword, + standaloneAllowLoad, + standaloneLoadFromOtherDatabases, + standaloneMultiDatabase, + standaloneDatabaseList, }, }); @@ -219,3 +227,9 @@ export const setParametersToLoadAfterConnecting = (parameters: any) => ({ type: SET_PARAMETERS_TO_LOAD_AFTER_CONNECTING, payload: { parameters }, }); + +export const SET_CUSTOM_HEADER = 'APPLICATION/SET_CUSTOM_HEADER'; +export const setCustomHeader = (customHeader: any) => ({ + type: SET_CUSTOM_HEADER, + payload: { customHeader }, +}); diff --git a/src/application/ApplicationReducer.ts b/src/application/ApplicationReducer.ts index dc53aa7ad..9c6f81068 100644 --- a/src/application/ApplicationReducer.ts +++ b/src/application/ApplicationReducer.ts @@ -31,7 +31,15 @@ import { SET_STANDALONE_MODE, SET_WAIT_FOR_SSO, SET_WELCOME_SCREEN_OPEN, + SET_CUSTOM_HEADER, } from './ApplicationActions'; +import { + SET_LOGGING_MODE, + SET_LOGGING_DATABASE, + SET_LOG_ERROR_NOTIFICATION, + LOGGING_PREFIX, +} from './logging/LoggingActions'; +import { loggingReducer, LOGGING_INITIAL_STATE } from './logging/LoggingReducer'; const update = (state, mutations) => Object.assign({}, state, mutations); @@ -56,6 +64,7 @@ const initialState = { dashboardToLoadAfterConnecting: null, waitForSSO: false, standalone: false, + logging: LOGGING_INITIAL_STATE, }; export const applicationReducer = (state = initialState, action: { type: any; payload: any }) => { const { type, payload } = action; @@ -82,6 +91,11 @@ export const applicationReducer = (state = initialState, action: { type: any; pa if (!action.type.startsWith('APPLICATION/')) { return state; } + if (action.type.startsWith(LOGGING_PREFIX)) { + const enrichedPayload = update(payload, { logging: state.logging }); + const enrichedAction = { type, payload: enrichedPayload }; + return { ...state, logging: loggingReducer(state.logging, enrichedAction) }; + } // Application state updates are handled here. switch (type) { @@ -134,6 +148,21 @@ export const applicationReducer = (state = initialState, action: { type: any; pa state = update(state, { standalone: standalone }); return state; } + case SET_LOGGING_MODE: { + const { loggingMode } = payload; + state = update(state, { loggingMode: loggingMode }); + return state; + } + case SET_LOGGING_DATABASE: { + const { loggingDatabase } = payload; + state = update(state, { loggingDatabase: loggingDatabase }); + return state; + } + case SET_LOG_ERROR_NOTIFICATION: { + const { logErrorNotification } = payload; + state = update(state, { logErrorNotification: logErrorNotification }); + return state; + } case SET_SSO_ENABLED: { const { enabled, discoveryUrl } = payload; state = update(state, { ssoEnabled: enabled, ssoDiscoveryUrl: discoveryUrl }); @@ -166,6 +195,10 @@ export const applicationReducer = (state = initialState, action: { type: any; pa standaloneDashboardURL, standaloneUsername, standalonePassword, + standaloneAllowLoad, + standaloneLoadFromOtherDatabases, + standaloneMultiDatabase, + standaloneDatabaseList, } = payload; state = update(state, { standalone: standalone, @@ -178,6 +211,10 @@ export const applicationReducer = (state = initialState, action: { type: any; pa standaloneDashboardURL: standaloneDashboardURL, standaloneUsername: standaloneUsername, standalonePassword: standalonePassword, + standaloneAllowLoad: standaloneAllowLoad, + standaloneLoadFromOtherDatabases: standaloneLoadFromOtherDatabases, + standaloneMultiDatabase: standaloneMultiDatabase, + standaloneDatabaseList: standaloneDatabaseList, }); return state; } @@ -268,6 +305,11 @@ export const applicationReducer = (state = initialState, action: { type: any; pa }); return state; } + case SET_CUSTOM_HEADER: { + const { customHeader } = payload; + state = update(state, { customHeader: customHeader }); + return state; + } default: { return state; } diff --git a/src/application/ApplicationSelectors.ts b/src/application/ApplicationSelectors.ts index e3dc20601..b4912a9b9 100644 --- a/src/application/ApplicationSelectors.ts +++ b/src/application/ApplicationSelectors.ts @@ -37,6 +37,10 @@ export const applicationGetConnectionDatabase = (state: any) => { return state.application.connection.database; }; +export const applicationGetConnectionUser = (state: any) => { + return state.application.connection.username; +}; + export const applicationGetShareDetails = (state: any) => { return state.application.shareDetails; }; @@ -45,6 +49,10 @@ export const applicationIsStandalone = (state: any) => { return state.application.standalone; }; +export const applicationGetLoggingMode = (state: any) => { + return state.application.loggingMode; +}; + export const applicationHasNeo4jDesktopConnection = (state: any) => { return state.application.desktopConnection != null; }; @@ -86,6 +94,10 @@ export const applicationGetStandaloneSettings = (state: any) => { standaloneDashboardURL: state.application.standaloneDashboardURL, standaloneUsername: state.application.standaloneUsername, standalonePassword: state.application.standalonePassword, + standaloneAllowLoad: state.application.standaloneAllowLoad, + standaloneLoadFromOtherDatabases: state.application.standaloneLoadFromOtherDatabases, + standaloneMultiDatabase: state.application.standaloneMultiDatabase, + standaloneDatabaseList: state.application.standaloneDatabaseList, }; }; @@ -112,3 +124,7 @@ export const applicationGetDebugState = (state: any) => { } return copy; }; + +export const applicationGetCustomHeader = (state: any) => { + return state.application.customHeader; +}; diff --git a/src/application/ApplicationThunks.ts b/src/application/ApplicationThunks.ts index 018fb7938..42d625d3e 100644 --- a/src/application/ApplicationThunks.ts +++ b/src/application/ApplicationThunks.ts @@ -40,8 +40,13 @@ import { setParametersToLoadAfterConnecting, setReportHelpModalOpen, setDraft, + setCustomHeader, } from './ApplicationActions'; +import { setLoggingMode, setLoggingDatabase, setLogErrorNotification } from './logging/LoggingActions'; import { version } from '../modal/AboutModal'; +import { applicationIsStandalone } from './ApplicationSelectors'; +import { applicationGetLoggingSettings } from './logging/LoggingSelectors'; +import { createLogThunk } from './logging/LoggingThunk'; import { createUUID } from '../utils/uuid'; /** @@ -60,6 +65,9 @@ import { createUUID } from '../utils/uuid'; */ export const createConnectionThunk = (protocol, url, port, database, username, password) => (dispatch: any, getState: any) => { + const loggingState = getState(); + const loggingSettings = applicationGetLoggingSettings(loggingState); + const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor'; try { const driver = createDriver(protocol, url, port, username, password, { userAgent: `neodash/v${version}` }); // eslint-disable-next-line no-console @@ -69,8 +77,24 @@ export const createConnectionThunk = console.log('Confirming connection was established...'); if (records && records[0] && records[0].error) { dispatch(createNotificationThunk('Unable to establish connection', records[0].error)); + if (loggingSettings.loggingMode > '0') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + username, + 'ERR - connect to DB', + database, + '', + `Error while trying to establish connection to Neo4j DB in ${ + neodashMode + } mode at ${ + Date(Date.now()).substring(0, 33)}` + ) + ); + } } 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)); @@ -79,6 +103,24 @@ export const createConnectionThunk = dispatch(updateSessionParameterThunk('session_uri', `${protocol}://${url}:${port}`)); dispatch(updateSessionParameterThunk('session_database', database)); dispatch(updateSessionParameterThunk('session_username', username)); + if (loggingSettings.loggingMode > '0') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + username, + 'INF - connect to DB', + database, + '', + `${username + } established connection to Neo4j DB in ${ + neodashMode + } mode at ${ + Date(Date.now()).substring(0, 33)}` + ) + ); + } // If we have remembered to load a specific dashboard after connecting to the database, take care of it here. const { application } = getState(); if ( @@ -357,6 +399,14 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState: standaloneDashboardName: 'My Dashboard', standaloneDashboardDatabase: 'dashboards', standaloneDashboardURL: '', + loggingMode: '0', + loggingDatabase: 'logs', + logErrorNotification: '3', + standaloneAllowLoad: false, + standaloneLoadFromOtherDatabases: false, + standaloneMultiDatabase: false, + standaloneDatabaseList: 'neo4j', + customHeader: '', }; try { config = await (await fetch('config.json')).json(); @@ -399,11 +449,22 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState: config.standaloneDashboardDatabase, config.standaloneDashboardURL, config.standaloneUsername, - config.standalonePassword + config.standalonePassword, + config.standaloneAllowLoad, + config.standaloneLoadFromOtherDatabases, + config.standaloneMultiDatabase, + config.standaloneDatabaseList ) ); + + dispatch(setLoggingMode(config.loggingMode)); + dispatch(setLoggingDatabase(config.loggingDatabase)); + dispatch(setLogErrorNotification('3')); + dispatch(setConnectionModalOpen(false)); + dispatch(setCustomHeader(config.customHeader)); + // Auto-upgrade the dashboard version if an old version is cached. if (state.dashboard && state.dashboard.version !== NEODASH_VERSION) { // Attempt upgrade if dashboard version is outdated. diff --git a/src/application/logging/LoggingActions.ts b/src/application/logging/LoggingActions.ts new file mode 100644 index 000000000..bfbf0fc8a --- /dev/null +++ b/src/application/logging/LoggingActions.ts @@ -0,0 +1,19 @@ +export const LOGGING_PREFIX = 'APPLICATION/LOGGING/'; + +export const SET_LOGGING_MODE = `${LOGGING_PREFIX}/SET_LOGGING_MODE`; +export const setLoggingMode = (loggingMode: string) => ({ + type: SET_LOGGING_MODE, + payload: { loggingMode }, +}); + +export const SET_LOGGING_DATABASE = `${LOGGING_PREFIX}/SET_LOGGING_DATABASE`; +export const setLoggingDatabase = (loggingDatabase: string) => ({ + type: SET_LOGGING_DATABASE, + payload: { loggingDatabase }, +}); + +export const SET_LOG_ERROR_NOTIFICATION = `${LOGGING_PREFIX}/SET_LOG_ERROR_NOTIFICATION`; +export const setLogErrorNotification = (logErrorNotification: any) => ({ + type: SET_LOG_ERROR_NOTIFICATION, + payload: { logErrorNotification }, +}); diff --git a/src/application/logging/LoggingReducer.ts b/src/application/logging/LoggingReducer.ts new file mode 100644 index 000000000..16e257e7b --- /dev/null +++ b/src/application/logging/LoggingReducer.ts @@ -0,0 +1,39 @@ +import { LOGGING_PREFIX, SET_LOGGING_DATABASE, SET_LOGGING_MODE, SET_LOG_ERROR_NOTIFICATION } from './LoggingActions'; + +const update = (state, mutations) => Object.assign({}, state, mutations); + +export const LOGGING_INITIAL_STATE = { + loggingMode: '0', + logErrorNotification: '3', + loggingDatabase: undefined, +}; + +export const loggingReducer = (state = LOGGING_INITIAL_STATE, action: { type: any; payload: any }) => { + const { type, payload } = action; + + if (!action.type.startsWith(LOGGING_PREFIX)) { + return state; + } + + // Logging state updates are handled here. + switch (type) { + case SET_LOGGING_MODE: { + const { loggingMode } = payload; + state = update(state, { loggingMode: loggingMode }); + return state; + } + case SET_LOGGING_DATABASE: { + const { loggingDatabase } = payload; + state = update(state, { loggingDatabase: loggingDatabase }); + return state; + } + case SET_LOG_ERROR_NOTIFICATION: { + const { logErrorNotification } = payload; + state = update(state, { logErrorNotification: logErrorNotification }); + return state; + } + default: { + return state; + } + } +}; diff --git a/src/application/logging/LoggingSelectors.ts b/src/application/logging/LoggingSelectors.ts new file mode 100644 index 000000000..c8cba54c4 --- /dev/null +++ b/src/application/logging/LoggingSelectors.ts @@ -0,0 +1,6 @@ +/** + * Selector function for retrieving logging settings from the application state. + * @param state - The application state. + * @returns An object with logging settings. + */ +export const applicationGetLoggingSettings = (state: any) => state.application.logging; diff --git a/src/application/logging/LoggingThunk.ts b/src/application/logging/LoggingThunk.ts new file mode 100644 index 000000000..e9c208567 --- /dev/null +++ b/src/application/logging/LoggingThunk.ts @@ -0,0 +1,77 @@ +import { createNotificationThunk } from '../../page/PageThunks'; +import { runCypherQuery } from '../../report/ReportQueryRunner'; +import { setLogErrorNotification } from './LoggingActions'; +import { applicationGetLoggingSettings } from './LoggingSelectors'; +import { createUUID } from '../../utils/uuid'; + +// Thunk to handle log events. + +export const createLogThunk = + (loggingDriver, loggingDatabase, neodashMode, logUser, logAction, logDatabase, logDashboard = '', logMessage) => + (dispatch: any, getState: any) => { + try { + const uuid = createUUID(); + // Generate a cypher query to save the log. + const query = + 'CREATE (n:_Neodash_Log) SET n.uuid = $uuid, n.user = $user, n.date = datetime(), n.neodash_mode = $neodashMode, n.action = $logAction, n.database = $logDatabase, n.dashboard = $logDashboard, n.message = $logMessage RETURN $uuid as uuid'; + + const parameters = { + uuid: uuid, + user: logUser, + logAction: logAction, + logDatabase: logDatabase, + neodashMode: neodashMode, + logDashboard: logDashboard, + logMessage: logMessage, + }; + runCypherQuery( + loggingDriver, + loggingDatabase, + query, + parameters, + 1, + () => {}, + (records) => { + if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) { + console.log(`log created: ${ uuid}`); + } else { + // we only show error notification one time + const state = getState(); + const loggingSettings = applicationGetLoggingSettings(state); + let LogErrorNotificationNum = Number(loggingSettings.logErrorNotification); + console.log(`Error creating log for ${ (LogErrorNotificationNum - 4) * -1 } times`); + if (LogErrorNotificationNum > 0) { + dispatch( + createNotificationThunk( + 'Error creating log', + LogErrorNotificationNum > 1 + ? `Please check logging configuration with your Neodash administrator` + : `Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session` + ) + ); + } + LogErrorNotificationNum -= 1; + dispatch(setLogErrorNotification(LogErrorNotificationNum.toString())); + } + } + ); + } catch (e) { + // we only show error notification 3 times + const state = getState(); + const loggingSettings = applicationGetLoggingSettings(state); + let LogErrorNotificationNum = Number(loggingSettings.logErrorNotification); + console.log(`Error creating log for ${ (LogErrorNotificationNum - 4) * -1 } times`); + if (LogErrorNotificationNum > 0) { + dispatch( + createNotificationThunk( + 'Error creating log', + LogErrorNotificationNum > 1 + ? `Please check logging configuration with your Neodash administrator` + : `Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session` + ) + ); + } + LogErrorNotificationNum -= 1; + dispatch(setLogErrorNotification(LogErrorNotificationNum.toString())); + } + }; diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index e564c8043..50b266e60 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -16,7 +16,7 @@ import NeoDashboardSidebar from './sidebar/DashboardSidebar'; const Dashboard = ({ pagenumber, connection, - applicationSettings, + standaloneSettings, onConnectionUpdate, onDownloadDashboardAsImage, onAboutModalOpen, @@ -67,7 +67,11 @@ const Dashboard = ({ position: 'relative', }} > - + {!standaloneSettings.standalone || (standaloneSettings.standalone && standaloneSettings.standaloneAllowLoad) ? ( + + ) : ( + <> + )}
{/* Main Content */} @@ -77,7 +81,7 @@ const Dashboard = ({ {/* The main content of the page */}
- {applicationSettings.standalonePassword ? ( + {standaloneSettings.standalonePassword ? (
Warning: NeoDash is running with a plaintext password in config.json.
@@ -102,7 +106,7 @@ const Dashboard = ({ const mapStateToProps = (state) => ({ connection: applicationGetConnection(state), pagenumber: getPageNumber(state), - applicationSettings: applicationGetStandaloneSettings(state), + standaloneSettings: applicationGetStandaloneSettings(state), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/dashboard/DashboardThunks.ts b/src/dashboard/DashboardThunks.ts index 667dd3254..c2ab6c14b 100644 --- a/src/dashboard/DashboardThunks.ts +++ b/src/dashboard/DashboardThunks.ts @@ -5,6 +5,9 @@ import { QueryStatus, runCypherQuery } from '../report/ReportQueryRunner'; import { setDraft, setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions'; import { updateGlobalParametersThunk, updateParametersToNeo4jTypeThunk } from '../settings/SettingsThunks'; import { createUUID } from '../utils/uuid'; +import { createLogThunk } from '../application/logging/LoggingThunk'; +import { applicationGetConnectionUser, applicationIsStandalone } from '../application/ApplicationSelectors'; +import { applicationGetLoggingSettings } from '../application/logging/LoggingSelectors'; import { NEODASH_VERSION, VERSION_TO_MIGRATE } from './DashboardReducer'; export const removePageThunk = (number) => (dispatch: any, getState: any) => { @@ -111,6 +114,7 @@ export const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) }); dispatch(setDashboard(dashboard)); + const { application } = getState(); dispatch(updateGlobalParametersThunk(application.parametersToLoadAfterConnecting)); @@ -127,57 +131,114 @@ export const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) } }; -export const saveDashboardToNeo4jThunk = (driver, database, dashboard, date, user, onSuccess) => (dispatch: any) => { - try { - let { uuid } = dashboard; +export const saveDashboardToNeo4jThunk = + (driver, database, dashboard, date, user, onSuccess) => (dispatch: any, getState: any) => { + const state = getState(); + const loggingSettings = applicationGetLoggingSettings(state); + const loguser = applicationGetConnectionUser(state); + const neodashMode = applicationIsStandalone(state) ? 'Standalone' : 'Editor'; - // 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(); - } + try { + let { uuid } = dashboard; - const { title, version } = 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(); + } - // 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 { title, version } = dashboard; - 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 { - dispatch( - createNotificationThunk( - 'Unable to save dashboard', - `Do you have write access to the '${database}' database?` - ) - ); + // 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); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'INF - save dashboard', + database, + `Name:${title}`, + `User ${loguser} saved dashboard to Neo4J in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } + } else { + dispatch( + createNotificationThunk( + 'Unable to save dashboard', + `Do you have write access to the '${database}' database?` + ) + ); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'ERR - save dashboard', + database, + `Name:${title}`, + `Error while trying to save dashboard to Neo4J in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } + } } + ); + } catch (e) { + dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e)); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'ERR - save dashboard', + database, + 'Name:Not fetched', + `Error while trying to save dashboard to Neo4J in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); } - ); - } catch (e) { - dispatch(createNotificationThunk('Unable to save dashboard to Neo4j', e)); - } -}; + } + }; export const deleteDashboardFromNeo4jThunk = (driver, database, uuid, onSuccess) => (dispatch: any) => { try { @@ -212,7 +273,12 @@ export const deleteDashboardFromNeo4jThunk = (driver, database, uuid, onSuccess) } }; -export const loadDashboardFromNeo4jThunk = (driver, database, uuid, callback) => (dispatch: any) => { +export const loadDashboardFromNeo4jThunk = (driver, database, uuid, callback) => (dispatch: any, getState: any) => { + const state = getState(); + const loggingSettings = applicationGetLoggingSettings(state); + const loguser = applicationGetConnectionUser(state); + const neodashMode = applicationIsStandalone(state) ? 'Standalone' : 'Editor'; + try { const query = 'MATCH (n:_Neodash_Dashboard) WHERE n.uuid = $uuid RETURN n.content as dashboard'; runCypherQuery( @@ -239,61 +305,188 @@ export const loadDashboardFromNeo4jThunk = (driver, database, uuid, callback) => `A dashboard with UUID '${uuid}' could not be loaded.` ) ); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'ERR - load dashboard', + database, + `UUID:${uuid}`, + `Error while trying to load dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } } else { callback(records[0]._fields[0]); + if (loggingSettings.loggingMode > '1') { + const dashboard = JSON.parse(records[0]._fields[0]); + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'INF - load dashboard', + database, + `Name:${dashboard.title}`, + `User ${loguser} Loaded dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } } } ); } catch (e) { dispatch(createNotificationThunk('Unable to load dashboard to Neo4j', e)); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'ERR - load dashboard', + database, + `UUID:${uuid}`, + `Error while trying to load dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring(0, 33)}` + ) + ); + } } }; -export const loadDashboardFromNeo4jByNameThunk = (driver, database, name, callback) => (dispatch: any) => { - try { - const query = - 'MATCH (d:_Neodash_Dashboard) WHERE d.title = $name RETURN d.content as dashboard ORDER by d.date DESC LIMIT 1'; - runCypherQuery( - driver, - database, - query, - { name: name }, - 1, - (status) => { - if (status == QueryStatus.NO_DATA) { - dispatch( - createNotificationThunk( - 'Unable to load dashboard.', - 'A dashboard with the provided name could not be found.' - ) - ); - } - }, - (records) => { - if (records.length == 0) { - dispatch( - createNotificationThunk( - 'Unable to load dashboard.', - 'A dashboard with the provided name could not be found.' - ) - ); - return; - } +export const loadDashboardFromNeo4jByNameThunk = + (driver, database, name, callback) => (dispatch: any, getState: any) => { + const loggingState = getState(); + const loggingSettings = applicationGetLoggingSettings(loggingState); + const loguser = applicationGetConnectionUser(loggingState); + const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor'; + try { + const query = + 'MATCH (d:_Neodash_Dashboard) WHERE d.title = $name RETURN d.content as dashboard ORDER by d.date DESC LIMIT 1'; + runCypherQuery( + driver, + database, + query, + { name: name }, + 1, + (status) => { + if (status == QueryStatus.NO_DATA) { + dispatch( + createNotificationThunk( + 'Unable to load dashboard.', + 'A dashboard with the provided name could not be found.' + ) + ); + } + }, + (records) => { + if (records.length == 0) { + dispatch( + createNotificationThunk( + 'Unable to load dashboard.', + 'A dashboard with the provided name could not be found.' + ) + ); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'ERR - load dashboard', + database, + `Name:${name}`, + `Error while trying to load dashboard by Name in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } + return; + } - if (records[0].error) { - dispatch(createNotificationThunk('Unable to load dashboard.', records[0].error)); - return; - } + if (records[0].error) { + dispatch(createNotificationThunk('Unable to load dashboard.', records[0].error)); + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'ERR - load dashboard', + database, + `Name:${name}`, + `Error while trying to load dashboard by Name in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } + return; + } - callback(records[0]._fields[0]); - } - ); - } catch (e) { - dispatch(createNotificationThunk('Unable to load dashboard from Neo4j', e)); - } -}; + if (loggingSettings.loggingMode > '1') { + dispatch( + createLogThunk( + driver, + loggingSettings.loggingDatabase, + neodashMode, + loguser, + 'INF - load dashboard', + database, + `Name:${name}`, + `User ${loguser} Loaded dashboard by UUID in ${neodashMode} mode at ${Date(Date.now()).substring( + 0, + 33 + )}` + ) + ); + } + callback(records[0]._fields[0]); + } + ); + } catch (e) { + dispatch(createNotificationThunk('Unable to load dashboard from Neo4j', e)); + } + }; export const loadDashboardListFromNeo4jThunk = (driver, database, callback) => (dispatch: any) => { + function setStatus(status) { + if (status == QueryStatus.NO_DATA) { + runCallback([]); + } + } + function runCallback(records) { + if (!records || !records[0] || !records[0]._fields) { + callback([]); + return; + } + const result = records.map((r, index) => { + return { + uuid: r._fields[0], + title: r._fields[1], + date: r._fields[2], + author: r._fields[3], + version: r._fields[4], + index: index, + }; + }); + callback(result); + } try { runCypherQuery( driver, @@ -301,24 +494,8 @@ export const loadDashboardListFromNeo4jThunk = (driver, database, callback) => ( '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, - () => {}, - (records) => { - if (!records || !records[0] || !records[0]._fields) { - callback([]); - return; - } - const result = records.map((r, index) => { - return { - uuid: r._fields[0], - title: r._fields[1], - date: r._fields[2], - author: r._fields[3], - version: r._fields[4], - index: index, - }; - }); - callback(result); - } + (status) => setStatus(status), + (records) => runCallback(records) ); } catch (e) { dispatch(createNotificationThunk('Unable to load dashboard list from Neo4j', e)); diff --git a/src/dashboard/header/DashboardHeader.tsx b/src/dashboard/header/DashboardHeader.tsx index c67d37c2c..707a22f3c 100644 --- a/src/dashboard/header/DashboardHeader.tsx +++ b/src/dashboard/header/DashboardHeader.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { setDashboardTitle } from '../DashboardActions'; import { getDashboardSettings, getDashboardTheme, getDashboardTitle, getPages } from '../DashboardSelectors'; import { setConnectionModalOpen } from '../../application/ApplicationActions'; -import { applicationIsStandalone } from '../../application/ApplicationSelectors'; +import { applicationGetStandaloneSettings, applicationGetCustomHeader } from '../../application/ApplicationSelectors'; import { getDashboardIsEditable, getPageNumber } from '../../settings/SettingsSelectors'; import { NeoDashboardHeaderLogo } from './DashboardHeaderLogo'; import NeoAboutButton from './DashboardHeaderAboutButton'; @@ -15,8 +15,9 @@ import { DASHBOARD_HEADER_BUTTON_COLOR } from '../../config/ApplicationConfig'; import { Tooltip } from '@mui/material'; export const NeoDashboardHeader = ({ - standalone, + standaloneSettings, dashboardTitle, + customHeader, connection, settings, onConnectionModalOpen, @@ -45,14 +46,15 @@ export const NeoDashboardHeader = ({ useEffect(() => { setTheme(isDarkMode ? 'dark' : 'light'); }, [isDarkMode]); - const content = (
@@ -72,7 +74,7 @@ export const NeoDashboardHeader = ({ {downloadImageEnabled && } - +
@@ -84,7 +86,8 @@ export const NeoDashboardHeader = ({ const mapStateToProps = (state) => ({ dashboardTitle: getDashboardTitle(state), - standalone: applicationIsStandalone(state), + standaloneSettings: applicationGetStandaloneSettings(state), + customHeader: applicationGetCustomHeader(state), pages: getPages(state), settings: getDashboardSettings(state), editable: getDashboardIsEditable(state), diff --git a/src/dashboard/header/DashboardHeaderLogoutButton.tsx b/src/dashboard/header/DashboardHeaderLogoutButton.tsx index c0fe918db..9b734af48 100644 --- a/src/dashboard/header/DashboardHeaderLogoutButton.tsx +++ b/src/dashboard/header/DashboardHeaderLogoutButton.tsx @@ -9,17 +9,17 @@ import { ArrowRightOnRectangleIconOutline } from '@neo4j-ndl/react/icons'; await StyleConfig.getInstance(); -export const NeoLogoutButton = ({ standalone, onConnectionModalOpen }) => { - return ( +export const NeoLogoutButton = ({ standaloneSettings, onConnectionModalOpen }) => { + return standaloneSettings.standalone && !standaloneSettings.standaloneMultiDatabase ? ( + <> + ) : ( { - if (!standalone) { - onConnectionModalOpen(); - } + onConnectionModalOpen(); }} size='large' clean diff --git a/src/dashboard/header/DashboardTitle.tsx b/src/dashboard/header/DashboardTitle.tsx index 312d841fa..efb8403f3 100644 --- a/src/dashboard/header/DashboardTitle.tsx +++ b/src/dashboard/header/DashboardTitle.tsx @@ -2,19 +2,16 @@ import React, { Suspense, useCallback, useEffect, useState } from 'react'; import debounce from 'lodash/debounce'; import { connect } from 'react-redux'; import { setDashboardTitle } from '../DashboardActions'; -import { applicationGetConnection, applicationIsStandalone } from '../../application/ApplicationSelectors'; +import { applicationGetConnection, applicationGetStandaloneSettings } from '../../application/ApplicationSelectors'; import { getDashboardTitle, getDashboardExtensions, getDashboardSettings } from '../DashboardSelectors'; import { getDashboardIsEditable } from '../../settings/SettingsSelectors'; import { updateDashboardSetting } from '../../settings/SettingsActions'; import { Typography, IconButton, Menu, MenuItems, TextInput } from '@neo4j-ndl/react'; import { CheckBadgeIconOutline, EllipsisHorizontalIconOutline, PencilSquareIconOutline } from '@neo4j-ndl/react/icons'; import NeoSettingsModal from '../../settings/SettingsModal'; -import NeoShareModal from '../sidebar/modal/legacy/LegacyShareModal'; import NeoExtensionsModal from '../../extensions/ExtensionsModal'; import { EXTENSIONS_DRAWER_BUTTONS } from '../../extensions/ExtensionConfig'; - import { Tooltip } from '@mui/material'; -import NeoDashboardSidebarExportModal from '../sidebar/modal/DashboardSidebarExportModal'; import NeoExportModal from '../../modal/ExportModal'; import { setDraft } from '../../application/ApplicationActions'; @@ -24,7 +21,7 @@ export const NeoDashboardTitle = ({ dashboardTitle, setDashboardTitle, editable, - isStandalone, + standaloneSettings, dashboardSettings, extensions, updateDashboardSetting, @@ -69,7 +66,8 @@ export const NeoDashboardTitle = ({ return (
{/* TODO : Replace with editable field if dashboard is editable */} - {editing ? ( + {/* only allow edit title if dashboard is not standalone - here we are in Title edit mode*/} + {editing && !standaloneSettings.standalone ? (
- ) : ( + ) : !standaloneSettings.standalone /* out of edit mode - if Not Standalone we display the edit button */ ? (
{dashboardTitle ? dashboardTitle : '(no title)'} @@ -117,9 +115,14 @@ export const NeoDashboardTitle = ({ )}
+ ) : ( + /* if we are in Standalone just title is displayed with no edit button */ +
+ {dashboardTitle} +
)} {/* If the app is not running in standalone mode (i.e. in edit mode) always show dashboard settings. */} - {!isStandalone ? ( + {!standaloneSettings.standalone ? (
{editable ? renderExtensionsButtons() : <>} {editable ? : <>} @@ -160,7 +163,7 @@ export const NeoDashboardTitle = ({ const mapStateToProps = (state) => ({ dashboardTitle: getDashboardTitle(state), editable: getDashboardIsEditable(state), - isStandalone: applicationIsStandalone(state), + standaloneSettings: applicationGetStandaloneSettings(state), dashboardSettings: getDashboardSettings(state), extensions: getDashboardExtensions(state), connection: applicationGetConnection(state), diff --git a/src/dashboard/sidebar/DashboardSidebar.tsx b/src/dashboard/sidebar/DashboardSidebar.tsx index 03e241bfb..5c27b8158 100644 --- a/src/dashboard/sidebar/DashboardSidebar.tsx +++ b/src/dashboard/sidebar/DashboardSidebar.tsx @@ -10,6 +10,7 @@ import { DashboardSidebarListItem } from './DashboardSidebarListItem'; import { applicationGetConnection, applicationGetConnectionDatabase, + applicationGetStandaloneSettings, applicationIsStandalone, dashboardIsDraft, } from '../../application/ApplicationSelectors'; @@ -78,6 +79,7 @@ export const NeoDashboardSidebar = ({ loadDashboardFromNeo4j, saveDashboardToNeo4j, deleteDashboardFromNeo4j, + standaloneSettings, }) => { const { driver } = useContext(Neo4jContext); const [expanded, setOnExpanded] = useState(false); @@ -256,7 +258,9 @@ export const NeoDashboardSidebar = ({ // We changed the active dashboard database, reload the list in the sidebar. loadDashboardListFromNeo4j(driver, newDatabase, (list) => { setDashboards(list); - setDraft(true); + if (!readonly) { + setDraft(true); + } }); }} open={menuOpen == Menu.DATABASE} @@ -338,7 +342,7 @@ export const NeoDashboardSidebar = ({ Dashboards {/* Only let users create dashboards and change database when running in editor mode. */} - {readonly == false ? ( + {!readonly || (readonly && standaloneSettings.standaloneLoadFromOtherDatabases) ? ( <> - - - + {!readonly ? ( + + + + ) : ( + <> + )} ) : ( <> @@ -469,6 +485,7 @@ const mapStateToProps = (state) => ({ dashboard: getDashboardJson(state), dashboardSettings: getDashboardSettings(state), database: applicationGetConnectionDatabase(state), + standaloneSettings: applicationGetStandaloneSettings(state), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/modal/ConnectionModal.tsx b/src/modal/ConnectionModal.tsx index fe1b6e7c7..61a821706 100644 --- a/src/modal/ConnectionModal.tsx +++ b/src/modal/ConnectionModal.tsx @@ -44,6 +44,16 @@ export default function NeoConnectionModal({ const discoveryAPIUrl = ssoSettings && ssoSettings.ssoDiscoveryUrl; + // since config is loaded asynchronously, value may not be yet defined when this runs for first time + let standaloneDatabaseList = [standaloneSettings.standaloneDatabase]; + try { + standaloneDatabaseList = standaloneSettings.standaloneDatabaseList + ? standaloneSettings.standaloneDatabaseList.split(',') + : standaloneDatabaseList; + } catch (e) { + console.log(e); + } + return ( <> neo4j+s protocol. Your current configuration may not work.
) : null} - setDatabase(e.target.value)} - label='Database (optional)' - placeholder='neo4j' - fluid - /> + {!standalone ? ( + setDatabase(e.target.value)} + label='Database (optional)' + placeholder='neo4j' + fluid + /> + ) : ( + { + setDatabase(newValue.value); + }, + options: standaloneDatabaseList.map((option) => ({ + label: option, + value: option, + })), + value: { label: database, value: database }, + menuPlacement: 'auto', + }} + fluid + > + )} {!ssoVisible ? (