diff --git a/.eslintrc.json b/.eslintrc.json index 285b4a5601c..8940dcdb13a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,23 +56,23 @@ "react" ], "rules": { - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/ban-types": "off", - "react/prop-types": "off", - "prefer-spread": "off", - "react/no-unescaped-entities": "off", - "no-var": "off", - "@typescript-eslint/no-unused-vars": "off", - "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/no-this-alias": "off", - "prefer-rest-params": "off", - "@typescript-eslint/no-var-requires": "off", - "react-hooks/rules-of-hooks": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/ban-types": "warn", + "react/prop-types": "warn", + "prefer-spread": "warn", + "react/no-unescaped-entities": "warn", + "no-var": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "react-hooks/exhaustive-deps": "warn", + "@typescript-eslint/no-this-alias": "warn", + "prefer-rest-params": "warn", + "@typescript-eslint/no-var-requires": "warn", + "react-hooks/rules-of-hooks": "warn", + "@typescript-eslint/explicit-module-boundary-types": "warn", "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off", // remove this when migrated fully to ts. right now it makes warnings unreadable - "@typescript-eslint/ban-ts-comment": "off", // remove this when typings for react-spring allow multiple animation stages, - "@typescript-eslint/no-non-null-assertion": "off" + "@typescript-eslint/no-explicit-any": "warn", // remove this when migrated fully to ts. right now it makes warnings unreadable + "@typescript-eslint/ban-ts-comment": "warn", // remove this when typings for react-spring allow multiple animation stages, + "@typescript-eslint/no-non-null-assertion": "warn" } } ] diff --git a/LICENSES.txt b/LICENSES.txt index 8505d916e1b..ba348d08656 100644 --- a/LICENSES.txt +++ b/LICENSES.txt @@ -1180,7 +1180,7 @@ SOFTWARE. ----- -The following software may be included in this product: @types/asap, @types/color-name, @types/dateformat, @types/file-saver, @types/hoist-non-react-statics, @types/jsonic, @types/long, @types/mdast, @types/parse-json, @types/prop-types, @types/shallowequal, @types/unist, @types/url-parse. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/asap), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/color-name), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/dateformat), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/file-saver), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://github.com/DefinitelyTyped/DefinitelyTyped.git.git (@types/jsonic), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/long), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mdast), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/shallowequal), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/unist), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/url-parse). This software contains the following license and notice below: +The following software may be included in this product: @types/asap, @types/color-name, @types/hoist-non-react-statics, @types/long, @types/mdast, @types/parse-json, @types/prop-types, @types/shallowequal, @types/unist. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/asap), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/color-name), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/long), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mdast), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/shallowequal), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/unist). This software contains the following license and notice below: MIT License @@ -1206,7 +1206,7 @@ MIT License ----- -The following software may be included in this product: @types/d3, @types/hast, @types/invariant, @types/node, @types/parse5, @types/react, @types/semver, @types/zen-observable. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/d3), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hast), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/invariant), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse5), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/semver), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: +The following software may be included in this product: @types/hast, @types/invariant, @types/node, @types/parse5, @types/react, @types/zen-observable. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hast), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/invariant), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse5), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: MIT License diff --git a/NOTICE.txt b/NOTICE.txt index 29a6f90b7b9..cba46311e4f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -223,7 +223,7 @@ Third-party licenses │ ├─ long@4.0.0 │ │ ├─ URL: https://github.com/dcodeIO/long.js.git │ │ └─ VendorName: Daniel Wirtz -│ ├─ neo4j-driver@4.1.2 +│ ├─ neo4j-driver@4.2.2 │ │ ├─ URL: git://github.com/neo4j/neo4j-javascript-driver.git │ │ └─ VendorName: Neo4j │ ├─ oauth-sign@0.9.0 @@ -283,6 +283,10 @@ Third-party licenses │ │ ├─ URL: http://github.com/garycourt/uri-js │ │ ├─ VendorName: Gary Court │ │ └─ VendorUrl: https://github.com/garycourt/uri-js +│ ├─ uri-js@4.4.1 +│ │ ├─ URL: http://github.com/garycourt/uri-js +│ │ ├─ VendorName: Gary Court +│ │ └─ VendorUrl: https://github.com/garycourt/uri-js │ └─ webidl-conversions@3.0.1 │ ├─ URL: https://github.com/jsdom/webidl-conversions.git │ ├─ VendorName: Domenic Denicola @@ -661,20 +665,12 @@ Third-party licenses │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/color-name@1.1.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/d3@3.5.44 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/dateformat@3.0.1 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/file-saver@2.0.1 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/hast@2.3.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/hoist-non-react-statics@3.3.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/invariant@2.2.34 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/jsonic@0.3.0 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git.git │ ├─ @types/long@4.0.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/mdast@3.0.3 @@ -691,14 +687,10 @@ Third-party licenses │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/react@16.9.49 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/semver@7.3.4 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/shallowequal@1.1.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/unist@2.0.3 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/url-parse@1.4.3 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/zen-observable@0.8.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @wry/context@0.5.2 @@ -1524,10 +1516,6 @@ Third-party licenses │ │ ├─ URL: https://github.com/reduxjs/react-redux.git │ │ ├─ VendorName: Dan Abramov │ │ └─ VendorUrl: https://github.com/reduxjs/react-redux -│ ├─ react-spring@8.0.27 -│ │ ├─ URL: git+https://github.com/drcmda/react-spring.git -│ │ ├─ VendorName: Paul Henschel -│ │ └─ VendorUrl: https://github.com/drcmda/react-spring#readme │ ├─ react-suber@1.0.4 │ │ ├─ URL: https://github.com/oskarhane/react-suber.git │ │ ├─ VendorName: Oskar Hane diff --git a/build_scripts/webpack-plugins.js b/build_scripts/webpack-plugins.js index 24a47b6b9d2..9d387d9cf8b 100644 --- a/build_scripts/webpack-plugins.js +++ b/build_scripts/webpack-plugins.js @@ -91,7 +91,7 @@ module.exports = () => { }), new ForkTsCheckerNotifierWebpackPlugin({ title: 'TypeScript', - excludeWarnings: false + excludeWarnings: true // remove as we move to ts }), new MonacoWebpackPlugin({ features: [ diff --git a/e2e_tests/integration/0.index.spec.ts b/e2e_tests/integration/0.index.spec.ts index 585e1c5f4ef..8b04a39db53 100644 --- a/e2e_tests/integration/0.index.spec.ts +++ b/e2e_tests/integration/0.index.spec.ts @@ -38,12 +38,12 @@ describe('Neo4j Browser', () => { cy.setInitialPassword(newPassword) cy.disconnect() }) - it('populates the editor when clicking the connect banner', () => { + + it(':server disconnect frame is re-runnable', () => { cy.get('[data-testid="disconnectedBannerCode"]').click() - cy.get('[data-testid="frameCommand"]') - .first() - .should('contain', ':server connect') - cy.get('[data-testid="activeEditor"] [data-testid="editor-discard"]') + cy.get('[data-testid="frameCommand"]').contains(':server connect') + cy.typeInFrame(':play movies{enter}', 0) + cy.get('[data-testid=frame]').contains('the Bacon Path') }) it('can connect', () => { const password = Cypress.config('password') @@ -55,9 +55,7 @@ describe('Neo4j Browser', () => { const query = 'MATCH (n) DETACH DELETE n' cy.executeCommand(query) cy.waitForCommandResult() - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains(query) cy.get('[data-testid="frameStatusbar"]', { timeout: 100000 }) .first() .contains(/completed/i) @@ -67,9 +65,7 @@ describe('Neo4j Browser', () => { const query = 'RETURN 1' cy.executeCommand(query) cy.waitForCommandResult() - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains(query) cy.get('[data-testid="frameStatusbar"]', { timeout: 10000 }) .first() .should('contain', 'Started streaming') @@ -78,9 +74,7 @@ describe('Neo4j Browser', () => { cy.executeCommand(':clear') const query = ':unknown' cy.executeCommand(query) - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains(query) cy.get('[data-testid="frame"]', { timeout: 10000 }) .first() .should('contain', 'Error') @@ -89,9 +83,7 @@ describe('Neo4j Browser', () => { cy.executeCommand(':clear') const query = ':play movies' cy.executeCommand(query) - cy.get('[data-testid="frameCommand"]') - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]').contains(query) cy.get(Carousel) .find('[data-testid="nextSlide"]') .click() @@ -106,9 +98,9 @@ describe('Neo4j Browser', () => { .click() cy.get(SubmitQueryButton).click() cy.waitForCommandResult() - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', 'Emil Eifrem') + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains( + 'Keanu Reeves' + ) }) it('can display meta items from side drawer', () => { cy.executeCommand(':clear') diff --git a/e2e_tests/integration/commands.spec.ts b/e2e_tests/integration/commands.spec.ts index 43e63feb724..83c8f6b2ea3 100644 --- a/e2e_tests/integration/commands.spec.ts +++ b/e2e_tests/integration/commands.spec.ts @@ -99,4 +99,12 @@ describe('Commands', () => { cy.executeCommand(':clear') }) }) + + it('can re-run all simple commands while connected without blowing up', () => { + cy.executeCommand('return 1') + commands.forEach(cmd => { + cy.typeInFrame(`${cmd}{enter}`) + cy.wait(300) + }) + }) }) diff --git a/e2e_tests/integration/connect-form.spec.ts b/e2e_tests/integration/connect-form.spec.ts index cf72bfb8cb0..bea150d8047 100644 --- a/e2e_tests/integration/connect-form.spec.ts +++ b/e2e_tests/integration/connect-form.spec.ts @@ -107,7 +107,7 @@ describe('Connect form', () => { `Automatic retry using the "${schemeWithEncryptionFlag('bolt')}"` ) cy.wait(7000) // auto retry is in 5 secs - getFirstFrameCommand().should('contain', ':play start') + getFirstFrameCommand().contains(':play start') cy.executeCommand(':server disconnect') cy.executeCommand(':clear') }) diff --git a/e2e_tests/integration/desktop-env-url.spec.ts b/e2e_tests/integration/desktop-env-url.spec.ts index f5dcd8d7ff0..6df58bd4efb 100644 --- a/e2e_tests/integration/desktop-env-url.spec.ts +++ b/e2e_tests/integration/desktop-env-url.spec.ts @@ -48,7 +48,7 @@ describe('Neo4j Desktop environment using url field', () => { frames.should('have.length', 2) // Auto connected = :play start - frames.first().should('contain', ':play start') + frames.first().contains(':play start') cy.wait(1000) }) it('switches connection when that event is triggered using url field', () => { @@ -63,7 +63,7 @@ describe('Neo4j Desktop environment using url field', () => { const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) frames.should('have.length', 1) - frames.first().should('contain', ':server switch success') + frames.first().contains(':server switch success') cy.get('[data-testid="frame"]', { timeout: 10000 }) .first() diff --git a/e2e_tests/integration/desktop-env.spec.ts b/e2e_tests/integration/desktop-env.spec.ts index de7e3db56b7..c6f64236351 100644 --- a/e2e_tests/integration/desktop-env.spec.ts +++ b/e2e_tests/integration/desktop-env.spec.ts @@ -42,7 +42,7 @@ describe('Neo4j Desktop environment', () => { frames.should('have.length', 2) // Auto connected = :play start - frames.first().should('contain', ':play start') + frames.first().contains(':play start') cy.wait(1000) }) it('switches connection when that event is triggered using host + port fields', () => { @@ -57,7 +57,7 @@ describe('Neo4j Desktop environment', () => { const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) frames.should('have.length', 1) - frames.first().should('contain', ':server switch success') + frames.first().contains(':server switch success') cy.get('[data-testid="frame"]', { timeout: 10000 }) .first() @@ -79,7 +79,7 @@ describe('Neo4j Desktop environment', () => { const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) frames.should('have.length', 1) - frames.first().should('contain', ':server switch fail') + frames.first().contains(':server switch fail') cy.get('[data-testid="disconnectedBanner"]', { timeout: 10000 }) .first() diff --git a/e2e_tests/integration/multi-db.spec.ts b/e2e_tests/integration/multi-db.spec.ts index 2cdfbb1c05c..1ad57166ccb 100644 --- a/e2e_tests/integration/multi-db.spec.ts +++ b/e2e_tests/integration/multi-db.spec.ts @@ -108,9 +108,9 @@ describe('Multi database', () => { // Select to use db, make sure backticked databaseOptionList().select('name-with-dash') - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', ':use `name-with-dash`') + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains( + ':use `name-with-dash`' + ) cy.resultContains( 'Queries from this point and forward are using the database' ) @@ -203,9 +203,7 @@ describe('Multi database', () => { ) // Click re-run - cy.get('[data-testid="rerunFrameButton"]', { timeout: 10000 }) - .first() - .click() + cy.get('[data-testid="rerunFrameButton"]', { timeout: 10000 }).click() // Make sure we have what we expect cy.get('[data-testid="frame"]', { timeout: 10000 }) diff --git a/e2e_tests/integration/multistatements.spec.ts b/e2e_tests/integration/multistatements.spec.ts index c0cdd5548bc..6cffab599e6 100644 --- a/e2e_tests/integration/multistatements.spec.ts +++ b/e2e_tests/integration/multistatements.spec.ts @@ -67,9 +67,9 @@ describe('Multi statements', () => { // Then // Error expected - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', validQuery) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains( + validQuery + ) cy.get('[data-testid="frameContents"]', { timeout: 10000 }) .first() .should('contain', 'Error') diff --git a/e2e_tests/integration/saved-scripts.spec.ts b/e2e_tests/integration/saved-scripts.spec.ts index 0d99b78030c..3606b635204 100644 --- a/e2e_tests/integration/saved-scripts.spec.ts +++ b/e2e_tests/integration/saved-scripts.spec.ts @@ -41,10 +41,12 @@ describe('Saved Scripts', () => { cy.get('[data-testid="scriptTitle-script name"]').click() cy.get('[data-testid="currentlyEditing"]').contains('script name') // Editing script updates name and content - const clearInput = - Cypress.platform == 'darwin' ? '{cmd}a{del}' : '{ctrl}a{del}' cy.get('[data-testid="activeEditor"] textarea') - .type(clearInput) + .type( + Cypress.platform === 'darwin' + ? '{cmd}a {backspace}' + : '{ctrl}a {backspace}' + ) .type('// Guide{shift}{enter}:play movies', { force: true }) cy.get('[title="Update favorite"]').click() diff --git a/e2e_tests/integration/style.spec.ts b/e2e_tests/integration/style.spec.ts index f49b725be5c..0806f542da4 100644 --- a/e2e_tests/integration/style.spec.ts +++ b/e2e_tests/integration/style.spec.ts @@ -39,9 +39,7 @@ describe(':style', () => { const query = ':style' cy.executeCommand(query) - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains(query) cy.get('[data-testid="frameContents"]', { timeout: 10000 }) .first() .should('contain', 'node {') diff --git a/e2e_tests/integration/topics.spec.ts b/e2e_tests/integration/topics.spec.ts index 0d4eb100a53..adb801c6957 100644 --- a/e2e_tests/integration/topics.spec.ts +++ b/e2e_tests/integration/topics.spec.ts @@ -31,9 +31,7 @@ describe('Help topics', () => { cy.executeCommand(':clear') const query = ':help commands' cy.executeCommand(query) - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains(query) cy.get('[data-testid="frameContents"]', { timeout: 10000 }) .first() .should('contain', ':help style') @@ -42,9 +40,7 @@ describe('Help topics', () => { cy.executeCommand(':clear') const query = ':help style' cy.executeCommand(query) - cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) - .first() - .should('contain', query) + cy.get('[data-testid="frameCommand"]', { timeout: 10000 }).contains(query) cy.get('[data-testid="frameContents"]', { timeout: 10000 }) .first() .should('contain', 'style command') diff --git a/e2e_tests/support/commands.ts b/e2e_tests/support/commands.ts index 804494f31e1..88e05ba30b1 100644 --- a/e2e_tests/support/commands.ts +++ b/e2e_tests/support/commands.ts @@ -1,7 +1,6 @@ const SubmitQueryButton = '[data-testid="editor-Run"]' const EditorTextField = '[data-testid="activeEditor"] textarea' const VisibleEditor = '#monaco-main-editor' - /* global Cypress, cy */ Cypress.Commands.add('getEditor', () => cy.get(VisibleEditor)) @@ -54,8 +53,8 @@ Cypress.Commands.add( cy.get('button[data-testid="changePassword"]').click() cy.get('input[data-testid="changePassword"]').should('not.be.visible') - cy.get('[data-testid="frameCommand"]', { timeout: 30000 }).should( - 'contain', + cy.get('[data-testid="frame"]', { timeout: 25000 }).should('have.length', 2) + cy.get('[data-testid="frameCommand"]', { timeout: 30000 }).contains( ':play start' ) } @@ -90,9 +89,7 @@ Cypress.Commands.add( 2 ) cy.wait(500) - cy.get('[data-testid="frameCommand"]') - .first() - .should('contain', ':play start') + cy.get('[data-testid="frameCommand"]').contains(':server connect') cy.executeCommand(':clear') } } @@ -102,6 +99,17 @@ Cypress.Commands.add('disconnect', () => { cy.executeCommand(query) }) +Cypress.Commands.add('typeInFrame', (cmd: string, frameIndex = 0) => { + cy.get('[id^=monaco-]') + .eq(frameIndex + 1) // the first monaco editor is the main one + .type( + Cypress.platform === 'darwin' + ? '{cmd}a {backspace}' + : '{ctrl}a {backspace}' + ) + .type(cmd) +}) + Cypress.Commands.add('executeCommand', (query, options = {}) => { cy.get(VisibleEditor).click() cy.get(EditorTextField).type(query, { force: true, ...options }) diff --git a/e2e_tests/support/global.d.ts b/e2e_tests/support/global.d.ts index 387be40e5c9..3b45a3d4e9a 100644 --- a/e2e_tests/support/global.d.ts +++ b/e2e_tests/support/global.d.ts @@ -24,6 +24,10 @@ declare global { boltUrl?: string, force?: boolean ): Cypress.Chainable + /** + * Custom command to type a command in a frame + */ + typeInFrame(cmd: string, frameIndex?: number): Cypress.Chainable /** * Custom command to type and submit query in cypher editor. * @example cy.executeCommand(':clear') diff --git a/package.json b/package.json index 5f9ebffa3a8..e307326bbba 100644 --- a/package.json +++ b/package.json @@ -70,14 +70,20 @@ "@testing-library/react-hooks": "^2.0.1", "@types/antlr4": "^4.7.2", "@types/apollo-upload-client": "^14.1.0", + "@types/d3": "^3", + "@types/dateformat": "^3.0.1", + "@types/file-saver": "^2.0.1", "@types/jest": "^24.9.0", + "@types/jsonic": "^0.3.0", "@types/lodash-es": "^4.17.3", "@types/react": "^16.9.23", "@types/react-dom": "^16.9.8", "@types/react-redux": "^7.1.9", "@types/react-svg-inline": "^2.1.1", "@types/redux-mock-store": "^1.0.2", + "@types/semver": "^7.3.4", "@types/styled-components": "^5.1.1", + "@types/url-parse": "^1.4.3", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/parser": "^3.9.0", @@ -157,12 +163,6 @@ "@sentry/integrations": "^5.29.0", "@sentry/react": "^5.29.0", "@sentry/tracing": "^5.27.0", - "@types/d3": "^3", - "@types/dateformat": "^3.0.1", - "@types/file-saver": "^2.0.1", - "@types/jsonic": "^0.3.0", - "@types/semver": "^7.3.4", - "@types/url-parse": "^1.4.3", "antlr4": "^4.8.0", "apollo-upload-client": "^14.1.2", "ascii-data-table": "^2.1.1", @@ -187,7 +187,6 @@ "react-dnd-html5-backend": "9.3.2", "react-dom": "^16.9.0", "react-redux": "^5.0.7", - "react-spring": "^8.0.27", "react-suber": "1.0.4", "react-svg-inline": "^2.1.1", "react-timeago": "^4.1.9", diff --git a/src/browser/components/icons/Icons.tsx b/src/browser/components/icons/Icons.tsx index 11a0c8c04e6..c0e7b84d9d6 100644 --- a/src/browser/components/icons/Icons.tsx +++ b/src/browser/components/icons/Icons.tsx @@ -23,7 +23,6 @@ import { keyframes, css } from 'styled-components' import { IconContainer } from './IconContainer' import ratingStar from 'icons/rating-star.svg' import databaseCheck from 'icons/database-check.svg' -import bookSearch from 'icons/book-search.svg' import cog from 'icons/cog.svg' import cloudCheck from 'icons/cloud-check.svg' import cloudRemove from 'icons/cloud-remove.svg' @@ -41,11 +40,9 @@ import arrowLeft1 from 'icons/arrow-left-1.svg' import arrowRight1 from 'icons/arrow-right-1.svg' import skipPrev from 'icons/skip-prev.svg' import file from 'icons/file.svg' -import floppyDisk from 'icons/floppy-disk.svg' -import update_file from 'icons/update_file.svg' import save_file from 'icons/save_file.svg' -import update_favorite from 'icons/update_favorite.svg' import save_favorite from 'icons/save_favorite.svg' +import run_icon from 'icons/run_icon.svg' import help from 'icons/help.svg' const inactive = ` @@ -275,6 +272,8 @@ export const ContractIcon = () => export const RefreshIcon = () => ( ) +export const RunIcon = () => + export const CloseIcon = () => export const UpIcon = () => export const DownIcon = () => diff --git a/src/browser/documentation/guides/intro.tsx b/src/browser/documentation/guides/intro.tsx index 1c42215ea09..eb204651769 100644 --- a/src/browser/documentation/guides/intro.tsx +++ b/src/browser/documentation/guides/intro.tsx @@ -19,7 +19,6 @@ */ import React from 'react' -import Carousel from '../../modules/Carousel/Carousel' import Slide from '../../modules/Carousel/Slide' const title = 'Intro' diff --git a/src/browser/modules/App/keyboardShortcuts.ts b/src/browser/modules/App/keyboardShortcuts.ts index 55047dbb5d7..d1384d2ec9f 100644 --- a/src/browser/modules/App/keyboardShortcuts.ts +++ b/src/browser/modules/App/keyboardShortcuts.ts @@ -85,10 +85,9 @@ export function useKeyboardShortcuts(bus: Bus): void { [trigger] ) - const keyboardShortcuts = useMemo( - () => [focusEditorOnSlash, fullscreenEditor], - [focusEditorOnSlash, fullscreenEditor] - ) + const keyboardShortcuts = useMemo(() => [focusEditorOnSlash], [ + focusEditorOnSlash + ]) useEffect(() => { keyboardShortcuts.forEach(shortcut => diff --git a/src/browser/modules/Editor/EditorFrame.tsx b/src/browser/modules/Editor/EditorFrame.tsx index fef817a1aac..8b9a1e60cd0 100644 --- a/src/browser/modules/Editor/EditorFrame.tsx +++ b/src/browser/modules/Editor/EditorFrame.tsx @@ -39,7 +39,6 @@ import { REMOVE_FAVORITE, updateFavorite } from 'shared/modules/favorites/favoritesDuck' -import { useSpring, animated } from 'react-spring' import { Bus } from 'suber' import { isMac, @@ -85,14 +84,14 @@ import { shouldEnableMultiStatementMode } from 'shared/modules/settings/settingsDuck' import { getUseDb } from 'shared/modules/connections/connectionsDuck' -import { getHistory, HistoryState } from 'shared/modules/history/historyDuck' +import { getHistory } from 'shared/modules/history/historyDuck' type EditorFrameProps = { bus: Bus codeFontLigatures: boolean enableMultiStatementMode: boolean executeCommand: (cmd: string, source: string) => void - history: HistoryState + history: string[] projectId: string theme: { linkHover: string } updateFavorite: (id: string, value: string) => void @@ -127,19 +126,15 @@ export function EditorFrame({ ) const editorRef = useRef(null) - const toggleFullscreen = useCallback(() => { - updateFullscreen(!isFullscreen) - }, [isFullscreen]) - - const updateFullscreen = (fullScreen: boolean) => { - setFullscreen(fullScreen) - editorRef.current?.resize(fullScreen) + const toggleFullscreen = () => { + setFullscreen(fs => !fs) } - useEffect(() => bus && bus.take(EXPAND, toggleFullscreen), [ - bus, - toggleFullscreen - ]) + useEffect(() => { + editorRef.current?.resize(isFullscreen) + }, [isFullscreen]) + + useEffect(() => bus && bus.take(EXPAND, toggleFullscreen), [bus]) useEffect( () => bus && @@ -204,7 +199,7 @@ export function EditorFrame({ function discardEditor() { editorRef.current?.setValue('') setCurrentlyEditing(null) - updateFullscreen(false) + setFullscreen(false) } const buttons = [ @@ -224,17 +219,12 @@ export function EditorFrame({ } ] - const props = useSpring({ - opacity: currentlyEditing ? 1 : 0.5, - height: currentlyEditing ? 'auto' : 0 - }) - function createRunCommandFunction(source: string) { return () => { executeCommand(editorRef.current?.getValue() || '', source) editorRef.current?.setValue('') setCurrentlyEditing(null) - updateFullscreen(false) + setFullscreen(false) } } @@ -254,21 +244,20 @@ export function EditorFrame({ currentlyEditing && !currentlyEditing?.isStatic ) + return ( {currentlyEditing && ( - - - - {currentlyEditing.isProjectFile ? ' Project file: ' : ' Favorite: '} - {getName(currentlyEditing)} - {showUnsaved ? '*' : ''} - {currentlyEditing.isStatic ? ' (read-only)' : ''} - - + + + {currentlyEditing.isProjectFile ? ' Project file: ' : ' Favorite: '} + {getName(currentlyEditing)} + {showUnsaved ? '*' : ''} + {currentlyEditing.isStatic ? ' (read-only)' : ''} + )}
@@ -277,6 +266,7 @@ export function EditorFrame({ bus={bus} enableMultiStatementMode={enableMultiStatementMode} history={history} + toggleFullscreen={toggleFullscreen} id={'main-editor'} fontLigatures={codeFontLigatures} onChange={() => { diff --git a/src/browser/modules/Editor/Monaco.test.tsx b/src/browser/modules/Editor/Monaco.test.tsx index 7cdb9878f43..73b239aede0 100644 --- a/src/browser/modules/Editor/Monaco.test.tsx +++ b/src/browser/modules/Editor/Monaco.test.tsx @@ -26,7 +26,13 @@ import Monaco from './Monaco' describe('Monaco', () => { it('renders a component that functions as a textbox', () => { const { getByRole, queryByDisplayValue } = render( - {} } as any} id="id" /> + { + /*noop */ + }} + bus={{ self: () => {} } as any} + id="id" + /> ) const value = 'hello world' diff --git a/src/browser/modules/Editor/Monaco.tsx b/src/browser/modules/Editor/Monaco.tsx index e638b294df2..a8f85ec0540 100644 --- a/src/browser/modules/Editor/Monaco.tsx +++ b/src/browser/modules/Editor/Monaco.tsx @@ -39,7 +39,6 @@ import { Bus } from 'suber' import { NEO4J_BROWSER_USER_ACTION_QUERY } from 'services/bolt/txMetadata' import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' -import { HistoryState } from 'shared/modules/history/historyDuck' const shouldCheckForHints = (code: any) => code.trim().length > 0 && @@ -58,6 +57,7 @@ export interface MonacoHandles { getValue: () => string setValue: (value: string) => void resize: (fillContainer?: boolean, fixedHeight?: number) => void + setPosition: (position: { lineNumber: number; column: number }) => void } const MonacoStyleWrapper = styled.div` @@ -73,13 +73,14 @@ interface MonacoProps { bus: Bus enableMultiStatementMode?: boolean fontLigatures?: boolean - history?: HistoryState + history?: string[] id: string value?: string onChange?: (value: string) => void onDisplayHelpKeys?: () => void - onExecute?: () => void + onExecute?: (value: string) => void useDb?: null | string + toggleFullscreen: () => void } const Monaco = forwardRef( @@ -94,7 +95,8 @@ const Monaco = forwardRef( onChange = () => undefined, onDisplayHelpKeys = () => undefined, onExecute = () => undefined, - useDb + useDb, + toggleFullscreen }: MonacoProps, ref ): JSX.Element => { @@ -113,6 +115,9 @@ const Monaco = forwardRef( }, resize(fillContainer = false) { resize(fillContainer) + }, + setPosition(pos: { lineNumber: number; column: number }) { + editorRef.current?.setPosition(pos) } })) @@ -122,8 +127,6 @@ const Monaco = forwardRef( const cursorPosition = editorRef?.current?.getPosition() as IPosition editorRef.current?.setValue(editorRef.current?.getValue() || '') editorRef.current?.setPosition(cursorPosition) - - updateGutterCharWidth(useDb || '') }, [useDb]) // Create monaco instance, listen to text changes and destroy @@ -143,7 +146,7 @@ const Monaco = forwardRef( lightbulb: { enabled: false }, lineHeight: 23, lineNumbers: (line: number) => - isMultiLine() ? '' + line : `${useDbRef.current || ''}$`, + isMultiLine() ? line.toString() : `${useDbRef.current || ''}$`, links: false, minimap: { enabled: false }, overviewRulerBorder: false, @@ -192,10 +195,11 @@ const Monaco = forwardRef( KeyMod.CtrlCmd | KeyCode.US_DOT, onDisplayHelpKeys ) + editorRef.current.addCommand(KeyCode.Escape, toggleFullscreen) onContentUpdate() - editorRef.current?.onDidChangeModelContent(onContentUpdate) + editorRef.current?.onDidChangeModelContent(() => onContentUpdate(true)) editorRef.current?.onDidContentSizeChange(() => resize(isFullscreenRef.current) @@ -212,6 +216,7 @@ const Monaco = forwardRef( return () => { editorRef.current?.dispose() + debouncedUpdateCode.cancel() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -239,7 +244,7 @@ const Monaco = forwardRef( useEffect(() => { onContentUpdate() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [enableMultiStatementMode]) + }, [enableMultiStatementMode, useDb]) useEffect(() => { editorRef.current?.updateOptions({ fontLigatures }) @@ -260,7 +265,7 @@ const Monaco = forwardRef( const onlyWhitespace = value.trim() === '' if (!onlyWhitespace) { - onExecute() + onExecute(value) historyIndexRef.current = -1 } } @@ -354,19 +359,19 @@ const Monaco = forwardRef( const updateGutterCharWidth = (dbName: string) => { editorRef.current?.updateOptions({ lineNumbersMinChars: - dbName.length && !isMultiLine() ? dbName.length * 1.2 : 2 + dbName.length && !isMultiLine() ? dbName.length * 1.3 : 2 }) } // On each text change, clear warnings and reset countdown to adding warnings - const onContentUpdate = () => { + const onContentUpdate = (preferRef = false) => { editor.setModelMarkers( editorRef.current?.getModel() as editor.ITextModel, monacoId, [] ) - updateGutterCharWidth(useDbRef.current || '') + updateGutterCharWidth((preferRef ? useDbRef.current : useDb) || '') debouncedUpdateCode() } diff --git a/src/browser/modules/Editor/styled.tsx b/src/browser/modules/Editor/styled.tsx index 565cbdae773..6fffa9a64c4 100644 --- a/src/browser/modules/Editor/styled.tsx +++ b/src/browser/modules/Editor/styled.tsx @@ -27,7 +27,7 @@ interface FullscreenProps { export const Header = styled.div` background-color: ${(props): string => props.theme.frameSidebarBackground}; flex-grow: 1; - min-width: 0; + min-width: 0; // Without the min width, the editor doesn't shrink on resize in safari display: flex; ` @@ -72,6 +72,7 @@ export const EditorContainer = styled.div` flex-grow: 1; min-width: 0; width: 0; // needed to prevent the editor from growing the text field + min-width: 0; ` export const FlexContainer = styled.div` display: flex; diff --git a/src/browser/modules/Frame/Frame.tsx b/src/browser/modules/Frame/DefaultFrame.tsx similarity index 96% rename from src/browser/modules/Frame/Frame.tsx rename to src/browser/modules/Frame/DefaultFrame.tsx index 57eae813287..b98edc88d80 100644 --- a/src/browser/modules/Frame/Frame.tsx +++ b/src/browser/modules/Frame/DefaultFrame.tsx @@ -20,7 +20,7 @@ import React from 'react' import FrameTemplate from './FrameTemplate' -const Frame = ({ frame }: any) => { +const DefaultFrame = ({ frame }: any): JSX.Element => { const errors = frame.errors || false const contents = frame.contents || false let frameContents = contents @@ -33,6 +33,7 @@ const Frame = ({ frame }: any) => { } else if (frame.type === 'unknown') { frameContents = 'Unknown command' } + return } -export default Frame +export default DefaultFrame diff --git a/src/browser/modules/Frame/FrameTemplate.tsx b/src/browser/modules/Frame/FrameTemplate.tsx index 56b3d253531..56535f51cf8 100644 --- a/src/browser/modules/Frame/FrameTemplate.tsx +++ b/src/browser/modules/Frame/FrameTemplate.tsx @@ -18,9 +18,10 @@ * along with this program. If not, see . */ -import React, { Component } from 'react' +import React, { useRef, useState, useEffect } from 'react' +import { Frame } from 'shared/modules/stream/streamDuck' import FrameTitlebar from './FrameTitlebar' -import Render from 'browser-components/Render' + import { StyledFrame, StyledFrameBody, @@ -30,137 +31,135 @@ import { StyledFrameAside } from './styled' -type State = any +type FrameTemplateProps = { + contents: JSX.Element | null | string + header?: Frame + className?: string + onResize?: (fullscreen: boolean, collapsed: boolean, height?: number) => void + numRecords?: number + getRecords?: () => any + visElement?: any + runQuery?: () => any + sidebar?: () => JSX.Element | null + aside?: JSX.Element | null + statusbar?: JSX.Element | null +} -class FrameTemplate extends Component { - frameContentElement: any - constructor(props: {}) { - super(props) - this.state = { - fullscreen: false, - collapse: false, - pinned: false, - lastHeight: 10 - } - } +function FrameTemplate({ + header, + contents, + onResize = () => { + /*noop*/ + }, + className, + numRecords = 0, + getRecords, + visElement, + runQuery, + sidebar, + aside, + statusbar +}: FrameTemplateProps): JSX.Element { + const [lastHeight, setLastHeight] = useState(10) + const frameContentElementRef = useRef(null) - toggleFullScreen() { - this.setState( - { fullscreen: !this.state.fullscreen }, - () => - this.props.onResize && - this.props.onResize( - this.state.fullscreen, - this.state.collapse, - this.frameContentElement.clientHeight - ) - ) - } + const { + isFullscreen, + isCollapsed, + isPinned, + toggleFullScreen, + toggleCollapse, + togglePin + } = useSizeToggles() - toggleCollapse() { - this.setState( - { collapse: !this.state.collapse }, - () => - this.props.onResize && - this.props.onResize( - this.state.fullscreen, - this.state.collapse, - this.state.lastHeight - ) - ) - } + useEffect(() => { + if (!frameContentElementRef.current?.clientHeight) return + const currHeight = frameContentElementRef.current.clientHeight + if (currHeight < 300) return // No need to report a transition - togglePin() { - this.setState( - { pinned: !this.state.pinned }, - () => - this.props.onResize && - this.props.onResize( - this.state.fullscreen, - this.state.collapse, - this.state.lastHeight - ) - ) - } - - componentDidUpdate() { - if (this.frameContentElement.clientHeight < 300) return // No need to report a transition - if ( - this.frameContentElement && - this.state.lastHeight !== this.frameContentElement.clientHeight - ) { - this.props.onResize && - this.props.onResize( - this.state.fullscreen, - this.state.collapse, - this.frameContentElement.clientHeight - ) - this.setState({ lastHeight: this.frameContentElement.clientHeight }) + if (lastHeight !== currHeight) { + onResize(isFullscreen, isCollapsed, currHeight) + setLastHeight(currHeight) } - } + }, [lastHeight, isPinned, isFullscreen, isCollapsed, onResize]) - setFrameContentElement = (el: any) => { - this.frameContentElement = el + const classNames = [] + if (className) { + classNames.push(className) + } + if (isFullscreen) { + classNames.push('is-fullscreen') } - render() { - const { className } = this.props - const classNames = [] - if (className) { - classNames.push(className) - } - if (this.state.fullscreen) { - classNames.push('is-fullscreen') - } - return ( - - {this.props.header && ( - - )} - - {this.props.sidebar ? this.props.sidebar() : null} - {this.props.aside && ( - {this.props.aside} - )} - - - {this.props.contents} - - - - - + {header && ( + + )} + + + {sidebar && sidebar()} + {aside && {aside}} + + - {this.props.statusbar} - - - - ) + {contents} + + + + + {statusbar && ( + + {statusbar} + + )} + + ) +} + +function useSizeToggles() { + const [isFullscreen, setFullscreen] = useState(false) + const [isCollapsed, setCollapsed] = useState(false) + const [isPinned, setPinned] = useState(false) + + function toggleFullScreen() { + setFullscreen(full => !full) + } + function toggleCollapse() { + setCollapsed(coll => !coll) + } + function togglePin() { + setPinned(pin => !pin) + } + return { + isFullscreen, + isCollapsed, + isPinned, + toggleFullScreen, + toggleCollapse, + togglePin } } diff --git a/src/browser/modules/Frame/FrameTitlebar.tsx b/src/browser/modules/Frame/FrameTitlebar.tsx index 25937fb0da2..7689c75d0ac 100644 --- a/src/browser/modules/Frame/FrameTitlebar.tsx +++ b/src/browser/modules/Frame/FrameTitlebar.tsx @@ -19,7 +19,7 @@ */ import { connect } from 'react-redux' -import React, { Component } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { withBus } from 'react-suber' import { saveAs } from 'file-saver' import { map } from 'lodash-es' @@ -27,20 +27,20 @@ import SVGInline from 'react-svg-inline' import controlsPlay from 'icons/controls-play.svg' import * as app from 'shared/modules/app/appDuck' -import * as editor from 'shared/modules/editor/editorDuck' import * as commands from 'shared/modules/commands/commandsDuck' import * as sidebar from 'shared/modules/sidebar/sidebarDuck' +import * as editor from 'shared/modules/editor/editorDuck' import { cancel as cancelRequest, getRequest, + Request, REQUEST_STATUS_PENDING } from 'shared/modules/requests/requestsDuck' -import { remove, pin, unpin } from 'shared/modules/stream/streamDuck' -import { removeComments, sleep } from 'shared/services/utils' +import { remove, pin, unpin, Frame } from 'shared/modules/stream/streamDuck' +import { sleep } from 'shared/services/utils' import { FrameButton } from 'browser-components/buttons' import Render from 'browser-components/Render' import { CSVSerializer } from 'services/serializer' - import { CloseIcon, ContractIcon, @@ -48,21 +48,22 @@ import { DownloadIcon, ExpandIcon, PinIcon, - RefreshIcon, + RunIcon, UpIcon, SaveFavorite, SaveFile } from 'browser-components/icons/Icons' import { - DottedLineHover, DropdownList, DropdownContent, DropdownButton, - DropdownItem + DropdownItem, + DottedLineHover } from '../Stream/styled' import { StyledFrameTitleBar, StyledFrameTitlebarButtonSection, + FrameTitleEditorContainer, StyledFrameCommand } from './styled' import { @@ -77,13 +78,78 @@ import { import { csvFormat, stringModifier } from 'services/bolt/cypherTypesFormatting' import arrayHasItems from 'shared/utils/array-has-items' import { stringifyMod } from 'services/utils' +import Monaco, { MonacoHandles } from '../Editor/Monaco' +import { Bus } from 'suber' +import FeatureToggle from '../FeatureToggle/FeatureToggle' +import { reusableFrame } from 'shared/modules/experimentalFeatures/experimentalFeaturesDuck' + +type FrameTitleBarBaseProps = { + frame: any + fullscreen: boolean + fullscreenToggle: () => void + collapse: boolean + collapseToggle: () => void + pinned: boolean + togglePin: () => void + numRecords: number + getRecords: () => any + visElement: any + runQuery: () => any + bus: Bus +} + +type FrameTitleBarProps = FrameTitleBarBaseProps & { + request: Request | null + isRelateAvailable: boolean + newFavorite: (cmd: string) => void + newProjectFile: (cmd: string) => void + onCloseClick: ( + frameId: string, + requestId: string, + request: Request | null + ) => void + onRunClick: () => void + reRun: (obj: Frame, cmd: string) => void + togglePinning: (id: string, isPinned: boolean) => void + onTitlebarClick: (cmd: string) => void +} + +function FrameTitlebar(props: FrameTitleBarProps) { + const [editorValue, setEditorValue] = useState(props.frame.cmd) + const editorRef = useRef(null) + + /* When the frametype is changed the titlebar is unmounted + and replaced with a new instance. This means focus cursor position are lost. + To regain editor focus we run an effect dependant on the isRerun prop. + However, when the frame prop changes in some way the effect is retriggered + although the "isRun" is still true. Use effect does not check for equality + but instead re-runs the effect to take focus again. To prevent this + we use the useCallback hook as well. As a best effort we set the cursor position + to be at the end of the query. + + A better solution is to change the frame titlebar to reside outside of the + frame contents. + */ + + const gainFocusCallback = useCallback(() => { + if (props.frame.isRerun) { + editorRef.current?.focus() -class FrameTitlebar extends Component { - hasData() { - return this.props.numRecords > 0 + const lines = (editorRef.current?.getValue() || '').split('\n') + const linesLength = lines.length + editorRef.current?.setPosition({ + lineNumber: linesLength, + column: lines[linesLength - 1].length + 1 + }) + } + }, [props.frame.isRerun]) + useEffect(gainFocusCallback, [gainFocusCallback]) + + function hasData() { + return props.numRecords > 0 } - exportCSV(records: any) { + function exportCSV(records: any) { const exportData = stringifyResultArray( csvFormat, transformResultRecordsToResultArray(records) @@ -97,12 +163,12 @@ class FrameTitlebar extends Component { saveAs(blob, 'export.csv') } - exportTXT = () => { - const { frame } = this.props + function exportTXT() { + const { frame } = props if (frame.type === 'history') { const asTxt = frame.result - .map((result: any) => { + .map((result: string) => { const safe = `${result}`.trim() if (safe.startsWith(':')) { @@ -120,7 +186,7 @@ class FrameTitlebar extends Component { } } - exportJSON(records: any) { + function exportJSON(records: any) { const exportData = map(records, recordToJSONMapper) const data = stringifyMod(exportData, stringModifier, true) const blob = new Blob([data], { @@ -129,178 +195,182 @@ class FrameTitlebar extends Component { saveAs(blob, 'records.json') } - exportPNG() { - const { svgElement, graphElement, type } = this.props.visElement + function exportPNG() { + const { svgElement, graphElement, type } = props.visElement downloadPNGFromSVG(svgElement, graphElement, type) } - exportSVG() { - const { svgElement, graphElement, type } = this.props.visElement + function exportSVG() { + const { svgElement, graphElement, type } = props.visElement downloadSVG(svgElement, graphElement, type) } - exportGrass(data: any) { + function exportGrass(data: any) { const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) saveAs(blob, 'style.grass') } - canExport = () => { - const props = this.props - const { frame = {} } = props + function canExport() { + const { frame = {}, visElement } = props return ( - this.canExportTXT() || - (frame.type === 'cypher' && (this.hasData() || this.props.visElement)) || - (frame.type === 'style' && this.hasData()) + canExportTXT() || + (frame.type === 'cypher' && (hasData() || visElement)) || + (frame.type === 'style' && hasData()) ) } - canExportTXT() { - const { frame = {} } = this.props + function canExportTXT() { + const { frame = {} } = props return frame.type === 'history' && arrayHasItems(frame.result) } - render() { - const props = this.props - const { frame = {} } = props - const fullscreenIcon = props.fullscreen ? : - const expandCollapseIcon = props.collapse ? : - const cmd = removeComments(frame.cmd) - return ( - - - : + const expandCollapseIcon = props.collapse ? : + + return ( + + + + + } + off={ + props.onTitlebarClick(editorValue)} data-testid="frameCommand" - onClick={() => props.onTitlebarClick(frame.cmd)} > - {cmd} - - - + {editorValue} + + } + /> + run(editorValue)} + > + + + + { + props.newFavorite(frame.cmd) + }} + > + + + { - props.newFavorite(frame.cmd) + props.newProjectFile(frame.cmd) }} > - {/* @ts-expect-error ts-migrate(2322) FIXME: Property 'width' does not exist on type 'Intrinsic... Remove this comment to see the full error message */} - + - - { - props.newProjectFile(frame.cmd) - }} - > - {/* @ts-expect-error ts-migrate(2322) FIXME: Property 'width' does not exist on type 'Intrinsic... Remove this comment to see the full error message */} - - - - - - - - - - this.exportCSV(props.getRecords())} - > - Export CSV - - this.exportJSON(props.getRecords())} - > - Export JSON - - - - this.exportPNG()}> - Export PNG - - this.exportSVG()}> - Export SVG - - - - - Export TXT - - - - this.exportGrass(props.getRecords())} - > - Export GraSS - - - - - - - { - props.togglePin() - // using frame.isPinned causes issues when there are multiple frames in one - props.togglePinning(frame.id, props.pinned) - }} - pressed={props.pinned} - > - - - - props.fullscreenToggle()} - > - {fullscreenIcon} - - - props.collapseToggle()} - > - {expandCollapseIcon} - - - props.onRunClick()}> - - - - - props.onReRunClick(frame)} - > - - - - - props.onCloseClick(frame.id, frame.requestId, props.request) - } - > - + + + + + + + + exportCSV(props.getRecords())}> + Export CSV + + exportJSON(props.getRecords())}> + Export JSON + + + + exportPNG()}> + Export PNG + + exportSVG()}> + Export SVG + + + + Export TXT + + + exportGrass(props.getRecords())} + > + Export GraSS + + + + + + + { + props.togglePin() + // using frame.isPinned causes issues when there are multiple frames in one + props.togglePinning(frame.id, props.pinned) + }} + pressed={props.pinned} + > + + + props.fullscreenToggle()} + > + {fullscreenIcon} + + props.collapseToggle()} + > + {expandCollapseIcon} + + + props.onRunClick()}> + - - - ) - } + + + props.onCloseClick(frame.id, frame.requestId, props.request) + } + > + + + + + ) } -const mapStateToProps = (state: any, ownProps: any) => { +const mapStateToProps = (state: any, ownProps: FrameTitleBarBaseProps) => { const request = ownProps.frame.requestId ? getRequest(state, ownProps.frame.requestId) : null @@ -311,18 +381,22 @@ const mapStateToProps = (state: any, ownProps: any) => { } } -const mapDispatchToProps = (dispatch: any, ownProps: any) => { +const mapDispatchToProps = ( + dispatch: any, + ownProps: FrameTitleBarBaseProps +) => { return { - newFavorite: (cmd: any) => { + newFavorite: (cmd: string) => { dispatch(sidebar.setDraftScript(cmd, 'favorites')) }, - newProjectFile: (cmd: any) => { + newProjectFile: (cmd: string) => { dispatch(sidebar.setDraftScript(cmd, 'project files')) }, - onTitlebarClick: (cmd: any) => { - ownProps.bus.send(editor.SET_CONTENT, editor.setContent(cmd)) - }, - onCloseClick: async (id: any, requestId: any, request: any) => { + onCloseClick: async ( + id: string, + requestId: string, + request: Request | null + ) => { if (request && request.status === REQUEST_STATUS_PENDING) { dispatch(cancelRequest(requestId)) await sleep(3000) // sleep for 3000 ms to let user read the cancel info @@ -332,10 +406,11 @@ const mapDispatchToProps = (dispatch: any, ownProps: any) => { onRunClick: () => { ownProps.runQuery() }, - onReRunClick: ({ cmd, useDb, id, requestId }: any) => { + reRun: ({ useDb, id, requestId }: Frame, cmd: string) => { if (requestId) { dispatch(cancelRequest(requestId)) } + dispatch( commands.executeCommand(cmd, { id, @@ -345,8 +420,11 @@ const mapDispatchToProps = (dispatch: any, ownProps: any) => { }) ) }, - togglePinning: (id: any, isPinned: any) => { + togglePinning: (id: string, isPinned: boolean) => { isPinned ? dispatch(unpin(id)) : dispatch(pin(id)) + }, + onTitlebarClick: (cmd: any) => { + ownProps.bus.send(editor.SET_CONTENT, editor.setContent(cmd)) } } } diff --git a/src/browser/modules/Frame/styled.tsx b/src/browser/modules/Frame/styled.tsx index 9067e108464..c35ae4f0003 100644 --- a/src/browser/modules/Frame/styled.tsx +++ b/src/browser/modules/Frame/styled.tsx @@ -18,47 +18,26 @@ * along with this program. If not, see . */ -import styled, { keyframes } from 'styled-components' +import styled from 'styled-components' import { dim } from 'browser-styles/constants' -const rollDownAnimation = keyframes` - from { - transform: translate(0, -${dim.frameBodyHeight}px); - max-height: 0; - } - to { - transform: translateY(0); - max-height: 500px; /* Greater than a frame can be */ - } -` +type FullscreenProps = { fullscreen: boolean } -// Frames -export const StyledFrame: any = styled.article` +export const StyledFrame = styled.article` width: auto; background-color: ${props => props.theme.secondaryBackground}; - animation: ${rollDownAnimation} 0.4s ease-in; border: ${props => props.theme.frameBorder}; - margin: ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? '0' : '0 0 10px 0'}; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'position: fixed' : null}; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'left: 0' : null}; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'top: 0' : null}; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'bottom: 0' : null}; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'right: 0' : null}; + ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'z-index: 1030' : null}; + props.fullscreen + ? `margin: 0; +position: fixed; +left: 0; +top: 0; +bottom: 0; +right: 0; +z-index: 1300;` + : 'margin 0 0 10px 0;'} &:hover .carousel-intro-animation { opacity: 0; @@ -68,23 +47,21 @@ export const StyledFrame: any = styled.article` border-radius: 2px; ` -export const StyledFrameBody: any = styled.div` +export const StyledFrameBody = styled.div< + FullscreenProps & { collapsed: boolean } +>` overflow: auto; min-height: ${dim.frameBodyHeight / 2}px; max-height: ${props => { - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collapsed' does not exist on type 'Theme... Remove this comment to see the full error message if (props.collapsed) { return 0 } - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message if (props.fullscreen) { return '100%' } return dim.frameBodyHeight - dim.frameStatusbarHeight + 1 + 'px' }}; - display: ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'collapsed' does not exist on type 'Theme... Remove this comment to see the full error message - props.collapsed ? 'none' : 'flex'}; + display: ${props => (props.collapsed ? 'none' : 'flex')}; flex-direction: row; width: 100%; padding: 30px; @@ -122,18 +99,15 @@ export const StyledFrameAside = styled.div` min-width: 120px; ` -export const StyledFrameContents: any = styled.div` +export const StyledFrameContents = styled.div` font-size: 14px; overflow: auto; min-height: ${dim.frameBodyHeight / 2}px; max-height: ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message props.fullscreen ? '100vh' : dim.frameBodyHeight - dim.frameStatusbarHeight * 2 + 'px'}; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'height: 100vh' : null}; + ${props => (props.fullscreen ? 'height: 100vh' : null)}; flex: auto; display: flex; width: 100%; @@ -147,12 +121,10 @@ export const StyledFrameContents: any = styled.div` } ` -export const StyledFrameStatusbar: any = styled.div` +export const StyledFrameStatusbar = styled.div` border-top: ${props => props.theme.inFrameBorder}; - height: ${dim.frameStatusbarHeight + 1}px; - ${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'fullscreen' does not exist on type 'Them... Remove this comment to see the full error message - props.fullscreen ? 'margin-top: -78px;' : ''}; + height: ${dim.frameStatusbarHeight - 1}px; + ${props => (props.fullscreen ? 'margin-top: -78px;' : '')}; display: flex; flex-direction: row; flex: none; @@ -187,19 +159,41 @@ export const StyledFrameTitlebarButtonSection = styled.ul` ` export const StyledFrameTitleBar = styled.div` - height: ${dim.frameTitlebarHeight}px; border-bottom: transparent; line-height: ${dim.frameTitlebarHeight}px; color: ${props => props.theme.frameTitlebarText}; display: flex; - flex-direction: row; ` export const StyledFrameStatusbarText = styled.label` flex: 1 1 auto; ` -export const StyledFrameCommand: any = styled.label` +export const CurrentDbText = styled.div` + color: ${props => props.theme.promptText}; +` + +export const FrameTitleEditorContainer = styled.div` + border-radius: 2px; + padding-left: 6px; + padding-top: 3px; + margin: 3px 5px 3px 3px; + + width: 0; // Prevents the editor from growing past flex-grow: 1 + flex-grow: 1; + display: flex; + + font-family: ${props => props.theme.editorFont}; + line-height: 2.2em; + font-size: 1.2em; + color: ${props => props.theme.secondaryButtonText}; + background-color: ${props => props.theme.frameSidebarBackground}; + .disable-font-ligatures & { + font-variant-ligatures: none !important; + } +` + +export const StyledFrameCommand = styled.label<{ selectedDb: string }>` font-family: ${props => props.theme.editorFont}; color: ${props => props.theme.secondaryButtonText}; background-color: ${props => props.theme.frameSidebarBackground}; @@ -216,9 +210,7 @@ export const StyledFrameCommand: any = styled.label` display: block; &::before { color: ${props => props.theme.promptText}; - content: "${props => - // @ts-expect-error ts-migrate(2339) FIXME: Property 'selectedDb' does not exist on type 'Them... Remove this comment to see the full error message - (props.selectedDb || '') + '$ '}"; + content: "${props => (props.selectedDb || '') + '$ '}"; } .disable-font-ligatures & { font-variant-ligatures: none !important; diff --git a/src/browser/modules/Sidebar/Settings.tsx b/src/browser/modules/Sidebar/Settings.tsx index 67623d43188..7e1c01852d9 100644 --- a/src/browser/modules/Sidebar/Settings.tsx +++ b/src/browser/modules/Sidebar/Settings.tsx @@ -250,14 +250,16 @@ export const Settings = ({ const tooltip = feature.tooltip || '' return ( - { - const on = event.target.checked - onFeatureChange(feature.name, on) - }} - checked={experimentalFeatures[feature.name].on} - /> - {visual} + + { + const on = event.target.checked + onFeatureChange(feature.name, on) + }} + checked={experimentalFeatures[feature.name].on} + /> + {visual} + ) }) diff --git a/src/browser/modules/Stream/Auth/ConnectForm.tsx b/src/browser/modules/Stream/Auth/ConnectForm.tsx index 7ea91f92c52..acb3915c0b2 100644 --- a/src/browser/modules/Stream/Auth/ConnectForm.tsx +++ b/src/browser/modules/Stream/Auth/ConnectForm.tsx @@ -33,8 +33,8 @@ import { import { NATIVE, NO_AUTH } from 'services/bolt/boltHelpers' import { toKeyString } from 'services/utils' import { stripScheme, getScheme } from 'services/boltscheme.utils' +import { AuthenticationMethod } from 'shared/modules/connections/connectionsDuck' -type AuthenticationMethod = typeof NATIVE | typeof NO_AUTH const readableauthenticationMethods: Record = { [NATIVE]: 'Username / Password', [NO_AUTH]: 'No authentication' diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap index fc8ccbeb565..710d9ea916a 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap @@ -6,7 +6,7 @@ exports[`CodeViews CodeStatusbar displays no statusBarMessage 1`] = ` class="sc-ckVGcZ fsAzIa" >
@@ -18,7 +18,7 @@ exports[`CodeViews CodeStatusbar displays statusBarMessage 1`] = ` class="sc-ckVGcZ fsAzIa" >
Started streaming 1 records after 5 ms and completed after 10 ms.
diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap index 405889f27dd..36761c4c301 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap @@ -10,7 +10,7 @@ exports[`ErrorsViews ErrorsStatusbar displays error 1`] = ` title="Test.Error: Test error description" > Test.Error: Test error description @@ -61,7 +61,7 @@ exports[`ErrorsViews ErrorsView displays procedure link if unknown procedure 1`] class="sc-gZMcBi jPwkqT" >  List available procedures diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap index cfeac9938f4..421cf0ae0bc 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Visualization renders 1`] = `
`; exports[`Visualization renders with result and escapes any HTML 1`] = `
@@ -145,7 +145,7 @@ exports[`Visualization renders with result and escapes any HTML 1`] = ` class="sc-cvbbAY ZoyPP zoom-out" > diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/relatable-view.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/relatable-view.test.tsx.snap index d75d7fc7e42..3e528ae7b3e 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/relatable-view.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/relatable-view.test.tsx.snap @@ -6,7 +6,7 @@ exports[`RelatableViews RelatableView displays bodyMessage if no rows 1`] = ` class="sc-ckVGcZ fsAzIa" >
(no changes, no records)
@@ -17,7 +17,7 @@ exports[`RelatableViews RelatableView displays bodyMessage if no rows 1`] = ` exports[`RelatableViews RelatableView does not display bodyMessage if rows, and escapes HTML 1`] = `
@@ -102,7 +102,7 @@ exports[`RelatableViews TableStatusbar displays statusBarMessage 1`] = ` class="sc-ckVGcZ fsAzIa" >
Started streaming 1 records after 5 ms and completed after 10 ms.
diff --git a/src/browser/modules/Stream/CypherFrame/index.tsx b/src/browser/modules/Stream/CypherFrame/index.tsx index 0206cb614a4..5f7244635be 100644 --- a/src/browser/modules/Stream/CypherFrame/index.tsx +++ b/src/browser/modules/Stream/CypherFrame/index.tsx @@ -71,7 +71,17 @@ import RelatableView, { } from 'browser/modules/Stream/CypherFrame/relatable-view' import { requestExceedsVisLimits } from 'browser/modules/Stream/CypherFrame/helpers' -type CypherFrameState = any +type CypherFrameState = { + openView?: string + fullscreen: boolean + collapse: boolean + frameHeight: number + hasVis: boolean + errors: any + _asciiMaxColWidth: any + _asciiSetColWidth: any + _planExpand: any +} export class CypherFrame extends Component { visElement: any = null @@ -98,7 +108,7 @@ export class CypherFrame extends Component { } } - shouldComponentUpdate(props: any, state: CypherFrameState) { + shouldComponentUpdate(props: any, state: CypherFrameState): boolean { return ( this.props.request.updated !== props.request.updated || this.state.openView !== state.openView || @@ -126,6 +136,15 @@ export class CypherFrame extends Component { this.visElement = null this.setState({ hasVis: false }) } + + // When frame re-use leads to result without visuzaliation + if (!this.canShowViz() && this.state.openView === viewTypes.VISUALIZATION) { + const view = initialView(this.props, { + ...this.state, + openView: undefined //intial view was not meant to override another view + }) + if (view) this.setState({ openView: view }) + } } componentDidMount() { @@ -140,95 +159,93 @@ export class CypherFrame extends Component { return [] } - sidebar = () => { - const canShowViz = - !requestExceedsVisLimits(this.props.request) && - resultHasNodes(this.props.request) && - !this.state.errors + canShowViz = () => + !requestExceedsVisLimits(this.props.request) && + resultHasNodes(this.props.request) && + !this.state.errors - return ( - - - { - this.changeView(viewTypes.VISUALIZATION) - }} - > - - - - - { - this.changeView(viewTypes.TABLE) - }} - > - - - - ( + + + { + this.changeView(viewTypes.VISUALIZATION) + }} > - { - this.changeView(viewTypes.TEXT) - }} - > - - - - - this.changeView(viewTypes.PLAN)} - > - - - - - { - this.changeView(viewTypes.WARNINGS) - }} - > - - - - - { - this.changeView(viewTypes.ERRORS) - }} - > - - - - - { - this.changeView(viewTypes.CODE) - }} - > - - - - - ) - } + + + + + { + this.changeView(viewTypes.TABLE) + }} + > + + + + + { + this.changeView(viewTypes.TEXT) + }} + > + + + + + this.changeView(viewTypes.PLAN)} + > + + + + + { + this.changeView(viewTypes.WARNINGS) + }} + > + + + + + { + this.changeView(viewTypes.ERRORS) + }} + > + + + + + { + this.changeView(viewTypes.CODE) + }} + > + + + + + ) getSpinner() { return ( @@ -392,7 +409,7 @@ export class CypherFrame extends Component { return ( !str.startsWith(':') +const isCypher = (str: string) => !str.startsWith(':') -class CypherScriptFrame extends Component { - render() { - const { frame, frames, requests = {} } = this.props - const contents = ( - - - { - return ( -
- {(frame.statements || []).map((id: any, index: any) => { - if (!requests[frames[id].requestId]) { - return - } - const status = frames[id].ignore - ? 'ignored' - : requests[frames[id].requestId].status - const { titleProps, contentProps } = getChildProps({ - index, - defaultActive: ['error'].includes(status) - }) - const SummaryC = isCypher(frames[id].cmd) - ? CypherSummary - : Summary - return ( -
- - - {frames[id].cmd} - - - - - - - - + requests: Record +} + +function CypherScriptFrame({ + frame, + frames, + requests = {} +}: CypherScriptFrameProps): JSX.Element { + const contents = ( + + + ( +
+ {(frame.statements || []).map((id: string, index: number) => { + if (!requests[frames[id].requestId]) { + return + } + const status = requests[frames[id].requestId].status + const { titleProps, contentProps } = getChildProps({ + index, + defaultActive: ['error'].includes(status) + }) + const SummaryC = isCypher(frames[id].cmd) + ? CypherSummary + : Summary + return ( +
+ + + {frames[id].cmd} + + + - - -
- ) - })} -
- ) - }} - /> -
-
- ) - return ( - - ) - } + + + + + + + +
+ ) + })} +
+ )} + /> +
+
+ ) + return ( + + ) } -const mapStateToProps = (state: any, ownProps: any) => { - if (!ownProps.frame.statements) return {} +const mapStateToProps = (state: any, ownProps: BaseFrameProps) => { + // frame.statements are added one by one as the frame renders and is undefined on first render + if (!ownProps.frame.statements) return { frames: {}, requests: {} } const frames = ownProps.frame.statements - .map((id: any) => getLatestFromFrameStack(getFrame(state, id))) - .reduce((all: any, curr: any) => { - all[curr.id] = curr - return all - }, {}) + .map(id => getFrame(state, id).stack[0]) + .reduce( + (all: Record, curr) => ({ ...all, [curr.id]: curr }), + {} + ) + const requests = Object.keys(frames) .map(id => { const requestId = frames[id].requestId if (!requestId) return false + const request = getRequest(state, requestId) if (!request) return false + request.id = requestId return request }) - .filter(a => !!a) - .reduce((all, curr) => { + .reduce((all: Record, curr: Request | false) => { + if (!curr) { + return all + } + all[curr.id] = curr return all }, {}) + return { frames, requests diff --git a/src/browser/modules/Stream/CypherScriptFrame/Icon.tsx b/src/browser/modules/Stream/CypherScriptFrame/Icon.tsx index 1c89f311f0d..f4f1ab8c515 100644 --- a/src/browser/modules/Stream/CypherScriptFrame/Icon.tsx +++ b/src/browser/modules/Stream/CypherScriptFrame/Icon.tsx @@ -26,8 +26,13 @@ import { ExclamationTriangleIcon } from 'browser-components/icons/Icons' import { WarningSpan, ErrorSpan, SuccessSpan } from './styled' +import { Status } from 'shared/modules/requests/requestsDuck' -export const Icon = ({ status }: any) => { +interface IconProps { + status: Status +} + +export const Icon = ({ status }: IconProps): JSX.Element => { switch (status) { case 'pending': return diff --git a/src/browser/modules/Stream/CypherScriptFrame/Summary.tsx b/src/browser/modules/Stream/CypherScriptFrame/Summary.tsx index a56496cd923..74273486c40 100644 --- a/src/browser/modules/Stream/CypherScriptFrame/Summary.tsx +++ b/src/browser/modules/Stream/CypherScriptFrame/Summary.tsx @@ -28,10 +28,15 @@ import { } from 'browser/modules/Stream/styled' import { MessageArea, PaddedStatsBar } from './styled' import { allowlistedMultiCommands } from 'shared/modules/commands/commandsDuck' +import { Status } from 'shared/modules/requests/requestsDuck' +import { Request } from 'shared/modules/requests/requestsDuck' -const ucFirst = (str: any) => str[0].toUpperCase() + str.slice(1) +const ucFirst = (str: string): string => str[0].toUpperCase() + str.slice(1) +type GenericSummaryProps = { status: Status } -const GenericSummary = ({ status }: any): any => { +const GenericSummary = ({ + status +}: GenericSummaryProps): JSX.Element | null => { switch (status) { case 'skipped': return ( @@ -73,10 +78,20 @@ const GenericSummary = ({ status }: any): any => { ) + default: + return null } } -export const CypherSummary = ({ status, request }: any): any => { +interface CypherSummaryProps { + status: Status + request: Request +} + +export const CypherSummary = ({ + status, + request +}: CypherSummaryProps): JSX.Element | null => { switch (status) { case 'skipped': return @@ -92,7 +107,7 @@ export const CypherSummary = ({ status, request }: any): any => { return ( SUCCESS - {ucFirst(bodyMessage)} + {ucFirst(bodyMessage || '')} ) case 'error': @@ -106,10 +121,20 @@ export const CypherSummary = ({ status, request }: any): any => { {fullError.message} ) + default: + return null } } -export const Summary = ({ status, request }: any): any => { +interface SummaryProps { + status: Status + request: Request +} + +export const Summary = ({ + status, + request +}: SummaryProps): JSX.Element | null => { switch (status) { case 'ignored': return @@ -135,5 +160,7 @@ export const Summary = ({ status, request }: any): any => { ) + default: + return null } } diff --git a/src/browser/modules/Stream/EditFrame.tsx b/src/browser/modules/Stream/EditFrame.tsx index 94f1836d4e0..7d6ec7a8f5a 100644 --- a/src/browser/modules/Stream/EditFrame.tsx +++ b/src/browser/modules/Stream/EditFrame.tsx @@ -70,6 +70,9 @@ const EditFrame = (props: EditFrameProps): JSX.Element => { id={props.frame.id} onChange={setText} value={text} + toggleFullscreen={() => { + /* don't allow fullscreening */ + }} /> } header={props.frame} diff --git a/src/browser/modules/Stream/PlayFrame.tsx b/src/browser/modules/Stream/PlayFrame.tsx index 56a1461efdc..f845124a6b5 100644 --- a/src/browser/modules/Stream/PlayFrame.tsx +++ b/src/browser/modules/Stream/PlayFrame.tsx @@ -50,7 +50,7 @@ const checkHtmlForSlides = (html: any) => { return !!slides.length } -export function PlayFrame({ stack, bus }: any) { +export function PlayFrame({ stack, bus }: any): JSX.Element { const [stackIndex, setStackIndex] = useState(0) const [atSlideStart, setAtSlideStart] = useState(null) const [atSlideEnd, setAtSlideEnd] = useState(null) @@ -65,9 +65,10 @@ export function PlayFrame({ stack, bus }: any) { useEffect(() => { stackIndex !== 0 && atSlideEnd && bus && bus.send(LAST_GUIDE_SLIDE) - }, [atSlideEnd]) + }, [stackIndex, bus, atSlideEnd]) useEffect(() => { + let stillMounted = true async function generate() { const shouldUseSlidePointer = initialPlay const { guide, aside, hasCarousel, isRemote } = await generateContent( @@ -76,11 +77,18 @@ export function PlayFrame({ stack, bus }: any) { onSlide, shouldUseSlidePointer ) - setInitialPlay(false) - setGuideObj({ guide, aside, hasCarousel, isRemote }) + if (stillMounted) { + setInitialPlay(false) + setGuideObj({ guide, aside, hasCarousel, isRemote }) + } } generate() - }, [currentFrame]) + + return () => { + stillMounted = false + } + // The full dependency array causes a re-run which switches to slide 1 + }, [bus, currentFrame]) const { guide, aside, hasCarousel, isRemote } = guideObj diff --git a/src/browser/modules/Stream/Stream.tsx b/src/browser/modules/Stream/Stream.tsx index 0e9b4b6b68f..bc43fa2e7e1 100644 --- a/src/browser/modules/Stream/Stream.tsx +++ b/src/browser/modules/Stream/Stream.tsx @@ -19,13 +19,12 @@ */ import { connect } from 'react-redux' -import React, { PureComponent } from 'react' -import { StyledStream, Padding } from './styled' - +import React, { memo, useRef, useEffect } from 'react' +import { StyledStream, Padding, AnimationContainer } from './styled' import CypherFrame from './CypherFrame/index' import HistoryFrame from './HistoryFrame' import PlayFrame from './PlayFrame' -import Frame from '../Frame/Frame' +import DefaultFrame from '../Frame/DefaultFrame' import PreFrame from './PreFrame' import ParamsFrame from './ParamsFrame' import ErrorFrame from './ErrorFrame' @@ -43,15 +42,17 @@ import ChangePasswordFrame from './Auth/ChangePasswordFrame' import QueriesFrame from './Queries/QueriesFrame' import UserList from '../User/UserList' import UserAdd from '../User/UserAdd' -import { getFrames } from 'shared/modules/stream/streamDuck' -import { getActiveConnectionData } from 'shared/modules/connections/connectionsDuck' +import { FrameStack, Frame, getFrames } from 'shared/modules/stream/streamDuck' +import { + getActiveConnectionData, + Connection +} from 'shared/modules/connections/connectionsDuck' import { getScrollToTop } from 'shared/modules/settings/settingsDuck' import DbsFrame from './Auth/DbsFrame' -import { getLatestFromFrameStack } from './stream.utils' import EditFrame from './EditFrame' -const getFrame = (type: any) => { - const trans: any = { +const getFrame = (type: string) => { + const trans: Record = { error: ErrorFrame, cypher: CypherFrame, 'cypher-script': CypherScriptFrame, @@ -78,65 +79,77 @@ const getFrame = (type: any) => { dbs: DbsFrame, style: StyleFrame, edit: EditFrame, - default: Frame + default: DefaultFrame } return trans[type] || trans.default } -class Stream extends PureComponent { - base: any - componentDidMount() { - this.base = React.createRef() - } +type StreamProps = { + frames: FrameStack[] + activeConnectionData: Connection | null + shouldScrollToTop: boolean +} - componentDidUpdate(prevProps: any) { +export interface BaseFrameProps { + frame: Frame & { isPinned: boolean } + activeConnectionData: Connection | null + stack: Frame[] +} + +function Stream(props: StreamProps): JSX.Element { + const base = useRef(null) + const lastFrameCount = useRef(0) + + useEffect(() => { // If we want to scroll to top when a new frame is added if ( - prevProps.frames.length < this.props.frames.length && - this.props.scrollToTop && - this.base && - this.base.current + lastFrameCount.current < props.frames.length && + props.shouldScrollToTop && + base.current ) { - this.base.current.scrollTop = 0 + base.current.scrollTop = 0 } - } - render() { - return ( - - {this.props.frames.map((frameObject: any) => { - const frame = getLatestFromFrameStack(frameObject) - const frameProps = { - frame: { ...frame, isPinned: frameObject.isPinned }, - activeConnectionData: this.props.activeConnectionData, - stack: frameObject.stack - } - let MyFrame = getFrame(frame.type) - if (frame.type === 'error') { - try { - const cmd = frame.cmd.replace(/^:/, '') - const Frame = cmd[0].toUpperCase() + cmd.slice(1) + 'Frame' - MyFrame = require('./Extras/index')[Frame] - if (!MyFrame) { - MyFrame = getFrame(frame.type) - } - } catch (e) {} - } - return - })} - - - ) - } -} + lastFrameCount.current = props.frames.length + }) -const mapStateToProps = (state: any) => { - const frames = getFrames(state) - return { - frames, - activeConnectionData: getActiveConnectionData(state), - scrollToTop: getScrollToTop(state) - } + return ( + + {props.frames.map(frameObject => { + const frame = frameObject.stack[0] + + const frameProps: BaseFrameProps = { + frame: { ...frame, isPinned: frameObject.isPinned }, + activeConnectionData: props.activeConnectionData, + stack: frameObject.stack + } + + let MyFrame = getFrame(frame.type) + if (frame.type === 'error') { + try { + const cmd = frame.cmd.replace(/^:/, '') + const Frame = cmd[0].toUpperCase() + cmd.slice(1) + 'Frame' + MyFrame = require('./Extras/index')[Frame] + if (!MyFrame) { + MyFrame = getFrame(frame.type) + } + } catch (e) {} + } + return ( + + + + ) + })} + + + ) } -export default connect(mapStateToProps)(Stream) +const mapStateToProps = (state: any) => ({ + frames: getFrames(state), + activeConnectionData: getActiveConnectionData(state), + shouldScrollToTop: getScrollToTop(state) +}) + +export default connect(mapStateToProps)(memo(Stream)) diff --git a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap index 6d6ca89626d..8f820de536b 100644 --- a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap +++ b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap @@ -7,16 +7,16 @@ exports[`SchemaFrame renders empty 1`] = ` >
@@ -24,10 +24,10 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -35,13 +35,13 @@ exports[`SchemaFrame renders empty 1`] = `
Indexes
None
@@ -49,10 +49,10 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -89,46 +89,46 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` >
Constraints
None
@@ -136,42 +136,42 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = `
Index Name Type Uniqueness EntityType LabelsOrTypes Properties State
None
@@ -179,10 +179,10 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` @@ -219,16 +219,16 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` >
Constraints
None
@@ -236,10 +236,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` @@ -247,13 +247,13 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = `
Indexes
ON :Movie(released) ONLINE
@@ -261,10 +261,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` diff --git a/src/browser/modules/Stream/stream.utils.ts b/src/browser/modules/Stream/stream.utils.ts index 19f6f65bdf5..5dd2c6b9f92 100644 --- a/src/browser/modules/Stream/stream.utils.ts +++ b/src/browser/modules/Stream/stream.utils.ts @@ -18,7 +18,9 @@ * along with this program. If not, see . */ -export function getLatestFromFrameStack(frameObj: any) { +import { Frame, FrameStack } from 'shared/modules/stream/streamDuck' + +export function getLatestFromFrameStack(frameObj: FrameStack): Frame | null { if (!frameObj.stack) { return null } diff --git a/src/browser/modules/Stream/styled.tsx b/src/browser/modules/Stream/styled.tsx index bfed8ae4a90..e47c30a89ef 100644 --- a/src/browser/modules/Stream/styled.tsx +++ b/src/browser/modules/Stream/styled.tsx @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -import styled from 'styled-components' +import styled, { keyframes } from 'styled-components' import { dim } from 'browser-styles/constants' export const StyledStream = styled.div` @@ -370,3 +370,17 @@ export const StyledHistoryRow = styled.li` background-color: ${props => props.theme.primaryBackground}; } ` + +const rollDownAnimation = keyframes` + from { + transform: translate(0, -${dim.frameBodyHeight}px); + max-height: 0; + } + to { + transform: translateY(0); + max-height: 500px; /* Greater than a frame can be */ + } +` +export const AnimationContainer = styled.div` + animation: ${rollDownAnimation} 0.4s ease-in; +` diff --git a/src/shared/modules/commands/commandsDuck.test.ts b/src/shared/modules/commands/commandsDuck.test.ts index d44a3fd61e6..0422b3790ad 100644 --- a/src/shared/modules/commands/commandsDuck.test.ts +++ b/src/shared/modules/commands/commandsDuck.test.ts @@ -154,11 +154,14 @@ describe('commandsDuck', () => { type: 'param', params: { x: 2 } } as any), - updateQueryResult( - undefined, - { result: { x: 2 }, type: 'param' }, - 'success' - ), + { + ...updateQueryResult( + 'id', + { result: { x: 2 }, type: 'param' }, + 'success' + ), + id: undefined + }, { type: 'NOOP' } ]) done() @@ -224,11 +227,14 @@ describe('commandsDuck', () => { type: 'params', params: {} } as any), - updateQueryResult( - undefined, - { result: { x: 2, y: 3 }, type: 'params' }, - 'success' - ), + { + ...updateQueryResult( + 'id', + { result: { x: 2, y: 3 }, type: 'params' }, + 'success' + ), + id: undefined + }, { type: 'NOOP' } ]) done() @@ -433,7 +439,6 @@ describe('commandsDuck', () => { }) test('does the right thing for history command with comments', done => { // Given - const comment = '//COMMENT FOR HISTORY' const cmdString = 'history' const cmd = `:${cmdString}` const id = 1 diff --git a/src/shared/modules/commands/commandsDuck.ts b/src/shared/modules/commands/commandsDuck.ts index 6887e3bc40c..2144128a9d6 100644 --- a/src/shared/modules/commands/commandsDuck.ts +++ b/src/shared/modules/commands/commandsDuck.ts @@ -200,12 +200,13 @@ export const handleCommandEpic = (action$: any, store: any) => // Single command return store.dispatch(executeSingleCommand(action.cmd, action)) } - const parentId = action.parentId || v4() + const parentId = (action.isRerun ? action.id : action.parentId) || v4() store.dispatch( addFrame({ type: 'cypher-script', id: parentId, - cmd: action.cmd + cmd: action.cmd, + isRerun: action.isRerun } as any) ) const jobs = statements.map((cmd: any) => { @@ -233,7 +234,9 @@ export const handleCommandEpic = (action$: any, store: any) => store.dispatch(updateQueryResult(requestId, null, 'waiting')) return { workFn: () => interpreted.exec(action, store.dispatch, store), - onStart: () => {}, + onStart: () => { + /* no op */ + }, onSkip: () => store.dispatch(updateQueryResult(requestId, null, 'skipped')) } diff --git a/src/shared/modules/commands/helpers/server.ts b/src/shared/modules/commands/helpers/server.ts index edc782ea227..93d48e81da9 100644 --- a/src/shared/modules/commands/helpers/server.ts +++ b/src/shared/modules/commands/helpers/server.ts @@ -38,7 +38,7 @@ export function handleServerCommand(action: any, put: any, store: any) { return handleUserCommand(action, props) } if (serverCmd === 'change-password') { - return handleChangePasswordCommand(action, props) + return handleChangePasswordCommand(action) } if (serverCmd === 'status') { return handleServerStatusCommand(action) @@ -70,7 +70,7 @@ function handleUserCommand(action: any, props: any) { } } -function handleChangePasswordCommand(action: any, _props: any) { +function handleChangePasswordCommand(action: any) { return { ...action, type: 'change-password' } } diff --git a/src/shared/modules/commands/multiCommands.test.ts b/src/shared/modules/commands/multiCommands.test.ts index d2fdd0090d6..7b9682a08c0 100644 --- a/src/shared/modules/commands/multiCommands.test.ts +++ b/src/shared/modules/commands/multiCommands.test.ts @@ -109,6 +109,7 @@ describe('handleCommandEpic', () => { addFrame({ type: 'cypher-script', id: parentId, + isRerun: false, cmd: action.cmd } as any) ) diff --git a/src/shared/modules/connections/connectionsDuck.ts b/src/shared/modules/connections/connectionsDuck.ts index 35d6044840f..6a742a5f00e 100644 --- a/src/shared/modules/connections/connectionsDuck.ts +++ b/src/shared/modules/connections/connectionsDuck.ts @@ -20,8 +20,8 @@ import Rx from 'rxjs/Rx' import bolt from 'services/bolt/bolt' -import { NO_AUTH } from 'services/bolt/boltHelpers' import * as discovery from 'shared/modules/discovery/discoveryDuck' +import { NATIVE, NO_AUTH } from 'services/bolt/boltHelpers' import { fetchMetaData, CLEAR as CLEAR_META @@ -79,6 +79,11 @@ interface ConnectionsState { useDb: null | string } +/* +Architectural note. Browser was originally meant to keep multiple connections +at the same time. However that's not been put into practise. Therefore +this duck is more complicated than it needs to be. +*/ const initialState: ConnectionsState = { allConnectionIds: [], connectionsById: {}, @@ -87,11 +92,25 @@ const initialState: ConnectionsState = { lastUpdate: 0, useDb: null } +export interface Connection { + id: string + name: string + db: string + host: string + username: string + password: string + authenticationMethod: AuthenticationMethod + authEnabled: boolean + requestedUseDb?: string + restApi?: string +} + +export type AuthenticationMethod = typeof NATIVE | typeof NO_AUTH /** * Selectors */ -export function getConnection(state: any, id: any) { +export function getConnection(state: any, id: string): Connection | null { return ( getConnections(state).find( connection => connection && connection.id === id @@ -119,11 +138,11 @@ export function isConnected(state: any) { return getConnectionState(state) === CONNECTED_STATE } -export function getActiveConnection(state: any) { +export function getActiveConnection(state: any): string { return state[NAME].activeConnection || initialState.activeConnection } -export function getActiveConnectionData(state: any) { +export function getActiveConnectionData(state: any): Connection | null { if (!state[NAME].activeConnection) return null return getConnectionData(state, state[NAME].activeConnection) } @@ -648,6 +667,10 @@ export const retainCredentialsSettingsEpic = (action$: any, store: any) => { .ofType(UPDATE_RETAIN_CREDENTIALS) .do((action: any) => { const connection = getActiveConnectionData(store.getState()) + if (!connection) { + return + } + if ( !action.shouldRetain && (connection.username || connection.password) diff --git a/src/shared/modules/dbMeta/dbMetaDuck.ts b/src/shared/modules/dbMeta/dbMetaDuck.ts index 11bba6193b7..69e0e28e225 100644 --- a/src/shared/modules/dbMeta/dbMetaDuck.ts +++ b/src/shared/modules/dbMeta/dbMetaDuck.ts @@ -452,14 +452,14 @@ const switchToRequestedDb = (store: any) => { const activeConnection = getActiveConnectionData(store.getState()) const requestedUseDb = activeConnection?.requestedUseDb - const useDefaultDb = () => { + const switchToDefaultDb = () => { const defaultDb = databases.find((db: any) => db.default) if (defaultDb) { store.dispatch(useDb(defaultDb.name)) } } - if (requestedUseDb) { + if (activeConnection && requestedUseDb) { const wantedDb = databases.find( ({ name }: any) => name.toLowerCase() === requestedUseDb.toLowerCase() ) @@ -478,10 +478,10 @@ const switchToRequestedDb = (store: any) => { store.dispatch(executeCommand(`:use ${requestedUseDb}`), { source: commandSources.auto }) - useDefaultDb() + switchToDefaultDb() } } else { - useDefaultDb() + switchToDefaultDb() } return Rx.Observable.of(null) } diff --git a/src/shared/modules/experimentalFeatures/experimentalFeaturesDuck.ts b/src/shared/modules/experimentalFeatures/experimentalFeaturesDuck.ts index 28ef59fc17e..3d09300c3b3 100644 --- a/src/shared/modules/experimentalFeatures/experimentalFeaturesDuck.ts +++ b/src/shared/modules/experimentalFeatures/experimentalFeaturesDuck.ts @@ -9,6 +9,7 @@ export const showFeature = (state: any, name: any) => !!(state[NAME][name] || {}).on export const experimentalFeatureSelfName = 'showSelf' +export const reusableFrame = 'reusableFrame' export const initialState = { [experimentalFeatureSelfName]: { @@ -16,6 +17,12 @@ export const initialState = { on: true, displayName: 'Show experimental features', tooltip: 'Show feature section in settings drawer' + }, + [reusableFrame]: { + name: reusableFrame, + on: true, + displayName: 'Enable reuseable frame', + tooltip: 'Edit and rerun right in a frame' } } diff --git a/src/shared/modules/history/historyDuck.ts b/src/shared/modules/history/historyDuck.ts index 734db566b24..679b30c4bc2 100644 --- a/src/shared/modules/history/historyDuck.ts +++ b/src/shared/modules/history/historyDuck.ts @@ -25,10 +25,10 @@ export const NAME = 'history' export const ADD = 'history/ADD' export const CLEAR = 'history/CLEAR' -export const getHistory = (state: GlobalState): HistoryState => state[NAME] +export const getHistory = (state: GlobalState): string[] => state[NAME] function addHistoryHelper( - state: HistoryState, + state: string[], newState: string, maxHistory: number ) { @@ -41,9 +41,7 @@ function addHistoryHelper( return newHistory.slice(0, maxHistory) } -export type HistoryState = string[] - -export default function(state: HistoryState = [], action: any) { +export default function(state: string[] = [], action: any) { switch (action.type) { case ADD: return addHistoryHelper(state, action.state, action.maxHistory) diff --git a/src/shared/modules/requests/requestsDuck.ts b/src/shared/modules/requests/requestsDuck.ts index a9219f5f74a..7496f586101 100644 --- a/src/shared/modules/requests/requestsDuck.ts +++ b/src/shared/modules/requests/requestsDuck.ts @@ -23,10 +23,10 @@ import bolt from 'services/bolt/bolt' import { APP_START } from 'shared/modules/app/appDuck' export const NAME = 'requests' -export const REQUEST_SENT = `${NAME}/SENT` -export const CANCEL_REQUEST = `${NAME}/CANCEL` -export const REQUEST_CANCELED = `${NAME}/CANCELED` -export const REQUEST_UPDATED = `${NAME}/UPDATED` +export const REQUEST_SENT = 'requests/SENT' +export const CANCEL_REQUEST = 'requests/CANCEL' +export const REQUEST_CANCELED = 'requests/CANCELED' +export const REQUEST_UPDATED = 'requests/UPDATED' export const REQUEST_STATUS_PENDING = 'pending' export const REQUEST_STATUS_SUCCESS = 'success' @@ -34,14 +34,38 @@ export const REQUEST_STATUS_ERROR = 'error' export const REQUEST_STATUS_CANCELING = 'canceling' export const REQUEST_STATUS_CANCELED = 'canceled' -const initialState = {} +type RequestStatus = 'pending' | 'success' | 'error' | 'canceling' | 'canceled' -export const getRequest = (state: any, id: any) => state[NAME][id] -export const getRequests = (state: any) => state[NAME] -export const isCancelStatus = (status: any) => +export type RequestState = Record +type GlobalState = { [NAME]: RequestState } +const initialState: RequestState = {} + +export const getRequest = (state: GlobalState, id: string): Request => + state[NAME][id] +export const getRequests = (state: GlobalState): RequestState => state[NAME] +export const isCancelStatus = (status: RequestStatus): boolean => [REQUEST_STATUS_CANCELED, REQUEST_STATUS_CANCELING].includes(status) -export default function reducer(state: any = initialState, action: any) { +export type Status = + | 'ignored' + | 'skipped' + | 'pending' + | 'success' + | 'waiting' + | 'error' + +export type Request = { + result?: any + status: Status + type: any + id: string + updated?: number +} + +export default function reducer( + state: RequestState = initialState, + action: any +) { switch (action.type) { case APP_START: return { ...initialState, ...state } @@ -73,46 +97,39 @@ export default function reducer(state: any = initialState, action: any) { } } -export const send = (requestType: any, id: any) => { - return { - type: REQUEST_SENT, - requestType, - id - } -} +export const send = (requestType: any, id: string) => ({ + type: REQUEST_SENT, + requestType, + id +}) -export const update = (id: any, result: any, status: any) => { - return { - type: REQUEST_UPDATED, - id, - result, - status - } -} +export const update = (id: string, result: any, status: Status) => ({ + type: REQUEST_UPDATED, + id, + result, + status +}) -export const cancel = (id: any) => { - return { - type: CANCEL_REQUEST, - status: REQUEST_STATUS_CANCELING, - id - } -} +export const cancel = (id: string) => ({ + type: CANCEL_REQUEST, + status: REQUEST_STATUS_CANCELING, + id +}) -const canceled = (id: any) => { - return { - type: REQUEST_CANCELED, - status: REQUEST_STATUS_CANCELED, - result: null, - id - } -} +const canceled = (id: string) => ({ + type: REQUEST_CANCELED, + status: REQUEST_STATUS_CANCELED, + result: null, + id +}) // Epics export const cancelRequestEpic = (action$: any) => - action$.ofType(CANCEL_REQUEST).mergeMap((action: any) => { - return new Promise(resolve => { - bolt.cancelTransaction(action.id, () => { - resolve(canceled(action.id)) + action$.ofType(CANCEL_REQUEST).mergeMap( + (action: { id: string }) => + new Promise(resolve => { + bolt.cancelTransaction(action.id, () => { + resolve(canceled(action.id)) + }) }) - }) - }) + ) diff --git a/src/shared/modules/stream/streamDuck.test.ts b/src/shared/modules/stream/streamDuck.test.ts index bdc340ed8ca..77d728b49b5 100644 --- a/src/shared/modules/stream/streamDuck.test.ts +++ b/src/shared/modules/stream/streamDuck.test.ts @@ -114,6 +114,9 @@ describe('streamDuck', () => { "test-id": Object { "stack": Array [ Object { + "history": Array [ + undefined, + ], "id": "test-id", "type": "after", }, diff --git a/src/shared/modules/stream/streamDuck.ts b/src/shared/modules/stream/streamDuck.ts index 44bd8355bc3..f51ff8d0cb0 100644 --- a/src/shared/modules/stream/streamDuck.ts +++ b/src/shared/modules/stream/streamDuck.ts @@ -40,8 +40,8 @@ export const SET_MAX_FRAMES = 'frames/SET_MAX_FRAMES' export interface GlobalState { [NAME]: FramesState - history: string[] [key: string]: Record + history: string[] } export function getFrame(state: GlobalState, id: string): FrameStack { @@ -61,15 +61,19 @@ export function getRecentView(state: GlobalState): null | FrameView { */ function addFrame(state: FramesState, newState: Frame) { if (newState.parentId && state.allIds.indexOf(newState.parentId) < 0) { - // No parent + // Can't find parent return state } const frameObject = state.byId[newState.id] || { stack: [], isPinned: false } - if (!newState.isRerun) { - frameObject.stack.unshift(newState) + const newFrame = { + ...newState, + history: [newState.cmd, ...(frameObject.stack[0]?.history || [])] + } + if (newState.isRerun) { + frameObject.stack = [newFrame] } else { - frameObject.stack = [newState] + frameObject.stack.unshift(newFrame) } let byId = { ...state.byId, @@ -80,7 +84,7 @@ function addFrame(state: FramesState, newState: Frame) { if (newState.parentId) { const currentStatements = byId[newState.parentId].stack[0].statements || [] // Need to add this id to parent's list of statements - if (!currentStatements.includes(newState.id as any)) { + if (!currentStatements.includes(newState.id)) { byId = { ...byId, [newState.parentId]: { @@ -88,7 +92,7 @@ function addFrame(state: FramesState, newState: Frame) { stack: [ { ...byId[newState.parentId].stack[0], - statements: currentStatements.concat(newState.id as any) + statements: currentStatements.concat(newState.id) } ] } @@ -109,10 +113,10 @@ function insertIntoAllIds( allIds: string[], newState: Frame ) { - if (allIds.indexOf(newState.id as any) < 0) { + if (allIds.indexOf(newState.id) < 0) { // new frame const pos = findFirstFreePos(state) - allIds.splice(pos, 0, newState.id as any) + allIds.splice(pos, 0, newState.id) } return allIds } @@ -227,9 +231,10 @@ export interface Frame { ts: number type: string useDb: string | null + history?: string[] } -interface FrameStack { +export interface FrameStack { stack: Frame[] isPinned: boolean } diff --git a/src/shared/modules/udc/udcDuck.ts b/src/shared/modules/udc/udcDuck.ts index bb2205597f0..416b6ef2857 100644 --- a/src/shared/modules/udc/udcDuck.ts +++ b/src/shared/modules/udc/udcDuck.ts @@ -19,7 +19,7 @@ */ import { v4 } from 'uuid' -import { APP_START, USER_CLEAR } from '../app/appDuck' +import { USER_CLEAR } from '../app/appDuck' import { AUTHORIZED, CLEAR_SYNC, @@ -45,9 +45,7 @@ import { } from 'shared/modules/favorites/favoritesDuck' import { shouldReportUdc, - getSettings, - REPLACE, - UPDATE + getSettings } from 'shared/modules/settings/settingsDuck' import { CONNECTION_SUCCESS } from 'shared/modules/connections/connectionsDuck' import { shouldTriggerConnectEvent, getTodayDate } from './udcHelpers' diff --git a/src/shared/services/commandInterpreterHelper.ts b/src/shared/services/commandInterpreterHelper.ts index c629014f65a..31e2eda17d3 100644 --- a/src/shared/services/commandInterpreterHelper.ts +++ b/src/shared/services/commandInterpreterHelper.ts @@ -218,23 +218,28 @@ const availableCommands = [ (db: any) => db.name.toLowerCase() === cleanDbName ) - function UseDbError({ code, message }: any) { - // @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message - this.code = code - // @ts-expect-error ts-migrate(2683) FIXME: 'this' implicitly has type 'any' because it does n... Remove this comment to see the full error message - this.message = message + class UseDbError extends Error { + code: string + message: string + + constructor( + { code, message }: { code: string; message: string }, + ...params: any[] + ) { + super(...params) + this.code = code + this.message = message + } } // Do we have a db with that name? if (!dbMeta) { - // @ts-expect-error ts-migrate(7009) FIXME: 'new' expression, whose target lacks a construct s... Remove this comment to see the full error message throw new UseDbError({ code: 'NotFound', message: `A database with the "${dbName}" name could not be found.` }) } if (dbMeta.status !== 'online') { - // @ts-expect-error ts-migrate(7009) FIXME: 'new' expression, whose target lacks a construct s... Remove this comment to see the full error message throw new UseDbError({ code: 'DatabaseUnavailable', message: `Database "${dbName}" is unavailable, its status is "${dbMeta.status}."` @@ -463,28 +468,29 @@ const availableCommands = [ match: (cmd: any) => /^server(\s)/.test(cmd), exec: (action: any, put: any, store: any) => { const response = handleServerCommand(action, put, store) - if (response && response.then) { - response.then((res: any) => { - if (res) { - put( - frames.add({ - useDb: getUseDb(store.getState()), - ...action, - ...res - }) - ) - } - }) - } else if (response) { - put( - frames.add({ - useDb: getUseDb(store.getState()), - ...action, - ...response + if (response) { + if (response.then) { + response.then((res: any) => { + if (res) { + put( + frames.add({ + useDb: getUseDb(store.getState()), + ...action, + ...res + }) + ) + } }) - ) + } else + put( + frames.add({ + useDb: getUseDb(store.getState()), + ...action, + ...response + }) + ) + return response } - return response } }, { @@ -495,10 +501,13 @@ const availableCommands = [ // We have a frame that generated this command if (action.id) { const originFrame = frames.getFrame(store.getState(), action.id) - // Only replace when the origin is a help frame + // Only replace when the origin is a play frame or the frame is reused if (originFrame) { const latest = getLatestFromFrameStack(originFrame) - if (latest && PLAY_FRAME_TYPES.includes(latest.type)) { + if ( + (latest && PLAY_FRAME_TYPES.includes(latest.type)) || + action.isRerun + ) { id = action.id } } else { @@ -554,10 +563,13 @@ const availableCommands = [ // We have a frame that generated this command if (action.id) { const originFrame = frames.getFrame(store.getState(), action.id) - // Only replace when the origin is a help frame + // Only replace when the origin is a play frame or the frame is reused if (originFrame) { const latest = getLatestFromFrameStack(originFrame) - if (latest && PLAY_FRAME_TYPES.includes(latest.type)) { + if ( + (latest && PLAY_FRAME_TYPES.includes(latest.type)) || + action.isRerun + ) { id = action.id } } else { @@ -632,10 +644,10 @@ const availableCommands = [ // We have a frame that generated this command if (action.id) { const originFrame = frames.getFrame(store.getState(), action.id) - // Only replace when the origin is a help frame + // Only replace when the origin is a help frame or re-run command if (originFrame) { const latest = getLatestFromFrameStack(originFrame) - if (latest && latest.type === HELP_FRAME_TYPE) { + if ((latest && latest.type === HELP_FRAME_TYPE) || action.isRerun) { id = action.id } } else { @@ -643,6 +655,7 @@ const availableCommands = [ id = v4() } } + put( frames.add({ useDb: getUseDb(store.getState()), @@ -664,6 +677,9 @@ const availableCommands = [ hostnameOnly: false }) const connectionData = getActiveConnectionData(store.getState()) + if (!connectionData) { + throw new Error('No connection') + } const isSameHostnameAsConnection = isLocalRequest( connectionData.host, r.url, @@ -778,7 +794,7 @@ const availableCommands = [ const url = param.startsWith('http') ? param : `http://${param}` const allowlist = getRemoteContentHostnameAllowlist(store.getState()) - fetchRemoteGrass(param, allowlist) + fetchRemoteGrass(url, allowlist) .then(response => { const parsedGrass = parseGrass(response) if (parsedGrass) { diff --git a/yarn.lock b/yarn.lock index 8ad1ec0f68d..2cfaee2f584 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,7 +917,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.11.2" resolved "https://neo.jfrog.io/neo/api/npm/npm/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha1-9UnBPHVMxAuHZEufqfCaapX+BzY= @@ -10965,14 +10965,6 @@ react-refresh@^0.8.3: resolved "https://neo.jfrog.io/neo/api/npm/npm/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha1-ch1GV2ctQAxePHXQY8SoX7LV1o8= -react-spring@^8.0.27: - version "8.0.27" - resolved "https://neo.jfrog.io/neo/api/npm/npm/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a" - integrity sha1-l9Te5nf0Hgsq3LaW84OWgKOqNWo= - dependencies: - "@babel/runtime" "^7.3.1" - prop-types "^15.5.8" - react-suber@1.0.4: version "1.0.4" resolved "https://neo.jfrog.io/neo/api/npm/npm/react-suber/-/react-suber-1.0.4.tgz#99e86d0d13304584ba142025d38688124a92ea7c"
Constraints
ON ( book:Book ) ASSERT book.isbn IS UNIQUE