Skip to content

Commit e860776

Browse files
author
Jon Harris
authored
add support for browser.retain_editor_history setting (#1419)
* add support for browser.retain_editor_history setting also change local defaults for community edition * fix localstorage unit tests (add edition to mock state) * add localstorage unit tests for retain editor history setting * update snapshot for new retain history setting * make sure history is not cleared in community edition misc code cleanup * never allow outgoing connections before edition is known also misc code cleanup
1 parent 5a08d7e commit e860776

File tree

8 files changed

+172
-13
lines changed

8 files changed

+172
-13
lines changed

docs/modules/ROOT/pages/operations.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,10 @@ Separate multiple commands with a semicolon (`;`).
446446
| `browser.retain_connection_credentials`
447447
| `true`
448448
| Configure the Neo4j Browser to store or not store user credentials.
449+
450+
| `browser.retain_editor_history`
451+
| `true`
452+
| Configure the Neo4j Browser to store or not store editor history.
449453
|===
450454

451455

src/browser/AppInit.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { APP_START } from 'shared/modules/app/appDuck'
4949
import { detectRuntimeEnv } from 'services/utils'
5050
import { NEO4J_CLOUD_DOMAINS } from 'shared/modules/settings/settingsDuck'
5151
import { version } from 'project-root/package.json'
52-
import { allowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
52+
import { shouldAllowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
5353
import { getUuid } from 'shared/modules/udc/udcDuck'
5454
import { DndProvider } from 'react-dnd'
5555
import { HTML5Backend } from 'react-dnd-html5-backend'
@@ -167,7 +167,7 @@ export function setupSentry(): void {
167167
}
168168
},
169169
beforeSend: event =>
170-
allowOutgoingConnections(store.getState())
170+
shouldAllowOutgoingConnections(store.getState())
171171
? scrubQueryParamsAndUrl(event)
172172
: null,
173173
environment: 'unset'

src/browser/modules/App/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { utilizeBrowserSync } from 'shared/modules/features/featuresDuck'
3434
import { getOpenDrawer } from 'shared/modules/sidebar/sidebarDuck'
3535
import { getErrorMessage } from 'shared/modules/commands/commandsDuck'
3636
import {
37-
allowOutgoingConnections,
37+
shouldAllowOutgoingConnections,
3838
getDatabases
3939
} from 'shared/modules/dbMeta/dbMetaDuck'
4040
import {
@@ -244,7 +244,7 @@ const mapStateToProps = (state: any) => {
244244
lastConnectionUpdate: getLastConnectionUpdate(state),
245245
errorMessage: getErrorMessage(state),
246246
loadExternalScripts:
247-
allowOutgoingConnections(state) !== false && isConnected(state),
247+
shouldAllowOutgoingConnections(state) !== false && isConnected(state),
248248
titleString: asTitleString(connectionData),
249249
defaultConnectionData: getConnectionData(state, CONNECTION_ID),
250250
syncConsent: state.syncConsent.consented,

src/browser/modules/Sync/BrowserSync.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import React, { Component } from 'react'
2222
import { connect } from 'react-redux'
2323
import TimeAgo from 'react-timeago'
24-
import { allowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
24+
import { shouldAllowOutgoingConnections } from 'shared/modules/dbMeta/dbMetaDuck'
2525
import {
2626
getConnectionState,
2727
DISCONNECTED_STATE
@@ -282,7 +282,7 @@ const mapStateToProps = (state: any) => {
282282
browserSyncConfig: getBrowserSyncConfig(state),
283283
syncConsent: state.syncConsent.consented,
284284
connectionState: getConnectionState(state),
285-
isAllowed: allowOutgoingConnections(state) !== false
285+
isAllowed: shouldAllowOutgoingConnections(state) !== false
286286
}
287287
}
288288

src/shared/modules/dbMeta/__snapshots__/dbMetaDuck.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Object {
2121
"browser.allow_outgoing_connections": false,
2222
"browser.remote_content_hostname_allowlist": "guides.neo4j.com, localhost",
2323
"browser.retain_connection_credentials": false,
24+
"browser.retain_editor_history": false,
2425
},
2526
}
2627
`;
@@ -45,6 +46,7 @@ Object {
4546
"browser.allow_outgoing_connections": false,
4647
"browser.remote_content_hostname_allowlist": "guides.neo4j.com, localhost",
4748
"browser.retain_connection_credentials": false,
49+
"browser.retain_editor_history": false,
4850
},
4951
}
5052
`;

src/shared/modules/dbMeta/dbMetaDuck.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import neo4j from 'neo4j-driver'
2222
import Rx from 'rxjs/Rx'
23+
import semver from 'semver'
2324
import bolt from 'services/bolt/bolt'
2425
import { isConfigValFalsy } from 'services/bolt/boltHelpers'
2526
import { APP_START } from 'shared/modules/app/appDuck'
@@ -59,6 +60,7 @@ import {
5960
isACausalCluster,
6061
setClientConfig
6162
} from '../features/featuresDuck'
63+
import { clearHistory } from 'shared/modules/history/historyDuck'
6264

6365
export const NAME = 'meta'
6466
export const UPDATE = 'meta/UPDATE'
@@ -96,6 +98,8 @@ export function getMetaInContext(state: any, context: any) {
9698
export const getVersion = (state: any) =>
9799
(state[NAME] || {}).server ? (state[NAME] || {}).server.version : 0
98100
export const getEdition = (state: any) => state[NAME].server.edition
101+
export const hasEdition = (state: any) =>
102+
state[NAME].server.edition !== initialState.server.edition
99103
export const getStoreSize = (state: any) => state[NAME].server.storeSize
100104
export const getClusterRole = (state: any) => state[NAME].role
101105
export const isEnterprise = (state: any) =>
@@ -106,20 +110,26 @@ export const getStoreId = (state: any) =>
106110

107111
export const getAvailableSettings = (state: any) =>
108112
(state[NAME] || initialState).settings
109-
export const allowOutgoingConnections = (state: any) =>
113+
export const getAllowOutgoingConnections = (state: any) =>
110114
getAvailableSettings(state)['browser.allow_outgoing_connections']
111115
export const credentialsTimeout = (state: any) =>
112116
getAvailableSettings(state)['browser.credential_timeout'] || 0
113117
export const getRemoteContentHostnameAllowlist = (state: any) =>
114118
getAvailableSettings(state)['browser.remote_content_hostname_allowlist']
115119
export const getDefaultRemoteContentHostnameAllowlist = (_state: any) =>
116120
initialState.settings['browser.remote_content_hostname_allowlist']
117-
export const shouldRetainConnectionCredentials = (state: any) => {
121+
export const getRetainConnectionCredentials = (state: any) => {
118122
const settings = getAvailableSettings(state)
119123
const conf = settings['browser.retain_connection_credentials']
120124
if (conf === null || typeof conf === 'undefined') return false
121125
return !isConfigValFalsy(conf)
122126
}
127+
export const getRetainEditorHistory = (state: any) => {
128+
const settings = getAvailableSettings(state)
129+
const conf = settings['browser.retain_editor_history']
130+
if (conf === null || typeof conf === 'undefined') return false
131+
return !isConfigValFalsy(conf)
132+
}
123133
export const getDatabases = (state: any) =>
124134
(state[NAME] || initialState).databases
125135
export const getActiveDbName = (state: any) =>
@@ -128,6 +138,24 @@ export const getActiveDbName = (state: any) =>
128138
* Helpers
129139
*/
130140

141+
export const VERSION_FOR_EDITOR_HISTORY_SETTING = '4.3.0'
142+
143+
export const versionHasEditorHistorySetting = (version: string) =>
144+
semver.gte(version, VERSION_FOR_EDITOR_HISTORY_SETTING)
145+
146+
export const supportsEditorHistorySetting = (state: any) =>
147+
isEnterprise(state) && versionHasEditorHistorySetting(getVersion(state))
148+
149+
export const shouldAllowOutgoingConnections = (state: any) =>
150+
(hasEdition(state) && !isEnterprise(state)) ||
151+
getAllowOutgoingConnections(state)
152+
153+
export const shouldRetainConnectionCredentials = (state: any) =>
154+
!isEnterprise(state) || getRetainConnectionCredentials(state)
155+
156+
export const shouldRetainEditorHistory = (state: any) =>
157+
!supportsEditorHistorySetting(state) || getRetainEditorHistory(state)
158+
131159
function updateMetaForContext(state: any, meta: any, context: any) {
132160
if (!meta || !meta.records || !meta.records.length) {
133161
return {
@@ -216,7 +244,8 @@ export const initialState = {
216244
settings: {
217245
'browser.allow_outgoing_connections': false,
218246
'browser.remote_content_hostname_allowlist': 'guides.neo4j.com, localhost',
219-
'browser.retain_connection_credentials': false
247+
'browser.retain_connection_credentials': false,
248+
'browser.retain_editor_history': false
220249
}
221250
}
222251

@@ -605,6 +634,13 @@ export const serverConfigEpic = (some$: any, store: any) =>
605634
}
606635
store.dispatch(setRetainCredentials(retainCredentials))
607636
value = retainCredentials
637+
} else if (name === 'browser.retain_editor_history') {
638+
let retainHistory = true
639+
// Check if we should wipe user history from localstorage
640+
if (typeof value !== 'undefined' && isConfigValFalsy(value)) {
641+
retainHistory = false
642+
}
643+
value = retainHistory
608644
} else if (name === 'browser.allow_outgoing_connections') {
609645
// Use isConfigValFalsy to cast undefined to true
610646
value = !isConfigValFalsy(value)
@@ -623,6 +659,10 @@ export const serverConfigEpic = (some$: any, store: any) =>
623659
updateUserCapability(USER_CAPABILITIES.serverConfigReadable, true)
624660
)
625661
store.dispatch(updateSettings(settings))
662+
// settings must be updated in state for this check to work
663+
if (!shouldRetainEditorHistory(store.getState())) {
664+
store.dispatch(clearHistory())
665+
}
626666
return Rx.Observable.of(null)
627667
})
628668
})

src/shared/services/localstorage.test.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ describe('localstorage', () => {
9797
})
9898

9999
describe('localstorage redux middleware', () => {
100-
const createAndInvokeMiddlewareWithRetainFlag = (retain: boolean) => {
100+
const createAndInvokeMiddlewareWithRetainConnectionFlag = (
101+
retain: boolean,
102+
edition = 'enterprise'
103+
) => {
101104
const setItemMock = jest.fn()
102105
ls.applyKeys('connections')
103106
ls.setStorage(({
@@ -109,6 +112,9 @@ describe('localstorage', () => {
109112
connectionsById: { $$discovery: { password: 'secret password' } }
110113
},
111114
meta: {
115+
server: {
116+
edition
117+
},
112118
settings: {
113119
'browser.retain_connection_credentials': retain
114120
}
@@ -128,7 +134,9 @@ describe('localstorage', () => {
128134
}
129135

130136
it('removes passwords from connection data if browser.retain_connection_credentials is false', () => {
131-
const setItemMock = createAndInvokeMiddlewareWithRetainFlag(false)
137+
const setItemMock = createAndInvokeMiddlewareWithRetainConnectionFlag(
138+
false
139+
)
132140

133141
expect(setItemMock).toHaveBeenCalledWith(
134142
'neo4j.connections',
@@ -139,7 +147,9 @@ describe('localstorage', () => {
139147
})
140148

141149
it('retains passwords in connection data if browser.retain_connection_credentials is true', () => {
142-
const setItemMock = createAndInvokeMiddlewareWithRetainFlag(true)
150+
const setItemMock = createAndInvokeMiddlewareWithRetainConnectionFlag(
151+
true
152+
)
143153

144154
expect(setItemMock).toHaveBeenCalledWith(
145155
'neo4j.connections',
@@ -148,5 +158,103 @@ describe('localstorage', () => {
148158
})
149159
)
150160
})
161+
162+
const existingHistory = ['history item']
163+
164+
const createAndInvokeMiddlewareWithRetainHistoryFlag = ({
165+
retain,
166+
edition = 'enterprise',
167+
version = '4.3.0'
168+
}: {
169+
retain?: boolean
170+
edition?: string
171+
version?: string
172+
}) => {
173+
const setItemMock = jest.fn()
174+
ls.applyKeys('history')
175+
ls.setStorage(({
176+
setItem: setItemMock
177+
} as Partial<Storage>) as Storage)
178+
179+
const state = {
180+
history: existingHistory,
181+
meta: {
182+
server: {
183+
version,
184+
edition
185+
},
186+
settings: {
187+
'browser.retain_editor_history': retain
188+
}
189+
},
190+
connections: {
191+
connectionsById: { $$discovery: { password: 'secret password' } }
192+
}
193+
}
194+
195+
const store = {
196+
getState: () => state,
197+
dispatch: jest.fn()
198+
}
199+
const next = jest.fn(action => action)
200+
const action = { type: 'some action' }
201+
202+
ls.createReduxMiddleware()(store)(next)(action)
203+
204+
return setItemMock
205+
}
206+
207+
it('removes history from data if browser.retain_editor_history is false', () => {
208+
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
209+
retain: false
210+
})
211+
212+
expect(setItemMock).toHaveBeenCalledWith(
213+
'neo4j.history',
214+
JSON.stringify([])
215+
)
216+
})
217+
218+
it('retains history from data if browser.retain_editor_history is true', () => {
219+
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
220+
retain: true
221+
})
222+
223+
expect(setItemMock).toHaveBeenCalledWith(
224+
'neo4j.history',
225+
JSON.stringify(existingHistory)
226+
)
227+
})
228+
229+
it('removes history from data if browser.retain_editor_history is undefined and server version is >= 4.3.0', () => {
230+
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({})
231+
232+
expect(setItemMock).toHaveBeenCalledWith(
233+
'neo4j.history',
234+
JSON.stringify([])
235+
)
236+
})
237+
238+
it('retains history from data if browser.retain_editor_history is undefined and server version is < 4.3.0', () => {
239+
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
240+
version: '4.2.0'
241+
})
242+
243+
expect(setItemMock).toHaveBeenCalledWith(
244+
'neo4j.history',
245+
JSON.stringify(existingHistory)
246+
)
247+
})
248+
249+
it('retains history from data if browser.retain_editor_history is undefined and server edition is not enterprise', () => {
250+
const setItemMock = createAndInvokeMiddlewareWithRetainHistoryFlag({
251+
edition: 'community'
252+
})
253+
254+
expect(setItemMock).toHaveBeenCalledWith(
255+
'neo4j.history',
256+
JSON.stringify(existingHistory)
257+
)
258+
})
151259
})
152260
})

src/shared/services/localstorage.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
import { Middleware } from 'redux'
2222
import { GlobalState } from 'shared/globalState'
23-
import { shouldRetainConnectionCredentials } from '../modules/dbMeta/dbMetaDuck'
23+
import {
24+
shouldRetainConnectionCredentials,
25+
shouldRetainEditorHistory
26+
} from '../modules/dbMeta/dbMetaDuck'
2427
import { initialState as settingsInitialState } from '../modules/settings/settingsDuck'
2528

2629
export const keyPrefix = 'neo4j.'
@@ -99,6 +102,8 @@ export function createReduxMiddleware(): Middleware {
99102
)
100103
)
101104
})
105+
} else if (key === 'history' && !shouldRetainEditorHistory(state)) {
106+
setItem(key, [])
102107
} else {
103108
setItem(key, state[key])
104109
}

0 commit comments

Comments
 (0)