From 35addee3ae158b92bc886083626ab7e108861cfd Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Fri, 18 Dec 2020 01:49:05 -0800 Subject: [PATCH] feat(explore-datasource): add new datasource tab to explore view (#12008) * update to datsource tab * second update * style updates * update style and fix metrics bug * updates to datsource panel * backgrounds and borders * more updates * shuffling some paddings around * more updates * moving some more paddings around! * Fixing sidebar width * using Global to adjust body layout * update test and fix bug * removing hotkeys * layout fixes for short content, simplifying some class names * more styles * add tooltip to collapse and div clickable * more updates * more updates for styles and add list component * update from comments * vising cosmetic issue with line-wrapping drop down caret on controls sections * controls area scrolling properly again. * change lists to old list and updates from comments * border radius from theme * add length field and updates from comments * more changes from comments * integrate health with new control * change callapse back from stylsheet more udpates * substitution string * more substitution strings * fix tests * datasource alignment * taking margin off the search input * update input to flex * fix lint * adjusting column/metric label stylng * fixing scrollable area layout, one more color variable * simplifying some styles * nixing a bad left margin * Using gridunit for padding * using gridUnit for padding * define types for datsource panel * fixing a padding issue Co-authored-by: Evan Rusackas --- .../integration/explore/control.test.ts | 1 + .../components/ControlPanelSection_spec.jsx | 6 +- .../components/DatasourceControl_spec.jsx | 4 +- .../components/DatasourcePanel_spec.jsx | 69 ++++++ .../components/ControlPanelSection.jsx | 11 +- .../components/ControlPanelsContainer.jsx | 7 +- .../explore/components/DatasourcePanel.tsx | 231 ++++++++++++++++++ .../components/ExploreViewContainer.jsx | 152 +++++++++++- .../explore/components/QueryAndSaveBtns.jsx | 26 +- .../components/controls/DatasourceControl.jsx | 116 ++------- 10 files changed, 492 insertions(+), 131 deletions(-) create mode 100644 superset-frontend/spec/javascripts/explore/components/DatasourcePanel_spec.jsx create mode 100644 superset-frontend/src/explore/components/DatasourcePanel.tsx diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts index 0024558629..20566ca90b 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts @@ -35,6 +35,7 @@ describe('Datasource control', () => { cy.visitChartByName('Num Births Trend'); cy.verifySliceSuccess({ waitAlias: '@postJson' }); + cy.get('[data-test="open-datasource-tab').click({ force: true }); cy.get('[data-test="datasource-menu-trigger"]').click(); cy.get('script').then(nodes => { diff --git a/superset-frontend/spec/javascripts/explore/components/ControlPanelSection_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ControlPanelSection_spec.jsx index 83e5512b1a..521a5532ef 100644 --- a/superset-frontend/spec/javascripts/explore/components/ControlPanelSection_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/ControlPanelSection_spec.jsx @@ -53,7 +53,11 @@ describe('ControlPanelSection', () => { }); it('renders a label if present', () => { - expect(wrapper.find(Panel.Title).dive().text()).toContain('my label'); + expect( + wrapper + .find('[data-test="clickable-control-panel-section-title"]') + .text(), + ).toContain('my label'); }); it('renders a InfoTooltipWithTrigger if label and tooltip is present', () => { diff --git a/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx index 1c91c1cd25..2996dcd111 100644 --- a/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx @@ -97,8 +97,8 @@ describe('DatasourceControl', () => { it('should render health check message', () => { const wrapper = setup(); - const alert = wrapper.find(Icon).first(); - expect(alert.prop('name')).toBe('alert-solid'); + const alert = wrapper.find(Icon); + expect(alert.at(1).prop('name')).toBe('alert-solid'); const tooltip = wrapper.find(Tooltip).at(1); expect(tooltip.prop('title')).toBe( defaultProps.datasource.health_check_message, diff --git a/superset-frontend/spec/javascripts/explore/components/DatasourcePanel_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DatasourcePanel_spec.jsx new file mode 100644 index 0000000000..1bfd67ad26 --- /dev/null +++ b/superset-frontend/spec/javascripts/explore/components/DatasourcePanel_spec.jsx @@ -0,0 +1,69 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import DatasourcePanel from 'src/explore/components/DatasourcePanel'; + +describe('datasourcepanel', () => { + const datasource = { + name: 'birth_names', + type: 'table', + uid: '1__table', + id: 1, + columns: [], + metrics: [], + database: { + backend: 'mysql', + name: 'main', + }, + }; + const props = { + datasource, + controls: { + datasource: { + validationErrors: null, + mapStateToProps: () => null, + type: 'DatasourceControl', + label: 'hello', + datasource, + }, + }, + actions: null, + }; + it('should render', () => { + const { container } = render( + + + , + ); + expect(container).toBeVisible(); + }); + + it('should display items in controls', () => { + render( + + + , + ); + expect(screen.getByText('birth_names')).toBeTruthy(); + expect(screen.getByText('Columns')).toBeTruthy(); + expect(screen.getByText('Metrics')).toBeTruthy(); + }); +}); diff --git a/superset-frontend/src/explore/components/ControlPanelSection.jsx b/superset-frontend/src/explore/components/ControlPanelSection.jsx index c49f92c97c..43af950745 100644 --- a/superset-frontend/src/explore/components/ControlPanelSection.jsx +++ b/superset-frontend/src/explore/components/ControlPanelSection.jsx @@ -20,6 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Panel } from 'react-bootstrap'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; +import { styled } from '@superset-ui/core'; const propTypes = { label: PropTypes.string, @@ -36,6 +37,14 @@ const defaultProps = { hasErrors: false, }; +const StyledPanelTitle = styled(Panel.Title)` + & > div { + display: flex; + align-items: center; + justify-content: space-between; + } +`; + export default class ControlPanelSection extends React.Component { constructor(props) { super(props); @@ -94,7 +103,7 @@ export default class ControlPanelSection extends React.Component { onToggle={this.toggleExpand} > - {this.renderHeader()} + {this.renderHeader()} {this.props.children} diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx index 0624e0a1d0..01a06c945d 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx @@ -159,7 +159,11 @@ class ControlPanelsContainer extends React.Component { // When the item is a React element return controlItem; } - if (controlItem.name && controlItem.config) { + if ( + controlItem.name && + controlItem.config && + controlItem.name !== 'datasource' + ) { return this.renderControl(controlItem); } return null; @@ -204,7 +208,6 @@ class ControlPanelsContainer extends React.Component { displaySectionsToRender.push(section); } }); - const showCustomizeTab = displaySectionsToRender.length > 0; return ( diff --git a/superset-frontend/src/explore/components/DatasourcePanel.tsx b/superset-frontend/src/explore/components/DatasourcePanel.tsx new file mode 100644 index 0000000000..b715ecd01d --- /dev/null +++ b/superset-frontend/src/explore/components/DatasourcePanel.tsx @@ -0,0 +1,231 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect, useState } from 'react'; +import { styled, t, QueryFormData } from '@superset-ui/core'; +import { Collapse } from 'src/common/components'; +import { + ColumnOption, + MetricOption, + ControlType, +} from '@superset-ui/chart-controls'; +import { ExploreActions } from '../actions/exploreActions'; +import Control from './Control'; + +interface DatasourceControl { + validationErrors: Array; + mapStateToProps: QueryFormData; + type: ControlType; + label: string; + datasource?: DatasourceControl; +} + +type Columns = { + column_name: string; + description: string | undefined; + expression: string | undefined; + filterable: boolean; + groupby: string | undefined; + id: number; + is_dttm: boolean; + python_date_format: string; + type: string; + verbose_name: string; +}; + +type Metrics = { + certification_details: string | undefined; + certified_by: string | undefined; + d3format: string | undefined; + description: string | undefined; + expression: string; + id: number; + is_certified: boolean; + metric_name: string; + verbose_name: string; + warning_text: string; +}; + +interface Props { + datasource: { + columns: Array; + metrics: Array; + }; + controls: { + datasource: DatasourceControl; + }; + actions: Partial & Pick; +} + +const DatasourceContainer = styled.div` + background-color: ${({ theme }) => theme.colors.grayscale.light4}; + position: relative; + height: 100%; + display: flex; + flex-direction: column; + max-height: 100%; + .ant-collapse { + height: auto; + border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + padding-bottom: ${({ theme }) => theme.gridUnit * 2}px; + background-color: ${({ theme }) => theme.colors.grayscale.light4}; + } + .ant-collapse > .ant-collapse-item > .ant-collapse-header { + padding-left: ${({ theme }) => theme.gridUnit * 2}px; + padding-bottom: 0px; + } + .form-control.input-sm { + margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; + } + .ant-collapse-item { + background-color: ${({ theme }) => theme.colors.grayscale.light4}; + .anticon.anticon-right.ant-collapse-arrow > svg { + transform: rotate(90deg) !important; + margin-right: ${({ theme }) => theme.gridUnit * -2}px; + } + } + .ant-collapse-item.ant-collapse-item-active { + .anticon.anticon-right.ant-collapse-arrow > svg { + transform: rotate(-90deg) !important; + } + .ant-collapse-header { + border: 0; + } + } + .header { + font-size: ${({ theme }) => theme.typography.sizes.l}px; + margin-left: ${({ theme }) => theme.gridUnit * -2}px; + } + .ant-collapse-borderless + > .ant-collapse-item + > .ant-collapse-content + > .ant-collapse-content-box { + padding: 0px; + } + .field-selections { + padding: ${({ theme }) => 2 * theme.gridUnit}px; + overflow: auto; + } + .field-length { + margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; + font-size: ${({ theme }) => theme.typography.sizes.s}px; + color: ${({ theme }) => theme.colors.grayscale.light1}; + } + .form-control.input-sm { + margin-bottom: 0; + } + .type-label { + font-weight: ${({ theme }) => theme.typography.weights.light}; + font-size: ${({ theme }) => theme.typography.sizes.s}px; + color: ${({ theme }) => theme.colors.grayscale.base}; + } + .Control { + padding-bottom: 0; + } +`; + +const DataSourcePanel = ({ + datasource, + controls: { datasource: datasourceControl }, + actions, +}: Props) => { + const { columns, metrics } = datasource; + const [lists, setList] = useState({ + columns, + metrics, + }); + const search = ({ target: { value } }: { target: { value: string } }) => { + if (value === '') { + setList({ columns, metrics }); + return; + } + const filteredColumns = lists.columns.filter( + column => column.column_name.indexOf(value) !== -1, + ); + const filteredMetrics = lists.metrics.filter( + metric => metric.metric_name.indexOf(value) !== -1, + ); + setList({ columns: filteredColumns, metrics: filteredMetrics }); + }; + useEffect(() => { + setList({ + columns, + metrics, + }); + }, [datasource]); + + const metricSlice = lists.metrics.slice(0, 50); + const columnSlice = lists.columns.slice(0, 50); + + return ( + + +
+ + + {t('Columns')}} + key="column" + > +
+ {t(`Showing %s of %s`, columnSlice.length, columns.length)} +
+ {columnSlice.map(col => ( +
+ +
+ ))} +
+
+ + {t('Metrics')}} + key="metrics" + > +
+ {t(`Showing %s of %s`, metricSlice.length, metrics.length)} +
+ {metricSlice.map(m => ( +
+ +
+ ))} +
+
+
+
+ ); +}; + +export default DataSourcePanel; diff --git a/superset-frontend/src/explore/components/ExploreViewContainer.jsx b/superset-frontend/src/explore/components/ExploreViewContainer.jsx index fd6d1e00b3..69cffd4950 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer.jsx @@ -21,12 +21,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { styled, logging, t } from '@superset-ui/core'; - +import { styled, logging, t, supersetTheme, css } from '@superset-ui/core'; +import { Global } from '@emotion/core'; +import { Tooltip } from 'src/common/components/Tooltip'; +import Icon from 'src/components/Icon'; import ExploreChartPanel from './ExploreChartPanel'; import ConnectedControlPanelsContainer from './ControlPanelsContainer'; import SaveModal from './SaveModal'; import QueryAndSaveBtns from './QueryAndSaveBtns'; +import DataSourcePanel from './DatasourcePanel'; import { getExploreLongUrl } from '../exploreUtils'; import { areObjectsEqual } from '../../reduxUtils'; import { getFormDataFromControls } from '../controlUtils'; @@ -57,21 +60,65 @@ const propTypes = { }; const Styles = styled.div` - height: ${({ height }) => height}; - min-height: ${({ height }) => height}; + background: ${({ theme }) => theme.colors.grayscale.light5}; text-align: left; position: relative; width: 100%; + height: 100%; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: stretch; - .control-pane { + border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + .explore-column { display: flex; flex-direction: column; - padding: 0 ${({ theme }) => 2 * theme.gridUnit}px; + padding: ${({ theme }) => 2 * theme.gridUnit}px 0; max-height: 100%; } + .data-source-selection { + background-color: ${({ theme }) => theme.colors.grayscale.light4}; + padding: ${({ theme }) => 2 * theme.gridUnit}px 0; + border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + } + .main-explore-content { + border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + } + .controls-column { + align-self: flex-start; + padding: 0; + } + .title-container { + position: relative; + display: flex; + flex-direction: row; + padding: 0 ${({ theme }) => 2 * theme.gridUnit}px; + justify-content: space-between; + .horizontal-text { + text-transform: uppercase; + color: ${({ theme }) => theme.colors.grayscale.light1}; + font-size: ${({ theme }) => 4 * theme.typography.sizes.s}; + } + } + .no-show { + display: none; + } + .vertical-text { + writing-mode: vertical-rl; + text-orientation: mixed; + } + .sidebar { + height: 100%; + background-color: ${({ theme }) => theme.colors.grayscale.light4}; + padding: ${({ theme }) => 2 * theme.gridUnit}px; + width: ${({ theme }) => 8 * theme.gridUnit}px; + } + .data-tab { + min-width: 288px; + } + .callpase-icon > svg { + color: ${({ theme }) => theme.colors.primary.base}; + } `; class ExploreViewContainer extends React.Component { @@ -84,6 +131,7 @@ class ExploreViewContainer extends React.Component { showModal: false, chartIsStale: false, refreshOverlayVisible: false, + collapse: true, }; this.addHistory = this.addHistory.bind(this); @@ -93,6 +141,7 @@ class ExploreViewContainer extends React.Component { this.onQuery = this.onQuery.bind(this); this.toggleModal = this.toggleModal.bind(this); this.handleKeydown = this.handleKeydown.bind(this); + this.toggleCollapse = this.toggleCollapse.bind(this); } componentDidMount() { @@ -259,6 +308,10 @@ class ExploreViewContainer extends React.Component { } } + toggleCollapse() { + this.setState(prevState => ({ collapse: !prevState.collapse })); + } + handleResize() { clearTimeout(this.resizeTimer); this.resizeTimer = setTimeout(() => { @@ -326,12 +379,34 @@ class ExploreViewContainer extends React.Component { } render() { + const { collapse } = this.state; if (this.props.standalone) { return this.renderChartContainer(); } - return ( - + + {this.state.showModal && ( )} -
+
+
+ {t('Datasource')} + + + +
+ +
+ {collapse ? ( +
+ + + + + + +
+ ) : null} +
-
{this.renderChartContainer()}
+
+ {this.renderChartContainer()} +
); } @@ -372,7 +503,6 @@ function mapStateToProps(state) { const form_data = getFormDataFromControls(explore.controls); const chartKey = Object.keys(charts)[0]; const chart = charts[chartKey]; - return { isDatasourceMetaLoading: explore.isDatasourceMetaLoading, datasource: explore.datasource, diff --git a/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx b/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx index 81db38b66c..2327deea92 100644 --- a/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx +++ b/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx @@ -23,7 +23,6 @@ import { t, styled } from '@superset-ui/core'; import { Tooltip } from 'src/common/components/Tooltip'; import Button from 'src/components/Button'; -import Hotkeys from '../../components/Hotkeys'; const propTypes = { canAdd: PropTypes.bool.isRequired, @@ -40,26 +39,14 @@ const defaultProps = { onSave: () => {}, }; -// Prolly need to move this to a global context -const keymap = { - RUN: 'ctrl + r, ctrl + enter', - SAVE: 'ctrl + s', -}; - -const getHotKeys = () => - Object.keys(keymap).map(k => ({ - name: k, - descr: keymap[k], - key: k, - })); - const Styles = styled.div` display: flex; flex-shrink: 0; flex-direction: row; align-items: center; - padding-bottom: ${({ theme }) => 2 * theme.gridUnit}px; - + padding: ${({ theme }) => 2 * theme.gridUnit}px + ${({ theme }) => 2 * theme.gridUnit}px 0 + ${({ theme }) => 4 * theme.gridUnit}px; .btn { /* just to make sure buttons don't jiggle */ width: 100px; @@ -134,13 +121,6 @@ export default function QueryAndSaveBtns({ )}
-
- -
); } diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl.jsx index f8f79d3649..e30aebe876 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl.jsx @@ -18,20 +18,15 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { Col, Collapse, Row, Well } from 'react-bootstrap'; import { t, styled, supersetTheme } from '@superset-ui/core'; -import { ColumnOption, MetricOption } from '@superset-ui/chart-controls'; import { Dropdown, Menu } from 'src/common/components'; import { Tooltip } from 'src/common/components/Tooltip'; import Icon from 'src/components/Icon'; import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal'; import DatasourceModal from 'src/datasource/DatasourceModal'; -import Label from 'src/components/Label'; import { postForm } from 'src/explore/exploreUtils'; -import ControlHeader from '../ControlHeader'; - const propTypes = { actions: PropTypes.object.isRequired, onChange: PropTypes.func, @@ -49,44 +44,46 @@ const defaultProps = { }; const Styles = styled.div` + .data-container { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + padding: ${({ theme }) => 2 * theme.gridUnit}px; + } .ant-dropdown-trigger { - margin-left: ${({ theme }) => theme.gridUnit}px; + margin-left: ${({ theme }) => 2 * theme.gridUnit}px; box-shadow: none; &:active { box-shadow: none; } } - .btn-group .open .dropdown-toggle { box-shadow: none; &.button-default { background: none; } } - i.angle { color: ${({ theme }) => theme.colors.primary.base}; } - svg.datasource-modal-trigger { color: ${({ theme }) => theme.colors.primary.base}; - vertical-align: middle; cursor: pointer; } - - .datasource-controls { - display: flex; + .title-select { + flex: 1 1 100%; + display: inline-block; + background-color: ${({ theme }) => theme.colors.grayscale.light3}; + padding: ${({ theme }) => theme.gridUnit * 2}px; + border-radius: ${({ theme }) => theme.borderRadius}px; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } -`; - -/** - * used in column details. - */ -const ColumnsCol = styled(Col)` - overflow: auto; /* for very very long columns names */ - white-space: nowrap; /* make sure tooltip trigger is on the same line as the metric */ - .and-more { - padding-left: 38px; + .dataset-svg { + margin-right: ${({ theme }) => 2 * theme.gridUnit}px; } `; @@ -107,7 +104,6 @@ class DatasourceControl extends React.PureComponent { ); this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this); this.toggleShowDatasource = this.toggleShowDatasource.bind(this); - this.renderDatasource = this.renderDatasource.bind(this); this.handleMenuItemClick = this.handleMenuItemClick.bind(this); } @@ -153,58 +149,9 @@ class DatasourceControl extends React.PureComponent { } } - renderDatasource() { - const { datasource } = this.props; - const { showDatasource } = this.state; - const maxNumColumns = 50; - return ( -
- -
- - {` ${datasource.database.name} `} -
- {showDatasource && ( - - - Columns - {datasource.columns.slice(0, maxNumColumns).map(col => ( -
- -
- ))} - {datasource.columns.length > maxNumColumns && ( -
...
- )} -
- - Metrics - {datasource.metrics.slice(0, maxNumColumns).map(m => ( -
- -
- ))} - {datasource.columns.length > maxNumColumns && ( -
...
- )} -
-
- )} -
-
- ); - } - render() { - const { - showChangeDatasourceModal, - showEditDatasourceModal, - showDatasource, - } = this.state; + const { showChangeDatasourceModal, showEditDatasourceModal } = this.state; const { datasource, onChange } = this.props; - const datasourceMenu = ( {this.props.isEditable && ( @@ -222,20 +169,10 @@ class DatasourceControl extends React.PureComponent { return ( - -
- - +
+ + + {datasource.name} {healthCheckMessage && ( @@ -259,9 +196,6 @@ class DatasourceControl extends React.PureComponent {
- - {this.renderDatasource()} - {showEditDatasourceModal && (