feat(theming): Custom label tokens (#38679)

This commit is contained in:
Mehmet Salih Yavuz
2026-03-23 18:21:47 +03:00
committed by GitHub
parent fdcb942f3c
commit 86a260e39b
8 changed files with 397 additions and 22 deletions

View File

@@ -110,6 +110,26 @@ export interface ColorVariants {
}
export interface SupersetSpecificTokens {
// Label variant tokens — Published/Draft (dashboard status)
labelPublishedColor?: string;
labelPublishedBg?: string;
labelPublishedBorderColor?: string;
labelPublishedIconColor?: string;
labelDraftColor?: string;
labelDraftBg?: string;
labelDraftBorderColor?: string;
labelDraftIconColor?: string;
// Label variant tokens — Dataset type (Physical/Virtual)
labelDatasetPhysicalColor?: string;
labelDatasetPhysicalBg?: string;
labelDatasetPhysicalBorderColor?: string;
labelDatasetPhysicalIconColor?: string;
labelDatasetVirtualColor?: string;
labelDatasetVirtualBg?: string;
labelDatasetVirtualBorderColor?: string;
labelDatasetVirtualIconColor?: string;
// Font-related
fontSizeXS: string;
fontSizeXXL: string;

View File

@@ -0,0 +1,128 @@
/**
* 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 { screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { supersetTheme } from '@apache-superset/core/theme';
import { DatasetTypeLabel } from './DatasetTypeLabel';
import { renderWithTheme } from './testUtils';
test('renders "Physical" text for physical dataset', () => {
renderWithTheme(<DatasetTypeLabel datasetType="physical" />);
expect(screen.getByText('Physical')).toBeInTheDocument();
});
test('renders "Virtual" text for virtual dataset', () => {
renderWithTheme(<DatasetTypeLabel datasetType="virtual" />);
expect(screen.getByText('Virtual')).toBeInTheDocument();
});
test('uses default primary color for physical label', () => {
renderWithTheme(<DatasetTypeLabel datasetType="physical" />);
const tag = screen
.getByText('Physical')
.closest('[data-test="dataset-type-label"]');
expect(tag).toHaveStyle({ color: supersetTheme.colorPrimaryText });
});
test('uses default color for virtual label', () => {
renderWithTheme(<DatasetTypeLabel datasetType="virtual" />);
const tag = screen
.getByText('Virtual')
.closest('[data-test="dataset-type-label"]');
expect(tag).toHaveStyle({ color: supersetTheme.colorPrimary });
});
test('applies custom labelDatasetPhysical tokens when set', () => {
renderWithTheme(<DatasetTypeLabel datasetType="physical" />, {
labelDatasetPhysicalColor: '#111111',
labelDatasetPhysicalBg: '#222222',
labelDatasetPhysicalBorderColor: '#333333',
});
const tag = screen
.getByText('Physical')
.closest('[data-test="dataset-type-label"]');
expect(tag).toHaveStyle({
color: '#111111',
backgroundColor: '#222222',
borderColor: '#333333',
});
});
test('applies custom labelDatasetVirtual tokens when set', () => {
renderWithTheme(<DatasetTypeLabel datasetType="virtual" />, {
labelDatasetVirtualColor: '#444444',
labelDatasetVirtualBg: '#555555',
labelDatasetVirtualBorderColor: '#666666',
});
const tag = screen
.getByText('Virtual')
.closest('[data-test="dataset-type-label"]');
expect(tag).toHaveStyle({
color: '#444444',
backgroundColor: '#555555',
borderColor: '#666666',
});
});
test('applies custom labelDatasetPhysicalIconColor to icon', () => {
const { container } = renderWithTheme(
<DatasetTypeLabel datasetType="physical" />,
{ labelDatasetPhysicalIconColor: '#aabbcc' },
);
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: '#aabbcc' });
});
test('applies custom labelDatasetVirtualIconColor to icon', () => {
const { container } = renderWithTheme(
<DatasetTypeLabel datasetType="virtual" />,
{ labelDatasetVirtualIconColor: '#ddeeff' },
);
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: '#ddeeff' });
});
test('uses default colorPrimary for physical dataset icon', () => {
const { container } = renderWithTheme(
<DatasetTypeLabel datasetType="physical" />,
);
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: supersetTheme.colorPrimary });
});
test('virtual dataset icon has no explicit icon color by default', () => {
const { container } = renderWithTheme(
<DatasetTypeLabel datasetType="virtual" />,
);
const svg = container.querySelector('[role="img"]') as HTMLElement;
expect(svg.style.color).toBe('');
});
test('partial token override uses custom bg with default color fallback', () => {
renderWithTheme(<DatasetTypeLabel datasetType="physical" />, {
labelDatasetPhysicalBg: '#ff0000',
});
const tag = screen
.getByText('Physical')
.closest('[data-test="dataset-type-label"]');
expect(tag).toHaveStyle({
backgroundColor: '#ff0000',
color: supersetTheme.colorPrimaryText,
});
});

View File

@@ -32,28 +32,41 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType,
}) => {
const theme = useTheme();
const label: string =
datasetType === 'physical' ? t('Physical') : t('Virtual');
const icon =
datasetType === 'physical' ? (
<Icons.InsertRowAboveOutlined
iconSize={SIZE}
iconColor={theme.colorPrimary}
/>
) : (
<Icons.ConsoleSqlOutlined iconSize={SIZE} />
);
const labelType = datasetType === 'physical' ? 'primary' : 'default';
const isPhysical = datasetType === 'physical';
const label: string = isPhysical ? t('Physical') : t('Virtual');
const labelType = isPhysical ? 'primary' : 'default';
const color = isPhysical
? (theme.labelDatasetPhysicalColor ?? theme.colorPrimaryText)
: (theme.labelDatasetVirtualColor ?? theme.colorPrimary);
const bg = isPhysical
? theme.labelDatasetPhysicalBg
: theme.labelDatasetVirtualBg;
const borderColor = isPhysical
? theme.labelDatasetPhysicalBorderColor
: theme.labelDatasetVirtualBorderColor;
const iconColor = isPhysical
? (theme.labelDatasetPhysicalIconColor ?? theme.colorPrimary)
: theme.labelDatasetVirtualIconColor;
const icon = isPhysical ? (
<Icons.InsertRowAboveOutlined iconSize={SIZE} iconColor={iconColor} />
) : (
<Icons.ConsoleSqlOutlined
iconSize={SIZE}
{...(iconColor && { iconColor })}
/>
);
return (
<Label
icon={icon}
type={labelType}
data-test="dataset-type-label"
style={{
color:
datasetType === 'physical'
? theme.colorPrimaryText
: theme.colorPrimary,
color,
...(bg && { backgroundColor: bg }),
...(borderColor && { borderColor }),
}}
>
{label}

View File

@@ -0,0 +1,120 @@
/**
* 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 { screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { supersetTheme } from '@apache-superset/core/theme';
import { PublishedLabel } from './PublishedLabel';
import { renderWithTheme } from './testUtils';
test('renders "Published" text when isPublished is true', () => {
renderWithTheme(<PublishedLabel isPublished />);
expect(screen.getByText('Published')).toBeInTheDocument();
});
test('renders "Draft" text when isPublished is false', () => {
renderWithTheme(<PublishedLabel isPublished={false} />);
expect(screen.getByText('Draft')).toBeInTheDocument();
});
test('uses default success color for published label', () => {
renderWithTheme(<PublishedLabel isPublished />);
const tag = screen.getByText('Published').closest('.ant-tag');
expect(tag).toHaveStyle({ color: supersetTheme.colorSuccessText });
});
test('uses default primary color for draft label', () => {
renderWithTheme(<PublishedLabel isPublished={false} />);
const tag = screen.getByText('Draft').closest('.ant-tag');
expect(tag).toHaveStyle({ color: supersetTheme.colorPrimaryText });
});
test('applies custom labelPublished tokens when set', () => {
renderWithTheme(<PublishedLabel isPublished />, {
labelPublishedColor: '#111111',
labelPublishedBg: '#222222',
labelPublishedBorderColor: '#333333',
});
const tag = screen.getByText('Published').closest('.ant-tag');
expect(tag).toHaveStyle({
color: '#111111',
backgroundColor: '#222222',
borderColor: '#333333',
});
});
test('applies custom labelDraft tokens when set', () => {
renderWithTheme(<PublishedLabel isPublished={false} />, {
labelDraftColor: '#444444',
labelDraftBg: '#555555',
labelDraftBorderColor: '#666666',
});
const tag = screen.getByText('Draft').closest('.ant-tag');
expect(tag).toHaveStyle({
color: '#444444',
backgroundColor: '#555555',
borderColor: '#666666',
});
});
test('applies custom labelPublishedIconColor to icon', () => {
const { container } = renderWithTheme(<PublishedLabel isPublished />, {
labelPublishedIconColor: '#aabbcc',
});
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: '#aabbcc' });
});
test('applies custom labelDraftIconColor to icon', () => {
const { container } = renderWithTheme(
<PublishedLabel isPublished={false} />,
{ labelDraftIconColor: '#ddeeff' },
);
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: '#ddeeff' });
});
test('uses default colorSuccess for published icon', () => {
const { container } = renderWithTheme(<PublishedLabel isPublished />);
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: supersetTheme.colorSuccess });
});
test('uses default colorPrimary for draft icon', () => {
const { container } = renderWithTheme(<PublishedLabel isPublished={false} />);
const svg = container.querySelector('[role="img"]');
expect(svg).toHaveStyle({ color: supersetTheme.colorPrimary });
});
test('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
renderWithTheme(<PublishedLabel isPublished onClick={handleClick} />);
fireEvent.click(screen.getByText('Published'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('partial token override uses custom bg with default color fallback', () => {
renderWithTheme(<PublishedLabel isPublished />, {
labelPublishedBg: '#ff0000',
});
const tag = screen.getByText('Published').closest('.ant-tag');
expect(tag).toHaveStyle({
backgroundColor: '#ff0000',
color: supersetTheme.colorSuccessText,
});
});

View File

@@ -33,20 +33,34 @@ export const PublishedLabel: React.FC<PublishedLabelProps> = ({
}) => {
const theme = useTheme();
const label = isPublished ? t('Published') : t('Draft');
const icon = isPublished ? (
<Icons.CheckCircleOutlined iconSize="s" iconColor={theme.colorSuccess} />
) : (
<Icons.MinusCircleOutlined iconSize="s" iconColor={theme.colorPrimary} />
);
const labelType = isPublished ? 'success' : 'primary';
const color = isPublished
? (theme.labelPublishedColor ?? theme.colorSuccessText)
: (theme.labelDraftColor ?? theme.colorPrimaryText);
const bg = isPublished ? theme.labelPublishedBg : theme.labelDraftBg;
const borderColor = isPublished
? theme.labelPublishedBorderColor
: theme.labelDraftBorderColor;
const iconColor = isPublished
? (theme.labelPublishedIconColor ?? theme.colorSuccess)
: (theme.labelDraftIconColor ?? theme.colorPrimary);
const icon = isPublished ? (
<Icons.CheckCircleOutlined iconSize="s" iconColor={iconColor} />
) : (
<Icons.MinusCircleOutlined iconSize="s" iconColor={iconColor} />
);
return (
<Label
type={labelType}
icon={icon}
onClick={onClick}
style={{
color: isPublished ? theme.colorSuccessText : theme.colorPrimaryText,
color,
...(bg && { backgroundColor: bg }),
...(borderColor && { borderColor }),
}}
>
{label}

View File

@@ -0,0 +1,32 @@
/**
* 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 { type ReactElement } from 'react';
import { render } from '@testing-library/react';
import { ThemeProvider } from '@emotion/react';
import { Theme, supersetTheme } from '@apache-superset/core/theme';
export function renderWithTheme(
ui: ReactElement,
tokenOverrides?: Record<string, string>,
) {
const theme = tokenOverrides
? Theme.fromConfig({ token: tokenOverrides }).theme
: supersetTheme;
return render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
}

View File

@@ -106,3 +106,31 @@ test('getAllValidTokenNames includes known Ant Design tokens', () => {
expect(result.antdTokens).toContain('fontSize');
expect(result.antdTokens).toContain('padding');
});
test('label variant tokens are recognized as valid Superset custom tokens', () => {
const labelTokens = [
// Published/Draft
'labelPublishedColor',
'labelPublishedBg',
'labelPublishedBorderColor',
'labelPublishedIconColor',
'labelDraftColor',
'labelDraftBg',
'labelDraftBorderColor',
'labelDraftIconColor',
// Dataset type
'labelDatasetPhysicalColor',
'labelDatasetPhysicalBg',
'labelDatasetPhysicalBorderColor',
'labelDatasetPhysicalIconColor',
'labelDatasetVirtualColor',
'labelDatasetVirtualBg',
'labelDatasetVirtualBorderColor',
'labelDatasetVirtualIconColor',
];
labelTokens.forEach(token => {
expect(isValidTokenName(token)).toBe(true);
expect(isSupersetCustomToken(token)).toBe(true);
});
});

View File

@@ -48,6 +48,26 @@ const SUPERSET_CUSTOM_TOKENS: Set<string> = new Set([
// Font loading
'fontUrls',
// Label variant tokens — Published/Draft (dashboard status)
'labelPublishedColor',
'labelPublishedBg',
'labelPublishedBorderColor',
'labelPublishedIconColor',
'labelDraftColor',
'labelDraftBg',
'labelDraftBorderColor',
'labelDraftIconColor',
// Label variant tokens — Dataset type (Physical/Virtual)
'labelDatasetPhysicalColor',
'labelDatasetPhysicalBg',
'labelDatasetPhysicalBorderColor',
'labelDatasetPhysicalIconColor',
'labelDatasetVirtualColor',
'labelDatasetVirtualBg',
'labelDatasetVirtualBorderColor',
'labelDatasetVirtualIconColor',
// Editor tokens
'colorEditorSelection',