refactor(explore): Enhance Dataset and Control panel Collapse components (#12218)

This commit is contained in:
Geido 2021-01-26 00:05:19 +01:00 committed by GitHub
parent 0fed1e04ef
commit 1b2611c211
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 107 additions and 240 deletions

View File

@ -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');

View File

@ -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();

View File

@ -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);
});
});
});

View File

@ -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);
});
});

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

@ -224,7 +224,6 @@ h1.section-header {
margin-bottom: 0;
margin-top: 0;
padding-bottom: 5px;
margin-left: -16px;
}
h2.section-header {