From a5676f692496d63cde6f8726df1b7a1eda6ea509 Mon Sep 17 00:00:00 2001 From: Bucky Schwarz Date: Wed, 26 Aug 2020 10:04:49 -0700 Subject: [PATCH 1/6] revert: refactor(templates): Remove legacy templates (#19300) --- ui/cypress/e2e/dashboardsIndex.test.ts | 76 ++- ui/cypress/e2e/tasks.test.ts | 43 +- ui/cypress/e2e/variables.test.ts | 57 ++- ui/src/dashboards/actions/thunks.ts | 104 +++++ .../components/DashboardExportOverlay.tsx | 78 ++++ .../components/DashboardImportOverlay.tsx | 90 ++++ .../CreateFromTemplateOverlay.scss | 91 ++++ .../dashboard_index/DashboardCard.tsx | 19 + .../dashboard_index/DashboardsIndex.tsx | 45 +- .../dashboard_index/DashboardsTableEmpty.tsx | 11 +- .../components/dashboard_index/Table.tsx | 27 +- ui/src/resources/components/GetResources.tsx | 6 + .../components/SettingsNavigation.tsx | 9 +- ui/src/shared/actions/app.ts | 1 + .../shared/components/AddResourceButton.tsx | 70 --- .../shared/components/AddResourceDropdown.tsx | 174 +++++++ ui/src/shared/components/ExportOverlay.scss | 3 + ui/src/shared/components/ExportOverlay.tsx | 165 +++++++ ui/src/shared/components/ImportOverlay.scss | 12 + ui/src/shared/components/ImportOverlay.tsx | 192 ++++++++ ui/src/shared/components/ViewOverlay.tsx | 108 +++++ .../generateSortItems.ts | 30 ++ ui/src/shared/containers/SetOrg.tsx | 14 +- ui/src/shared/copy/notifications.ts | 92 +++- .../shared/utils/resourceToTemplate.test.ts | 438 ++++++++++++++++++ ui/src/shared/utils/resourceToTemplate.ts | 350 ++++++++++++++ ui/src/style/chronograf.scss | 3 + ui/src/tasks/actions/thunks.ts | 50 ++ ui/src/tasks/components/EmptyTasksList.tsx | 20 +- ui/src/tasks/components/TaskCard.tsx | 10 + ui/src/tasks/components/TaskExportOverlay.tsx | 63 +++ .../TaskImportFromTemplateOverlay.tsx | 162 +++++++ ui/src/tasks/components/TaskImportOverlay.tsx | 79 ++++ ui/src/tasks/components/TasksHeader.tsx | 11 +- ui/src/tasks/components/TasksList.tsx | 12 +- .../tasks/components/TemplateBrowserEmpty.tsx | 60 +++ ui/src/tasks/containers/TasksPage.tsx | 46 ++ ui/src/templates/actions/creators.ts | 76 ++- ui/src/templates/actions/thunks.ts | 269 ++++++++++- ui/src/templates/api/index.ts | 114 +++++ .../CommunityTemplateImportOverlay.tsx | 3 +- .../components/EmptyTemplatesList.tsx | 47 ++ .../components/StaticTemplateCard.tsx | 114 +++++ .../components/StaticTemplateViewOverlay.tsx | 55 +++ .../components/StaticTemplatesList.tsx | 81 ++++ ui/src/templates/components/TemplateCard.tsx | 206 ++++++++ .../components/TemplateExportOverlay.tsx | 70 +++ .../components/TemplateImportOverlay.tsx | 91 ++++ .../components/TemplateViewOverlay.tsx | 81 ++++ ui/src/templates/components/TemplatesList.tsx | 76 +++ ui/src/templates/components/TemplatesPage.tsx | 219 +++++++++ .../CreateFromTemplateOverlay.scss | 91 ++++ .../CreateFromTemplateOverlay.tsx | 210 +++++++++ .../TemplateBrowser.tsx | 48 ++ .../TemplateBrowserDetails.tsx | 146 ++++++ .../TemplateBrowserEmpty.tsx | 60 +++ .../TemplateBrowserList.tsx | 43 ++ .../TemplateBrowserListItem.tsx | 49 ++ .../containers/CommunityTemplatesIndex.tsx | 11 +- .../templates/containers/TemplatesIndex.tsx | 88 ++++ ui/src/templates/reducers/index.test.ts | 131 ++++++ ui/src/templates/reducers/index.ts | 66 ++- ui/src/templates/utils/index.ts | 3 + ui/src/variables/actions/thunks.ts | 69 +++ .../components/VariableExportOverlay.tsx | 63 +++ .../components/VariableImportOverlay.tsx | 85 ++++ ui/src/variables/components/VariablesTab.tsx | 18 +- .../variables/containers/VariablesIndex.tsx | 10 + .../variables/utils/exportVariables.test.ts | 65 +++ ui/src/variables/utils/exportVariables.ts | 46 ++ 70 files changed, 5497 insertions(+), 128 deletions(-) create mode 100644 ui/src/dashboards/components/DashboardExportOverlay.tsx create mode 100644 ui/src/dashboards/components/DashboardImportOverlay.tsx create mode 100644 ui/src/dashboards/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss delete mode 100644 ui/src/shared/components/AddResourceButton.tsx create mode 100644 ui/src/shared/components/AddResourceDropdown.tsx create mode 100644 ui/src/shared/components/ExportOverlay.scss create mode 100644 ui/src/shared/components/ExportOverlay.tsx create mode 100644 ui/src/shared/components/ImportOverlay.scss create mode 100644 ui/src/shared/components/ImportOverlay.tsx create mode 100644 ui/src/shared/components/ViewOverlay.tsx create mode 100644 ui/src/shared/utils/resourceToTemplate.test.ts create mode 100644 ui/src/shared/utils/resourceToTemplate.ts create mode 100644 ui/src/tasks/components/TaskExportOverlay.tsx create mode 100644 ui/src/tasks/components/TaskImportFromTemplateOverlay.tsx create mode 100644 ui/src/tasks/components/TaskImportOverlay.tsx create mode 100644 ui/src/tasks/components/TemplateBrowserEmpty.tsx create mode 100644 ui/src/templates/components/EmptyTemplatesList.tsx create mode 100644 ui/src/templates/components/StaticTemplateCard.tsx create mode 100644 ui/src/templates/components/StaticTemplateViewOverlay.tsx create mode 100644 ui/src/templates/components/StaticTemplatesList.tsx create mode 100644 ui/src/templates/components/TemplateCard.tsx create mode 100644 ui/src/templates/components/TemplateExportOverlay.tsx create mode 100644 ui/src/templates/components/TemplateImportOverlay.tsx create mode 100644 ui/src/templates/components/TemplateViewOverlay.tsx create mode 100644 ui/src/templates/components/TemplatesList.tsx create mode 100644 ui/src/templates/components/TemplatesPage.tsx create mode 100644 ui/src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss create mode 100644 ui/src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.tsx create mode 100644 ui/src/templates/components/createFromTemplateOverlay/TemplateBrowser.tsx create mode 100644 ui/src/templates/components/createFromTemplateOverlay/TemplateBrowserDetails.tsx create mode 100644 ui/src/templates/components/createFromTemplateOverlay/TemplateBrowserEmpty.tsx create mode 100644 ui/src/templates/components/createFromTemplateOverlay/TemplateBrowserList.tsx create mode 100644 ui/src/templates/components/createFromTemplateOverlay/TemplateBrowserListItem.tsx create mode 100644 ui/src/templates/containers/TemplatesIndex.tsx create mode 100644 ui/src/templates/reducers/index.test.ts create mode 100644 ui/src/variables/components/VariableExportOverlay.tsx create mode 100644 ui/src/variables/components/VariableImportOverlay.tsx create mode 100644 ui/src/variables/utils/exportVariables.test.ts create mode 100644 ui/src/variables/utils/exportVariables.ts diff --git a/ui/cypress/e2e/dashboardsIndex.test.ts b/ui/cypress/e2e/dashboardsIndex.test.ts index 9b1a1ac2d7..a895d7cbcc 100644 --- a/ui/cypress/e2e/dashboardsIndex.test.ts +++ b/ui/cypress/e2e/dashboardsIndex.test.ts @@ -28,7 +28,7 @@ describe('Dashboards', () => { "Looks like you don't have any Dashboards, why not create one?" ) }) - cy.getByTestID('add-resource-button').should($b => { + cy.getByTestID('add-resource-dropdown--button').should($b => { expect($b).to.have.length(1) expect($b).to.contain('Create Dashboard') }) @@ -39,16 +39,18 @@ describe('Dashboards', () => { it('can CRUD dashboards from empty state, header, and a Template', () => { // Create from empty state cy.getByTestID('empty-dashboards-list').within(() => { - cy.getByTestID('add-resource-button') - .click() - .then(() => { - cy.fixture('routes').then(({orgs}) => { - cy.get('@org').then(({id}: Organization) => { - cy.visit(`${orgs}/${id}/dashboards-list`) - }) + cy.getByTestID('add-resource-dropdown--button').click() + }) + + cy.getByTestID('add-resource-dropdown--new') + .click() + .then(() => { + cy.fixture('routes').then(({orgs}) => { + cy.get('@org').then(({id}: Organization) => { + cy.visit(`${orgs}/${id}/dashboards-list`) }) }) - }) + }) const newName = 'new 🅱️ashboard' @@ -68,8 +70,14 @@ describe('Dashboards', () => { cy.getByTestID('dashboard-card').should('contain', newName) + // Open Export overlay + cy.getByTestID('context-menu-item-export').click({force: true}) + cy.getByTestID('export-overlay--text-area').should('exist') + cy.get('.cf-overlay--dismiss').click() + // Create from header - cy.getByTestID('add-resource-button').click() + cy.getByTestID('add-resource-dropdown--button').click() + cy.getByTestID('add-resource-dropdown--new').click() cy.fixture('routes').then(({orgs}) => { cy.get('@org').then(({id}: Organization) => { @@ -77,6 +85,21 @@ describe('Dashboards', () => { }) }) + // Create from Template + cy.get('@org').then(({id}: Organization) => { + cy.createDashboardTemplate(id) + }) + + 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', 3) + // Delete dashboards cy.getByTestID('dashboard-card') .first() @@ -94,9 +117,42 @@ describe('Dashboards', () => { cy.getByTestID('context-delete-dashboard').click() }) + cy.getByTestID('dashboard-card') + .first() + .trigger('mouseover') + .within(() => { + cy.getByTestID('context-delete-menu').click() + cy.getByTestID('context-delete-dashboard').click() + }) + cy.getByTestID('empty-dashboards-list').should('exist') }) + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.getByTestID('page-control-bar').within(() => { + cy.getByTestID('add-resource-dropdown--button').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 16545f0217..443b58e6cc 100644 --- a/ui/cypress/e2e/tasks.test.ts +++ b/ui/cypress/e2e/tasks.test.ts @@ -60,7 +60,19 @@ from(bucket: "${name}"{rightarrow} .should('have.length', 1) .and('contain', taskName) - cy.getByTestID('add-resource-button').click() + // TODO: extend to create from template overlay + cy.getByTestID('add-resource-dropdown--button').click() + cy.getByTestID('add-resource-dropdown--template').click() + cy.getByTestID('task-import-template--overlay').within(() => { + cy.get('.cf-overlay--dismiss').click() + }) + + // TODO: extend to create a template from JSON + cy.getByTestID('add-resource-dropdown--button').click() + cy.getByTestID('add-resource-dropdown--import').click() + cy.getByTestID('task-import--overlay').within(() => { + cy.get('.cf-overlay--dismiss').click() + }) }) // this test is broken due to a failure on the post route it.skip('can create a task using http.post', () => { @@ -80,6 +92,31 @@ http.post( .and('contain', taskName) }) + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.getByTestID('page-control-bar').within(() => { + cy.getByTestID('add-resource-dropdown--button').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) => { @@ -358,9 +395,11 @@ function createFirstTask( offset: string = '20m' ) { cy.getByTestID('empty-tasks-list').within(() => { - cy.getByTestID('add-resource-button').click() + cy.getByTestID('add-resource-dropdown--button').click() }) + cy.getByTestID('add-resource-dropdown--new').click() + cy.get('@bucket').then(bucket => { cy.getByTestID('flux-editor').within(() => { cy.get('textarea.inputarea') diff --git a/ui/cypress/e2e/variables.test.ts b/ui/cypress/e2e/variables.test.ts index fa06a4b21e..55b18ff46d 100644 --- a/ui/cypress/e2e/variables.test.ts +++ b/ui/cypress/e2e/variables.test.ts @@ -32,7 +32,9 @@ describe('Variables', () => { 'windowPeriod' ) - cy.getByTestID('add-resource-button').click() + cy.getByTestID('add-resource-dropdown--button').click() + + cy.getByTestID('add-resource-dropdown--new').click() cy.getByTestID('variable-type-dropdown--button').click() cy.getByTestID('variable-type-dropdown-constant').click() @@ -143,7 +145,9 @@ describe('Variables', () => { ) // Create a Map variable from scratch - cy.getByTestID('add-resource-button').click() + cy.getByTestID('add-resource-dropdown--button').click() + + cy.getByTestID('add-resource-dropdown--new').click() cy.getByTestID('variable-type-dropdown--button').click() cy.getByTestID('variable-type-dropdown-map').click() @@ -167,7 +171,9 @@ describe('Variables', () => { cy.getByTestID(`variable-card--name ${mapVariableName}`).should('exist') // Create a Query variable from scratch - cy.getByTestID('add-resource-button').click() + cy.getByTestID('add-resource-dropdown--button').click() + + cy.getByTestID('add-resource-dropdown--new').click() cy.getByTestID('variable-type-dropdown--button').click() cy.getByTestID('variable-type-dropdown-map').click() @@ -195,6 +201,45 @@ describe('Variables', () => { cy.getByTestID(`variable-card--name ${queryVariableName}`).contains( queryVariableName ) + + //create variable by uploader + cy.getByTestID('add-resource-dropdown--button').click() + + cy.getByTestID('add-resource-dropdown--import').click() + + const yourFixturePath = 'data-for-variable.json' + cy.get('.drag-and-drop').attachFile(yourFixturePath, { + subjectType: 'drag-n-drop', + }) + + cy.getByTestID('submit-button Variable').click() + + cy.getByTestID('resource-card variable') + .should('have.length', 4) + .contains('agent_host') + }) + + it('keeps user input in text area when attempting to import invalid JSON', () => { + cy.getByTestID('tabbed-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').contains('this is invalid') }) it('can create and delete a label and filter a variable by label name & sort by variable name', () => { @@ -209,7 +254,11 @@ describe('Variables', () => { cy.getByTestID('overlay--children').should('not.exist') - cy.getByTestID('add-resource-button').click() + cy.getByTestID('add-resource-dropdown--button').click() + + cy.getByTestID('add-resource-dropdown--new').should('have.length', 1) + + cy.getByTestID('add-resource-dropdown--new').click() cy.getByTestID('variable-type-dropdown--button').click() cy.getByTestID('variable-type-dropdown-constant').click() diff --git a/ui/src/dashboards/actions/thunks.ts b/ui/src/dashboards/actions/thunks.ts index 7651a9b253..01c950dd47 100644 --- a/ui/src/dashboards/actions/thunks.ts +++ b/ui/src/dashboards/actions/thunks.ts @@ -6,6 +6,7 @@ import {push} from 'connected-react-router' // APIs import * as dashAPI from 'src/dashboards/apis' import * as api from 'src/client' +import * as tempAPI from 'src/templates/api' import {createCellWithView} from 'src/cells/actions/thunks' // Schemas @@ -28,6 +29,7 @@ import { updateTimeRangeFromQueryParams, } from 'src/dashboards/actions/ranges' import {getVariables, hydrateVariables} from 'src/variables/actions/thunks' +import {setExportTemplate} from 'src/templates/actions/creators' import {checkDashboardLimits} from 'src/cloud/actions/limits' import {setCells, Action as CellAction} from 'src/cells/actions/creators' import { @@ -40,6 +42,9 @@ import {setLabelOnResource} from 'src/labels/actions/creators' import * as creators from 'src/dashboards/actions/creators' // Utils +import {filterUnusedVars} from 'src/shared/utils/filterUnusedVars' +import {dashboardToTemplate} from 'src/shared/utils/resourceToTemplate' +import {exportVariables} from 'src/variables/utils/exportVariables' import {getSaveableView} from 'src/timeMachine/selectors' import {incrementCloneName} from 'src/utils/naming' import {isLimitError} from 'src/cloud/utils/limits' @@ -57,14 +62,18 @@ import { GetState, View, Cell, + DashboardTemplate, Label, RemoteDataState, DashboardEntities, ViewEntities, ResourceType, + VariableEntities, + Variable, LabelEntities, } from 'src/types' import {CellsWithViewProperties} from 'src/client' +import {arrayOfVariables} from 'src/schemas/variables' type Action = creators.Action @@ -282,6 +291,37 @@ export const getDashboards = () => async ( } } +export const createDashboardFromTemplate = ( + template: DashboardTemplate +) => async (dispatch, getState: GetState) => { + try { + const org = getOrg(getState()) + + await tempAPI.createDashboardFromTemplate(template, org.id) + + const resp = await api.getDashboards({query: {orgID: org.id}}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const dashboards = normalize( + resp.data.dashboards, + arrayOfDashboards + ) + + dispatch(creators.setDashboards(RemoteDataState.Done, dashboards)) + dispatch(notify(copy.importDashboardSucceeded())) + dispatch(checkDashboardLimits()) + } catch (error) { + if (isLimitError(error)) { + dispatch(notify(copy.resourceLimitReached('dashboards'))) + } else { + dispatch(notify(copy.importDashboardFailed(error))) + } + } +} + export const deleteDashboard = (dashboardID: string, name: string) => async ( dispatch ): Promise => { @@ -455,6 +495,70 @@ export const removeDashboardLabel = ( } } +export const convertToTemplate = (dashboardID: string) => async ( + dispatch, + getState: GetState +): Promise => { + try { + dispatch(setExportTemplate(RemoteDataState.Loading)) + const state = getState() + const org = getOrg(state) + + const dashResp = await api.getDashboard({dashboardID}) + + if (dashResp.status !== 200) { + throw new Error(dashResp.data.message) + } + + const {entities, result} = normalize( + dashResp.data, + dashboardSchema + ) + + const dashboard = entities.dashboards[result] + const cells = dashboard.cells.map(cellID => entities.cells[cellID]) + + const pendingViews = dashboard.cells.map(cellID => + dashAPI.getView(dashboardID, cellID) + ) + + const views = await Promise.all(pendingViews) + const resp = await api.getVariables({query: {orgID: org.id}}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + let vars = [] + + // dumb bug + // https://github.com/paularmstrong/normalizr/issues/290 + if (resp.data.variables.length) { + const normVars = normalize( + resp.data.variables, + arrayOfVariables + ) + + vars = Object.values(normVars.entities.variables) + } + + const variables = filterUnusedVars(vars, views) + const exportedVariables = exportVariables(variables, vars) + const dashboardTemplate = dashboardToTemplate( + state, + dashboard, + cells, + views, + exportedVariables + ) + + dispatch(setExportTemplate(RemoteDataState.Done, dashboardTemplate)) + } catch (error) { + console.error(error) + dispatch(setExportTemplate(RemoteDataState.Error)) + dispatch(notify(copy.createTemplateFailed(error))) + } +} + export const saveVEOView = (dashboardID: string) => async ( dispatch, getState: GetState diff --git a/ui/src/dashboards/components/DashboardExportOverlay.tsx b/ui/src/dashboards/components/DashboardExportOverlay.tsx new file mode 100644 index 0000000000..724efbf49c --- /dev/null +++ b/ui/src/dashboards/components/DashboardExportOverlay.tsx @@ -0,0 +1,78 @@ +import React, {PureComponent} from 'react' +import {connect, ConnectedProps} from 'react-redux' +import {withRouter, RouteComponentProps} from 'react-router-dom' + +// Components +import ExportOverlay from 'src/shared/components/ExportOverlay' + +// Actions +import {convertToTemplate as convertToTemplateAction} from 'src/dashboards/actions/thunks' +import {clearExportTemplate as clearExportTemplateAction} from 'src/templates/actions/thunks' + +// Types +import {AppState} from 'src/types' + +import { + dashboardCopySuccess, + dashboardCopyFailed, +} from 'src/shared/copy/notifications' + +type ReduxProps = ConnectedProps +type Props = ReduxProps & + RouteComponentProps<{orgID: string; dashboardID: string}> + +class DashboardExportOverlay extends PureComponent { + public componentDidMount() { + const { + match: { + params: {dashboardID}, + }, + convertToTemplate, + } = this.props + + convertToTemplate(dashboardID) + } + + public render() { + const {status, dashboardTemplate} = this.props + + const notes = (_text, success) => { + if (success) { + return dashboardCopySuccess() + } + + return dashboardCopyFailed() + } + + return ( + + ) + } + + private onDismiss = () => { + const {history, clearExportTemplate} = this.props + + history.goBack() + clearExportTemplate() + } +} + +const mstp = (state: AppState) => ({ + dashboardTemplate: state.resources.templates.exportTemplate.item, + status: state.resources.templates.exportTemplate.status, +}) + +const mdtp = { + convertToTemplate: convertToTemplateAction, + clearExportTemplate: clearExportTemplateAction, +} + +const connector = connect(mstp, mdtp) + +export default connector(withRouter(DashboardExportOverlay)) diff --git a/ui/src/dashboards/components/DashboardImportOverlay.tsx b/ui/src/dashboards/components/DashboardImportOverlay.tsx new file mode 100644 index 0000000000..589d323e2d --- /dev/null +++ b/ui/src/dashboards/components/DashboardImportOverlay.tsx @@ -0,0 +1,90 @@ +// Libraries +import React, {PureComponent} from 'react' +import {withRouter, RouteComponentProps} from 'react-router-dom' +import {isEmpty} from 'lodash' +import {connect, ConnectedProps} from 'react-redux' + +// Components +import ImportOverlay from 'src/shared/components/ImportOverlay' + +// Copy +import {invalidJSON} from 'src/shared/copy/notifications' + +// Actions +import { + getDashboards, + createDashboardFromTemplate as createDashboardFromTemplateAction, +} from 'src/dashboards/actions/thunks' +import {notify as notifyAction} from 'src/shared/actions/notifications' + +// Types +import {ComponentStatus} from '@influxdata/clockface' + +// Utils +import jsonlint from 'jsonlint-mod' + +interface State { + status: ComponentStatus +} + +type ReduxProps = ConnectedProps +type Props = RouteComponentProps<{orgID: string}> & ReduxProps + +class DashboardImportOverlay extends PureComponent { + public state: State = { + status: ComponentStatus.Default, + } + + public render() { + return ( + + ) + } + + 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 = jsonlint.parse(uploadContent) + } catch (error) { + this.updateOverlayStatus(ComponentStatus.Error) + notify(invalidJSON(error.message)) + return + } + + if (isEmpty(template)) { + this.onDismiss() + } + + createDashboardFromTemplate(template) + populateDashboards() + this.onDismiss() + } + + private onDismiss = (): void => { + const {history} = this.props + history.goBack() + } +} + +const mdtp = { + notify: notifyAction, + populateDashboards: getDashboards, + createDashboardFromTemplate: createDashboardFromTemplateAction, +} + +const connector = connect(null, mdtp) + +export default connector(withRouter(DashboardImportOverlay)) diff --git a/ui/src/dashboards/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss b/ui/src/dashboards/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss new file mode 100644 index 0000000000..e1ad1803c4 --- /dev/null +++ b/ui/src/dashboards/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss @@ -0,0 +1,91 @@ +.import-template-overlay, +.import-template-overlay--empty { + height: 500px; + display: flex; +} + +.import-template-overlay { + align-items: stretch; +} + +.import-template-overlay--templates { + flex: 2 0 0; + border-radius: $ix-radius; +} + +.import-template-overlay--details { + flex: 5 0 0; + margin-left: $ix-marg-b; +} + +.import-template-overlay--panel { + min-height: 500px; +} + +.import-template-overlay--name { + margin-top: 0; + margin-bottom: $ix-marg-b; +} + +.import-template-overlay--description { + margin-top: 0; + margin-bottom: $ix-marg-d; +} + +.import-template-overlay--heading { + margin-top: 0; + border-bottom: $ix-border solid $g5-pepper; + padding-bottom: $ix-marg-b; +} + +.import-templates-overlay--included { + margin-bottom: 0; +} + +.import-template-overlay--name.missing, +.import-template-overlay--included.missing, +.import-template-overlay--description.missing { + font-style: italic; + color: $g9-mountain; +} + +.import-template-overlay--empty { + background-color: $g3-castle; + border-radius: $ix-radius; + align-content: center; + align-items: center; + justify-content: center; +} + +.import-template-overlay--template { + user-select: none; + border-radius: $ix-radius; + padding: $ix-marg-b; + background-color: $g1-raven; + margin-bottom: $ix-border; + border: $ix-border solid $g1-raven; + color: $g11-sidewalk; + display: flex; + flex-wrap: none; + align-items: center; + transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease; + + &:hover { + border-color: $g5-pepper; + background-color: $g5-pepper; + color: $g18-cloud; + cursor: pointer; + } + + &.active { + border-color: $c-rainforest; + color: $g18-cloud; + background-color: $g5-pepper; + } +} + +.import-template-overlay--list-label { + font-size: 13px; + font-weight: 500; + margin-left: $ix-marg-b; +} \ No newline at end of file diff --git a/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx b/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx index eb8cbd102a..041c6e9f50 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx @@ -98,6 +98,13 @@ class DashboardCard extends PureComponent { private get contextMenu(): JSX.Element { return ( + + + { onRemoveDashboardLabel(id, label) } + + private handleExport = () => { + const { + history, + match: { + params: {orgID}, + }, + id, + } = this.props + + history.push(`/orgs/${orgID}/dashboards-list/${id}/export`) + } } const mdtp = { diff --git a/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx b/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx index 9c06df231d..21c6e96a4a 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx @@ -2,6 +2,7 @@ import React, {PureComponent} from 'react' import {RouteComponentProps} from 'react-router-dom' import {connect, ConnectedProps} from 'react-redux' +import {Switch, Route} from 'react-router-dom' // Decorators import {ErrorHandling} from 'src/shared/decorators/errors' @@ -10,10 +11,13 @@ import {ErrorHandling} from 'src/shared/decorators/errors' import DashboardsIndexContents from 'src/dashboards/components/dashboard_index/DashboardsIndexContents' import {Page} from '@influxdata/clockface' import SearchWidget from 'src/shared/components/search_widget/SearchWidget' -import AddResourceButton from 'src/shared/components/AddResourceButton' +import AddResourceDropdown from 'src/shared/components/AddResourceDropdown' import GetAssetLimits from 'src/cloud/components/GetAssetLimits' import RateLimitAlert from 'src/cloud/components/RateLimitAlert' import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown' +import DashboardImportOverlay from 'src/dashboards/components/DashboardImportOverlay' +import CreateFromTemplateOverlay from 'src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay' +import DashboardExportOverlay from 'src/dashboards/components/DashboardExportOverlay' // Utils import {pageTitleSuffixer} from 'src/shared/utils/pageTitles' @@ -76,9 +80,12 @@ class DashboardIndex extends PureComponent { /> - @@ -99,6 +106,20 @@ class DashboardIndex extends PureComponent { + + + + + ) } @@ -114,6 +135,26 @@ class DashboardIndex extends PureComponent { private handleFilterDashboards = (searchTerm: string): void => { this.setState({searchTerm}) } + + private summonImportOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import`) + } + + private summonImportFromTemplateOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import/template`) + } } const mstp = (state: AppState) => { diff --git a/ui/src/dashboards/components/dashboard_index/DashboardsTableEmpty.tsx b/ui/src/dashboards/components/dashboard_index/DashboardsTableEmpty.tsx index 68e7429705..6e05dbe9c4 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardsTableEmpty.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardsTableEmpty.tsx @@ -3,7 +3,7 @@ import React, {FC} from 'react' // Components import {EmptyState, ComponentSize} from '@influxdata/clockface' -import AddResourceButton from 'src/shared/components/AddResourceButton' +import AddResourceDropdown from 'src/shared/components/AddResourceDropdown' // Actions import {createDashboard} from 'src/dashboards/actions/thunks' @@ -11,11 +11,15 @@ import {createDashboard} from 'src/dashboards/actions/thunks' interface ComponentProps { searchTerm?: string onCreateDashboard: typeof createDashboard + summonImportOverlay: () => void + summonImportFromTemplateOverlay: () => void } const DashboardsTableEmpty: FC = ({ searchTerm, onCreateDashboard, + summonImportOverlay, + summonImportFromTemplateOverlay, }) => { if (searchTerm) { return ( @@ -30,9 +34,12 @@ const DashboardsTableEmpty: FC = ({ Looks like you don't have any Dashboards, why not create one? - ) diff --git a/ui/src/dashboards/components/dashboard_index/Table.tsx b/ui/src/dashboards/components/dashboard_index/Table.tsx index 22f803099d..3712c32f6d 100644 --- a/ui/src/dashboards/components/dashboard_index/Table.tsx +++ b/ui/src/dashboards/components/dashboard_index/Table.tsx @@ -1,6 +1,7 @@ // Libraries import React, {PureComponent} from 'react' import {connect, ConnectedProps} from 'react-redux' +import {withRouter, RouteComponentProps} from 'react-router-dom' // Components import DashboardCards from 'src/dashboards/components/dashboard_index/DashboardCards' @@ -29,7 +30,7 @@ interface OwnProps { } type ReduxProps = ConnectedProps -type Props = OwnProps & ReduxProps +type Props = OwnProps & ReduxProps & RouteComponentProps<{orgID: string}> class DashboardsTable extends PureComponent { public componentDidMount() { @@ -54,6 +55,8 @@ class DashboardsTable extends PureComponent { ) } @@ -68,6 +71,26 @@ class DashboardsTable extends PureComponent { /> ) } + + private summonImportOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import`) + } + + private summonImportFromTemplateOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import/template`) + } } const mstp = (state: AppState) => { @@ -86,4 +109,4 @@ const mdtp = { const connector = connect(mstp, mdtp) -export default connector(DashboardsTable) +export default connector(withRouter(DashboardsTable)) diff --git a/ui/src/resources/components/GetResources.tsx b/ui/src/resources/components/GetResources.tsx index 2234a1721e..62b47a6d37 100644 --- a/ui/src/resources/components/GetResources.tsx +++ b/ui/src/resources/components/GetResources.tsx @@ -15,6 +15,7 @@ import {getPlugins} from 'src/dataLoaders/actions/telegrafEditor' import {getScrapers} from 'src/scrapers/actions/thunks' import {getTasks} from 'src/tasks/actions/thunks' import {getTelegrafs} from 'src/telegrafs/actions/thunks' +import {getTemplates} from 'src/templates/actions/thunks' import {getVariables} from 'src/variables/actions/thunks' //Utils @@ -99,6 +100,10 @@ class GetResources extends PureComponent { return this.props.getAuthorizations() } + case ResourceType.Templates: { + return this.props.getTemplates() + } + case ResourceType.Members: { return this.props.getMembers() } @@ -153,6 +158,7 @@ const mdtp = { getAuthorizations: getAuthorizations, getDashboards: getDashboards, getTasks: getTasks, + getTemplates: getTemplates, getMembers: getMembers, getChecks: getChecks, getNotificationRules: getNotificationRules, diff --git a/ui/src/settings/components/SettingsNavigation.tsx b/ui/src/settings/components/SettingsNavigation.tsx index a76840a5f7..969eaf048b 100644 --- a/ui/src/settings/components/SettingsNavigation.tsx +++ b/ui/src/settings/components/SettingsNavigation.tsx @@ -6,9 +6,6 @@ import {withRouter, RouteComponentProps} from 'react-router-dom' // Components import TabbedPageTabs from 'src/shared/tabbedPage/TabbedPageTabs' -// Utils -import {isFlagEnabled} from 'src/shared/utils/featureFlag' - // Types import {TabbedPageTab} from 'src/shared/tabbedPage/TabbedPageTabs' @@ -46,13 +43,9 @@ class SettingsNavigation extends PureComponent { }, ] - const displayedTabs = isFlagEnabled('communityTemplates') - ? tabs - : tabs.filter(t => t.id !== 'templates') - return ( diff --git a/ui/src/shared/actions/app.ts b/ui/src/shared/actions/app.ts index 442c5c7a3d..56fb614d31 100644 --- a/ui/src/shared/actions/app.ts +++ b/ui/src/shared/actions/app.ts @@ -18,6 +18,7 @@ export enum ActionTypes { SetNotebookMiniMapState = 'SET_NOTEBOOK_MINI_MAP_STATE', SetAutoRefresh = 'SET_AUTOREFRESH', SetTimeZone = 'SET_APP_TIME_ZONE', + TemplateControlBarVisibilityToggled = 'TemplateControlBarVisibilityToggledAction', Noop = 'NOOP', } diff --git a/ui/src/shared/components/AddResourceButton.tsx b/ui/src/shared/components/AddResourceButton.tsx deleted file mode 100644 index 0f2149a5ca..0000000000 --- a/ui/src/shared/components/AddResourceButton.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// Libraries -import React, {FC} from 'react' -import {connect, ConnectedProps} from 'react-redux' -import _ from 'lodash' - -// Components -import { - IconFont, - ComponentColor, - ComponentSize, - Button, - ComponentStatus, -} from '@influxdata/clockface' - -// Actions -import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays' - -// Types -import {LimitStatus} from 'src/cloud/actions/limits' - -// Constants -import {CLOUD} from 'src/shared/constants' - -interface Props { - onSelectNew: () => void - resourceName: string - status?: ComponentStatus - limitStatus?: LimitStatus -} - -type ReduxProps = ConnectedProps - -const AddResourceButton: FC = ({ - resourceName, - onSelectNew, - onShowOverlay, - onDismissOverlay, - limitStatus = LimitStatus.OK, - status = ComponentStatus.Default, -}) => { - const showLimitOverlay = () => - onShowOverlay('asset-limit', {asset: `${resourceName}s`}, onDismissOverlay) - - const onClick = - CLOUD && limitStatus === LimitStatus.EXCEEDED - ? showLimitOverlay - : onSelectNew - - return ( -