feat: redesign labels (#31575)

This commit is contained in:
Maxime Beauchemin 2025-01-10 09:30:12 -08:00 committed by GitHub
parent 8a2aada58d
commit 740fbf72d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 330 additions and 131 deletions

View File

@ -25,23 +25,96 @@ export default {
export const ThemeColors = () => {
const { colors } = supersetTheme;
return Object.keys(colors).map(collection => (
// Define tones to be displayed in columns
const tones = [
'dark2',
'dark1',
'base',
'light1',
'light2',
'light3',
'light4',
'light5',
];
const colorTypes = [
'primary',
'secondary',
'grayscale',
'error',
'warning',
'alert',
'success',
'info',
];
return (
<div>
<h2>{collection}</h2>
<table style={{ width: '300px' }}>
{Object.keys(colors[collection]).map(k => {
const hex = colors[collection][k];
return (
<tr>
<td>{k}</td>
<td>
<code>{hex}</code>
<h1>Theme Colors</h1>
<table
style={{ borderCollapse: 'collapse', width: '100%', textAlign: 'left' }}
>
<thead>
<tr>
<th style={{ border: '1px solid #ddd', padding: '8px' }}>
Category
</th>
{tones.map(tone => (
<th
key={tone}
style={{ border: '1px solid #ddd', padding: '8px' }}
>
{tone}
</th>
))}
</tr>
</thead>
<tbody>
{colorTypes.map(category => (
<tr key={category}>
<td style={{ border: '1px solid #ddd', padding: '8px' }}>
<strong>{category}</strong>
</td>
<td style={{ width: '150px', backgroundColor: hex }} />
{tones.map(tone => {
const color = colors[category][tone];
return (
<td
key={tone}
style={{
border: '1px solid #ddd',
padding: '8px',
backgroundColor: color || '#fff',
}}
>
{color ? <code>{color}</code> : '-'}
</td>
);
})}
</tr>
);
})}
))}
</tbody>
</table>
<h3>
text.label: <code>{colors.text.label}</code>
</h3>
<div style={{ color: `#${colors.text.label}` }}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</div>
<h3>
text.help: <code>{colors.text.help}</code>
</h3>
<div style={{ color: `#${colors.text.help}` }}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</div>
<h3>The supersetTheme object</h3>
<code>
<pre>{JSON.stringify(supersetTheme, null, 2)}</pre>
</code>
</div>
));
);
};

View File

@ -19,9 +19,11 @@
import { useCallback, useEffect, useMemo, useState, FC } from 'react';
import { isEqual, isEmpty } from 'lodash';
import { QueryFormData, styled, t } from '@superset-ui/core';
import { QueryFormData, t } from '@superset-ui/core';
import { sanitizeFormData } from 'src/explore/exploreUtils/formData';
import getControlsForVizType from 'src/utils/getControlsForVizType';
import Label from 'src/components/Label';
import Icons from 'src/components/Icons';
import { safeStringify } from 'src/utils/safeStringify';
import { Tooltip } from 'src/components/Tooltip';
import ModalTrigger from '../ModalTrigger';
@ -68,18 +70,6 @@ export type RowType = {
control: string;
};
const StyledLabel = styled.span`
${({ theme }) => `
font-size: ${theme.typography.sizes.s}px;
color: ${theme.colors.grayscale.dark1};
background-color: ${theme.colors.alert.base};
&:hover {
background-color: ${theme.colors.alert.dark1};
}
`}
`;
export const alterForComparison = (
value?: string | null | [],
): string | null => {
@ -228,7 +218,14 @@ const AlteredSliceTag: FC<AlteredSliceTagProps> = props => {
const triggerNode = useMemo(
() => (
<Tooltip id="difference-tooltip" title={t('Click to see difference')}>
<StyledLabel className="label">{t('Altered')}</StyledLabel>
<Label
icon={<Icons.Warning iconSize="m" />}
className="label"
type="alert"
onClick={() => {}}
>
{t('Altered')}
</Label>
</Tooltip>
),
[],

View File

@ -36,6 +36,10 @@ const AntdIconComponent = ({
export const StyledIcon = styled(AntdIconComponent)<IconType>`
${({ iconColor }) => iconColor && `color: ${iconColor};`};
span {
// Fixing alignement on some of the icons
line-height: 0px;
}
font-size: ${({ iconSize, theme }) =>
iconSize
? `${theme.typography.sizes[iconSize] || theme.typography.sizes.m}px`

View File

@ -17,14 +17,17 @@
* under the License.
*/
import { action } from '@storybook/addon-actions';
import Label, { Type } from './index';
import { Meta, StoryFn } from '@storybook/react';
import Label, { Type, DatasetTypeLabel, PublishedLabel } from './index';
// Define the default export with Storybook configuration
export default {
title: 'Label',
component: Label,
excludeStories: 'options',
};
excludeStories: ['options'],
} as Meta<typeof Label>;
// Explicitly type the options array as an array of `Type`
export const options: Type[] = [
'default',
'alert',
@ -36,39 +39,60 @@ export const options: Type[] = [
'secondary',
];
export const LabelGallery = () => (
<>
<h4>Non-interactive</h4>
{Object.values(options).map((opt: Type) => (
<Label key={opt} type={opt}>
{`style: "${opt}"`}
</Label>
))}
<br />
<h4>Interactive</h4>
{Object.values(options).map((opt: Type) => (
<Label key={opt} type={opt} onClick={action('clicked')}>
{`style: "${opt}"`}
</Label>
))}
</>
);
// Define the props for the `LabelGallery` component
interface LabelGalleryProps {
hasOnClick?: boolean;
monospace?: boolean;
}
// Use the `StoryFn` type for LabelGallery
export const LabelGallery: StoryFn<LabelGalleryProps> = (
props: LabelGalleryProps,
) => {
const onClick = props.hasOnClick ? action('clicked') : undefined;
export const InteractiveLabel = (args: any) => {
const { hasOnClick, label, monospace, ...rest } = args;
return (
<Label
onClick={hasOnClick ? action('clicked') : undefined}
monospace={monospace}
{...rest}
>
{label}
</Label>
<>
<h4>Non-interactive</h4>
{options.map((opt: Type) => (
<Label key={opt} type={opt}>
{`style: "${opt}"`}
</Label>
))}
<br />
<h4>Interactive</h4>
{options.map((opt: Type) => (
<Label key={opt} type={opt} {...props} onClick={onClick}>
{`style: "${opt}"`}
</Label>
))}
<h4>Reusable Labels</h4>
<h5>DatasetType</h5>
<div>
<DatasetTypeLabel datasetType="physical" />
<DatasetTypeLabel datasetType="virtual" />
</div>
<h5>PublishedLabel</h5>
<PublishedLabel isPublished />
<PublishedLabel isPublished={false} />
</>
);
};
InteractiveLabel.args = {
// Define default arguments for Storybook
LabelGallery.args = {
hasOnClick: true,
label: 'Example',
monospace: true,
monospace: false,
};
// Define argument types for Storybook controls
LabelGallery.argTypes = {
monospace: {
name: 'monospace',
control: { type: 'boolean' },
},
hasOnClick: {
name: 'hasOnClick',
control: { type: 'boolean' },
},
};

View File

@ -37,7 +37,9 @@ test('works with an onClick handler', () => {
// test stories from the storybook!
test('renders all the storybook gallery variants', () => {
const { container } = render(<LabelGallery />);
const nonInteractiveLabelCount = 4;
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;
expect(container.querySelectorAll('.ant-tag')).toHaveLength(
options.length * 2,
renderedLabelCount,
);
});

View File

@ -25,6 +25,8 @@ import {
import { Tag } from 'src/components';
import { useTheme } from '@superset-ui/core';
import DatasetTypeLabel from 'src/components/Label/reusable/DatasetTypeLabel';
import PublishedLabel from 'src/components/Label/reusable/PublishedLabel';
export type OnClickHandler = MouseEventHandler<HTMLElement>;
@ -47,6 +49,7 @@ export interface LabelProps extends HTMLAttributes<HTMLSpanElement> {
children?: ReactNode;
role?: string;
monospace?: boolean;
icon?: ReactNode;
}
export default function Label(props: LabelProps) {
@ -58,6 +61,7 @@ export default function Label(props: LabelProps) {
style,
onClick,
children,
icon,
...rest
} = props;
const {
@ -71,37 +75,44 @@ export default function Label(props: LabelProps) {
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 !== 'default') {
color = grayscale.light4;
let baseColor;
if (type === 'alert') {
color = grayscale.dark1;
baseColor = alert;
} else 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';
let baseColor;
if (type === 'primary') {
baseColor = primary;
} else if (type === 'secondary') {
baseColor = secondary;
} else if (type === 'success') {
baseColor = success;
} else if (type === 'alert') {
baseColor = alert;
} else if (type === 'warning') {
baseColor = warning;
} else if (type === 'danger') {
baseColor = error;
} else if (type === 'info') {
baseColor = info;
} else {
baseColor = grayscale;
}
const color = baseColor.dark2;
let borderColor = baseColor.light1;
let backgroundColor = baseColor.light2;
// TODO - REMOVE IF BLOCK LOGIC WHEN shades are fixed to be aligned in terms of brightness
// currently shades for >=light2 are not aligned for primary, default and secondary
if (['default', 'primary', 'secondary'].includes(type)) {
// @ts-ignore
backgroundColor = baseColor.light4;
borderColor = baseColor.light2;
}
const backgroundColorHover = onClick ? baseColor.light1 : backgroundColor;
const borderColorHover = onClick ? baseColor.base : borderColor;
if (type === 'default') {
// Lighter for default
backgroundColor = grayscale.light3;
}
const css = {
transition: `background-color ${transitionTiming}s`,
whiteSpace: 'nowrap',
@ -109,11 +120,14 @@ export default function Label(props: LabelProps) {
overflow: 'hidden',
textOverflow: 'ellipsis',
backgroundColor,
borderRadius: 8,
borderColor,
borderRadius: 21,
padding: '0.35em 0.8em',
lineHeight: 1,
color,
display: 'inline-flex',
verticalAlign: 'middle',
alignItems: 'center',
maxWidth: '100%',
'&:hover': {
backgroundColor: backgroundColorHover,
@ -124,12 +138,12 @@ export default function Label(props: LabelProps) {
if (monospace) {
css['font-family'] = theme.typography.families.monospace;
}
return (
<Tag
onClick={onClick}
role={onClick ? 'button' : undefined}
style={style}
icon={icon}
{...rest}
css={css}
>
@ -137,3 +151,4 @@ export default function Label(props: LabelProps) {
</Tag>
);
}
export { DatasetTypeLabel, PublishedLabel };

View File

@ -0,0 +1,49 @@
/**
* 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 Icons from 'src/components/Icons';
import Label from 'src/components/Label';
import { t } from '@superset-ui/core';
// Define the prop types for DatasetTypeLabel
interface DatasetTypeLabelProps {
datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
}
const SIZE = 's'; // Define the size as a constant
const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({ datasetType }) => {
const label: string =
datasetType === 'physical' ? t('Physical') : t('Virtual');
const icon =
datasetType === 'physical' ? (
<Icons.Table iconSize={SIZE} />
) : (
<Icons.ConsoleSqlOutlined iconSize={SIZE} />
);
const labelType: 'primary' | 'secondary' =
datasetType === 'physical' ? 'primary' : 'secondary';
return (
<Label icon={icon} type={labelType}>
{label}
</Label>
);
};
export default DatasetTypeLabel;

View File

@ -0,0 +1,48 @@
/**
* 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 Icons from 'src/components/Icons';
import Label from 'src/components/Label';
import { t } from '@superset-ui/core';
// Define props for the PublishedLabel component
interface PublishedLabelProps {
isPublished: boolean; // Whether the item is published
onClick?: () => void; // Optional click handler
}
const PublishedLabel: React.FC<PublishedLabelProps> = ({
isPublished,
onClick,
}) => {
const label = isPublished ? t('Published') : t('Draft');
const icon = isPublished ? (
<Icons.CircleCheck iconSize="s" />
) : (
<Icons.Minus iconSize="s" />
);
const labelType = isPublished ? 'primary' : 'secondary';
return (
<Label type={labelType} icon={icon} onClick={onClick}>
{label}
</Label>
);
};
export default PublishedLabel;

View File

@ -19,6 +19,7 @@
import { useEffect, useRef, useState } from 'react';
import { styled } from '@superset-ui/core';
import Label, { Type } from 'src/components/Label';
import Icons from 'src/components/Icons';
import { now, fDuration } from 'src/utils/dates';
@ -68,7 +69,7 @@ export default function Timer({
}, [endTime, isRunning, startTime]);
return (
<TimerLabel type={status} role="timer">
<TimerLabel icon={<Icons.Clock iconSize="m" />} type={status} role="timer">
{clockStr}
</TimerLabel>
);

View File

@ -19,7 +19,7 @@
import { Component } from 'react';
import { t } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import Label from 'src/components/Label';
import { PublishedLabel } from 'src/components/Label';
import { HeaderProps, HeaderDropdownProps } from '../Header/types';
export type DashboardPublishedStatusType = {
@ -67,13 +67,12 @@ export default class PublishedStatus extends Component<DashboardPublishedStatusT
placement="bottom"
title={draftButtonTooltip}
>
<Label
onClick={() => {
this.togglePublished();
}}
>
{t('Draft')}
</Label>
<div>
<PublishedLabel
isPublished={isPublished}
onClick={this.togglePublished}
/>
</div>
</Tooltip>
);
}
@ -83,7 +82,9 @@ export default class PublishedStatus extends Component<DashboardPublishedStatusT
placement="bottom"
title={draftDivTooltip}
>
<Label>{t('Draft')}</Label>
<div>
<PublishedLabel isPublished={isPublished} />
</div>
</Tooltip>
);
}
@ -96,13 +97,12 @@ export default class PublishedStatus extends Component<DashboardPublishedStatusT
placement="bottom"
title={publishedTooltip}
>
<Label
onClick={() => {
this.togglePublished();
}}
>
{t('Published')}
</Label>
<div>
<PublishedLabel
isPublished={isPublished}
onClick={this.togglePublished}
/>
</div>
</Tooltip>
);
}

View File

@ -34,6 +34,7 @@ import {
} from 'src/views/CRUD/utils';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import { PublishedLabel } from 'src/components/Label';
import { TagsList } from 'src/components/Tags';
import handleResourceExport from 'src/utils/export';
import Loading from 'src/components/Loading';
@ -343,8 +344,9 @@ function DashboardList(props: DashboardListProps) {
row: {
original: { status },
},
}: any) =>
status === DashboardStatus.PUBLISHED ? t('Published') : t('Draft'),
}: any) => (
<PublishedLabel isPublished={status === DashboardStatus.PUBLISHED} />
),
Header: t('Status'),
accessor: 'published',
size: 'xl',

View File

@ -41,6 +41,7 @@ import ListView, {
Filters,
FilterOperator,
} from 'src/components/ListView';
import { DatasetTypeLabel } from 'src/components/Label';
import Loading from 'src/components/Loading';
import SubMenu, { SubMenuProps, ButtonProps } from 'src/features/home/SubMenu';
import Owner from 'src/types/Owner';
@ -279,24 +280,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
row: {
original: { kind },
},
}: any) => {
if (kind === 'physical') {
return (
<Tooltip
id="physical-dataset-tooltip"
title={t('Physical dataset')}
>
<Icons.DatasetPhysical />
</Tooltip>
);
}
return (
<Tooltip id="virtual-dataset-tooltip" title={t('Virtual dataset')}>
<Icons.DatasetVirtual />
</Tooltip>
);
},
}: any) => null,
accessor: 'kind_icon',
disableSortBy: true,
size: 'xs',
@ -360,7 +344,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
row: {
original: { kind },
},
}: any) => (kind === 'physical' ? t('Physical') : t('Virtual')),
}: any) => <DatasetTypeLabel datasetType={kind} />,
Header: t('Type'),
accessor: 'kind',
disableSortBy: true,