diff --git a/e2e_tests/integration/editor.spec.ts b/e2e_tests/integration/editor.spec.ts index dfbe5eaa41c..02b7bb7a85c 100644 --- a/e2e_tests/integration/editor.spec.ts +++ b/e2e_tests/integration/editor.spec.ts @@ -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) diff --git a/e2e_tests/integration/multistatements.spec.ts b/e2e_tests/integration/multistatements.spec.ts index d9e9386ab91..94ba1663f83 100644 --- a/e2e_tests/integration/multistatements.spec.ts +++ b/e2e_tests/integration/multistatements.spec.ts @@ -32,6 +32,7 @@ describe('Multi statements', () => { after(() => { cy.disableMultiStatement() }) + it('can connect', () => { const password = Cypress.config('password') cy.connect('neo4j', password) @@ -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() @@ -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;' @@ -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', () => { diff --git a/e2e_tests/integration/params.spec.ts b/e2e_tests/integration/params.spec.ts index 05fa97f205c..8d16b96914c 100644 --- a/e2e_tests/integration/params.spec.ts +++ b/e2e_tests/integration/params.spec.ts @@ -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) diff --git a/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.test.tsx b/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.test.tsx index 7b319b6e3a2..4c75281b174 100644 --- a/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.test.tsx @@ -38,8 +38,7 @@ const mount = (props: Partial, state?: any) => { params: {}, executeCmd: jest.fn(), setEditorContent: jest.fn(), - neo4jVersion: null, - gqlErrorsEnabled: true + neo4jVersion: null } const combinedProps = { @@ -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', @@ -116,11 +115,8 @@ describe('ErrorsView', () => { const state = { meta: { server: { - version: '5.26.0' + version: '5.27.0' } - }, - settings: { - enableGqlErrorsAndNotifications: true } } @@ -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', @@ -159,11 +155,8 @@ describe('ErrorsView', () => { const state = { meta: { server: { - version: '5.26.0' + version: '5.27.0' } - }, - settings: { - enableGqlErrorsAndNotifications: true } } diff --git a/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx b/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx index 465bba3b2c3..e69f6993048 100644 --- a/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx +++ b/src/browser/modules/Stream/CypherFrame/ErrorsView/ErrorsView.tsx @@ -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 { + render(): null | JSX.Element { + const { formattedError, nested = false } = this.props + + return ( + + + {!nested && ( + + ERROR + {formattedError.title} + + )} + {nested &&
{formattedError.title}
} +
+ {formattedError.description && ( + + + {formattedError?.description} + + + )} +
+ ) + } +} export type ErrorsViewProps = { result: BrowserRequestResult @@ -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 { - shouldComponentUpdate(props: ErrorsViewProps): boolean { +class ErrorsViewComponent extends Component { + 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) ) } @@ -92,19 +140,32 @@ class ErrorsViewComponent extends Component { 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 @@ -115,24 +176,9 @@ class ErrorsViewComponent extends Component { } return ( - 0}> + - - {depth === 0 && ( - ERROR - )} - {formattedError.title} - - {formattedError.description && ( - - - {formattedError?.description} - - - )} - {formattedError.innerError && ( - - )} + {isUnknownProcedureError(error) && ( { } /> )} + {togglable && ( + + + setNestedErrorsToggled(!this.state.nestedErrorsToggled) + } + > +   + {this.state.nestedErrorsToggled ? 'Show less' : 'Show more'} + + + )} + {this.state.nestedErrorsToggled && + nestedErrors.map((nestedError, index) => ( + + ))} ) } } -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 = ( @@ -206,6 +267,7 @@ const mapDispatchToProps = ( } } } + export const ErrorsView = withBus( connect(mapStateToProps, mapDispatchToProps)(ErrorsViewComponent) ) diff --git a/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsView.test.tsx.snap index 3cd5929ddd4..7e4def5d96d 100644 --- a/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/ErrorsView/__snapshots__/ErrorsView.test.tsx.snap @@ -5,34 +5,38 @@ exports[`ErrorsView displays nothing if no errors 1`] = `
`; exports[`ErrorsView displays procedure link if unknown procedure 1`] = `
- ERROR +
+ ERROR +
+

+ Neo.ClientError.Procedure.ProcedureNotFound +

-

- Neo.ClientError.Procedure.ProcedureNotFound -

-
-
-
-          not found
-        
+
+            not found
+          
+
`; -exports[`ErrorsView does display a nested error for gql status codes 1`] = ` +exports[`ErrorsView does display a nested error for GQL status codes 1`] = `
- ERROR +
+ ERROR +
+

+ 22N27: Data exception - invalid entity type +

-

- 42N51: Syntax error or access rule violation - invalid parameter -

-
-
-
-          Invalid parameter $\`param\`.
-        
+
+            Invalid input '******' for $\`param\`. Expected to be STRING.
+          
+
-
-
-

- 22G03: Data exception - invalid value type -

-
- -
-
-
-

- 22N27: Data exception - invalid entity type -

-
-
-
-                  Invalid input '******' for $\`param\`. Expected to be STRING.
-                
-
-
-
-
+   + Show more +
@@ -137,71 +110,79 @@ exports[`ErrorsView does display a nested error for gql status codes 1`] = ` exports[`ErrorsView does display an error 1`] = `
- ERROR +
+ ERROR +
+

+ Test.Error +

-

- Test.Error -

-
-
-
-          Test error description
-        
+
+            Test error description
+          
+
`; -exports[`ErrorsView does display an error for gql status codes 1`] = ` +exports[`ErrorsView does display an error for GQL status codes 1`] = `
- ERROR +
+ ERROR +
+

+ 22N14: Data exception - invalid temporal value combination +

-

- 22N14: Data exception - invalid temporal value combination -

-
-
-
-          Cannot select both epochSeconds and 'datetime'.
-        
+
+            Cannot select both epochSeconds and 'datetime'.
+          
+
diff --git a/src/browser/modules/Stream/CypherFrame/WarningsView.test.tsx b/src/browser/modules/Stream/CypherFrame/WarningsView.test.tsx index c05ca983e77..98743b7b049 100644 --- a/src/browser/modules/Stream/CypherFrame/WarningsView.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/WarningsView.test.tsx @@ -36,8 +36,7 @@ const withProvider = (store: any, children: any) => { const mount = (props: DeepPartial, state?: any) => { const defaultProps: WarningsViewProps = { result: null, - bus: createBus(), - gqlWarningsEnabled: false + bus: createBus() } const combinedProps = { @@ -139,11 +138,8 @@ describe('WarningsView', () => { const state = { meta: { server: { - version: '5.23.0' + version: '5.27.0' } - }, - settings: { - enableGqlErrorsAndNotifications: true } } @@ -235,11 +231,8 @@ describe('WarningsView', () => { const state = { meta: { server: { - version: '5.23.0' + version: '5.27.0' } - }, - settings: { - enableGqlErrorsAndNotifications: true } } diff --git a/src/browser/modules/Stream/CypherFrame/WarningsView.tsx b/src/browser/modules/Stream/CypherFrame/WarningsView.tsx index 5c554e44272..484db187b31 100644 --- a/src/browser/modules/Stream/CypherFrame/WarningsView.tsx +++ b/src/browser/modules/Stream/CypherFrame/WarningsView.tsx @@ -34,20 +34,13 @@ import { StyledCypherInfoMessage } from '../styled' import { deepEquals } from 'neo4j-arc/common' -import { - formatSummaryFromGqlStatusObjects, - formatSummaryFromNotifications, - FormattedNotification -} from './warningUtilts' +import { formatSummary, FormattedNotification } from './warningUtilts' import { NotificationSeverityLevel, QueryResult } from 'neo4j-driver-core' import { connect } from 'react-redux' import { withBus } from 'react-suber' -import { GlobalState } from 'shared/globalState' import { Bus } from 'suber' -import { getSemanticVersion } from 'shared/modules/dbMeta/dbMetaDuck' -import { gte } from 'semver' -import { FIRST_GQL_NOTIFICATIONS_SUPPORT } from 'shared/modules/features/versionedFeatures' -import { shouldShowGqlErrorsAndNotifications } from 'shared/modules/settings/settingsDuck' +import { gqlErrorsAndNotificationsEnabled } from 'services/gqlUtils' +import { GlobalState } from 'shared/globalState' const getWarningComponent = (severity?: string | NotificationSeverityLevel) => { if (severity === 'ERROR') { @@ -64,7 +57,7 @@ const getWarningComponent = (severity?: string | NotificationSeverityLevel) => { export type WarningsViewProps = { result?: QueryResult | null bus: Bus - gqlWarningsEnabled: boolean + gqlErrorsAndNotificationsEnabled?: boolean } class WarningsViewComponent extends Component { @@ -81,10 +74,12 @@ class WarningsViewComponent extends Component { ) return null + const { gqlErrorsAndNotificationsEnabled = false } = this.props const { summary } = this.props.result - const notifications = this.props.gqlWarningsEnabled - ? formatSummaryFromGqlStatusObjects(summary) - : formatSummaryFromNotifications(summary) + const notifications = formatSummary( + summary, + gqlErrorsAndNotificationsEnabled + ) const { text: cypher = '' } = summary.query if (!notifications || !cypher) { @@ -130,16 +125,8 @@ class WarningsViewComponent extends Component { } } -const gqlWarningsEnabled = (state: GlobalState): boolean => { - const featureEnabled = shouldShowGqlErrorsAndNotifications(state) - const version = getSemanticVersion(state) - return version - ? featureEnabled && gte(version, FIRST_GQL_NOTIFICATIONS_SUPPORT) - : false -} - const mapStateToProps = (state: GlobalState) => ({ - gqlWarningsEnabled: gqlWarningsEnabled(state) + gqlErrorsAndNotificationsEnabled: gqlErrorsAndNotificationsEnabled(state) }) export const WarningsView = withBus( diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/WarningsView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/WarningsView.test.tsx.snap index 1f4580d240f..2ad7bfac2d5 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/WarningsView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/WarningsView.test.tsx.snap @@ -5,7 +5,7 @@ exports[`WarningsView displays nothing if no notifications 1`] = `
`; exports[`WarningsView does display a warning 1`] = `
{ + test('formats a legacy error with no gql fields correctly', () => { + const error = { + type: 'Neo4jError' as ErrorType, + message: 'epochSeconds cannot be selected together with datetime.', + code: 'Neo.ClientError.Statement.ArgumentError' + } + + const result = formatError(error, false) + expect(result).toEqual({ + description: 'epochSeconds cannot be selected together with datetime.', + title: 'Neo.ClientError.Statement.ArgumentError' + }) + }) + test('formats an error with no gql fields correctly', () => { const error = { type: 'Neo4jError' as ErrorType, @@ -29,7 +43,7 @@ describe('error formatting', () => { code: 'Neo.ClientError.Statement.ArgumentError' } - const result = formatError(error) + const result = formatError(error, true) expect(result).toEqual({ description: 'epochSeconds cannot be selected together with datetime.', title: 'Neo.ClientError.Statement.ArgumentError' @@ -44,7 +58,7 @@ describe('error formatting', () => { code: 'Neo.DatabaseError.Statement.ExecutionFailed' } - const result = formatError(error) + const result = formatError(error, true) expect(result).toEqual({ description: 'The shortest path algorithm does not work when the start and end nodes are the same. This can happen if you perform a shortestPath search after a cartesian product that might have the same start and end nodes for some of the rows passed to shortestPath. If you would rather not experience this exception, and can accept the possibility of missing results for those rows, disable this in the Neo4j configuration by setting `dbms.cypher.forbid_shortestpath_common_nodes` to false. If you cannot accept missing results, and really want the shortestPath between two common nodes, then re-write the query using a standard Cypher variable length pattern expression followed by ordering by path length and limiting to one result.', @@ -52,6 +66,32 @@ describe('error formatting', () => { }) }) + test('formats a legacy error with gql fields correctly', () => { + const error = { + type: 'Neo4jError' as ErrorType, + message: 'Expected parameter(s): param', + code: 'Neo.ClientError.Statement.ParameterMissing', + gqlStatus: '42N51', + gqlStatusDescription: + 'error: syntax error or access rule violation - invalid parameter. Invalid parameter $`param`.', + cause: { + gqlStatus: '22G03', + gqlStatusDescription: '22G03', + cause: { + gqlStatus: '22N27', + gqlStatusDescription: + "error: data exception - invalid entity type. Invalid input '******' for $`param`. Expected to be STRING." + } + } + } + + const result = formatError(error, false) + expect(result).toEqual({ + description: 'Expected parameter(s): param', + title: 'Neo.ClientError.Statement.ParameterMissing' + }) + }) + test('formats a gql error correctly', () => { const error = { type: 'Neo4jError' as ErrorType, @@ -71,7 +111,7 @@ describe('error formatting', () => { } } - const result = formatErrorGqlStatusObject(error) + const result = formatError(error, true) expect(result).toEqual({ description: 'Invalid parameter $`param`.', innerError: { @@ -102,7 +142,7 @@ describe('error formatting', () => { } } - const result = formatErrorGqlStatusObject(error) + const result = formatError(error, true) expect(result).toEqual({ description: '', title: '22007: Data exception - invalid date, time, or datetime format', @@ -124,7 +164,7 @@ describe('error formatting', () => { cause: undefined } - const result = formatErrorGqlStatusObject(error) + const result = formatError(error, true) expect(result).toEqual({ description: '', title: '22G03', @@ -143,7 +183,7 @@ describe('error formatting', () => { cause: undefined } - const result = formatErrorGqlStatusObject(error) + const result = formatError(error, true) expect(result).toEqual({ description: "Invalid input '******' for $`param`. Expected to be STRING.", @@ -164,7 +204,7 @@ describe('error formatting', () => { cause: undefined } - const result = formatErrorGqlStatusObject(error) + const result = formatError(error, true) expect(result).toEqual({ description: "Cannot find the shortest path when the start and end nodes are the same. To enable this behavior, set 'dbms.cypher.forbid_shortestpath_common_nodes' to false.", diff --git a/src/browser/modules/Stream/CypherFrame/errorUtils.ts b/src/browser/modules/Stream/CypherFrame/errorUtils.ts index 8fd6b757157..9110ab01f7c 100644 --- a/src/browser/modules/Stream/CypherFrame/errorUtils.ts +++ b/src/browser/modules/Stream/CypherFrame/errorUtils.ts @@ -24,6 +24,7 @@ import { formatTitleFromGqlStatusDescription } from './gqlStatusUtils' import { BrowserError } from 'services/exceptions' +import { cloneDeep } from 'lodash-es' export function isBrowserError(object: unknown): object is BrowserError { if (object !== null && typeof object === 'object') { @@ -38,7 +39,7 @@ export function isBrowserError(object: unknown): object is BrowserError { return false } -type FormattedError = { +export type FormattedError = { title?: string description?: string innerError?: Pick< @@ -59,12 +60,15 @@ const mapBrowserErrorToFormattedError = ( ) const title = isNonEmptyString(gqlStatusTitle) ? gqlStatusTitle : description return { - title: isNonEmptyString(title) ? `${gqlStatus}: ${title}` : gqlStatus, + title: + isNonEmptyString(title) && title !== gqlStatus + ? `${gqlStatus}: ${title}` + : gqlStatus, description } } -export const hasPopulatedGqlFields = ( +const hasPopulatedGqlFields = ( error: BrowserError | Error ): error is BrowserError & { gqlStatus: string @@ -80,16 +84,14 @@ export const hasPopulatedGqlFields = ( ) } -export const formatErrorGqlStatusObject = ( - error: BrowserError -): FormattedError => { +const formatErrorGqlStatusObject = (error: BrowserError): FormattedError => { return { ...mapBrowserErrorToFormattedError(error), innerError: error.cause } } -export const formatError = (error: BrowserError): FormattedError => { +const formatLegacyError = (error: BrowserError): FormattedError => { const { code: title, message: description } = error return { @@ -97,3 +99,44 @@ export const formatError = (error: BrowserError): FormattedError => { description } } + +export const formatError = ( + error: BrowserError, + gqlErrorsAndNotificationsEnabled: boolean +): FormattedError => { + if (hasPopulatedGqlFields(error) && gqlErrorsAndNotificationsEnabled) { + return formatErrorGqlStatusObject(error) + } + + return formatLegacyError(error) +} + +export const flattenAndInvertErrors = ( + error: BrowserError, + gqlErrorsAndNotificationsEnabled: boolean +): BrowserError[] => { + const flattenErrorsToArray = ( + currentError: BrowserError, + errors: BrowserError[] = [] + ): BrowserError[] => { + const causeIsGqlError = + hasPopulatedGqlFields(currentError) && + currentError.cause !== undefined && + currentError.cause !== null && + hasPopulatedGqlFields(currentError.cause) + const cause = causeIsGqlError ? currentError.cause : undefined + + errors.push(currentError) + + if (cause !== undefined && cause !== null) { + return flattenErrorsToArray(cause, errors) + } + + return errors + } + + const errors = gqlErrorsAndNotificationsEnabled + ? flattenErrorsToArray(cloneDeep(error)) + : [error] + return errors.reverse() +} diff --git a/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts index c1f14a0222b..3b260e59786 100644 --- a/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts +++ b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts @@ -58,19 +58,38 @@ describe('gql status formatting', () => { ) }) - test('formats a title from a gql status description with no matches correctly', () => { + test('formats a description with line breaks correctly', () => { + const gqlStatusDescription = `error: general processing exception - unexpected error. The shortest path algorithm does not work when the start and end nodes are the same. This can happen if you +perform a shortestPath search after a cartesian product that might have the same start and end nodes for some +of the rows passed to shortestPath.` + + const result = + formatDescriptionFromGqlStatusDescription(gqlStatusDescription) + + expect(result) + .toEqual(`The shortest path algorithm does not work when the start and end nodes are the same. This can happen if you +perform a shortestPath search after a cartesian product that might have the same start and end nodes for some +of the rows passed to shortestPath.`) + }) + + test('formats a title from a gql status description with no error|info|warn prefix correctly', () => { const gqlStatusDescription = - 'Unfortunately, no one can be told what the Matrix is. You have to see it for yourself' + 'Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.' + const result = formatTitleFromGqlStatusDescription(gqlStatusDescription) - expect(result).toEqual('') + expect(result).toEqual( + 'Unfortunately, no one can be told what the Matrix is' + ) }) - test('formats a description from a gql status description with no matches correctly', () => { - const gqlStatusDescription = 'Believe the unbelievable' + test('formats a description from a gql status description with no error|info|warn correctly', () => { + const gqlStatusDescription = + 'Unfortunately, no one can be told what the Matrix is. You have to see it for yourself.' + const result = formatDescriptionFromGqlStatusDescription(gqlStatusDescription) - expect(result).toEqual('') + expect(result).toEqual('You have to see it for yourself.') }) }) diff --git a/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts index 72095e80554..d0026ec69c4 100644 --- a/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts +++ b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts @@ -31,7 +31,7 @@ const formatPropertyFromStatusDescripton = ( ): string | undefined => { const matches = gqlStatusDescription?.match( - /^(?:error|info|warn):\s(.+?)(?:\.(.+?))?\.?$/ + /^(?:(?:error|info|warn):\s)?([\s\S]+?)(?:\.([\s\S]+?))?\.?$/ ) ?? [] return matches[index] === undefined diff --git a/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts b/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts index a9fd5a9075e..c65e02e6829 100644 --- a/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts +++ b/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts @@ -23,12 +23,9 @@ import { notificationCategory, notificationSeverityLevel } from 'neo4j-driver-core' -import { - formatSummaryFromGqlStatusObjects, - formatSummaryFromNotifications -} from './warningUtilts' +import { formatSummary } from './warningUtilts' -describe('format rseult summary', () => { +describe('format result summary', () => { test('formats result summary for notifications', () => { const resultSummary = { server: { @@ -63,7 +60,7 @@ describe('format rseult summary', () => { ] } - const result = formatSummaryFromNotifications(resultSummary) + const result = formatSummary(resultSummary, false) expect(result).toEqual([ { @@ -139,7 +136,7 @@ describe('format rseult summary', () => { gqlStatusObjects } - const result = formatSummaryFromGqlStatusObjects(resultSummary) + const result = formatSummary(resultSummary, true) expect(result).toEqual([ { diff --git a/src/browser/modules/Stream/CypherFrame/warningUtilts.ts b/src/browser/modules/Stream/CypherFrame/warningUtilts.ts index 6b373180910..fa25c502f11 100644 --- a/src/browser/modules/Stream/CypherFrame/warningUtilts.ts +++ b/src/browser/modules/Stream/CypherFrame/warningUtilts.ts @@ -75,8 +75,21 @@ const mapNotificationsToFormattedNotifications = ( const SEVERITY_LEVELS = ['ERROR', 'WARNING', 'INFORMATION'] -export const formatSummaryFromNotifications = ( - resultSummary?: Partial +const hasPopulatedGqlFields = ( + resultSummary: Partial | undefined +): resultSummary is Partial & { + gqlStatusObjects: GqlStatusObject[] + notifications: Notification[] +} => { + return ( + resultSummary !== undefined && + 'gqlStatusObjects' in resultSummary && + resultSummary.gqlStatusObjects !== undefined + ) +} + +const formatSummaryFromNotifications = ( + resultSummary: Partial | undefined ): FormattedNotification[] => { const filteredNotifications = resultSummary?.notifications?.filter(x => @@ -85,8 +98,8 @@ export const formatSummaryFromNotifications = ( return mapNotificationsToFormattedNotifications(filteredNotifications) } -export const formatSummaryFromGqlStatusObjects = ( - resultSummary?: Partial +const formatSummaryFromGqlStatusObjects = ( + resultSummary: Partial | undefined ): FormattedNotification[] => { const filteredStatusObjects = resultSummary?.gqlStatusObjects?.filter(x => @@ -94,3 +107,13 @@ export const formatSummaryFromGqlStatusObjects = ( ) ?? [] return mapGqlStatusObjectsToFormattedNotifications(filteredStatusObjects) } + +export const formatSummary = ( + resultSummary: Partial | undefined, + gqlErrorsAndNotificationsEnabled: boolean +): FormattedNotification[] => { + return hasPopulatedGqlFields(resultSummary) && + gqlErrorsAndNotificationsEnabled + ? formatSummaryFromGqlStatusObjects(resultSummary) + : formatSummaryFromNotifications(resultSummary) +} diff --git a/src/browser/modules/Stream/styled.tsx b/src/browser/modules/Stream/styled.tsx index 8fa26d88aa3..b6bc7e51d64 100644 --- a/src/browser/modules/Stream/styled.tsx +++ b/src/browser/modules/Stream/styled.tsx @@ -49,10 +49,8 @@ export const DottedLineHover = styled.span` text-overflow: ellipsis; ` -export const StyledHelpFrame = styled.div<{ nested?: boolean }>` - padding: 0 30px 30px 0; - padding-top: ${props => (props.nested ? '0' : '30px')}; - padding-left: ${props => (props.nested ? '80px' : '30px')}; +export const StyledHelpFrame = styled.div` + padding: 0 30px 30px 30px; ` export const StyledHelpContent = styled.div` padding-top: 10px; diff --git a/src/shared/modules/features/versionedFeatures.ts b/src/shared/modules/features/versionedFeatures.ts index fa61293b5f7..8b924095b76 100644 --- a/src/shared/modules/features/versionedFeatures.ts +++ b/src/shared/modules/features/versionedFeatures.ts @@ -32,8 +32,7 @@ export const FIRST_MULTI_DB_SUPPORT = NEO4J_4_0 // compatible bolt server. export const FIRST_NO_MULTI_DB_SUPPORT = '3.4.0' -export const FIRST_GQL_NOTIFICATIONS_SUPPORT = '5.23.0' -export const FIRST_GQL_ERRORS_SUPPORT = '5.26.0' +export const FIRST_GQL_ERRORS_NOTIFICATIONS_SUPPORT = '5.27.0' export const getShowCurrentUserProcedure = (serverVersion: string) => { const serverVersionGuessed = guessSemverVersion(serverVersion) diff --git a/src/shared/modules/settings/settingsDuck.ts b/src/shared/modules/settings/settingsDuck.ts index d9fd71e2641..2b2eaaa4fc3 100644 --- a/src/shared/modules/settings/settingsDuck.ts +++ b/src/shared/modules/settings/settingsDuck.ts @@ -94,8 +94,6 @@ export const getAllowUserStats = (state: GlobalState): boolean => state[NAME].allowUserStats ?? initialState.allowUserStats export const shouldShowWheelZoomInfo = (state: GlobalState) => state[NAME].showWheelZoomInfo -export const shouldShowGqlErrorsAndNotifications = (state: any) => - state[NAME].enableGqlErrorsAndNotifications // Ideally the string | number types would be only numbers // but they're saved as strings in the settings component diff --git a/src/shared/services/gqlUtils.ts b/src/shared/services/gqlUtils.ts new file mode 100644 index 00000000000..23cee664005 --- /dev/null +++ b/src/shared/services/gqlUtils.ts @@ -0,0 +1,11 @@ +import { gte } from 'semver' +import { GlobalState } from 'shared/globalState' +import { getSemanticVersion } from 'shared/modules/dbMeta/dbMetaDuck' +import { FIRST_GQL_ERRORS_NOTIFICATIONS_SUPPORT } from 'shared/modules/features/versionedFeatures' + +export const gqlErrorsAndNotificationsEnabled = ( + state: GlobalState +): boolean => { + const version = getSemanticVersion(state) + return version ? gte(version, FIRST_GQL_ERRORS_NOTIFICATIONS_SUPPORT) : false +}