Skip to content

Release GQL Errors and Notifications support #2009

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion e2e_tests/integration/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('Cypher Editor', () => {
// It can take a little while for the label meta-data to update in the background
cy.getEditor().type(selectAllAndDelete)
cy.executeCommand('return extraTimeForMetadataupdate')
cy.resultContains('extraTimeForMetadataupdate')
cy.resultContains('ERROR')
cy.wait(5000)

cy.getEditor().type(selectAllAndDelete)
Expand Down
5 changes: 4 additions & 1 deletion e2e_tests/integration/multistatements.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('Multi statements', () => {
after(() => {
cy.disableMultiStatement()
})

it('can connect', () => {
const password = Cypress.config('password')
cy.connect('neo4j', password)
Expand Down Expand Up @@ -69,7 +70,7 @@ describe('Multi statements', () => {
)
cy.get('[data-testid="frameContents"]', { timeout: 10000 })
.first()
.should('contain', 'Error')
.should('contain', 'ERROR')

cy.get('[data-testid="navigationSettings"]').click()
cy.get('[data-testid="setting-enableMultiStatementMode"]').click()
Expand All @@ -94,6 +95,7 @@ describe('Multi statements', () => {
.first()
.should('contain', 'ERROR')
})

it('Takes any statements (not just valid cypher and client commands)', () => {
cy.executeCommand(':clear')
const query = 'RETURN 1; hello1; RETURN 2; hello2;'
Expand All @@ -112,6 +114,7 @@ describe('Multi statements', () => {
.first()
.should('contain', 'ERROR')
})

if (Cypress.config('serverVersion') >= 4.1) {
if (isEnterpriseEdition()) {
it('Can use :use command in multi-statements', () => {
Expand Down
4 changes: 2 additions & 2 deletions e2e_tests/integration/params.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ testData.forEach(testData => {
it('can generate a set params template to use if query is missing params', () => {
cy.executeCommand(':clear')
cy.executeCommand('return $test1, $test2')
const expectedMessage = `Expected parameter(s): test1, test2`
cy.get('[data-testid="cypherFrameErrorMessage"]', { timeout: 20000 })
const expectedMessage = `Use this template to add missing parameter(s):`
cy.get('[data-testid="frameContents"]', { timeout: 20000 })
.first()
.should('contain', expectedMessage)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ const mount = (props: Partial<ErrorsViewProps>, state?: any) => {
params: {},
executeCmd: jest.fn(),
setEditorContent: jest.fn(),
neo4jVersion: null,
gqlErrorsEnabled: true
neo4jVersion: null
}

const combinedProps = {
Expand Down Expand Up @@ -97,7 +96,7 @@ describe('ErrorsView', () => {
expect(container).toMatchSnapshot()
})

test('does display an error for gql status codes', () => {
test('does display an error for GQL status codes', () => {
// Given
const error: BrowserError = {
code: 'Test.Error',
Expand All @@ -116,11 +115,8 @@ describe('ErrorsView', () => {
const state = {
meta: {
server: {
version: '5.26.0'
version: '5.27.0'
}
},
settings: {
enableGqlErrorsAndNotifications: true
}
}

Expand All @@ -131,7 +127,7 @@ describe('ErrorsView', () => {
expect(container).toMatchSnapshot()
})

test('does display a nested error for gql status codes', () => {
test('does display a nested error for GQL status codes', () => {
// Given
const error: BrowserError = {
code: 'Test.Error',
Expand Down Expand Up @@ -159,11 +155,8 @@ describe('ErrorsView', () => {
const state = {
meta: {
server: {
version: '5.26.0'
version: '5.27.0'
}
},
settings: {
enableGqlErrorsAndNotifications: true
}
}

Expand Down
144 changes: 103 additions & 41 deletions src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,50 @@ import {
import { BrowserError } from 'services/exceptions'
import { deepEquals } from 'neo4j-arc/common'
import { getSemanticVersion } from 'shared/modules/dbMeta/dbMetaDuck'
import { gte, SemVer } from 'semver'
import { SemVer } from 'semver'
import {
flattenAndInvertErrors,
formatError,
formatErrorGqlStatusObject,
hasPopulatedGqlFields
FormattedError
} from '../errorUtils'
import { FIRST_GQL_ERRORS_SUPPORT } from 'shared/modules/features/versionedFeatures'
import { shouldShowGqlErrorsAndNotifications } from 'shared/modules/settings/settingsDuck'
import { gqlErrorsAndNotificationsEnabled } from 'services/gqlUtils'
import styled from 'styled-components'

const StyledErrorsViewInnerComponentContent = styled.div<{ nested: boolean }>`
padding-left: ${props => (props.nested ? '20px' : '0')};
`

type ErrorsViewInnerProps = {
formattedError: FormattedError
nested?: boolean
}

class ErrorsViewInnerComponent extends Component<ErrorsViewInnerProps> {
render(): null | JSX.Element {
const { formattedError, nested = false } = this.props

return (
<StyledErrorsViewInnerComponentContent nested={nested}>
<StyledHelpDescription>
{!nested && (
<React.Fragment>
<StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage>
<StyledErrorH4>{formattedError.title}</StyledErrorH4>
</React.Fragment>
)}
{nested && <h6>{formattedError.title}</h6>}
</StyledHelpDescription>
{formattedError.description && (
<StyledDiv>
<StyledPreformattedArea data-testid={'cypherFrameErrorMessage'}>
{formattedError?.description}
</StyledPreformattedArea>
</StyledDiv>
)}
</StyledErrorsViewInnerComponentContent>
)
}
}

export type ErrorsViewProps = {
result: BrowserRequestResult
Expand All @@ -74,14 +110,26 @@ export type ErrorsViewProps = {
executeCmd: (cmd: string) => void
setEditorContent: (cmd: string) => void
depth?: number
gqlErrorsEnabled: boolean
gqlErrorsAndNotificationsEnabled?: boolean
}

type ErrorsViewState = {
nestedErrorsToggled: boolean
}

class ErrorsViewComponent extends Component<ErrorsViewProps> {
shouldComponentUpdate(props: ErrorsViewProps): boolean {
class ErrorsViewComponent extends Component<ErrorsViewProps, ErrorsViewState> {
state = {
nestedErrorsToggled: false
}

shouldComponentUpdate(
props: ErrorsViewProps,
state: ErrorsViewState
): boolean {
return (
!deepEquals(props.result, this.props.result) ||
!deepEquals(props.params, this.props.params)
!deepEquals(props.params, this.props.params) ||
!deepEquals(state, this.state)
)
}

Expand All @@ -92,19 +140,32 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
executeCmd,
setEditorContent,
neo4jVersion,
depth = 0,
gqlErrorsEnabled
gqlErrorsAndNotificationsEnabled = false
} = this.props

const error = this.props.result as BrowserError

const invertedErrors = flattenAndInvertErrors(
error,
gqlErrorsAndNotificationsEnabled
)
const [deepestError] = invertedErrors
const nestedErrors = invertedErrors.slice(1)
const togglable = nestedErrors.length > 0
const setNestedErrorsToggled = (toggled: boolean) => {
this.setState({
nestedErrorsToggled: toggled
})
}

if (!error) {
return null
}

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

if (!formattedError?.title) {
return null
Expand All @@ -115,24 +176,9 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
}

return (
<StyledHelpFrame nested={depth > 0}>
<StyledHelpFrame>
<StyledHelpContent>
<StyledHelpDescription>
{depth === 0 && (
<StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage>
)}
<StyledErrorH4>{formattedError.title}</StyledErrorH4>
</StyledHelpDescription>
{formattedError.description && (
<StyledDiv>
<StyledPreformattedArea data-testid={'cypherFrameErrorMessage'}>
{formattedError?.description}
</StyledPreformattedArea>
</StyledDiv>
)}
{formattedError.innerError && (
<ErrorsView result={formattedError.innerError} depth={depth + 1} />
)}
<ErrorsViewInnerComponent formattedError={formattedError} />
{isUnknownProcedureError(error) && (
<StyledLinkContainer>
<StyledLink
Expand Down Expand Up @@ -170,24 +216,39 @@ class ErrorsViewComponent extends Component<ErrorsViewProps> {
}
/>
)}
{togglable && (
<StyledLinkContainer>
<StyledLink
onClick={() =>
setNestedErrorsToggled(!this.state.nestedErrorsToggled)
}
>
&nbsp;
{this.state.nestedErrorsToggled ? 'Show less' : 'Show more'}
</StyledLink>
</StyledLinkContainer>
)}
{this.state.nestedErrorsToggled &&
nestedErrors.map((nestedError, index) => (
<ErrorsViewInnerComponent
key={index}
nested={true}
formattedError={formatError(
nestedError,
gqlErrorsAndNotificationsEnabled
)}
/>
))}
</StyledHelpContent>
</StyledHelpFrame>
)
}
}

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)
gqlErrorsAndNotificationsEnabled: gqlErrorsAndNotificationsEnabled(state)
})

const mapDispatchToProps = (
Expand All @@ -206,6 +267,7 @@ const mapDispatchToProps = (
}
}
}

export const ErrorsView = withBus(
connect(mapStateToProps, mapDispatchToProps)(ErrorsViewComponent)
)
Loading