mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
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.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 => {
|
||||
|
@ -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', () => {
|
||||
|
@ -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,
|
||||
|
@ -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 { 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>
|
||||
|
@ -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>
|
||||
|
231
superset-frontend/src/explore/components/DatasourcePanel.tsx
Normal file
231
superset-frontend/src/explore/components/DatasourcePanel.tsx
Normal 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;
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user