mirror of https://github.com/apache/superset.git
refactor(explore): Enhance Dataset and Control panel Collapse components (#12218)
This commit is contained in:
parent
0fed1e04ef
commit
1b2611c211
|
@ -28,7 +28,7 @@ describe('Advanced analytics', () => {
|
|||
cy.visitChartByName('Num Births Trend');
|
||||
cy.verifySliceSuccess({ waitAlias: '@postJson' });
|
||||
|
||||
cy.get('.panel-title').contains('Advanced Analytics').click();
|
||||
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
||||
|
||||
cy.get('[data-test=time_compare]').find('.Select__control').click();
|
||||
cy.get('[data-test=time_compare]')
|
||||
|
@ -47,7 +47,7 @@ describe('Advanced analytics', () => {
|
|||
chartSelector: 'svg',
|
||||
});
|
||||
|
||||
cy.get('.panel-title').contains('Advanced Analytics').click();
|
||||
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
||||
cy.get('[data-test=time_compare]')
|
||||
.find('.Select__multi-value__label')
|
||||
.contains('28 days');
|
||||
|
|
|
@ -112,7 +112,7 @@ describe('VizType control', () => {
|
|||
// should load mathjs for line chart
|
||||
cy.get('script[src*="mathjs"]').should('have.length', 1);
|
||||
cy.get('script').then(nodes => {
|
||||
expect(nodes.length).to.greaterThan(numScripts);
|
||||
expect(nodes.length).to.eq(numScripts);
|
||||
});
|
||||
|
||||
cy.get('button[data-test="run-query-button"]').click();
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/**
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import ControlPanelSection from 'src/explore/components/ControlPanelSection';
|
||||
|
||||
const defaultProps = {
|
||||
children: <div>a child element</div>,
|
||||
};
|
||||
|
||||
const optionalProps = {
|
||||
label: 'my label',
|
||||
description: 'my description',
|
||||
tooltip: 'my tooltip',
|
||||
};
|
||||
|
||||
describe('ControlPanelSection', () => {
|
||||
let wrapper;
|
||||
let props;
|
||||
it('is a valid element', () => {
|
||||
expect(
|
||||
React.isValidElement(<ControlPanelSection {...defaultProps} />),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a Panel component', () => {
|
||||
wrapper = shallow(<ControlPanelSection {...defaultProps} />);
|
||||
expect(wrapper.find(Panel)).toExist();
|
||||
});
|
||||
|
||||
describe('with optional props', () => {
|
||||
beforeEach(() => {
|
||||
props = Object.assign(defaultProps, optionalProps);
|
||||
wrapper = shallow(<ControlPanelSection {...props} />);
|
||||
});
|
||||
|
||||
it('renders a label if present', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="clickable-control-panel-section-title"]')
|
||||
.text(),
|
||||
).toContain('my label');
|
||||
});
|
||||
|
||||
it('renders a InfoTooltipWithTrigger if label and tooltip is present', () => {
|
||||
expect(
|
||||
wrapper.find(Panel).dive().find(InfoTooltipWithTrigger),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,7 +22,7 @@ import { getChartControlPanelRegistry, t } from '@superset-ui/core';
|
|||
import { defaultControls } from 'src/explore/store';
|
||||
import { getFormDataFromControls } from 'src/explore/controlUtils';
|
||||
import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer';
|
||||
import ControlPanelSection from 'src/explore/components/ControlPanelSection';
|
||||
import Collapse from 'src/common/components/Collapse';
|
||||
|
||||
describe('ControlPanelsContainer', () => {
|
||||
let wrapper;
|
||||
|
@ -91,6 +91,6 @@ describe('ControlPanelsContainer', () => {
|
|||
|
||||
it('renders ControlPanelSections', () => {
|
||||
wrapper = shallow(<ControlPanelsContainer {...getDefaultProps()} />);
|
||||
expect(wrapper.find(ControlPanelSection)).toHaveLength(5);
|
||||
expect(wrapper.find(Collapse.Panel)).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,11 +26,12 @@ interface CollapseProps extends AntdCollapseProps {
|
|||
light?: boolean;
|
||||
bigger?: boolean;
|
||||
bold?: boolean;
|
||||
animateArrows?: boolean;
|
||||
}
|
||||
|
||||
const Collapse = Object.assign(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
styled(({ light, bigger, bold, ...props }: CollapseProps) => (
|
||||
styled(({ light, bigger, bold, animateArrows, ...props }: CollapseProps) => (
|
||||
<AntdCollapse {...props} />
|
||||
))`
|
||||
height: 100%;
|
||||
|
@ -48,6 +49,20 @@ const Collapse = Object.assign(
|
|||
font-size: ${({ bigger, theme }) =>
|
||||
bigger ? `${theme.gridUnit * 4}px` : 'inherit'};
|
||||
|
||||
.ant-collapse-arrow svg {
|
||||
transition: ${({ animateArrows }) =>
|
||||
animateArrows ? 'transform 0.24s' : 'none'};
|
||||
}
|
||||
|
||||
${({ expandIconPosition }) =>
|
||||
expandIconPosition &&
|
||||
expandIconPosition === 'right' &&
|
||||
`
|
||||
.anticon.anticon-right.ant-collapse-arrow > svg {
|
||||
transform: rotate(90deg) !important;
|
||||
}
|
||||
`}
|
||||
|
||||
${({ light, theme }) =>
|
||||
light &&
|
||||
`
|
||||
|
@ -56,6 +71,13 @@ const Collapse = Object.assign(
|
|||
color: ${theme.colors.grayscale.light4};
|
||||
}
|
||||
`}
|
||||
|
||||
${({ ghost, bordered, theme }) =>
|
||||
ghost &&
|
||||
bordered &&
|
||||
`
|
||||
border-bottom: 1px solid ${theme.colors.grayscale.light3};
|
||||
`}
|
||||
}
|
||||
.ant-collapse-content {
|
||||
height: 100%;
|
||||
|
@ -68,6 +90,18 @@ const Collapse = Object.assign(
|
|||
}
|
||||
}
|
||||
}
|
||||
.ant-collapse-item-active {
|
||||
.ant-collapse-header {
|
||||
${({ expandIconPosition }) =>
|
||||
expandIconPosition &&
|
||||
expandIconPosition === 'right' &&
|
||||
`
|
||||
.anticon.anticon-right.ant-collapse-arrow > svg {
|
||||
transform: rotate(-90deg) !important;
|
||||
}
|
||||
`}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
Panel: AntdCollapse.Panel,
|
||||
|
|
|
@ -322,6 +322,16 @@ export const CollapseTextLight = () => (
|
|||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
);
|
||||
export const CollapseAnimateArrows = () => (
|
||||
<Collapse animateArrows defaultActiveKey={['1']}>
|
||||
<Collapse.Panel header="Hi! I am a header" key="1">
|
||||
Hi! I am a sample content
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header="Hi! I am another header" key="2">
|
||||
Hi! I am another sample content
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
);
|
||||
export function StyledCronPicker() {
|
||||
// @ts-ignore
|
||||
const inputRef = useRef<Input>(null);
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
/**
|
||||
* 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 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,
|
||||
description: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
startExpanded: PropTypes.bool,
|
||||
hasErrors: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
label: null,
|
||||
description: null,
|
||||
startExpanded: 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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { expanded: this.props.startExpanded };
|
||||
this.toggleExpand = this.toggleExpand.bind(this);
|
||||
}
|
||||
|
||||
toggleExpand() {
|
||||
this.setState(prevState => ({ expanded: !prevState.expanded }));
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const { label, description, hasErrors } = this.props;
|
||||
return (
|
||||
label && (
|
||||
<div>
|
||||
<span>
|
||||
<span
|
||||
data-test="clickable-control-panel-section-title"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={this.toggleExpand}
|
||||
>
|
||||
{label}
|
||||
</span>{' '}
|
||||
{description && (
|
||||
<InfoTooltipWithTrigger label={label} tooltip={description} />
|
||||
)}
|
||||
{hasErrors && (
|
||||
<InfoTooltipWithTrigger
|
||||
label="validation-errors"
|
||||
bsStyle="danger"
|
||||
tooltip="This section contains validation errors"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<i
|
||||
role="button"
|
||||
aria-label="Toggle expand"
|
||||
tabIndex={0}
|
||||
className={`float-right fa-lg text-primary expander fa fa-angle-${
|
||||
this.state.expanded ? 'up' : 'down'
|
||||
}`}
|
||||
onClick={this.toggleExpand.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
className="control-panel-section"
|
||||
expanded={this.state.expanded}
|
||||
onToggle={this.toggleExpand}
|
||||
>
|
||||
<Panel.Heading>
|
||||
<StyledPanelTitle>{this.renderHeader()}</StyledPanelTitle>
|
||||
</Panel.Heading>
|
||||
<Panel.Collapse>
|
||||
<Panel.Body>{this.props.children}</Panel.Body>
|
||||
</Panel.Collapse>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ControlPanelSection.propTypes = propTypes;
|
||||
ControlPanelSection.defaultProps = defaultProps;
|
|
@ -25,9 +25,10 @@ import { Alert } from 'react-bootstrap';
|
|||
import { t, styled, getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
|
||||
import Tabs from 'src/common/components/Tabs';
|
||||
import { Collapse } from 'src/common/components';
|
||||
import { PluginContext } from 'src/components/DynamicPlugins';
|
||||
import Loading from 'src/components/Loading';
|
||||
import ControlPanelSection from './ControlPanelSection';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import ControlRow from './ControlRow';
|
||||
import Control from './Control';
|
||||
import { sectionsToRender } from '../controlUtils';
|
||||
|
@ -75,8 +76,10 @@ const ControlPanelsTabs = styled(Tabs)`
|
|||
.ant-tabs-content-holder {
|
||||
overflow: visible;
|
||||
}
|
||||
.ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
class ControlPanelsContainer extends React.Component {
|
||||
// trigger updates to the component when async plugins load
|
||||
static contextType = PluginContext;
|
||||
|
@ -96,6 +99,13 @@ class ControlPanelsContainer extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
sectionsToExpand(sections) {
|
||||
return sections.reduce(
|
||||
(acc, cur) => (cur.expanded ? [...acc, cur.label] : acc),
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
removeAlert() {
|
||||
this.props.actions.removeControlPanelAlert();
|
||||
}
|
||||
|
@ -137,6 +147,7 @@ class ControlPanelsContainer extends React.Component {
|
|||
|
||||
renderControlPanelSection(section) {
|
||||
const { controls } = this.props;
|
||||
const { label, description } = section;
|
||||
|
||||
const hasErrors = section.controlSetRows.some(rows =>
|
||||
rows.some(
|
||||
|
@ -146,14 +157,27 @@ class ControlPanelsContainer extends React.Component {
|
|||
controls[s].validationErrors.length > 0,
|
||||
),
|
||||
);
|
||||
const PanelHeader = () => (
|
||||
<span>
|
||||
<span>{label}</span>{' '}
|
||||
{description && (
|
||||
<InfoTooltipWithTrigger label={label} tooltip={description} />
|
||||
)}
|
||||
{hasErrors && (
|
||||
<InfoTooltipWithTrigger
|
||||
label="validation-errors"
|
||||
bsStyle="danger"
|
||||
tooltip="This section contains validation errors"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<ControlPanelSection
|
||||
<Collapse.Panel
|
||||
className="control-panel-section"
|
||||
header={PanelHeader()}
|
||||
key={section.label}
|
||||
label={section.label}
|
||||
startExpanded={section.expanded}
|
||||
hasErrors={hasErrors}
|
||||
description={section.description}
|
||||
>
|
||||
{section.controlSetRows.map((controlSets, i) => {
|
||||
const renderedControls = controlSets
|
||||
|
@ -188,7 +212,7 @@ class ControlPanelsContainer extends React.Component {
|
|||
/>
|
||||
);
|
||||
})}
|
||||
</ControlPanelSection>
|
||||
</Collapse.Panel>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -223,7 +247,13 @@ class ControlPanelsContainer extends React.Component {
|
|||
displaySectionsToRender.push(section);
|
||||
}
|
||||
});
|
||||
|
||||
const showCustomizeTab = displaySectionsToRender.length > 0;
|
||||
const expandedQuerySections = this.sectionsToExpand(querySectionsToRender);
|
||||
const expandedCustomSections = this.sectionsToExpand(
|
||||
displaySectionsToRender,
|
||||
);
|
||||
|
||||
return (
|
||||
<Styles>
|
||||
{this.props.alert && (
|
||||
|
@ -245,11 +275,25 @@ class ControlPanelsContainer extends React.Component {
|
|||
fullWidth={showCustomizeTab}
|
||||
>
|
||||
<Tabs.TabPane key="query" tab={t('Data')}>
|
||||
{querySectionsToRender.map(this.renderControlPanelSection)}
|
||||
<Collapse
|
||||
bordered
|
||||
defaultActiveKey={expandedQuerySections}
|
||||
expandIconPosition="right"
|
||||
ghost
|
||||
>
|
||||
{querySectionsToRender.map(this.renderControlPanelSection)}
|
||||
</Collapse>
|
||||
</Tabs.TabPane>
|
||||
{showCustomizeTab && (
|
||||
<Tabs.TabPane key="display" tab={t('Customize')}>
|
||||
{displaySectionsToRender.map(this.renderControlPanelSection)}
|
||||
<Collapse
|
||||
bordered
|
||||
defaultActiveKey={expandedCustomSections}
|
||||
expandIconPosition="right"
|
||||
ghost
|
||||
>
|
||||
{displaySectionsToRender.map(this.renderControlPanelSection)}
|
||||
</Collapse>
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
</ControlPanelsTabs>
|
||||
|
|
|
@ -83,44 +83,9 @@ const DatasourceContainer = styled.div`
|
|||
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;
|
||||
}
|
||||
.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 ${2 * theme.gridUnit}px ${
|
||||
4 * theme.gridUnit
|
||||
}px`};
|
||||
padding: ${({ theme }) => `0 0 ${4 * theme.gridUnit}px`};
|
||||
overflow: auto;
|
||||
}
|
||||
.field-length {
|
||||
|
@ -247,9 +212,10 @@ export default function DataSourcePanel({
|
|||
/>
|
||||
<div className="field-selections">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
bordered
|
||||
defaultActiveKey={['metrics', 'column']}
|
||||
expandIconPosition="right"
|
||||
ghost
|
||||
>
|
||||
<Collapse.Panel
|
||||
header={<span className="header">{t('Metrics')}</span>}
|
||||
|
|
|
@ -224,7 +224,6 @@ h1.section-header {
|
|||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
padding-bottom: 5px;
|
||||
margin-left: -16px;
|
||||
}
|
||||
|
||||
h2.section-header {
|
||||
|
|
Loading…
Reference in New Issue