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.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' }); 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]').find('.Select__control').click();
cy.get('[data-test=time_compare]') cy.get('[data-test=time_compare]')
@ -47,7 +47,7 @@ describe('Advanced analytics', () => {
chartSelector: 'svg', 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]') cy.get('[data-test=time_compare]')
.find('.Select__multi-value__label') .find('.Select__multi-value__label')
.contains('28 days'); .contains('28 days');

View File

@ -112,7 +112,7 @@ describe('VizType control', () => {
// should load mathjs for line chart // should load mathjs for line chart
cy.get('script[src*="mathjs"]').should('have.length', 1); cy.get('script[src*="mathjs"]').should('have.length', 1);
cy.get('script').then(nodes => { 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(); 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 { defaultControls } from 'src/explore/store';
import { getFormDataFromControls } from 'src/explore/controlUtils'; import { getFormDataFromControls } from 'src/explore/controlUtils';
import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer'; import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer';
import ControlPanelSection from 'src/explore/components/ControlPanelSection'; import Collapse from 'src/common/components/Collapse';
describe('ControlPanelsContainer', () => { describe('ControlPanelsContainer', () => {
let wrapper; let wrapper;
@ -91,6 +91,6 @@ describe('ControlPanelsContainer', () => {
it('renders ControlPanelSections', () => { it('renders ControlPanelSections', () => {
wrapper = shallow(<ControlPanelsContainer {...getDefaultProps()} />); 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; light?: boolean;
bigger?: boolean; bigger?: boolean;
bold?: boolean; bold?: boolean;
animateArrows?: boolean;
} }
const Collapse = Object.assign( const Collapse = Object.assign(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
styled(({ light, bigger, bold, ...props }: CollapseProps) => ( styled(({ light, bigger, bold, animateArrows, ...props }: CollapseProps) => (
<AntdCollapse {...props} /> <AntdCollapse {...props} />
))` ))`
height: 100%; height: 100%;
@ -48,6 +49,20 @@ const Collapse = Object.assign(
font-size: ${({ bigger, theme }) => font-size: ${({ bigger, theme }) =>
bigger ? `${theme.gridUnit * 4}px` : 'inherit'}; 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, theme }) =>
light && light &&
` `
@ -56,6 +71,13 @@ const Collapse = Object.assign(
color: ${theme.colors.grayscale.light4}; color: ${theme.colors.grayscale.light4};
} }
`} `}
${({ ghost, bordered, theme }) =>
ghost &&
bordered &&
`
border-bottom: 1px solid ${theme.colors.grayscale.light3};
`}
} }
.ant-collapse-content { .ant-collapse-content {
height: 100%; 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, Panel: AntdCollapse.Panel,

View File

@ -322,6 +322,16 @@ export const CollapseTextLight = () => (
</Collapse.Panel> </Collapse.Panel>
</Collapse> </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() { export function StyledCronPicker() {
// @ts-ignore // @ts-ignore
const inputRef = useRef<Input>(null); 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 { t, styled, getChartControlPanelRegistry } from '@superset-ui/core';
import Tabs from 'src/common/components/Tabs'; import Tabs from 'src/common/components/Tabs';
import { Collapse } from 'src/common/components';
import { PluginContext } from 'src/components/DynamicPlugins'; import { PluginContext } from 'src/components/DynamicPlugins';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import ControlPanelSection from './ControlPanelSection'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
import ControlRow from './ControlRow'; import ControlRow from './ControlRow';
import Control from './Control'; import Control from './Control';
import { sectionsToRender } from '../controlUtils'; import { sectionsToRender } from '../controlUtils';
@ -75,8 +76,10 @@ const ControlPanelsTabs = styled(Tabs)`
.ant-tabs-content-holder { .ant-tabs-content-holder {
overflow: visible; overflow: visible;
} }
.ant-tabs-tabpane {
height: 100%;
}
`; `;
class ControlPanelsContainer extends React.Component { class ControlPanelsContainer extends React.Component {
// trigger updates to the component when async plugins load // trigger updates to the component when async plugins load
static contextType = PluginContext; 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() { removeAlert() {
this.props.actions.removeControlPanelAlert(); this.props.actions.removeControlPanelAlert();
} }
@ -137,6 +147,7 @@ class ControlPanelsContainer extends React.Component {
renderControlPanelSection(section) { renderControlPanelSection(section) {
const { controls } = this.props; const { controls } = this.props;
const { label, description } = section;
const hasErrors = section.controlSetRows.some(rows => const hasErrors = section.controlSetRows.some(rows =>
rows.some( rows.some(
@ -146,14 +157,27 @@ class ControlPanelsContainer extends React.Component {
controls[s].validationErrors.length > 0, 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 ( return (
<ControlPanelSection <Collapse.Panel
className="control-panel-section"
header={PanelHeader()}
key={section.label} key={section.label}
label={section.label}
startExpanded={section.expanded}
hasErrors={hasErrors}
description={section.description}
> >
{section.controlSetRows.map((controlSets, i) => { {section.controlSetRows.map((controlSets, i) => {
const renderedControls = controlSets 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); displaySectionsToRender.push(section);
} }
}); });
const showCustomizeTab = displaySectionsToRender.length > 0; const showCustomizeTab = displaySectionsToRender.length > 0;
const expandedQuerySections = this.sectionsToExpand(querySectionsToRender);
const expandedCustomSections = this.sectionsToExpand(
displaySectionsToRender,
);
return ( return (
<Styles> <Styles>
{this.props.alert && ( {this.props.alert && (
@ -245,11 +275,25 @@ class ControlPanelsContainer extends React.Component {
fullWidth={showCustomizeTab} fullWidth={showCustomizeTab}
> >
<Tabs.TabPane key="query" tab={t('Data')}> <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> </Tabs.TabPane>
{showCustomizeTab && ( {showCustomizeTab && (
<Tabs.TabPane key="display" tab={t('Customize')}> <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> </Tabs.TabPane>
)} )}
</ControlPanelsTabs> </ControlPanelsTabs>

View File

@ -83,44 +83,9 @@ const DatasourceContainer = styled.div`
max-height: 100%; max-height: 100%;
.ant-collapse { .ant-collapse {
height: auto; 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 { .field-selections {
padding: ${({ theme }) => padding: ${({ theme }) => `0 0 ${4 * theme.gridUnit}px`};
`${2 * theme.gridUnit}px ${2 * theme.gridUnit}px ${
4 * theme.gridUnit
}px`};
overflow: auto; overflow: auto;
} }
.field-length { .field-length {
@ -247,9 +212,10 @@ export default function DataSourcePanel({
/> />
<div className="field-selections"> <div className="field-selections">
<Collapse <Collapse
bordered={false} bordered
defaultActiveKey={['metrics', 'column']} defaultActiveKey={['metrics', 'column']}
expandIconPosition="right" expandIconPosition="right"
ghost
> >
<Collapse.Panel <Collapse.Panel
header={<span className="header">{t('Metrics')}</span>} header={<span className="header">{t('Metrics')}</span>}

View File

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