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:
parent
af130ea5e9
commit
35addee3ae
|
|
@ -35,6 +35,7 @@ describe('Datasource control', () => {
|
||||||
cy.visitChartByName('Num Births Trend');
|
cy.visitChartByName('Num Births Trend');
|
||||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||||
|
|
||||||
|
cy.get('[data-test="open-datasource-tab').click({ force: true });
|
||||||
cy.get('[data-test="datasource-menu-trigger"]').click();
|
cy.get('[data-test="datasource-menu-trigger"]').click();
|
||||||
|
|
||||||
cy.get('script').then(nodes => {
|
cy.get('script').then(nodes => {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ describe('ControlPanelSection', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a label if present', () => {
|
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', () => {
|
it('renders a InfoTooltipWithTrigger if label and tooltip is present', () => {
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,8 @@ describe('DatasourceControl', () => {
|
||||||
|
|
||||||
it('should render health check message', () => {
|
it('should render health check message', () => {
|
||||||
const wrapper = setup();
|
const wrapper = setup();
|
||||||
const alert = wrapper.find(Icon).first();
|
const alert = wrapper.find(Icon);
|
||||||
expect(alert.prop('name')).toBe('alert-solid');
|
expect(alert.at(1).prop('name')).toBe('alert-solid');
|
||||||
const tooltip = wrapper.find(Tooltip).at(1);
|
const tooltip = wrapper.find(Tooltip).at(1);
|
||||||
expect(tooltip.prop('title')).toBe(
|
expect(tooltip.prop('title')).toBe(
|
||||||
defaultProps.datasource.health_check_message,
|
defaultProps.datasource.health_check_message,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -20,6 +20,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Panel } from 'react-bootstrap';
|
import { Panel } from 'react-bootstrap';
|
||||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
|
import { styled } from '@superset-ui/core';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
|
|
@ -36,6 +37,14 @@ const defaultProps = {
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledPanelTitle = styled(Panel.Title)`
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default class ControlPanelSection extends React.Component {
|
export default class ControlPanelSection extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -94,7 +103,7 @@ export default class ControlPanelSection extends React.Component {
|
||||||
onToggle={this.toggleExpand}
|
onToggle={this.toggleExpand}
|
||||||
>
|
>
|
||||||
<Panel.Heading>
|
<Panel.Heading>
|
||||||
<Panel.Title>{this.renderHeader()}</Panel.Title>
|
<StyledPanelTitle>{this.renderHeader()}</StyledPanelTitle>
|
||||||
</Panel.Heading>
|
</Panel.Heading>
|
||||||
<Panel.Collapse>
|
<Panel.Collapse>
|
||||||
<Panel.Body>{this.props.children}</Panel.Body>
|
<Panel.Body>{this.props.children}</Panel.Body>
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,11 @@ class ControlPanelsContainer extends React.Component {
|
||||||
// When the item is a React element
|
// When the item is a React element
|
||||||
return controlItem;
|
return controlItem;
|
||||||
}
|
}
|
||||||
if (controlItem.name && controlItem.config) {
|
if (
|
||||||
|
controlItem.name &&
|
||||||
|
controlItem.config &&
|
||||||
|
controlItem.name !== 'datasource'
|
||||||
|
) {
|
||||||
return this.renderControl(controlItem);
|
return this.renderControl(controlItem);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -204,7 +208,6 @@ class ControlPanelsContainer extends React.Component {
|
||||||
displaySectionsToRender.push(section);
|
displaySectionsToRender.push(section);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const showCustomizeTab = displaySectionsToRender.length > 0;
|
const showCustomizeTab = displaySectionsToRender.length > 0;
|
||||||
return (
|
return (
|
||||||
<Styles>
|
<Styles>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -21,12 +21,15 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-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 ExploreChartPanel from './ExploreChartPanel';
|
||||||
import ConnectedControlPanelsContainer from './ControlPanelsContainer';
|
import ConnectedControlPanelsContainer from './ControlPanelsContainer';
|
||||||
import SaveModal from './SaveModal';
|
import SaveModal from './SaveModal';
|
||||||
import QueryAndSaveBtns from './QueryAndSaveBtns';
|
import QueryAndSaveBtns from './QueryAndSaveBtns';
|
||||||
|
import DataSourcePanel from './DatasourcePanel';
|
||||||
import { getExploreLongUrl } from '../exploreUtils';
|
import { getExploreLongUrl } from '../exploreUtils';
|
||||||
import { areObjectsEqual } from '../../reduxUtils';
|
import { areObjectsEqual } from '../../reduxUtils';
|
||||||
import { getFormDataFromControls } from '../controlUtils';
|
import { getFormDataFromControls } from '../controlUtils';
|
||||||
|
|
@ -57,21 +60,65 @@ const propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Styles = styled.div`
|
const Styles = styled.div`
|
||||||
height: ${({ height }) => height};
|
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||||
min-height: ${({ height }) => height};
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
.control-pane {
|
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
|
.explore-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 ${({ theme }) => 2 * theme.gridUnit}px;
|
padding: ${({ theme }) => 2 * theme.gridUnit}px 0;
|
||||||
max-height: 100%;
|
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 {
|
class ExploreViewContainer extends React.Component {
|
||||||
|
|
@ -84,6 +131,7 @@ class ExploreViewContainer extends React.Component {
|
||||||
showModal: false,
|
showModal: false,
|
||||||
chartIsStale: false,
|
chartIsStale: false,
|
||||||
refreshOverlayVisible: false,
|
refreshOverlayVisible: false,
|
||||||
|
collapse: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.addHistory = this.addHistory.bind(this);
|
this.addHistory = this.addHistory.bind(this);
|
||||||
|
|
@ -93,6 +141,7 @@ class ExploreViewContainer extends React.Component {
|
||||||
this.onQuery = this.onQuery.bind(this);
|
this.onQuery = this.onQuery.bind(this);
|
||||||
this.toggleModal = this.toggleModal.bind(this);
|
this.toggleModal = this.toggleModal.bind(this);
|
||||||
this.handleKeydown = this.handleKeydown.bind(this);
|
this.handleKeydown = this.handleKeydown.bind(this);
|
||||||
|
this.toggleCollapse = this.toggleCollapse.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
@ -259,6 +308,10 @@ class ExploreViewContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleCollapse() {
|
||||||
|
this.setState(prevState => ({ collapse: !prevState.collapse }));
|
||||||
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
clearTimeout(this.resizeTimer);
|
clearTimeout(this.resizeTimer);
|
||||||
this.resizeTimer = setTimeout(() => {
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
|
@ -326,12 +379,34 @@ class ExploreViewContainer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { collapse } = this.state;
|
||||||
if (this.props.standalone) {
|
if (this.props.standalone) {
|
||||||
return this.renderChartContainer();
|
return this.renderChartContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 && (
|
{this.state.showModal && (
|
||||||
<SaveModal
|
<SaveModal
|
||||||
onHide={this.toggleModal}
|
onHide={this.toggleModal}
|
||||||
|
|
@ -340,7 +415,57 @@ class ExploreViewContainer extends React.Component {
|
||||||
sliceName={this.props.sliceName}
|
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
|
<QueryAndSaveBtns
|
||||||
canAdd={!!(this.props.can_add || this.props.can_overwrite)}
|
canAdd={!!(this.props.can_add || this.props.can_overwrite)}
|
||||||
onQuery={this.onQuery}
|
onQuery={this.onQuery}
|
||||||
|
|
@ -359,7 +484,13 @@ class ExploreViewContainer extends React.Component {
|
||||||
isDatasourceMetaLoading={this.props.isDatasourceMetaLoading}
|
isDatasourceMetaLoading={this.props.isDatasourceMetaLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</Styles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +503,6 @@ function mapStateToProps(state) {
|
||||||
const form_data = getFormDataFromControls(explore.controls);
|
const form_data = getFormDataFromControls(explore.controls);
|
||||||
const chartKey = Object.keys(charts)[0];
|
const chartKey = Object.keys(charts)[0];
|
||||||
const chart = charts[chartKey];
|
const chart = charts[chartKey];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
isDatasourceMetaLoading: explore.isDatasourceMetaLoading,
|
||||||
datasource: explore.datasource,
|
datasource: explore.datasource,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { t, styled } from '@superset-ui/core';
|
||||||
|
|
||||||
import { Tooltip } from 'src/common/components/Tooltip';
|
import { Tooltip } from 'src/common/components/Tooltip';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import Hotkeys from '../../components/Hotkeys';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
canAdd: PropTypes.bool.isRequired,
|
canAdd: PropTypes.bool.isRequired,
|
||||||
|
|
@ -40,26 +39,14 @@ const defaultProps = {
|
||||||
onSave: () => {},
|
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`
|
const Styles = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
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 {
|
.btn {
|
||||||
/* just to make sure buttons don't jiggle */
|
/* just to make sure buttons don't jiggle */
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
|
@ -134,13 +121,6 @@ export default function QueryAndSaveBtns({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="m-l-5 text-muted">
|
|
||||||
<Hotkeys
|
|
||||||
header="Keyboard shortcuts"
|
|
||||||
hotkeys={getHotKeys()}
|
|
||||||
placement="right"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Styles>
|
</Styles>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,15 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Col, Collapse, Row, Well } from 'react-bootstrap';
|
|
||||||
import { t, styled, supersetTheme } from '@superset-ui/core';
|
import { t, styled, supersetTheme } from '@superset-ui/core';
|
||||||
import { ColumnOption, MetricOption } from '@superset-ui/chart-controls';
|
|
||||||
|
|
||||||
import { Dropdown, Menu } from 'src/common/components';
|
import { Dropdown, Menu } from 'src/common/components';
|
||||||
import { Tooltip } from 'src/common/components/Tooltip';
|
import { Tooltip } from 'src/common/components/Tooltip';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
|
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
|
||||||
import DatasourceModal from 'src/datasource/DatasourceModal';
|
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||||
import Label from 'src/components/Label';
|
|
||||||
import { postForm } from 'src/explore/exploreUtils';
|
import { postForm } from 'src/explore/exploreUtils';
|
||||||
|
|
||||||
import ControlHeader from '../ControlHeader';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
actions: PropTypes.object.isRequired,
|
actions: PropTypes.object.isRequired,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
|
@ -49,44 +44,46 @@ const defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Styles = styled.div`
|
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 {
|
.ant-dropdown-trigger {
|
||||||
margin-left: ${({ theme }) => theme.gridUnit}px;
|
margin-left: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
&:active {
|
&:active {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group .open .dropdown-toggle {
|
.btn-group .open .dropdown-toggle {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
&.button-default {
|
&.button-default {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i.angle {
|
i.angle {
|
||||||
color: ${({ theme }) => theme.colors.primary.base};
|
color: ${({ theme }) => theme.colors.primary.base};
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.datasource-modal-trigger {
|
svg.datasource-modal-trigger {
|
||||||
color: ${({ theme }) => theme.colors.primary.base};
|
color: ${({ theme }) => theme.colors.primary.base};
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.title-select {
|
||||||
.datasource-controls {
|
flex: 1 1 100%;
|
||||||
display: flex;
|
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;
|
||||||
}
|
}
|
||||||
`;
|
.dataset-svg {
|
||||||
|
margin-right: ${({ theme }) => 2 * theme.gridUnit}px;
|
||||||
/**
|
|
||||||
* <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;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -107,7 +104,6 @@ class DatasourceControl extends React.PureComponent {
|
||||||
);
|
);
|
||||||
this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this);
|
this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this);
|
||||||
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
this.toggleShowDatasource = this.toggleShowDatasource.bind(this);
|
||||||
this.renderDatasource = this.renderDatasource.bind(this);
|
|
||||||
this.handleMenuItemClick = this.handleMenuItemClick.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() {
|
render() {
|
||||||
const {
|
const { showChangeDatasourceModal, showEditDatasourceModal } = this.state;
|
||||||
showChangeDatasourceModal,
|
|
||||||
showEditDatasourceModal,
|
|
||||||
showDatasource,
|
|
||||||
} = this.state;
|
|
||||||
const { datasource, onChange } = this.props;
|
const { datasource, onChange } = this.props;
|
||||||
|
|
||||||
const datasourceMenu = (
|
const datasourceMenu = (
|
||||||
<Menu onClick={this.handleMenuItemClick}>
|
<Menu onClick={this.handleMenuItemClick}>
|
||||||
{this.props.isEditable && (
|
{this.props.isEditable && (
|
||||||
|
|
@ -222,20 +169,10 @@ class DatasourceControl extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styles className="DatasourceControl">
|
<Styles className="DatasourceControl">
|
||||||
<ControlHeader {...this.props} />
|
<div className="data-container">
|
||||||
<div className="datasource-controls">
|
<Icon name="dataset-physical" className="dataset-svg" />
|
||||||
<Tooltip title={t('Expand/collapse dataset configuration')}>
|
<Tooltip title={datasource.name}>
|
||||||
<Label
|
<span className="title-select">{datasource.name}</span>
|
||||||
style={{ textTransform: 'none' }}
|
|
||||||
onClick={this.toggleShowDatasource}
|
|
||||||
>
|
|
||||||
{datasource.name}{' '}
|
|
||||||
<i
|
|
||||||
className={`angle fa fa-angle-${
|
|
||||||
showDatasource ? 'up' : 'down'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{healthCheckMessage && (
|
{healthCheckMessage && (
|
||||||
<Tooltip title={healthCheckMessage}>
|
<Tooltip title={healthCheckMessage}>
|
||||||
|
|
@ -259,9 +196,6 @@ class DatasourceControl extends React.PureComponent {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<Collapse in={this.state.showDatasource}>
|
|
||||||
{this.renderDatasource()}
|
|
||||||
</Collapse>
|
|
||||||
{showEditDatasourceModal && (
|
{showEditDatasourceModal && (
|
||||||
<DatasourceModal
|
<DatasourceModal
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue