From 7e3f0acd97eb366992261c117b14616306bbd662 Mon Sep 17 00:00:00 2001 From: Timmy Luong Date: Tue, 17 Dec 2019 16:16:52 -0800 Subject: [PATCH] fix(ui): retain user input when parsing invalid JSON during import --- CHANGELOG.md | 2 +- ui/cypress/e2e/dashboardsIndex.test.ts | 38 ++++++++++++++----- ui/cypress/e2e/tasks.test.ts | 25 ++++++++++++ ui/cypress/e2e/templates.test.ts | 32 ++++++++++++++++ ui/cypress/e2e/variables.test.ts | 25 ++++++++++++ ui/package.json | 1 + .../components/DashboardImportOverlay.tsx | 25 +++++++++++- ui/src/shared/components/ImportOverlay.tsx | 17 ++++++++- ui/src/tasks/components/TaskImportOverlay.tsx | 23 ++++++++++- .../components/TemplateImportOverlay.tsx | 21 +++++++++- .../components/VariableImportOverlay.tsx | 24 +++++++++++- ui/yarn.lock | 19 ++++++++++ 12 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 ui/cypress/e2e/templates.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6596f17f3..5881dc4ae81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ ### Bug Fixes 1. [16225](https://github.com/influxdata/influxdb/pull/16225): Ensures env vars are applied consistently across cmd, and fixes issue where INFLUX\_ env var prefix was not set globally. - 1. [16235](https://github.com/influxdata/influxdb/pull/16235): Removed default frontend sorting when flux queries specify sorting 1. [16238](https://github.com/influxdata/influxdb/pull/16238): Store canceled task runs in the correct bucket 1. [16237](https://github.com/influxdata/influxdb/pull/16237): Updated Sortby functionality for table frontend sorts to sort numbers correctly +1. [16255](https://github.com/influxdata/influxdb/pull/16255): Retain user input when parsing invalid JSON during import ### UI Improvements diff --git a/ui/cypress/e2e/dashboardsIndex.test.ts b/ui/cypress/e2e/dashboardsIndex.test.ts index e2b58e2d6f1..57818ced7d4 100644 --- a/ui/cypress/e2e/dashboardsIndex.test.ts +++ b/ui/cypress/e2e/dashboardsIndex.test.ts @@ -74,21 +74,41 @@ describe('Dashboards', () => { cy.createDashboardTemplate(id) }) - cy.getByTestID('empty-dashboards-list') - .getByTestID('add-resource-dropdown--button') - .click() - - cy.getByTestID('add-resource-dropdown--template').click() - + cy.getByTestID('empty-dashboards-list').within(() => { + cy.getByTestID('add-resource-dropdown--button').click() + cy.getByTestID('add-resource-dropdown--template').click() + }) cy.getByTestID('template--Bashboard-Template').click() - cy.getByTestID('template-panel').should('exist') - cy.getByTestID('create-dashboard-button').click() - cy.getByTestID('dashboard-card').should('have.length', 1) }) + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.getByTestID('page-header').within(() => { + cy.contains('Create').click() + }) + + cy.getByTestID('add-resource-dropdown--import').click() + cy.contains('Paste').click() + cy.getByTestID('import-overlay--textarea') + .click() + .type('this is invalid JSON') + cy.get('button[title*="Import JSON"]').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid JSON') + ) + cy.getByTestID('import-overlay--textarea').type( + '{backspace}{backspace}{backspace}{backspace}{backspace}' + ) + cy.get('button[title*="Import JSON"]').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid') + ) + }) + describe('Dashboard List', () => { beforeEach(() => { cy.get('@org').then(({id}: Organization) => { diff --git a/ui/cypress/e2e/tasks.test.ts b/ui/cypress/e2e/tasks.test.ts index feca938a455..b57d0682de2 100644 --- a/ui/cypress/e2e/tasks.test.ts +++ b/ui/cypress/e2e/tasks.test.ts @@ -77,6 +77,31 @@ http.post( .and('contain', taskName) }) + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.getByTestID('page-header').within(() => { + cy.contains('Create').click() + }) + + cy.getByTestID('add-resource-dropdown--import').click() + cy.contains('Paste').click() + cy.getByTestID('import-overlay--textarea') + .click() + .type('this is invalid JSON') + cy.get('button[title*="Import JSON"]').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid JSON') + ) + cy.getByTestID('import-overlay--textarea').type( + '{backspace}{backspace}{backspace}{backspace}{backspace}' + ) + cy.get('button[title*="Import JSON"]').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid') + ) + }) + describe('When tasks already exist', () => { beforeEach(() => { cy.get('@org').then(({id}: Organization) => { diff --git a/ui/cypress/e2e/templates.test.ts b/ui/cypress/e2e/templates.test.ts new file mode 100644 index 00000000000..9d904850e57 --- /dev/null +++ b/ui/cypress/e2e/templates.test.ts @@ -0,0 +1,32 @@ +describe('Templates', () => { + beforeEach(() => { + cy.flush() + + cy.signin().then(({body}) => { + cy.wrap(body.org).as('org') + cy.visit(`orgs/${body.org.id}/settings/templates`) + }) + }) + + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.get('button[title*="Import"]').click() + + cy.contains('Paste').click() + cy.getByTestID('import-overlay--textarea') + .click() + .type('this is invalid JSON') + cy.get('button[title*="Import JSON"').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid JSON') + ) + cy.getByTestID('import-overlay--textarea').type( + '{backspace}{backspace}{backspace}{backspace}{backspace}' + ) + cy.get('button[title*="Import JSON"').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid') + ) + }) +}) diff --git a/ui/cypress/e2e/variables.test.ts b/ui/cypress/e2e/variables.test.ts index 8b851597d71..9c8f7f172db 100644 --- a/ui/cypress/e2e/variables.test.ts +++ b/ui/cypress/e2e/variables.test.ts @@ -31,6 +31,31 @@ describe('Variables', () => { cy.getByTestID('resource-card').should('have.length', 1) }) + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.get('.tabbed-page-section--header').within(() => { + cy.contains('Create').click() + }) + + cy.getByTestID('add-resource-dropdown--import').click() + cy.contains('Paste').click() + cy.getByTestID('import-overlay--textarea') + .click() + .type('this is invalid JSON') + cy.get('button[title*="Import JSON"]').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid JSON') + ) + cy.getByTestID('import-overlay--textarea').type( + '{backspace}{backspace}{backspace}{backspace}{backspace}' + ) + cy.get('button[title*="Import JSON"]').click() + cy.getByTestID('import-overlay--textarea--error').should('have.length', 1) + cy.getByTestID('import-overlay--textarea').should($s => + expect($s).to.contain('this is invalid') + ) + }) + it.skip('can delete a variable', () => { cy.get('@org').then(({id}) => { cy.createVariable(id) diff --git a/ui/package.json b/ui/package.json index 78c26444c83..32231942eff 100644 --- a/ui/package.json +++ b/ui/package.json @@ -148,6 +148,7 @@ "honeybadger-js": "^1.0.2", "immer": "^1.9.3", "intersection-observer": "^0.7.0", + "jsonlint-mod": "^1.7.5", "lodash": "^4.3.0", "memoize-one": "^4.0.2", "moment": "^2.13.0", diff --git a/ui/src/dashboards/components/DashboardImportOverlay.tsx b/ui/src/dashboards/components/DashboardImportOverlay.tsx index 318badeba80..f22e146bb8e 100644 --- a/ui/src/dashboards/components/DashboardImportOverlay.tsx +++ b/ui/src/dashboards/components/DashboardImportOverlay.tsx @@ -4,6 +4,9 @@ import {withRouter, WithRouterProps} from 'react-router' import _ from 'lodash' import {connect} from 'react-redux' +// Components +import ImportOverlay from 'src/shared/components/ImportOverlay' + // Copy import {invalidJSON} from 'src/shared/copy/notifications' @@ -15,7 +18,14 @@ import { import {notify as notifyAction} from 'src/shared/actions/notifications' // Types -import ImportOverlay from 'src/shared/components/ImportOverlay' +import {ComponentStatus} from '@influxdata/clockface' + +// Utils +import jsonlint from 'jsonlint-mod' + +interface State { + status: ComponentStatus +} interface DispatchProps { createDashboardFromTemplate: typeof createDashboardFromTemplateAction @@ -30,6 +40,10 @@ interface OwnProps extends WithRouterProps { type Props = OwnProps & DispatchProps class DashboardImportOverlay extends PureComponent { + public state: State = { + status: ComponentStatus.Default, + } + public render() { return ( { onDismissOverlay={this.onDismiss} resourceName="Dashboard" onSubmit={this.handleImportDashboard} + status={this.state.status} + updateStatus={this.updateOverlayStatus} /> ) } + private updateOverlayStatus = (status: ComponentStatus) => + this.setState(() => ({status})) + private handleImportDashboard = (uploadContent: string) => { const {createDashboardFromTemplate, notify, populateDashboards} = this.props let template + this.updateOverlayStatus(ComponentStatus.Default) try { - template = JSON.parse(uploadContent) + template = jsonlint.parse(uploadContent) } catch (error) { + this.updateOverlayStatus(ComponentStatus.Error) notify(invalidJSON(error.message)) return } diff --git a/ui/src/shared/components/ImportOverlay.tsx b/ui/src/shared/components/ImportOverlay.tsx index 63779680558..6a3fe912447 100644 --- a/ui/src/shared/components/ImportOverlay.tsx +++ b/ui/src/shared/components/ImportOverlay.tsx @@ -29,6 +29,8 @@ interface OwnProps { resourceName: string onSubmit: (importString: string, orgID: string) => void isVisible?: boolean + status?: ComponentStatus + updateStatus?: (status: ComponentStatus) => void } interface State { @@ -96,6 +98,7 @@ class ImportOverlay extends PureComponent { private get importBody(): JSX.Element { const {selectedImportOption, importContent} = this.state + const {status = ComponentStatus.Default} = this.props if (selectedImportOption === ImportOption.Upload) { return ( @@ -110,7 +113,12 @@ class ImportOverlay extends PureComponent { } if (selectedImportOption === ImportOption.Paste) { return ( -