Skip to content

Commit

Permalink
Adding GQL compliant errors and notifications to Browser (#1989)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
daveajrussell authored Dec 16, 2024
1 parent c14e516 commit 26f4817
Show file tree
Hide file tree
Showing 22 changed files with 1,533 additions and 160 deletions.
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 => {
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

0 comments on commit 26f4817

Please sign in to comment.