Migrates Label component from Bootstrap to AntD. (#12774)

This commit is contained in:
Michael S. Molina 2021-02-02 03:15:07 -03:00 committed by GitHub
parent 388edbf7b2
commit 2adfb85597
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 162 additions and 225 deletions

View File

@ -104,7 +104,7 @@ describe('VizType control', () => {
numScripts = nodes.length;
});
cy.get('.Control .label').contains('Table').click();
cy.get('[data-test="visualization-type"]').contains('Table').click();
cy.get('[role="button"]').contains('Line Chart').click();

View File

@ -161,7 +161,7 @@ describe('Visualization > Table', () => {
cy.verifySliceContainer('table');
expect(response?.body.result[0].data.length).to.eq(limit);
});
cy.get('span.label-danger').contains('10 rows');
cy.get('[data-test="row-count-label"]').contains('10 rows');
});
it('Test table with columns and row limit', () => {

View File

@ -21,7 +21,7 @@
*/
import React from 'react';
import { render, sleep, waitFor } from 'spec/helpers/testing-library';
import Timer from 'src/components/Timer';
import Timer, { TimerProps } from 'src/components/Timer';
import { now } from 'src/modules/dates';
function parseTime(text?: string | null) {
@ -29,7 +29,7 @@ function parseTime(text?: string | null) {
}
describe('Timer', () => {
const mockProps = {
const mockProps: TimerProps = {
startTime: now(),
endTime: undefined,
isRunning: true,
@ -41,7 +41,6 @@ describe('Timer', () => {
const node = screen.getByRole('timer');
let text = node.textContent || '';
expect(node).toBeInTheDocument();
expect(node).toHaveClass('label-warning');
expect(node).toHaveTextContent('00:00:00.00');
// should start running
await waitFor(() => {

View File

@ -45,6 +45,6 @@ describe('RowCountLabel', () => {
limit: 100,
};
const wrapper = shallow(<RowCountLabel {...props} />);
expect(wrapper.find(Label).first().props().bsStyle).toBe('danger');
expect(wrapper.find(Label).first().props().type).toBe('danger');
});
});

View File

@ -19,7 +19,7 @@
import React from 'react';
import { styledMount as mount } from 'spec/helpers/theming';
import Security from 'src/profile/components/Security';
import Label from 'src/components/Label';
import { user, userNoPerms } from './fixtures';
describe('Security', () => {
@ -31,19 +31,19 @@ describe('Security', () => {
});
it('renders 2 role labels', () => {
const wrapper = mount(<Security {...mockedProps} />);
expect(wrapper.find('.roles').find('.label')).toHaveLength(2);
expect(wrapper.find('.roles').find(Label)).toHaveLength(2);
});
it('renders 2 datasource labels', () => {
const wrapper = mount(<Security {...mockedProps} />);
expect(wrapper.find('.datasources').find('.label')).toHaveLength(2);
expect(wrapper.find('.datasources').find(Label)).toHaveLength(2);
});
it('renders 3 database labels', () => {
const wrapper = mount(<Security {...mockedProps} />);
expect(wrapper.find('.databases').find('.label')).toHaveLength(3);
expect(wrapper.find('.databases').find(Label)).toHaveLength(3);
});
it('renders no permission label when empty', () => {
const wrapper = mount(<Security user={userNoPerms} />);
expect(wrapper.find('.datasources').find('.label')).not.toExist();
expect(wrapper.find('.databases').find('.label')).not.toExist();
expect(wrapper.find('.datasources').find(Label)).not.toExist();
expect(wrapper.find('.databases').find(Label)).not.toExist();
});
});

View File

@ -20,16 +20,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import Label from 'src/components/Label';
import { STATE_BSSTYLE_MAP } from '../constants';
import { STATE_TYPE_MAP } from '../constants';
const propTypes = {
query: PropTypes.object.isRequired,
};
export default function QueryStateLabel({ query }) {
const bsStyle = STATE_BSSTYLE_MAP[query.state];
const type = STATE_TYPE_MAP[query.state];
return (
<Label className="m-r-3" bsStyle={bsStyle}>
<Label className="m-r-3" type={type}>
{query.state}
</Label>
);

View File

@ -143,7 +143,7 @@ const QueryTable = props => {
<ModalTrigger
className="ResultsModal"
triggerNode={
<Label bsStyle="info" className="pointer">
<Label type="info" className="pointer">
{t('View results')}
</Label>
}

View File

@ -33,7 +33,7 @@ import QueryHistory from './QueryHistory';
import ResultSet from './ResultSet';
import {
STATUS_OPTIONS,
STATE_BSSTYLE_MAP,
STATE_TYPE_MAP,
LOCALSTORAGE_MAX_QUERY_AGE_MS,
} from '../constants';
@ -97,10 +97,7 @@ export class SouthPane extends React.PureComponent {
render() {
if (this.props.offline) {
return (
<Label
className="m-r-3"
bsStyle={STATE_BSSTYLE_MAP[STATUS_OPTIONS.offline]}
>
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
{STATUS_OPTIONS.offline}
</Label>
);

View File

@ -71,7 +71,7 @@ import ShareSqlLabQuery from './ShareSqlLabQuery';
import SqlEditorLeftBar from './SqlEditorLeftBar';
import AceEditorWrapper from './AceEditorWrapper';
import {
STATE_BSSTYLE_MAP,
STATE_TYPE_MAP,
SQL_EDITOR_GUTTER_HEIGHT,
SQL_EDITOR_GUTTER_MARGIN,
SQL_TOOLBAR_HEIGHT,
@ -575,7 +575,7 @@ class SqlEditor extends React.PureComponent {
this.props.latestQuery.rows,
)}
>
<Label bsStyle="warning">LIMIT</Label>
<Label type="warning">LIMIT</Label>
</Tooltip>
);
}
@ -670,7 +670,7 @@ class SqlEditor extends React.PureComponent {
<Timer
startTime={this.props.latestQuery.startDttm}
endTime={this.props.latestQuery.endDttm}
state={STATE_BSSTYLE_MAP[this.props.latestQuery.state]}
state={STATE_TYPE_MAP[this.props.latestQuery.state]}
isRunning={this.props.latestQuery.state === 'running'}
/>
)}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
export const STATE_BSSTYLE_MAP = {
export const STATE_TYPE_MAP = {
offline: 'danger',
failed: 'danger',
pending: 'info',

View File

@ -134,7 +134,7 @@ export default class AddSliceContainer extends React.PureComponent<
name="select-vis-type"
onChange={this.changeVisType}
value={this.state.visType}
labelBsStyle="primary"
labelType="primary"
/>
</div>
<br />

View File

@ -52,6 +52,7 @@ export {
Select,
Skeleton,
Switch,
Tag,
Tabs,
Tooltip,
Input as AntdInput,

View File

@ -67,12 +67,12 @@ class CacheLabel extends React.PureComponent {
}
render() {
const labelStyle = this.state.hovered ? 'primary' : 'default';
const labelType = this.state.hovered ? 'primary' : 'default';
return (
<TooltipWrapper tooltip={this.state.tooltipContent} label="cache-desc">
<Label
className={`${this.props.className}`}
bsStyle={labelStyle}
type={labelType}
onClick={this.props.onClick}
onMouseOver={this.mouseOver.bind(this)}
onMouseOut={this.mouseOut.bind(this)}

View File

@ -54,6 +54,11 @@ const DatabaseSelectorWrapper = styled.div`
}
`;
const DatabaseOption = styled.span`
display: inline-flex;
align-items: center;
`;
interface DatabaseSelectorProps {
dbId: number;
formMode?: boolean;
@ -184,9 +189,9 @@ export default function DatabaseSelector({
function renderDatabaseOption(db: any) {
return (
<span title={db.database_name}>
<Label bsStyle="default">{db.backend}</Label> {db.database_name}
</span>
<DatabaseOption title={db.database_name}>
<Label type="default">{db.backend}</Label> {db.database_name}
</DatabaseOption>
);
}

View File

@ -18,59 +18,52 @@
*/
import React from 'react';
import { action } from '@storybook/addon-actions';
import { withKnobs, select, boolean, text } from '@storybook/addon-knobs';
import Label from './index';
import Label, { Type } from './index';
export default {
title: 'Label',
component: Label,
decorators: [withKnobs],
excludeStories: /.*Knob$/,
excludeStories: 'options',
};
export const bsStyleKnob = {
label: 'Types',
options: {
default: 'default',
info: 'info',
success: 'success',
warning: 'warning',
danger: 'danger',
secondary: 'secondary',
primary: 'primary',
},
defaultValue: 'default',
};
export const options = [
'default',
'info',
'success',
'warning',
'danger',
'primary',
'secondary',
];
export const LabelGallery = () => (
<>
<h4>Non-interactive</h4>
{Object.values(bsStyleKnob.options).map(opt => (
<Label key={opt} bsStyle={opt}>
{Object.values(options).map((opt: Type) => (
<Label key={opt} type={opt}>
{`style: "${opt}"`}
</Label>
))}
<br />
<h4>Interactive</h4>
{Object.values(bsStyleKnob.options).map(opt => (
<Label key={opt} bsStyle={opt} onClick={action('clicked')}>
{Object.values(options).map((opt: Type) => (
<Label key={opt} type={opt} onClick={action('clicked')}>
{`style: "${opt}"`}
</Label>
))}
</>
);
export const InteractiveLabel = () => (
<Label
bsStyle={select(
bsStyleKnob.label,
bsStyleKnob.options,
bsStyleKnob.defaultValue,
)}
onClick={
boolean('Has onClick action', false) ? action('clicked') : undefined
}
>
{text('Label', 'Label!')}
</Label>
);
export const InteractiveLabel = (args: any) => {
const { hasOnClick, label, ...rest } = args;
return (
<Label onClick={hasOnClick ? action('clicked') : undefined} {...rest}>
{label}
</Label>
);
};
InteractiveLabel.args = {
hasOnClick: true,
label: 'Example',
};

View File

@ -21,7 +21,7 @@ import React from 'react';
import { ReactWrapper } from 'enzyme';
import { styledMount as mount } from 'spec/helpers/theming';
import Label from '.';
import { LabelGallery, bsStyleKnob } from './Label.stories';
import { LabelGallery, options } from './Label.stories';
describe('Label', () => {
let wrapper: ReactWrapper;
@ -34,23 +34,13 @@ describe('Label', () => {
it('works with an onClick handler', () => {
const mockAction = jest.fn();
wrapper = mount(<Label onClick={mockAction} />);
wrapper.find('.label').simulate('click');
wrapper.find(Label).simulate('click');
expect(mockAction).toHaveBeenCalled();
});
// test stories from the storybook!
it('renders all the sorybook gallery variants', () => {
it('renders all the storybook gallery variants', () => {
wrapper = mount(<LabelGallery />);
Object.values(bsStyleKnob.options).forEach(opt => {
expect(wrapper.find(`.label-${opt}`).at(0).text()).toEqual(
`style: "${opt}"`,
);
});
});
// test things NOT in the storybook!
it('renders custom label styles without melting', () => {
wrapper = mount(<Label bsStyle="foobar" />);
expect(wrapper.find('Label.label-foobar')).toHaveLength(1);
expect(wrapper.find(Label).length).toEqual(options.length * 2);
});
});

View File

@ -17,149 +17,98 @@
* under the License.
*/
import React, { CSSProperties } from 'react';
import { Label as BootstrapLabel } from 'react-bootstrap';
import { styled } from '@superset-ui/core';
import cx from 'classnames';
import { Tag } from 'src/common/components';
import { useTheme } from '@superset-ui/core';
export type OnClickHandler = React.MouseEventHandler<BootstrapLabel>;
export type OnClickHandler = React.MouseEventHandler<HTMLElement>;
export type Type =
| 'success'
| 'warning'
| 'danger'
| 'info'
| 'default'
| 'primary'
| 'secondary';
export interface LabelProps {
key?: string;
className?: string;
id?: string;
tooltip?: string;
placement?: string;
onClick?: OnClickHandler;
bsStyle?: string;
type?: Type;
style?: CSSProperties;
children?: React.ReactNode;
role?: string;
}
const SupersetLabel = styled(BootstrapLabel)`
/* un-bunch them! */
margin-right: ${({ theme }) => theme.gridUnit}px;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
&:first-of-type {
margin-left: 0;
}
&:last-of-type {
margin-right: 0;
}
display: inline-block;
border-width: 1px;
border-style: solid;
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
transition: background-color ${({ theme }) => theme.transitionTiming}s;
&.label-warning {
background-color: ${({ theme }) => theme.colors.warning.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.warning.dark1 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.warning.dark1 : theme.colors.warning.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.warning.dark2 : 'transparent'};
}
}
&.label-danger {
background-color: ${({ theme }) => theme.colors.error.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.error.dark1 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.error.dark1 : theme.colors.error.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.error.dark2 : 'transparent'};
}
}
&.label-success {
background-color: ${({ theme }) => theme.colors.success.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.success.dark1 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.success.dark1 : theme.colors.success.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.success.dark2 : 'transparent'};
}
}
&.label-default {
background-color: ${({ theme }) => theme.colors.grayscale.light3};
color: ${({ theme }) => theme.colors.grayscale.dark1};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.grayscale.light2 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.primary.light2 : theme.colors.grayscale.light3};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.primary.light1 : 'transparent'};
}
}
&.label-info {
background-color: ${({ theme }) => theme.colors.info};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.info.dark1 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.info.dark1 : theme.colors.info.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.info.dark2 : 'transparent'};
}
}
&.label-primary {
background-color: ${({ theme }) => theme.colors.primary.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.primary.dark1 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.primary.dark2 : theme.colors.primary.base};
border-color: ${({ theme, onClick }) =>
onClick
? theme.colors.primary.dark2
: 'transparent'}; /* would be nice if we had a darker color, but that's the floor! */
}
}
/* note this is NOT a supported bootstrap label Style... this overrides default */
&.label-secondary {
background-color: ${({ theme }) => theme.colors.secondary.base};
color: ${({ theme }) => theme.colors.grayscale.light4};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.secondary.dark1 : 'transparent'};
&:hover {
background-color: ${({ theme, onClick }) =>
onClick ? theme.colors.secondary.dark1 : theme.colors.secondary.base};
border-color: ${({ theme, onClick }) =>
onClick ? theme.colors.secondary.dark2 : 'transparent'};
}
}
`;
export default function Label(props: LabelProps) {
const officialBootstrapStyles = [
'success',
'warning',
'danger',
'info',
'default',
'primary',
];
const labelProps = {
...props,
placement: props.placement || 'top',
bsStyle: officialBootstrapStyles.includes(props.bsStyle || '')
? props.bsStyle
: 'default',
className: cx(props.className, {
[`label-${props.bsStyle}`]: !officialBootstrapStyles.includes(
props.bsStyle || '',
),
}),
};
return <SupersetLabel {...labelProps}>{props.children}</SupersetLabel>;
const theme = useTheme();
const { colors, transitionTiming } = theme;
const { type, onClick, children, ...rest } = props;
const {
primary,
secondary,
grayscale,
success,
warning,
error,
info,
} = colors;
let backgroundColor = grayscale.light3;
let backgroundColorHover = onClick ? primary.light2 : grayscale.light3;
let borderColor = onClick ? grayscale.light2 : 'transparent';
let borderColorHover = onClick ? primary.light1 : 'transparent';
let color = grayscale.dark1;
if (type && type !== 'default') {
color = grayscale.light4;
let baseColor;
if (type === 'success') {
baseColor = success;
} else if (type === 'warning') {
baseColor = warning;
} else if (type === 'danger') {
baseColor = error;
} else if (type === 'info') {
baseColor = info;
} else if (type === 'secondary') {
baseColor = secondary;
} else {
baseColor = primary;
}
backgroundColor = baseColor.base;
backgroundColorHover = onClick ? baseColor.dark1 : baseColor.base;
borderColor = onClick ? baseColor.dark1 : 'transparent';
borderColorHover = onClick ? baseColor.dark2 : 'transparent';
}
return (
<Tag
css={{
transition: `background-color ${transitionTiming}s`,
whiteSpace: 'nowrap',
cursor: onClick ? 'pointer' : 'default',
overflow: 'hidden',
textOverflow: 'ellipsis',
backgroundColor,
borderColor,
borderRadius: 21,
padding: '0.35em 0.8em',
lineHeight: 1,
color,
'&:hover': {
backgroundColor: backgroundColorHover,
borderColor: borderColorHover,
opacity: 1,
},
}}
onClick={onClick}
{...rest}
>
{children}
</Tag>
);
}

View File

@ -18,19 +18,18 @@
*/
import React, { useEffect, useRef, useState } from 'react';
import { styled } from '@superset-ui/core';
import Label from 'src/components/Label';
import Label, { Type } from 'src/components/Label';
import { now, fDuration } from 'src/modules/dates';
interface TimerProps {
export interface TimerProps {
endTime?: number;
isRunning: boolean;
startTime?: number;
status?: string;
status?: Type;
}
const TimerLabel = styled(Label)`
line-height: 13px;
text-align: left;
width: 91px;
`;
@ -69,7 +68,7 @@ export default function Timer({
}, [endTime, isRunning, startTime]);
return (
<TimerLabel bsStyle={status} role="timer">
<TimerLabel type={status} role="timer">
{clockStr}
</TimerLabel>
);

View File

@ -37,7 +37,7 @@ const defaultProps = {
export default function RowCountLabel({ rowcount, limit, suffix, loading }) {
const limitReached = rowcount === limit;
const bsStyle =
const type =
limitReached || (rowcount === 0 && !loading) ? 'danger' : 'default';
const formattedRowCount = getNumberFormatter()(rowcount);
const tooltip = (
@ -48,7 +48,7 @@ export default function RowCountLabel({ rowcount, limit, suffix, loading }) {
);
return (
<TooltipWrapper label="tt-rowcount" tooltip={tooltip}>
<Label bsStyle={bsStyle}>
<Label type={type} data-test="row-count-label">
{loading ? 'Loading...' : `${formattedRowCount} ${suffix}`}
</Label>
</TooltipWrapper>

View File

@ -34,12 +34,12 @@ const propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func,
value: PropTypes.string.isRequired,
labelBsStyle: PropTypes.string,
labelType: PropTypes.string,
};
const defaultProps = {
onChange: () => {},
labelBsStyle: 'default',
labelType: 'default',
};
const registry = getChartMetadataRegistry();
@ -162,7 +162,7 @@ const VizTypeControl = props => {
);
};
const { value, labelBsStyle } = props;
const { value, labelType } = props;
const filterString = filter.toLowerCase();
const filteredTypes = DEFAULT_ORDER.filter(type => registry.has(type))
@ -201,7 +201,11 @@ const VizTypeControl = props => {
title={t('Click to change visualization type')}
>
<>
<Label onClick={toggleModal} bsStyle={labelBsStyle}>
<Label
onClick={toggleModal}
type={labelType}
data-test="visualization-type"
>
{registry.has(value) ? registry.get(value).name : `${value}`}
</Label>
<VizSupportValidation vizType={value} />

View File

@ -137,7 +137,7 @@ export default function ChartCard({
description={t('Last modified %s', chart.changed_on_delta_humanized)}
coverLeft={<FacePile users={chart.owners || []} />}
coverRight={
<Label bsStyle="secondary">{chart.datasource_name_text}</Label>
<Label type="secondary">{chart.datasource_name_text}</Label>
}
actions={
<ListViewCard.Actions