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 <evan@preset.io>
This commit is contained in:
Phillip Kelley-Dotson 2020-12-18 01:49:05 -08:00 committed by GitHub
parent af130ea5e9
commit 35addee3ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 492 additions and 131 deletions

View File

@ -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 => {

View File

@ -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', () => {

View File

@ -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,

View File

@ -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(
<ThemeProvider theme={supersetTheme}>
<DatasourcePanel {...props} />
</ThemeProvider>,
);
expect(container).toBeVisible();
});
it('should display items in controls', () => {
render(
<ThemeProvider theme={supersetTheme}>
<DatasourcePanel {...props} />
</ThemeProvider>,
);
expect(screen.getByText('birth_names')).toBeTruthy();
expect(screen.getByText('Columns')).toBeTruthy();
expect(screen.getByText('Metrics')).toBeTruthy();
});
});

View File

@ -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}
>
<Panel.Heading>
<Panel.Title>{this.renderHeader()}</Panel.Title>
<StyledPanelTitle>{this.renderHeader()}</StyledPanelTitle>
</Panel.Heading>
<Panel.Collapse>
<Panel.Body>{this.props.children}</Panel.Body>

View File

@ -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 (
<Styles>

View File

@ -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<any>;
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<Columns>;
metrics: Array<Metrics>;
};
controls: {
datasource: DatasourceControl;
};
actions: Partial<ExploreActions> & Pick<ExploreActions, 'setControlValue'>;
}
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 (
<DatasourceContainer>
<Control
{...datasourceControl}
name="datasource"
validationErrors={datasourceControl.validationErrors}
actions={actions}
formData={datasourceControl.mapStateToProps}
/>
<div className="field-selections">
<input
type="text"
onChange={search}
className="form-control input-sm"
placeholder={t('Search Metrics & Columns')}
/>
<Collapse
accordion
bordered={false}
defaultActiveKey={['column', 'metrics']}
expandIconPosition="right"
>
<Collapse.Panel
header={<span className="header">{t('Columns')}</span>}
key="column"
>
<div className="field-length">
{t(`Showing %s of %s`, columnSlice.length, columns.length)}
</div>
{columnSlice.map(col => (
<div key={col.column_name} className="column">
<ColumnOption column={col} showType />
</div>
))}
</Collapse.Panel>
</Collapse>
<Collapse accordion bordered={false} expandIconPosition="right">
<Collapse.Panel
header={<span className="header">{t('Metrics')}</span>}
key="metrics"
>
<div className="field-length">
{t(`Showing %s of %s`, metricSlice.length, metrics.length)}
</div>
{metricSlice.map(m => (
<div key={m.metric_name} className="column">
<MetricOption metric={m} showType />
</div>
))}
</Collapse.Panel>
</Collapse>
</div>
</DatasourceContainer>
);
};
export default DataSourcePanel;

View File

@ -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 (
<Styles id="explore-container" height={this.state.height}>
<Styles id="explore-container">
<Global
styles={css`
.navbar {
margin-bottom: 0;
}
body {
max-height: 100vh;
overflow: hidden;
}
#app-menu,
#app {
flex: 1 1 auto;
}
#app {
flex-basis: 100%;
overflow: hidden;
}
#app-menu {
flex-shrink: 0;
}
`}
/>
{this.state.showModal && (
<SaveModal
onHide={this.toggleModal}
@ -340,7 +415,57 @@ class ExploreViewContainer extends React.Component {
sliceName={this.props.sliceName}
/>
)}
<div className="col-sm-4 control-pane">
<div
className={
collapse
? 'no-show'
: 'data-tab explore-column data-source-selection'
}
>
<div className="title-container">
<span className="horizontal-text">{t('Datasource')}</span>
<span
role="button"
tabIndex={0}
className="action-button"
onClick={this.toggleCollapse}
>
<Icon
name="expand"
color={supersetTheme.colors.primary.base}
className="collapse-icon"
width={16}
/>
</span>
</div>
<DataSourcePanel
datasource={this.props.datasource}
controls={this.props.controls}
actions={this.props.actions}
/>
</div>
{collapse ? (
<div
className="sidebar"
onClick={this.toggleCollapse}
data-test="open-datasource-tab"
role="button"
tabIndex={0}
>
<span role="button" tabIndex={0} className="action-button">
<Tooltip title={t('Open Datasource Tab')}>
<Icon
name="collapse"
color={supersetTheme.colors.primary.base}
className="collapse-icon"
width={16}
/>
</Tooltip>
</span>
<Icon name="dataset-physical" width={16} />
</div>
) : null}
<div className="col-sm-3 explore-column controls-column">
<QueryAndSaveBtns
canAdd={!!(this.props.can_add || this.props.can_overwrite)}
onQuery={this.onQuery}
@ -359,7 +484,13 @@ class ExploreViewContainer extends React.Component {
isDatasourceMetaLoading={this.props.isDatasourceMetaLoading}
/>
</div>
<div className="col-sm-8">{this.renderChartContainer()}</div>
<div
className={`main-explore-content ${
collapse ? 'col-sm-9' : 'col-sm-7'
}`}
>
{this.renderChartContainer()}
</div>
</Styles>
);
}
@ -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,

View File

@ -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({
</span>
)}
</div>
<div className="m-l-5 text-muted">
<Hotkeys
header="Keyboard shortcuts"
hotkeys={getHotKeys()}
placement="right"
/>
</div>
</Styles>
);
}

View File

@ -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;
}
`;
/**
* <Col> 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 (
<div className="m-t-10">
<Well className="m-t-0">
<div className="m-b-10">
<Label>
<i className="fa fa-database" /> {datasource.database.backend}
</Label>
{` ${datasource.database.name} `}
</div>
{showDatasource && (
<Row className="datasource-container">
<ColumnsCol md={6}>
<strong>Columns</strong>
{datasource.columns.slice(0, maxNumColumns).map(col => (
<div key={col.column_name}>
<ColumnOption showType column={col} />
</div>
))}
{datasource.columns.length > maxNumColumns && (
<div className="and-more">...</div>
)}
</ColumnsCol>
<ColumnsCol md={6}>
<strong>Metrics</strong>
{datasource.metrics.slice(0, maxNumColumns).map(m => (
<div key={m.metric_name}>
<MetricOption metric={m} showType />
</div>
))}
{datasource.columns.length > maxNumColumns && (
<div className="and-more">...</div>
)}
</ColumnsCol>
</Row>
)}
</Well>
</div>
);
}
render() {
const {
showChangeDatasourceModal,
showEditDatasourceModal,
showDatasource,
} = this.state;
const { showChangeDatasourceModal, showEditDatasourceModal } = this.state;
const { datasource, onChange } = this.props;
const datasourceMenu = (
<Menu onClick={this.handleMenuItemClick}>
{this.props.isEditable && (
@ -222,20 +169,10 @@ class DatasourceControl extends React.PureComponent {
return (
<Styles className="DatasourceControl">
<ControlHeader {...this.props} />
<div className="datasource-controls">
<Tooltip title={t('Expand/collapse dataset configuration')}>
<Label
style={{ textTransform: 'none' }}
onClick={this.toggleShowDatasource}
>
{datasource.name}{' '}
<i
className={`angle fa fa-angle-${
showDatasource ? 'up' : 'down'
}`}
/>
</Label>
<div className="data-container">
<Icon name="dataset-physical" className="dataset-svg" />
<Tooltip title={datasource.name}>
<span className="title-select">{datasource.name}</span>
</Tooltip>
{healthCheckMessage && (
<Tooltip title={healthCheckMessage}>
@ -259,9 +196,6 @@ class DatasourceControl extends React.PureComponent {
</Tooltip>
</Dropdown>
</div>
<Collapse in={this.state.showDatasource}>
{this.renderDatasource()}
</Collapse>
{showEditDatasourceModal && (
<DatasourceModal
datasource={datasource}