Skip to content

Commit 26f4817

Browse files
Adding GQL compliant errors and notifications to Browser (#1989)
* migrate notifications to gql status objects * implementing gql errors * update error frame tests * add copyright notice to string utils * place errors behind feature flag * update tooltip * rename feature flag, change to hidden config, remove protocol version, change to server version * update state in tests * regen snaps
1 parent c14e516 commit 26f4817

22 files changed

+1533
-160
lines changed

src/browser/modules/Stream/CypherFrame/CypherFrame.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
BrowserRequestResult
2929
} from 'shared/modules/requests/requestsDuck'
3030

31+
import { initialState as initialExperimentalFeatureState } from 'shared/modules/experimentalFeatures/experimentalFeaturesDuck'
32+
3133
const createProps = (
3234
status: string,
3335
result: BrowserRequestResult
@@ -62,7 +64,9 @@ describe('CypherFrame', () => {
6264
maxRows: 1000,
6365
maxFieldItems: 1000
6466
},
65-
app: {}
67+
app: {},
68+
connections: {},
69+
experimentalFeatures: initialExperimentalFeatureState
6670
})
6771
}
6872
test('renders accordingly from pending to success to error to success', () => {

src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.test.tsx

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,50 @@
1919
*/
2020
import { render } from '@testing-library/react'
2121
import React from 'react'
22-
import { combineReducers, createStore } from 'redux'
2322
import { createBus } from 'suber'
2423

2524
import { ErrorsView, ErrorsViewProps } from './ErrorsView'
26-
import reducers from 'project-root/src/shared/rootReducer'
2725
import { BrowserError } from 'services/exceptions'
26+
import { Provider } from 'react-redux'
27+
import { initialState as initialMetaState } from 'shared/modules/dbMeta/dbMetaDuck'
28+
import { initialState as initialSettingsState } from 'shared/modules/settings/settingsDuck'
2829

29-
const mount = (partOfProps: Partial<ErrorsViewProps>) => {
30+
const withProvider = (store: any, children: any) => {
31+
return <Provider store={store}>{children}</Provider>
32+
}
33+
34+
const mount = (props: Partial<ErrorsViewProps>, state?: any) => {
3035
const defaultProps: ErrorsViewProps = {
3136
result: null,
3237
bus: createBus(),
3338
params: {},
3439
executeCmd: jest.fn(),
3540
setEditorContent: jest.fn(),
36-
neo4jVersion: null
41+
neo4jVersion: null,
42+
gqlErrorsEnabled: true
3743
}
38-
const props = {
44+
45+
const combinedProps = {
3946
...defaultProps,
40-
...partOfProps
47+
...props
48+
}
49+
50+
const initialState = {
51+
meta: initialMetaState,
52+
settings: initialSettingsState
53+
}
54+
55+
const combinedState = { ...initialState, ...state }
56+
57+
const store = {
58+
subscribe: () => {},
59+
dispatch: () => {},
60+
getState: () => ({
61+
...combinedState
62+
})
4163
}
42-
const reducer = combineReducers({ ...(reducers as any) })
43-
const store: any = createStore(reducer)
44-
return render(<ErrorsView store={store} {...props} />)
64+
65+
return render(withProvider(store, <ErrorsView {...combinedProps} />))
4566
}
4667

4768
describe('ErrorsView', () => {
@@ -57,7 +78,8 @@ describe('ErrorsView', () => {
5778
// Then
5879
expect(container).toMatchSnapshot()
5980
})
60-
test('does displays an error', () => {
81+
82+
test('does display an error', () => {
6183
// Given
6284
const error: BrowserError = {
6385
code: 'Test.Error',
@@ -74,6 +96,84 @@ describe('ErrorsView', () => {
7496
// Then
7597
expect(container).toMatchSnapshot()
7698
})
99+
100+
test('does display an error for gql status codes', () => {
101+
// Given
102+
const error: BrowserError = {
103+
code: 'Test.Error',
104+
message: 'Test error description',
105+
type: 'Neo4jError',
106+
gqlStatus: '22N14',
107+
gqlStatusDescription:
108+
"error: data exception - invalid temporal value combination. Cannot select both epochSeconds and 'datetime'.",
109+
cause: undefined
110+
}
111+
112+
const props = {
113+
result: error
114+
}
115+
116+
const state = {
117+
meta: {
118+
server: {
119+
version: '5.26.0'
120+
}
121+
},
122+
settings: {
123+
enableGqlErrorsAndNotifications: true
124+
}
125+
}
126+
127+
// When
128+
const { container } = mount(props, state)
129+
130+
// Then
131+
expect(container).toMatchSnapshot()
132+
})
133+
134+
test('does display a nested error for gql status codes', () => {
135+
// Given
136+
const error: BrowserError = {
137+
code: 'Test.Error',
138+
message: 'Test error description',
139+
type: 'Neo4jError',
140+
gqlStatus: '42N51',
141+
gqlStatusDescription:
142+
'error: syntax error or access rule violation - invalid parameter. Invalid parameter $`param`. ',
143+
cause: {
144+
gqlStatus: '22G03',
145+
gqlStatusDescription: 'error: data exception - invalid value type',
146+
cause: {
147+
gqlStatus: '22N27',
148+
gqlStatusDescription:
149+
"error: data exception - invalid entity type. Invalid input '******' for $`param`. Expected to be STRING.",
150+
cause: undefined
151+
}
152+
}
153+
}
154+
155+
const props = {
156+
result: error
157+
}
158+
159+
const state = {
160+
meta: {
161+
server: {
162+
version: '5.26.0'
163+
}
164+
},
165+
settings: {
166+
enableGqlErrorsAndNotifications: true
167+
}
168+
}
169+
170+
// When
171+
const { container } = mount(props, state)
172+
173+
// Then
174+
expect(container).toMatchSnapshot()
175+
})
176+
77177
test('displays procedure link if unknown procedure', () => {
78178
// Given
79179
const error: BrowserError = {
@@ -92,6 +192,7 @@ describe('ErrorsView', () => {
92192
expect(container).toMatchSnapshot()
93193
expect(getByText('List available procedures')).not.toBeUndefined()
94194
})
195+
95196
test('displays procedure link if periodic commit error', () => {
96197
// Given
97198
const error: BrowserError = {

src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { Bus } from 'suber'
2525

2626
import { PlayIcon } from 'browser-components/icons/LegacyIcons'
2727

28-
import { errorMessageFormater } from '../../errorMessageFormater'
2928
import {
3029
StyledCypherErrorMessage,
3130
StyledDiv,
@@ -58,7 +57,14 @@ import {
5857
import { BrowserError } from 'services/exceptions'
5958
import { deepEquals } from 'neo4j-arc/common'
6059
import { getSemanticVersion } from 'shared/modules/dbMeta/dbMetaDuck'
61-
import { SemVer } from 'semver'
60+
import { gte, SemVer } from 'semver'
61+
import {
62+
formatError,
63+
formatErrorGqlStatusObject,
64+
hasPopulatedGqlFields
65+
} from '../errorUtils'
66+
import { FIRST_GQL_ERRORS_SUPPORT } from 'shared/modules/features/versionedFeatures'
67+
import { shouldShowGqlErrorsAndNotifications } from 'shared/modules/settings/settingsDuck'
6268

6369
export type ErrorsViewProps = {
6470
result: BrowserRequestResult
@@ -67,6 +73,8 @@ export type ErrorsViewProps = {
6773
params: Record<string, unknown>
6874
executeCmd: (cmd: string) => void
6975
setEditorContent: (cmd: string) => void
76+
depth?: number
77+
gqlErrorsEnabled: boolean
7078
}
7179

7280
class ErrorsViewComponent extends Component<ErrorsViewProps> {
@@ -78,31 +86,53 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
7886
}
7987

8088
render(): null | JSX.Element {
81-
const { bus, params, executeCmd, setEditorContent, neo4jVersion } =
82-
this.props
89+
const {
90+
bus,
91+
params,
92+
executeCmd,
93+
setEditorContent,
94+
neo4jVersion,
95+
depth = 0,
96+
gqlErrorsEnabled
97+
} = this.props
8398

8499
const error = this.props.result as BrowserError
85-
if (!error || !error.code) {
100+
if (!error) {
101+
return null
102+
}
103+
104+
const formattedError =
105+
gqlErrorsEnabled && hasPopulatedGqlFields(error)
106+
? formatErrorGqlStatusObject(error)
107+
: formatError(error)
108+
109+
if (!formattedError?.title) {
86110
return null
87111
}
88-
const fullError = errorMessageFormater(null, error.message)
89112

90113
const handleSetMissingParamsTemplateHelpMessageClick = () => {
91114
bus.send(GENERATE_SET_MISSING_PARAMS_TEMPLATE, undefined)
92115
}
93116

94117
return (
95-
<StyledHelpFrame>
118+
<StyledHelpFrame nested={depth > 0}>
96119
<StyledHelpContent>
97120
<StyledHelpDescription>
98-
<StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage>
99-
<StyledErrorH4>{error.code}</StyledErrorH4>
121+
{depth === 0 && (
122+
<StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage>
123+
)}
124+
<StyledErrorH4>{formattedError.title}</StyledErrorH4>
100125
</StyledHelpDescription>
101-
<StyledDiv>
102-
<StyledPreformattedArea data-testid={'cypherFrameErrorMessage'}>
103-
{fullError.message}
104-
</StyledPreformattedArea>
105-
</StyledDiv>
126+
{formattedError.description && (
127+
<StyledDiv>
128+
<StyledPreformattedArea data-testid={'cypherFrameErrorMessage'}>
129+
{formattedError?.description}
130+
</StyledPreformattedArea>
131+
</StyledDiv>
132+
)}
133+
{formattedError.innerError && (
134+
<ErrorsView result={formattedError.innerError} depth={depth + 1} />
135+
)}
106136
{isUnknownProcedureError(error) && (
107137
<StyledLinkContainer>
108138
<StyledLink
@@ -146,12 +176,20 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
146176
}
147177
}
148178

149-
const mapStateToProps = (state: GlobalState) => {
150-
return {
151-
params: getParams(state),
152-
neo4jVersion: getSemanticVersion(state)
153-
}
179+
const gqlErrorsEnabled = (state: GlobalState): boolean => {
180+
const featureEnabled = shouldShowGqlErrorsAndNotifications(state)
181+
const version = getSemanticVersion(state)
182+
return version
183+
? featureEnabled && gte(version, FIRST_GQL_ERRORS_SUPPORT)
184+
: false
154185
}
186+
187+
const mapStateToProps = (state: GlobalState) => ({
188+
params: getParams(state),
189+
neo4jVersion: getSemanticVersion(state),
190+
gqlErrorsEnabled: gqlErrorsEnabled(state)
191+
})
192+
155193
const mapDispatchToProps = (
156194
_dispatch: Dispatch<Action>,
157195
ownProps: ErrorsViewProps

0 commit comments

Comments
 (0)