Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding GQL compliant errors and notifications to Browser #1989

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/browser/modules/Stream/CypherFrame/CypherFrame.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
BrowserRequestResult
} from 'shared/modules/requests/requestsDuck'

import { initialState as initialExperimentalFeatureState } from 'shared/modules/experimentalFeatures/experimentalFeaturesDuck'

const createProps = (
status: string,
result: BrowserRequestResult
Expand Down Expand Up @@ -62,7 +64,9 @@ describe('CypherFrame', () => {
maxRows: 1000,
maxFieldItems: 1000
},
app: {}
app: {},
connections: {},
experimentalFeatures: initialExperimentalFeatureState
})
}
test('renders accordingly from pending to success to error to success', () => {
Expand Down
121 changes: 111 additions & 10 deletions src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,50 @@
*/
import { render } from '@testing-library/react'
import React from 'react'
import { combineReducers, createStore } from 'redux'
import { createBus } from 'suber'

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

const mount = (partOfProps: Partial<ErrorsViewProps>) => {
const withProvider = (store: any, children: any) => {
return <Provider store={store}>{children}</Provider>
}

const mount = (props: Partial<ErrorsViewProps>, state?: any) => {
const defaultProps: ErrorsViewProps = {
result: null,
bus: createBus(),
params: {},
executeCmd: jest.fn(),
setEditorContent: jest.fn(),
neo4jVersion: null
neo4jVersion: null,
gqlErrorsEnabled: true
}
const props = {

const combinedProps = {
...defaultProps,
...partOfProps
...props
}

const initialState = {
meta: initialMetaState,
settings: initialSettingsState
}

const combinedState = { ...initialState, ...state }

const store = {
subscribe: () => {},
dispatch: () => {},
getState: () => ({
...combinedState
})
}
const reducer = combineReducers({ ...(reducers as any) })
const store: any = createStore(reducer)
return render(<ErrorsView store={store} {...props} />)

return render(withProvider(store, <ErrorsView {...combinedProps} />))
}

describe('ErrorsView', () => {
Expand All @@ -57,7 +78,8 @@ describe('ErrorsView', () => {
// Then
expect(container).toMatchSnapshot()
})
test('does displays an error', () => {

test('does display an error', () => {
// Given
const error: BrowserError = {
code: 'Test.Error',
Expand All @@ -74,6 +96,84 @@ describe('ErrorsView', () => {
// Then
expect(container).toMatchSnapshot()
})

test('does display an error for gql status codes', () => {
// Given
const error: BrowserError = {
code: 'Test.Error',
message: 'Test error description',
type: 'Neo4jError',
gqlStatus: '22N14',
gqlStatusDescription:
"error: data exception - invalid temporal value combination. Cannot select both epochSeconds and 'datetime'.",
cause: undefined
}

const props = {
result: error
}

const state = {
meta: {
server: {
version: '5.26.0'
}
},
settings: {
enableGqlErrorsAndNotifications: true
}
}

// When
const { container } = mount(props, state)

// Then
expect(container).toMatchSnapshot()
})

test('does display a nested error for gql status codes', () => {
// Given
const error: BrowserError = {
code: 'Test.Error',
message: 'Test error description',
type: 'Neo4jError',
gqlStatus: '42N51',
gqlStatusDescription:
'error: syntax error or access rule violation - invalid parameter. Invalid parameter $`param`. ',
cause: {
gqlStatus: '22G03',
gqlStatusDescription: 'error: data exception - invalid value type',
cause: {
gqlStatus: '22N27',
gqlStatusDescription:
"error: data exception - invalid entity type. Invalid input '******' for $`param`. Expected to be STRING.",
cause: undefined
}
}
}

const props = {
result: error
}

const state = {
meta: {
server: {
version: '5.26.0'
}
},
settings: {
enableGqlErrorsAndNotifications: true
}
}

// When
const { container } = mount(props, state)

// Then
expect(container).toMatchSnapshot()
})

test('displays procedure link if unknown procedure', () => {
// Given
const error: BrowserError = {
Expand All @@ -92,6 +192,7 @@ describe('ErrorsView', () => {
expect(container).toMatchSnapshot()
expect(getByText('List available procedures')).not.toBeUndefined()
})

test('displays procedure link if periodic commit error', () => {
// Given
const error: BrowserError = {
Expand Down
76 changes: 57 additions & 19 deletions src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { Bus } from 'suber'

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

import { errorMessageFormater } from '../../errorMessageFormater'
import {
StyledCypherErrorMessage,
StyledDiv,
Expand Down Expand Up @@ -58,7 +57,14 @@ import {
import { BrowserError } from 'services/exceptions'
import { deepEquals } from 'neo4j-arc/common'
import { getSemanticVersion } from 'shared/modules/dbMeta/dbMetaDuck'
import { SemVer } from 'semver'
import { gte, SemVer } from 'semver'
import {
formatError,
formatErrorGqlStatusObject,
hasPopulatedGqlFields
} from '../errorUtils'
import { FIRST_GQL_ERRORS_SUPPORT } from 'shared/modules/features/versionedFeatures'
import { shouldShowGqlErrorsAndNotifications } from 'shared/modules/settings/settingsDuck'

export type ErrorsViewProps = {
result: BrowserRequestResult
Expand All @@ -67,6 +73,8 @@ export type ErrorsViewProps = {
params: Record<string, unknown>
executeCmd: (cmd: string) => void
setEditorContent: (cmd: string) => void
depth?: number
gqlErrorsEnabled: boolean
}

class ErrorsViewComponent extends Component<ErrorsViewProps> {
Expand All @@ -78,31 +86,53 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
}

render(): null | JSX.Element {
const { bus, params, executeCmd, setEditorContent, neo4jVersion } =
this.props
const {
bus,
params,
executeCmd,
setEditorContent,
neo4jVersion,
depth = 0,
gqlErrorsEnabled
} = this.props

const error = this.props.result as BrowserError
if (!error || !error.code) {
if (!error) {
return null
}

const formattedError =
gqlErrorsEnabled && hasPopulatedGqlFields(error)
? formatErrorGqlStatusObject(error)
: formatError(error)

if (!formattedError?.title) {
return null
}
const fullError = errorMessageFormater(null, error.message)

const handleSetMissingParamsTemplateHelpMessageClick = () => {
bus.send(GENERATE_SET_MISSING_PARAMS_TEMPLATE, undefined)
}

return (
<StyledHelpFrame>
<StyledHelpFrame nested={depth > 0}>
<StyledHelpContent>
<StyledHelpDescription>
<StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage>
<StyledErrorH4>{error.code}</StyledErrorH4>
{depth === 0 && (
<StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage>
)}
<StyledErrorH4>{formattedError.title}</StyledErrorH4>
</StyledHelpDescription>
<StyledDiv>
<StyledPreformattedArea data-testid={'cypherFrameErrorMessage'}>
{fullError.message}
</StyledPreformattedArea>
</StyledDiv>
{formattedError.description && (
<StyledDiv>
<StyledPreformattedArea data-testid={'cypherFrameErrorMessage'}>
{formattedError?.description}
</StyledPreformattedArea>
</StyledDiv>
)}
{formattedError.innerError && (
<ErrorsView result={formattedError.innerError} depth={depth + 1} />
)}
{isUnknownProcedureError(error) && (
<StyledLinkContainer>
<StyledLink
Expand Down Expand Up @@ -146,12 +176,20 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
}
}

const mapStateToProps = (state: GlobalState) => {
return {
params: getParams(state),
neo4jVersion: getSemanticVersion(state)
}
const gqlErrorsEnabled = (state: GlobalState): boolean => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

despite my other comment about not being able to capture the protocol version at exception time, I think this is quite a neat way of showing either the existing error fields, or the GQL error fields

const featureEnabled = shouldShowGqlErrorsAndNotifications(state)
const version = getSemanticVersion(state)
return version
? featureEnabled && gte(version, FIRST_GQL_ERRORS_SUPPORT)
: false
}

const mapStateToProps = (state: GlobalState) => ({
params: getParams(state),
neo4jVersion: getSemanticVersion(state),
gqlErrorsEnabled: gqlErrorsEnabled(state)
})

const mapDispatchToProps = (
_dispatch: Dispatch<Action>,
ownProps: ErrorsViewProps
Expand Down
Loading
Loading