Compare commits

...
Author SHA1 Message Date
rusackasandClaude Opus 4.8 ad7ea1fb11 test(extensions): cover host colors.ts registry wiring
Adds unit coverage for getCategoricalSchemeNames/getSchemeColors/
getCategoricalSchemes in the host implementation, addressing the
codecov coverage gap flagged in review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 07:31:03 -07:00
rusackasandClaude Opus 4.8 2c42568e78 refactor(colors): simplify theme exports, unify scheme naming, add getCategoricalSchemes
Addresses reviewer feedback on the @apache-superset/core/theme color API:
use `export *` instead of an explicit re-export list, rename
ColorSchemeConfig/SequentialSchemeConfig to CategoricalScheme/SequentialScheme
(dropping the now-redundant duplicate CategoricalScheme interface), and add
getCategoricalSchemes() so extensions can fetch full scheme metadata without
a second round-trip through getSchemeColors per name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 21:21:32 -07:00
Evan RusackasandClaude Opus 4.8 b1362738ce feat(extensions): expose Superset color schemes via @apache-superset/core/theme
Defines the color-scheme API contract in @apache-superset/core/theme so
extensions can enumerate and read the host's registered categorical palettes
through the federated-module boundary.

- @apache-superset/core/theme: ColorSchemeGroup enum, ColorSchemeConfig /
  SequentialSchemeConfig types, CategoricalScheme / CategoricalSchemeRegistryLike
  interfaces, and declare-only getCategoricalSchemeNames() / getSchemeColors()
  bridge functions (no runtime implementation — the host provides it, same
  pattern as authentication/navigation).
- Palette hex data stays in @superset-ui/core; the contract package exposes
  only the API surface (no duplication).
- src/core/theme supplies the host implementation, wiring
  getCategoricalSchemeRegistry() from @superset-ui/core onto
  window.superset.theme at startup, and is registered like the other namespaces.
- Adds palette coverage tests in @superset-ui/core.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-19 21:14:42 -07:00
10 changed files with 318 additions and 0 deletions
@@ -0,0 +1,26 @@
/**
* 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 { ColorSchemeGroup } from '@apache-superset/core/theme';
test('ColorSchemeGroup has the expected string values', () => {
expect(ColorSchemeGroup.Custom).toBe('custom');
expect(ColorSchemeGroup.Featured).toBe('featured');
expect(ColorSchemeGroup.Other).toBe('other');
});
@@ -0,0 +1,81 @@
/**
* 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.
*/
/**
* Grouping/tier for a color scheme — controls how it appears in the
* scheme picker UI (e.g. Featured palettes are shown first).
*
* Mirrors @superset-ui/core's ColorSchemeGroup; kept here so
* palette configs have no dependency on @superset-ui/core.
*/
export enum ColorSchemeGroup {
Custom = 'custom',
Featured = 'featured',
Other = 'other',
}
/** Plain configuration object for a categorical color scheme. */
export interface CategoricalScheme {
id: string;
label?: string;
colors: string[];
description?: string;
isDefault?: boolean;
group?: ColorSchemeGroup;
}
/**
* A sequential / diverging color scheme. The only difference from a
* categorical scheme is the optional `isDiverging` flag.
*/
export interface SequentialScheme extends CategoricalScheme {
isDiverging?: boolean;
}
/**
* Minimal interface for the categorical color scheme registry.
* Mirrors the public surface of @superset-ui/core's ColorSchemeRegistry.
*/
export interface CategoricalSchemeRegistryLike {
keys(): string[];
get(name: string): CategoricalScheme | null | undefined;
}
/**
* Returns an alphabetically sorted list of all registered categorical color
* scheme names. The host app provides the implementation via
* window.superset.theme.
*/
export declare function getCategoricalSchemeNames(): string[];
/**
* Returns the full list of registered categorical color schemes (id, colors,
* and any other available metadata), sorted alphabetically by id. Prefer
* this over `getCategoricalSchemeNames` when scheme metadata (label,
* description, etc.) is needed, since extracting just the names would
* require a second round-trip through `getSchemeColors` per scheme.
* The host app provides the implementation via window.superset.theme.
*/
export declare function getCategoricalSchemes(): CategoricalScheme[];
/**
* Returns the color array for a named scheme, or null if not found.
* The host app provides the implementation via window.superset.theme.
*/
export declare function getSchemeColors(schemeName: string): string[] | null;
@@ -88,3 +88,7 @@ export type {
// Export theme utility functions
export * from './utils/themeUtils';
export * from './utils';
// Color scheme API — types, enum, and declare-function bridge for extensions.
// The host app provides the runtime implementations on window.superset.theme.
export * from './colors';
@@ -24,6 +24,12 @@ import {
CategoricalD3,
CategoricalGoogle,
CategoricalLyft,
CategoricalModernSunset,
CategoricalColorsOfRainbow,
CategoricalBlueToGreen,
CategoricalRedToYellow,
CategoricalWavesOfBlue,
CategoricalPresetSuperset,
SequentialCommon,
SequentialD3,
CategoricalScheme,
@@ -41,22 +47,59 @@ describe('Color Schemes', () => {
CategoricalLyft,
CategoricalSuperset,
CategoricalPreset,
CategoricalModernSunset,
CategoricalColorsOfRainbow,
CategoricalBlueToGreen,
CategoricalRedToYellow,
CategoricalWavesOfBlue,
CategoricalPresetSuperset,
].forEach(group => {
expect(group).toBeInstanceOf(Array);
expect(group.length).toBeGreaterThan(0);
group.forEach(scheme =>
expect(scheme).toBeInstanceOf(CategoricalScheme),
);
});
});
test('each scheme has a non-empty id and at least one color', () => {
[
...CategoricalAirbnb,
...CategoricalD3,
...CategoricalEcharts,
...CategoricalGoogle,
...CategoricalLyft,
...CategoricalPreset,
...CategoricalSuperset,
...CategoricalPresetSuperset,
...CategoricalModernSunset,
...CategoricalColorsOfRainbow,
...CategoricalBlueToGreen,
...CategoricalRedToYellow,
...CategoricalWavesOfBlue,
].forEach(scheme => {
expect(scheme.id).toBeTruthy();
expect(scheme.colors.length).toBeGreaterThan(0);
});
});
});
describe('sequential', () => {
test('returns an array of SequentialScheme', () => {
[SequentialCommon, SequentialD3].forEach(group => {
expect(group).toBeInstanceOf(Array);
expect(group.length).toBeGreaterThan(0);
group.forEach(scheme =>
expect(scheme).toBeInstanceOf(SequentialScheme),
);
});
});
test('each scheme has a non-empty id and at least two colors', () => {
[...SequentialCommon, ...SequentialD3].forEach(scheme => {
expect(scheme.id).toBeTruthy();
expect(scheme.colors.length).toBeGreaterThanOrEqual(2);
});
});
});
});
+1
View File
@@ -36,5 +36,6 @@ export * from './models';
export * from './navigation';
export * from './sqlLab';
export * from './storage';
export * from './theme';
export * from './utils';
export * from './views';
@@ -0,0 +1,88 @@
/**
* 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 { getCategoricalSchemeRegistry } from '@superset-ui/core';
import { theme } from './colors';
jest.mock('@superset-ui/core', () => ({
getCategoricalSchemeRegistry: jest.fn(),
}));
const mockGetRegistry = getCategoricalSchemeRegistry as jest.Mock;
const supersetScheme = { id: 'supersetColors', colors: ['#111', '#222'] };
const airbnbScheme = { id: 'airbnb', colors: ['#333', '#444'] };
beforeEach(() => {
jest.clearAllMocks();
});
test('getCategoricalSchemeNames returns an empty array when there is no registry', () => {
mockGetRegistry.mockReturnValue(null);
expect(theme.getCategoricalSchemeNames()).toEqual([]);
});
test('getCategoricalSchemeNames returns registered names sorted alphabetically', () => {
mockGetRegistry.mockReturnValue({
keys: () => ['supersetColors', 'airbnb'],
get: jest.fn(),
});
expect(theme.getCategoricalSchemeNames()).toEqual([
'airbnb',
'supersetColors',
]);
});
test('getSchemeColors returns null when there is no registry', () => {
mockGetRegistry.mockReturnValue(null);
expect(theme.getSchemeColors('airbnb')).toBeNull();
});
test('getSchemeColors returns null when the scheme is not found', () => {
mockGetRegistry.mockReturnValue({ keys: () => [], get: () => undefined });
expect(theme.getSchemeColors('unknown')).toBeNull();
});
test('getSchemeColors returns the colors for a registered scheme', () => {
mockGetRegistry.mockReturnValue({
keys: () => ['airbnb'],
get: (name: string) => (name === 'airbnb' ? airbnbScheme : undefined),
});
expect(theme.getSchemeColors('airbnb')).toEqual(airbnbScheme.colors);
});
test('getCategoricalSchemes returns full scheme metadata sorted by name', () => {
mockGetRegistry.mockReturnValue({
keys: () => ['supersetColors', 'airbnb'],
get: (name: string) => {
if (name === 'supersetColors') return supersetScheme;
if (name === 'airbnb') return airbnbScheme;
return undefined;
},
});
expect(theme.getCategoricalSchemes()).toEqual([airbnbScheme, supersetScheme]);
});
test('getCategoricalSchemes filters out names the registry cannot resolve', () => {
mockGetRegistry.mockReturnValue({
keys: () => ['airbnb', 'missing'],
get: (name: string) => (name === 'airbnb' ? airbnbScheme : undefined),
});
expect(theme.getCategoricalSchemes()).toEqual([airbnbScheme]);
});
@@ -0,0 +1,52 @@
/**
* 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 { theme as themeApi } from '@apache-superset/core';
import { getCategoricalSchemeRegistry } from '@superset-ui/core';
import type {
CategoricalScheme,
CategoricalSchemeRegistryLike,
} from '@apache-superset/core/theme';
const getRegistry = () =>
getCategoricalSchemeRegistry() as CategoricalSchemeRegistryLike | null;
const getCategoricalSchemeNames: typeof themeApi.getCategoricalSchemeNames =
() => (getRegistry()?.keys() ?? []).sort();
const getCategoricalSchemes: typeof themeApi.getCategoricalSchemes = () => {
const registry = getRegistry();
return getCategoricalSchemeNames()
.map(name => registry?.get(name))
.filter((scheme): scheme is CategoricalScheme => scheme != null);
};
const getSchemeColors: typeof themeApi.getSchemeColors = schemeName =>
getRegistry()?.get(schemeName)?.colors ?? null;
/**
* Host implementation of the @apache-superset/core/theme color API.
* Spreads the contract namespace (types, enum, styling helpers) and supplies
* the runtime implementations for the declare-only registry bridge functions.
*/
export const theme: typeof themeApi = {
...themeApi,
getCategoricalSchemeNames,
getCategoricalSchemes,
getSchemeColors,
};
+19
View File
@@ -0,0 +1,19 @@
/**
* 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.
*/
export * from './colors';
@@ -31,6 +31,7 @@ import {
navigation,
useNavigationTracker,
sqlLab,
theme,
views,
} from 'src/core';
import { useSelector } from 'react-redux';
@@ -64,6 +65,7 @@ const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
menus,
navigation,
sqlLab,
theme,
views,
};
@@ -36,6 +36,7 @@ import type {
menus,
navigation,
sqlLab,
theme,
views,
} from 'src/core';
@@ -50,6 +51,7 @@ export interface Namespaces {
menus: typeof menus;
navigation: typeof navigation;
sqlLab: typeof sqlLab;
theme: typeof theme;
views: typeof views;
}