diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 08cc119d..84a961e0 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -24,7 +24,7 @@ jobs:
enable_go_cache: false
enable_npm: false
- name: golangci-lint
- uses: golangci/golangci-lint-action@v9.2.1
+ uses: golangci/golangci-lint-action@v9.3.0
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v2.12.2
diff --git a/client/package.json b/client/package.json
index 0b1a35fc..06d74d12 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/client",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"description": "Functions as an API client or Data fetching Layer for interacting with a backend service",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
@@ -15,9 +15,12 @@
"main": "dist/cjs/index.js",
"types": "dist/index.d.ts",
"dependencies": {
- "@perses-dev/spec": "0.2.0-beta.6",
+ "@perses-dev/spec": "0.2.0",
"zod": "^3.21.4"
},
+ "peerDependencies": {
+ "react": "^17.0.2 || ^18.0.0"
+ },
"scripts": {
"clean": "rimraf dist/",
"build": "concurrently \"npm:build:*\"",
diff --git a/client/src/context/FetchContext.test.tsx b/client/src/context/FetchContext.test.tsx
new file mode 100644
index 00000000..fff34488
--- /dev/null
+++ b/client/src/context/FetchContext.test.tsx
@@ -0,0 +1,90 @@
+// Copyright The Perses Authors
+// Licensed 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 { render, screen, waitFor } from '@testing-library/react';
+import { FetchProvider, useFetch, FetchFn } from './FetchContext';
+
+function TestConsumer(): React.ReactElement {
+ const { fetch } = useFetch();
+ return ;
+}
+
+function TestJsonConsumer({ url }: { url: string }): React.ReactElement {
+ const { fetchJson } = useFetch();
+ return (
+
+ );
+}
+
+describe('FetchContext', () => {
+ describe('useFetch without provider', () => {
+ it('returns the default fetch wrapper from @perses-dev/client', () => {
+ let hookResult: ReturnType | undefined;
+ function Capture(): React.ReactNode {
+ hookResult = useFetch();
+ return null;
+ }
+ render();
+ expect(hookResult).toBeDefined();
+ expect(typeof hookResult!.fetch).toBe('function');
+ expect(typeof hookResult!.fetchJson).toBe('function');
+ });
+ });
+
+ describe('FetchProvider with custom fetchFn', () => {
+ it('provides the custom fetch to useFetch consumers', async () => {
+ const customFetch: FetchFn = jest.fn().mockResolvedValue({
+ ok: true,
+ } as unknown as Response);
+
+ render(
+
+
+
+ );
+
+ screen.getByText('fire').click();
+
+ await waitFor(() => {
+ expect(customFetch).toHaveBeenCalledWith('/test');
+ });
+ });
+
+ it('derives fetchJson from the custom fetch', async () => {
+ const customFetch: FetchFn = jest.fn().mockResolvedValue({
+ ok: true,
+ json: jest.fn().mockResolvedValue({ ok: true }),
+ } as unknown as Response);
+
+ render(
+
+
+
+ );
+
+ screen.getByText('json').click();
+
+ await waitFor(() => {
+ expect(customFetch).toHaveBeenCalledWith('/api/data');
+ expect(document.title).toBe('{"ok":true}');
+ });
+ });
+ });
+});
diff --git a/client/src/context/FetchContext.tsx b/client/src/context/FetchContext.tsx
new file mode 100644
index 00000000..b3ae5906
--- /dev/null
+++ b/client/src/context/FetchContext.tsx
@@ -0,0 +1,45 @@
+// Copyright The Perses Authors
+// Licensed 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 { createContext, ReactElement, ReactNode, useCallback, useContext } from 'react';
+import { fetch as defaultFetch } from '../util/fetch';
+
+export type FetchFn = (...args: Parameters) => Promise;
+
+const FetchContext = createContext(defaultFetch);
+
+export interface FetchProviderProps {
+ fetchFn: FetchFn;
+ children: ReactNode;
+}
+
+export function FetchProvider({ fetchFn, children }: FetchProviderProps): ReactElement {
+ return {children};
+}
+
+export function useFetch(): {
+ fetch: FetchFn;
+ fetchJson: (...args: Parameters) => Promise;
+} {
+ const fetch = useContext(FetchContext);
+
+ const fetchJson = useCallback(
+ async (...args: Parameters): Promise => {
+ const response = await fetch(...args);
+ return await response.json();
+ },
+ [fetch]
+ );
+
+ return { fetch, fetchJson };
+}
diff --git a/client/src/context/index.ts b/client/src/context/index.ts
new file mode 100644
index 00000000..5a5c0fff
--- /dev/null
+++ b/client/src/context/index.ts
@@ -0,0 +1,14 @@
+// Copyright The Perses Authors
+// Licensed 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 './FetchContext';
diff --git a/client/src/index.ts b/client/src/index.ts
index a3861034..d7179afc 100644
--- a/client/src/index.ts
+++ b/client/src/index.ts
@@ -14,3 +14,4 @@
export * from './util';
export * from './model';
export * from './schema';
+export * from './context';
diff --git a/components/package.json b/components/package.json
index 5b771f31..5cc325c7 100644
--- a/components/package.json
+++ b/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/components",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"description": "Common UI components used across Perses features",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
@@ -34,8 +34,8 @@
"@date-fns/tz": "^1.4.1",
"@fontsource/inter": "^5.0.0",
"@mui/x-date-pickers": "^7.23.1",
- "@perses-dev/spec": "0.2.0-beta.6",
- "@perses-dev/client": "0.54.0-beta.10",
+ "@perses-dev/spec": "0.2.0",
+ "@perses-dev/client": "0.54.0",
"numbro": "^2.3.6",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.5",
@@ -49,7 +49,6 @@
"notistack": "^3.0.2",
"react-colorful": "^5.6.1",
"react-error-boundary": "^3.1.4",
- "react-hook-form": "^7.51.3",
"react-virtuoso": "^4.12.2"
},
"devDependencies": {
diff --git a/components/src/EChart/index.ts b/components/src/EChart/index.ts
index 466ab36b..a40ab1cd 100644
--- a/components/src/EChart/index.ts
+++ b/components/src/EChart/index.ts
@@ -12,3 +12,4 @@
// limitations under the License.
export * from './EChart';
+export * from './timezone-formatter';
diff --git a/components/src/EChart/timezone-formatter.test.ts b/components/src/EChart/timezone-formatter.test.ts
new file mode 100644
index 00000000..49d36d86
--- /dev/null
+++ b/components/src/EChart/timezone-formatter.test.ts
@@ -0,0 +1,79 @@
+// Copyright The Perses Authors
+// Licensed 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 { createTimezoneAwareAxisFormatter } from './timezone-formatter';
+
+// Mock formatWithTimeZone since it's from @perses-dev/components
+jest.mock('@perses-dev/components', () => ({
+ formatWithTimeZone: jest.fn((date: Date, format: string, timeZone: string) => {
+ // Simple mock that returns format pattern with timezone
+ return `${format}[${timeZone}]`;
+ }),
+}));
+
+describe('createTimezoneAwareAxisFormatter', () => {
+ const testTimestamp = 1640995200000; // 2022-01-01 00:00:00 UTC
+ const timeZone = 'America/New_York';
+
+ it('should format for ranges > 5 years with year format', () => {
+ const formatter = createTimezoneAwareAxisFormatter(6 * 365 * 24 * 60 * 60 * 1000, timeZone);
+ const result = formatter(testTimestamp);
+ expect(result).toBe('yyyy[America/New_York]');
+ });
+
+ it('should format for ranges > 6 months with month-year format', () => {
+ const formatter = createTimezoneAwareAxisFormatter(3 * 365 * 24 * 60 * 60 * 1000, timeZone);
+ const result = formatter(testTimestamp);
+ expect(result).toBe('MMM yyyy[America/New_York]');
+ });
+
+ it('should format for ranges between 10 days and 6 months with day-month format', () => {
+ const formatter = createTimezoneAwareAxisFormatter(30 * 24 * 60 * 60 * 1000, timeZone); // 30 days
+ const result = formatter(testTimestamp);
+ expect(result).toBe('dd.MM[America/New_York]');
+ });
+
+ it('should format for ranges between 2-10 days with day-month-time format', () => {
+ const formatter = createTimezoneAwareAxisFormatter(5 * 24 * 60 * 60 * 1000, timeZone); // 5 days
+ const result = formatter(testTimestamp);
+ expect(result).toBe('dd.MM HH:mm[America/New_York]');
+ });
+
+ it('should format for ranges <= 2 days with time format', () => {
+ const formatter = createTimezoneAwareAxisFormatter(6 * 60 * 60 * 1000, timeZone); // 6 hours
+ const result = formatter(testTimestamp);
+ expect(result).toBe('HH:mm[America/New_York]');
+ });
+
+ it('should handle different timezones', () => {
+ const formatter = createTimezoneAwareAxisFormatter(6 * 60 * 60 * 1000, 'Europe/Prague');
+ const result = formatter(testTimestamp);
+ expect(result).toBe('HH:mm[Europe/Prague]');
+ });
+
+ it('should handle edge case at exactly 5 years', () => {
+ const fiveYears = 5 * 365 * 24 * 60 * 60 * 1000;
+ const formatter = createTimezoneAwareAxisFormatter(fiveYears, timeZone);
+ const result = formatter(testTimestamp);
+ // Should use MMM yyyy format (not > 5 years)
+ expect(result).toBe('MMM yyyy[America/New_York]');
+ });
+
+ it('should handle edge case at exactly 2 days', () => {
+ const twoDays = 2 * 24 * 60 * 60 * 1000;
+ const formatter = createTimezoneAwareAxisFormatter(twoDays, timeZone);
+ const result = formatter(testTimestamp);
+ // Should use HH:mm format (not > 2 days)
+ expect(result).toBe('HH:mm[America/New_York]');
+ });
+});
diff --git a/components/src/EChart/timezone-formatter.ts b/components/src/EChart/timezone-formatter.ts
new file mode 100644
index 00000000..dfb8c038
--- /dev/null
+++ b/components/src/EChart/timezone-formatter.ts
@@ -0,0 +1,50 @@
+// Copyright The Perses Authors
+// Licensed 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 { formatWithTimeZone } from '@perses-dev/components';
+
+const DAY_MS = 1000 * 60 * 60 * 24;
+const MONTH_MS = DAY_MS * 30;
+const YEAR_MS = DAY_MS * 365;
+
+/**
+ * Creates a timezone-aware axis formatter function for different time ranges
+ */
+export function createTimezoneAwareAxisFormatter(rangeMs: number, timeZone: string) {
+ return function (value: number): string {
+ const timeStamp = new Date(Number(value));
+
+ // more than 5 years
+ if (rangeMs > YEAR_MS * 5) {
+ return formatWithTimeZone(timeStamp, 'yyyy', timeZone);
+ }
+
+ // more than 6 months
+ if (rangeMs > MONTH_MS * 6) {
+ return formatWithTimeZone(timeStamp, 'MMM yyyy', timeZone);
+ }
+
+ // more than 10 days
+ if (rangeMs > DAY_MS * 10) {
+ return formatWithTimeZone(timeStamp, 'dd.MM', timeZone);
+ }
+
+ // more than 2 days
+ if (rangeMs > DAY_MS * 2) {
+ return formatWithTimeZone(timeStamp, 'dd.MM HH:mm', timeZone);
+ }
+
+ // less or equal 2 days
+ return formatWithTimeZone(timeStamp, 'HH:mm', timeZone);
+ };
+}
diff --git a/components/src/LinksEditor/index.ts b/components/src/LinksEditor/index.ts
index 3547b52a..59bfccf0 100644
--- a/components/src/LinksEditor/index.ts
+++ b/components/src/LinksEditor/index.ts
@@ -11,5 +11,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-export * from './LinksEditor';
export * from './LinkEditorForm';
diff --git a/components/src/TimeSeriesTooltip/TimeChartTooltip.tsx b/components/src/TimeSeriesTooltip/TimeChartTooltip.tsx
index db7fe68f..42d1cf1d 100644
--- a/components/src/TimeSeriesTooltip/TimeChartTooltip.tsx
+++ b/components/src/TimeSeriesTooltip/TimeChartTooltip.tsx
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { memo, MutableRefObject, useRef, useState } from 'react';
+import { memo, MutableRefObject, useCallback, useLayoutEffect, useRef, useState } from 'react';
import { Box, Portal, Stack } from '@mui/material';
import { ECharts as EChartsInstance } from 'echarts/core';
import { TimeSeries } from '@perses-dev/spec';
@@ -57,26 +57,49 @@ export const TimeChartTooltip = memo(function TimeChartTooltip({
}: TimeChartTooltipProps) {
const [showAllSeries, setShowAllSeries] = useState(false);
const transform = useRef();
+ const tooltipElementRef = useRef(null);
const mousePos = useMousePosition();
const { height, width, ref: tooltipRef } = useResizeObserver();
const isTooltipPinned = pinnedPos !== null && enablePinning;
+ // Stable callback — prevents the ResizeObserver from disconnecting/reconnecting on every render.
+ const setTooltipRef = useCallback(
+ (node: HTMLDivElement | null): void => {
+ tooltipElementRef.current = node;
+ tooltipRef(node);
+ },
+ [tooltipRef]
+ );
+
+ const containerElement = containerId ? document.querySelector(containerId) : undefined;
+
+ // Synchronously reposition after every render to prevent one-frame viewport overflow.
+ useLayoutEffect(() => {
+ if (mousePos === null) return;
+ const node = tooltipElementRef.current;
+ if (!node) return;
+ const rect = node.getBoundingClientRect();
+ if (rect.height === 0 || rect.width === 0) return;
+ const nextTransform = assembleTransform(mousePos, pinnedPos, rect.height, rect.width, containerElement);
+ if (nextTransform && nextTransform !== transform.current) {
+ transform.current = nextTransform;
+ node.style.transform = nextTransform;
+ }
+ });
+
if (mousePos === null || mousePos.target === null || data === null) return null;
- // Ensure user is hovering over a chart before checking for nearby series.
if (pinnedPos === null && (mousePos.target as HTMLElement).tagName !== 'CANVAS') return null;
const chart = chartRef.current;
- const containerElement = containerId ? document.querySelector(containerId) : undefined;
- // if tooltip is attached to a container, set max height to the height of the container so tooltip does not get cut off
+ // Cap height to container so the tooltip is not cut off.
const maxHeight = containerElement ? containerElement.getBoundingClientRect().height : undefined;
transform.current = assembleTransform(mousePos, pinnedPos, height ?? 0, width ?? 0, containerElement);
- // Get series nearby the cursor and pass into tooltip content children.
const nearbySeries = getNearbySeriesData({
mousePos,
data,
@@ -96,7 +119,7 @@ export const TimeChartTooltip = memo(function TimeChartTooltip({
return (
getTooltipStyles(theme, pinnedPos, maxHeight)}
style={{
transform: transform.current,
diff --git a/components/src/TimeSeriesTooltip/TooltipContent.tsx b/components/src/TimeSeriesTooltip/TooltipContent.tsx
index 59227aaf..862e068b 100644
--- a/components/src/TimeSeriesTooltip/TooltipContent.tsx
+++ b/components/src/TimeSeriesTooltip/TooltipContent.tsx
@@ -13,7 +13,7 @@
import { ReactElement, useMemo } from 'react';
import { Box } from '@mui/material';
-import { NearbySeriesArray } from './nearby-series';
+import { NearbySeriesArray } from './types';
import { SeriesInfo } from './SeriesInfo';
export interface TooltipContentProps {
diff --git a/components/src/TimeSeriesTooltip/TooltipHeader.tsx b/components/src/TimeSeriesTooltip/TooltipHeader.tsx
index 56dc677b..d556521d 100644
--- a/components/src/TimeSeriesTooltip/TooltipHeader.tsx
+++ b/components/src/TimeSeriesTooltip/TooltipHeader.tsx
@@ -16,7 +16,7 @@ import Pin from 'mdi-material-ui/Pin';
import PinOutline from 'mdi-material-ui/PinOutline';
import { memo, ReactElement } from 'react';
import { useTimeZone } from '../context/TimeZoneProvider';
-import { NearbySeriesArray } from './nearby-series';
+import { NearbySeriesArray } from './types';
import {
TOOLTIP_BG_COLOR_FALLBACK,
TOOLTIP_MAX_WIDTH,
diff --git a/components/src/TimeSeriesTooltip/index.ts b/components/src/TimeSeriesTooltip/index.ts
index fd2a2d08..02c39e87 100644
--- a/components/src/TimeSeriesTooltip/index.ts
+++ b/components/src/TimeSeriesTooltip/index.ts
@@ -19,4 +19,5 @@ export * from './TooltipContent';
export * from './TooltipHeader';
export * from './nearby-series';
export * from './tooltip-model';
+export * from './types';
export * from './utils';
diff --git a/components/src/TimeSeriesTooltip/nearby-series.test.ts b/components/src/TimeSeriesTooltip/nearby-series.test.ts
index d0bfae26..35b6d73d 100644
--- a/components/src/TimeSeriesTooltip/nearby-series.test.ts
+++ b/components/src/TimeSeriesTooltip/nearby-series.test.ts
@@ -11,8 +11,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { EChartsDataFormat, FormatOptions } from '../model';
-import { legacyCheckforNearbySeries, getYBuffer, isWithinPercentageRange } from './nearby-series';
+import { ECharts as EChartsInstance } from 'echarts/core';
+import { TimeSeries } from '@perses-dev/spec';
+import { EChartsDataFormat, FormatOptions, TimeChartSeriesMapping } from '../model';
+import {
+ checkforNearbyTimeSeries,
+ legacyCheckforNearbySeries,
+ getYBuffer,
+ isWithinPercentageRange,
+} from './nearby-series';
+import { calculateVisualYForSeries } from './utils';
describe('legacyCheckforNearbySeries', () => {
const chartData: EChartsDataFormat = {
@@ -122,3 +130,151 @@ describe('isWithinPercentageRange', () => {
expect(result).toBe(false);
});
});
+
+describe('calculateVisualYForSeries', () => {
+ it('returns the raw yValue for non-stacked series and does not touch the totals map', () => {
+ const seriesMapping = [{ type: 'line', name: 'a' }] as unknown as TimeChartSeriesMapping;
+ const totals = new Map();
+
+ const result = calculateVisualYForSeries(0, 42, seriesMapping, totals);
+
+ expect(result).toBe(42);
+ expect(totals.size).toBe(0);
+ });
+
+ it('accumulates stacked totals per stack id across sequential calls', () => {
+ const seriesMapping = [
+ { type: 'line', name: 'a', stack: 'total' },
+ { type: 'line', name: 'b', stack: 'total' },
+ { type: 'line', name: 'c', stack: 'total' },
+ ] as unknown as TimeChartSeriesMapping;
+
+ const totals = new Map();
+
+ expect(calculateVisualYForSeries(0, 10, seriesMapping, totals)).toBe(10);
+ expect(calculateVisualYForSeries(1, 20, seriesMapping, totals)).toBe(30);
+ expect(calculateVisualYForSeries(2, 5, seriesMapping, totals)).toBe(35);
+ expect(totals.get('total')).toBe(35);
+ });
+
+ it('keeps totals separate across different stack ids', () => {
+ const seriesMapping = [
+ { type: 'line', name: 'a', stack: 'left' },
+ { type: 'line', name: 'b', stack: 'right' },
+ { type: 'line', name: 'c', stack: 'left' },
+ ] as unknown as TimeChartSeriesMapping;
+
+ const totals = new Map();
+
+ expect(calculateVisualYForSeries(0, 100, seriesMapping, totals)).toBe(100);
+ expect(calculateVisualYForSeries(1, 50, seriesMapping, totals)).toBe(50);
+ expect(calculateVisualYForSeries(2, 25, seriesMapping, totals)).toBe(125);
+ expect(totals.get('left')).toBe(125);
+ expect(totals.get('right')).toBe(50);
+ });
+});
+
+describe('checkforNearbyTimeSeries — stacked lines', () => {
+ const TIMESTAMP = 1_700_000_000_000;
+
+ function buildStackedFixture(): {
+ data: TimeSeries[];
+ seriesMapping: TimeChartSeriesMapping;
+ } {
+ const data: TimeSeries[] = [
+ { name: 'series-a', values: [[TIMESTAMP, 10]] },
+ { name: 'series-b', values: [[TIMESTAMP, 20]] },
+ ];
+ const seriesMapping = [
+ { type: 'line', name: 'series-a', color: '#111', stack: 'group', id: 'a' },
+ { type: 'line', name: 'series-b', color: '#222', stack: 'group', id: 'b' },
+ ] as unknown as TimeChartSeriesMapping;
+ return { data, seriesMapping };
+ }
+
+ function buildChartMock(): {
+ chart: EChartsInstance;
+ dispatched: Array<{ type: string; seriesIndex?: number | number[]; dataIndex?: number }>;
+ } {
+ const dispatched: Array<{ type: string; seriesIndex?: number | number[]; dataIndex?: number }> = [];
+ const chart = {
+ dispatchAction: (action: { type: string; seriesIndex?: number | number[]; dataIndex?: number }): void => {
+ dispatched.push(action);
+ },
+ // Simple identity-style mock: return the value passed in as pixel Y.
+ convertToPixel: (_finder: unknown, value: number[]): number[] => [value[0] ?? 0, value[1] ?? 0],
+ getDom: (): null => null,
+ } as unknown as EChartsInstance;
+ return { chart, dispatched };
+ }
+
+ it('emphasizes the visually-hovered stacked series (top of stack), not the one with the matching raw value', () => {
+ const { data, seriesMapping } = buildStackedFixture();
+ const { chart, dispatched } = buildChartMock();
+
+ const yBuffer = 5;
+ const result = checkforNearbyTimeSeries(data, seriesMapping, [TIMESTAMP, 30], yBuffer, chart);
+
+ const winner = result.find((series) => series.isClosestToCursor);
+ expect(winner).toBeDefined();
+ expect(winner?.seriesName).toBe('series-b');
+ // y should be the raw per-series value (20), not the accumulated visual Y (30)
+ expect(winner?.y).toBe(20);
+
+ const seriesA = result.find((series) => series.seriesName === 'series-a');
+ expect(seriesA).toBeUndefined();
+
+ // Non-candidate series (a) must be explicitly downplayed to clear stale emphasis.
+ const downplays = dispatched.filter((action) => action.type === 'downplay');
+ const downplayedSeriesIdxs = new Set();
+ for (const action of downplays) {
+ if (Array.isArray(action.seriesIndex)) {
+ for (const idx of action.seriesIndex) downplayedSeriesIdxs.add(idx);
+ } else if (typeof action.seriesIndex === 'number') {
+ downplayedSeriesIdxs.add(action.seriesIndex);
+ }
+ }
+ expect(downplayedSeriesIdxs.has(0)).toBe(true);
+ });
+
+ it('emphasizes series-a when the cursor is near its visual position (bottom of stack)', () => {
+ const { data, seriesMapping } = buildStackedFixture();
+ const { chart } = buildChartMock();
+
+ const result = checkforNearbyTimeSeries(data, seriesMapping, [TIMESTAMP, 10], 5, chart);
+ const winner = result.find((series) => series.isClosestToCursor);
+ expect(winner?.seriesName).toBe('series-a');
+ });
+
+ it('downplays previously-hovered non-candidate series (fix for persistent emphasis)', () => {
+ const data: TimeSeries[] = [
+ { name: 'a', values: [[TIMESTAMP, 1]] },
+ { name: 'b', values: [[TIMESTAMP, 2]] },
+ { name: 'c', values: [[TIMESTAMP, 3]] },
+ ];
+ const seriesMapping = [
+ { type: 'line', name: 'a', color: '#111', id: 'a' },
+ { type: 'line', name: 'b', color: '#222', id: 'b' },
+ { type: 'line', name: 'c', color: '#333', id: 'c' },
+ ] as unknown as TimeChartSeriesMapping;
+
+ const { chart, dispatched } = buildChartMock();
+
+ const result = checkforNearbyTimeSeries(data, seriesMapping, [TIMESTAMP, 1000], 0.5, chart);
+
+ expect(result).toEqual([]);
+
+ const downplayed = new Set();
+ for (const action of dispatched) {
+ if (action.type !== 'downplay') continue;
+ if (Array.isArray(action.seriesIndex)) {
+ for (const idx of action.seriesIndex) downplayed.add(idx);
+ } else if (typeof action.seriesIndex === 'number') {
+ downplayed.add(action.seriesIndex);
+ }
+ }
+ expect(downplayed.has(0)).toBe(true);
+ expect(downplayed.has(1)).toBe(true);
+ expect(downplayed.has(2)).toBe(true);
+ });
+});
diff --git a/components/src/TimeSeriesTooltip/nearby-series.ts b/components/src/TimeSeriesTooltip/nearby-series.ts
index 6764f67d..134f8797 100644
--- a/components/src/TimeSeriesTooltip/nearby-series.ts
+++ b/components/src/TimeSeriesTooltip/nearby-series.ts
@@ -12,7 +12,7 @@
// limitations under the License.
import { ECharts as EChartsInstance } from 'echarts/core';
-import { LineSeriesOption } from 'echarts/charts';
+import { BarSeriesOption } from 'echarts/charts';
import { TimeSeries, TimeSeriesValueTuple } from '@perses-dev/spec';
import {
EChartsDataFormat,
@@ -24,25 +24,273 @@ import {
} from '../model';
import { batchDispatchNearbySeriesActions, getPointInGrid, getClosestTimestamp } from '../utils';
import { CursorCoordinates, CursorData, EMPTY_TOOLTIP_DATA } from './tooltip-model';
+import {
+ calculateBarBandwidth,
+ calculateBarSegmentBounds,
+ calculateBarYBounds,
+ calculateVisualYForSeries,
+ getPixelXFromGrid,
+} from './utils';
+import { Candidate, GetYBufferParams, IsWithinPercentageRangeParams, NearbySeriesArray } from './types';
+
+export type { NearbySeriesArray, NearbySeriesInfo } from './types';
// increase multipliers to show more series in tooltip
export const INCREASE_NEARBY_SERIES_MULTIPLIER = 5.5; // adjusts how many series show in tooltip (higher == more series shown)
export const DYNAMIC_NEARBY_SERIES_MULTIPLIER = 30; // used for adjustment after series number divisor
export const SHOW_FEWER_SERIES_LIMIT = 5;
-export interface NearbySeriesInfo {
- seriesIdx: number | null;
- datumIdx: number | null;
- seriesName: string;
- date: number;
- markerColor: string;
- x: number;
- y: number;
- formattedY: string;
- isClosestToCursor: boolean;
+function gatherCandidates(
+ data: TimeSeries[],
+ seriesMapping: TimeChartSeriesMapping,
+ closestTimestamp: number,
+ cursorX: number,
+ cursorY: number,
+ cursorXPixel: number | null,
+ cursorPixelY: number | undefined,
+ yBuffer: number,
+ yBufferPixels: number | null,
+ chart: EChartsInstance
+): Candidate[] {
+ const candidates: Candidate[] = [];
+ const totalSeries = data.length;
+
+ const stackTotals = new Map();
+
+ let sortedTimestamps: number[] = [];
+ const firstValues = data[0]?.values;
+ if (firstValues && firstValues.length > 0) {
+ const seen = new Set();
+ for (const [ts] of firstValues) {
+ if (!seen.has(ts)) {
+ seen.add(ts);
+ sortedTimestamps.push(ts);
+ }
+ }
+ sortedTimestamps = sortedTimestamps.sort((a, b) => a - b);
+ }
+
+ // Bar-only indexes: ECharts groups bars independently of lines, so bar-relative index and count must exclude line series.
+ const barSeriesIndexes: number[] = [];
+ for (let i = 0; i < totalSeries; i++) {
+ if ((seriesMapping[i]?.type ?? 'line') === 'bar') barSeriesIndexes.push(i);
+ }
+
+ // Computed once outside the loop — both depend only on the timestamp, not the series index.
+ let barBandwidth: number | null = null;
+ let barCenterPixelX: number | null = null;
+ if (barSeriesIndexes.length > 0 && cursorXPixel !== null) {
+ barBandwidth = calculateBarBandwidth(closestTimestamp, sortedTimestamps, chart);
+ barCenterPixelX = getPixelXFromGrid(closestTimestamp, chart);
+ }
+
+ for (let seriesIdx = 0; seriesIdx < totalSeries; seriesIdx++) {
+ const currentSeries = seriesMapping[seriesIdx];
+ if (!currentSeries) continue;
+
+ const currentDataset = data[seriesIdx];
+ if (!currentDataset) continue;
+
+ const currentDatasetValues: TimeSeriesValueTuple[] | undefined = currentDataset.values;
+ if (!currentDatasetValues || !Array.isArray(currentDatasetValues)) continue;
+
+ const seriesType = currentSeries.type ?? 'line';
+ const currentSeriesName = currentSeries.name ? currentSeries.name.toString() : '';
+ const seriesId = currentSeries.id ? currentSeries.id.toString() : '';
+ const markerColor = (currentSeries.color ?? '#000').toString();
+
+ let datumIdx = -1;
+ let xValue = 0;
+ let yValue: number | null | undefined;
+ for (let i = 0; i < currentDatasetValues.length; i++) {
+ const tuple = currentDatasetValues[i];
+ if (!tuple) continue;
+ if (tuple[0] === closestTimestamp) {
+ datumIdx = i;
+ xValue = tuple[0];
+ yValue = tuple[1];
+ break;
+ }
+ }
+ if (datumIdx === -1) continue;
+
+ if (yValue === null || yValue === undefined) continue;
+
+ let isCandidate = false;
+ let visualY = yValue;
+ let distance = Infinity;
+
+ if (seriesType === 'line') {
+ visualY = calculateVisualYForSeries(seriesIdx, yValue, seriesMapping, stackTotals);
+
+ if (cursorPixelY !== undefined && yBufferPixels !== null) {
+ try {
+ const dataPointPixel = chart.convertToPixel({ seriesIndex: seriesIdx }, [datumIdx, visualY]);
+ if (dataPointPixel && dataPointPixel[1] !== undefined) {
+ const pixelDistance = Math.abs(cursorPixelY - dataPointPixel[1]);
+ isCandidate = pixelDistance <= yBufferPixels;
+ distance = pixelDistance;
+ } else {
+ const verticalDistance = Math.abs(visualY - cursorY);
+ isCandidate = verticalDistance <= yBuffer;
+ distance = verticalDistance;
+ }
+ } catch {
+ const verticalDistance = Math.abs(visualY - cursorY);
+ isCandidate = verticalDistance <= yBuffer;
+ distance = verticalDistance;
+ }
+ } else {
+ const verticalDistance = Math.abs(visualY - cursorY);
+ isCandidate = verticalDistance <= yBuffer;
+ distance = verticalDistance;
+ }
+ } else if (seriesType === 'bar') {
+ if (cursorXPixel === null || barBandwidth === null || barCenterPixelX === null) continue;
+
+ const barRelativeIdx = barSeriesIndexes.indexOf(seriesIdx);
+ if (barRelativeIdx === -1) continue;
+
+ const segmentBounds = calculateBarSegmentBounds(
+ barRelativeIdx,
+ barBandwidth,
+ barCenterPixelX,
+ barSeriesIndexes.length
+ );
+
+ const isWithinXBounds = cursorXPixel >= segmentBounds.left && cursorXPixel <= segmentBounds.right;
+ if (!isWithinXBounds) continue;
+
+ const stackId = (currentSeries as BarSeriesOption).stack;
+ let isHoveringYBounds = true;
+
+ if (stackId) {
+ const stackIdStr = stackId.toString();
+ const visualYBottom = stackTotals.get(stackIdStr) ?? 0;
+ visualY = calculateVisualYForSeries(seriesIdx, yValue, seriesMapping, stackTotals);
+ const yBounds = calculateBarYBounds(visualYBottom, visualY, chart);
+
+ if (yBounds) {
+ const cursorYPixel = chart.convertToPixel('grid', [0, cursorY]);
+ if (cursorYPixel && cursorYPixel[1] !== undefined) {
+ isHoveringYBounds = cursorYPixel[1] >= yBounds.top && cursorYPixel[1] <= yBounds.bottom;
+ }
+ }
+ } else {
+ visualY = yValue;
+ }
+
+ if (!isHoveringYBounds) continue;
+
+ const segmentCenter = (segmentBounds.left + segmentBounds.right) / 2;
+ distance = Math.abs(cursorXPixel - segmentCenter);
+ isCandidate = true;
+ }
+
+ if (isCandidate) {
+ candidates.push({
+ seriesIdx,
+ datumIdx,
+ seriesId,
+ seriesName: currentSeriesName,
+ date: closestTimestamp,
+ markerColor,
+ x: xValue,
+ y: yValue,
+ visualY,
+ distance,
+ });
+ }
+ }
+
+ return candidates;
}
-export type NearbySeriesArray = NearbySeriesInfo[];
+function findClosestCandidate(candidates: Candidate[]): Candidate | null {
+ if (candidates.length === 0) return null;
+ let winner: Candidate | null = null;
+ for (const candidate of candidates) {
+ if (winner === null || candidate.distance < winner.distance) {
+ winner = candidate;
+ }
+ }
+ return winner;
+}
+
+function processCandidates(
+ candidates: Candidate[],
+ winner: Candidate | null,
+ format: FormatOptions | undefined,
+ seriesFormatMap: Map | undefined,
+ chart: EChartsInstance,
+ nonCandidateSeriesIndexes: number[]
+): NearbySeriesArray {
+ const nearbySeriesIndexes: number[] = [];
+ const emphasizedSeriesIndexes: number[] = [];
+ const nonEmphasizedSeriesIndexes: number[] = [...nonCandidateSeriesIndexes];
+ const emphasizedDatapoints: DatapointInfo[] = [];
+ const duplicateDatapoints: DatapointInfo[] = [];
+ const yValueCounts: Map = new Map();
+
+ const result: NearbySeriesArray = [];
+
+ for (const candidate of candidates) {
+ const seriesFormat = seriesFormatMap?.get(candidate.seriesId) ?? format;
+ // Use raw y, not visualY — visualY is for proximity detection only.
+ const displayY = candidate.y;
+ const formattedY = formatValue(displayY, seriesFormat);
+ const isClosestToCursor = winner !== null && candidate.seriesIdx === winner.seriesIdx;
+
+ if (isClosestToCursor) {
+ emphasizedSeriesIndexes.push(candidate.seriesIdx);
+
+ const duplicateValuesCount = yValueCounts.get(displayY) ?? 0;
+ yValueCounts.set(displayY, duplicateValuesCount + 1);
+ if (duplicateValuesCount > 0) {
+ duplicateDatapoints.push({
+ seriesIndex: candidate.seriesIdx,
+ dataIndex: candidate.datumIdx,
+ seriesName: candidate.seriesName,
+ yValue: displayY,
+ });
+ }
+
+ emphasizedDatapoints.push({
+ seriesIndex: candidate.seriesIdx,
+ dataIndex: candidate.datumIdx,
+ seriesName: candidate.seriesName,
+ yValue: displayY,
+ });
+ } else {
+ nonEmphasizedSeriesIndexes.push(candidate.seriesIdx);
+ }
+
+ result.push({
+ seriesIdx: candidate.seriesIdx,
+ datumIdx: candidate.datumIdx,
+ seriesName: candidate.seriesName,
+ date: candidate.date,
+ x: candidate.x,
+ y: displayY,
+ formattedY,
+ markerColor: candidate.markerColor,
+ isClosestToCursor,
+ });
+
+ nearbySeriesIndexes.push(candidate.seriesIdx);
+ }
+
+ batchDispatchNearbySeriesActions(
+ chart,
+ nearbySeriesIndexes,
+ emphasizedSeriesIndexes,
+ nonEmphasizedSeriesIndexes,
+ emphasizedDatapoints,
+ duplicateDatapoints
+ );
+
+ return result;
+}
/**
* Returns formatted series data for the points that are close to the user's cursor.
@@ -57,40 +305,23 @@ export function checkforNearbyTimeSeries(
format?: FormatOptions,
seriesFormatMap?: Map,
// in the case of multi-axis, we need the cursor Y position in pixel space
- cursorPixelY?: number
+ cursorPixelY?: number,
+ cursorXPixel?: number | null
): NearbySeriesArray {
- const currentNearbySeriesData: NearbySeriesArray = [];
const cursorX: number | null = pointInGrid[0] ?? null;
const cursorY: number | null = pointInGrid[1] ?? null;
- if (cursorX === null || cursorY === null) return currentNearbySeriesData;
+ if (cursorX === null || cursorY === null) return EMPTY_TOOLTIP_DATA;
+ if (chart.dispatchAction === undefined) return EMPTY_TOOLTIP_DATA;
+ if (!Array.isArray(data)) return EMPTY_TOOLTIP_DATA;
- if (chart.dispatchAction === undefined) return currentNearbySeriesData;
-
- if (!Array.isArray(data)) return currentNearbySeriesData;
- const nearbySeriesIndexes: number[] = [];
- const emphasizedSeriesIndexes: number[] = [];
- const nonEmphasizedSeriesIndexes: number[] = [];
- const emphasizedDatapoints: DatapointInfo[] = [];
- const duplicateDatapoints: DatapointInfo[] = [];
-
- const totalSeries = data.length;
-
- const yValueCounts: Map = new Map();
-
- // Only need to loop through first dataset source since getCommonTimeScale ensures xAxis timestamps are consistent
+ // All series share the same x-axis timestamps (enforced by getCommonTimeScale).
const firstTimeSeriesValues = data[0]?.values;
const closestTimestamp = getClosestTimestamp(firstTimeSeriesValues, cursorX);
+ if (closestTimestamp === null) return EMPTY_TOOLTIP_DATA;
- if (closestTimestamp === null) {
- return EMPTY_TOOLTIP_DATA;
- }
-
- // For multi-axis support: convert yBuffer to pixel space for consistent comparison
- // This allows us to compare series on different Y axes fairly
let yBufferPixels: number | null = null;
if (cursorPixelY !== undefined) {
- // Convert a point at cursorY and cursorY + yBuffer to pixels to get the buffer in pixel space
const cursorPoint = chart.convertToPixel('grid', [0, cursorY]);
const bufferPoint = chart.convertToPixel('grid', [0, cursorY + yBuffer]);
if (cursorPoint && bufferPoint && cursorPoint[1] !== undefined && bufferPoint[1] !== undefined) {
@@ -98,143 +329,31 @@ export function checkforNearbyTimeSeries(
}
}
- // find the timestamp with data that is closest to cursorX
- for (let seriesIdx = 0; seriesIdx < totalSeries; seriesIdx++) {
- const currentSeries = seriesMapping[seriesIdx];
- if (!currentSeries) break;
-
- const currentDataset = totalSeries > 0 ? data[seriesIdx] : null;
- if (!currentDataset) break;
-
- const currentDatasetValues: TimeSeriesValueTuple[] = currentDataset.values;
- if (currentDatasetValues === undefined || !Array.isArray(currentDatasetValues)) break;
- const lineSeries = currentSeries as LineSeriesOption;
- const currentSeriesName = lineSeries.name ? lineSeries.name.toString() : '';
- const seriesId = lineSeries.id ? lineSeries.id.toString() : '';
- const markerColor = lineSeries.color ?? '#000';
-
- // Get the format for this series (from seriesFormatMap or fallback to default format)
- const seriesFormat = seriesFormatMap?.get(seriesId) ?? format;
-
- if (Array.isArray(data)) {
- for (let datumIdx = 0; datumIdx < currentDatasetValues.length; datumIdx++) {
- const nearbyTimeSeries = currentDatasetValues[datumIdx];
- if (nearbyTimeSeries === undefined || !Array.isArray(nearbyTimeSeries)) break;
-
- const xValue = nearbyTimeSeries[0];
- const yValue = nearbyTimeSeries[1];
- // TODO: ensure null values not displayed in tooltip
- if (yValue !== undefined && yValue !== null) {
- if (closestTimestamp === xValue) {
- // Check if this series is nearby the cursor
- let isNearby = false;
-
- // For multi-axis: compare in pixel space
- if (cursorPixelY !== undefined && yBufferPixels !== null) {
- const dataPointPixel = chart.convertToPixel({ seriesIndex: seriesIdx }, [datumIdx, yValue]);
- if (dataPointPixel && dataPointPixel[1] !== undefined) {
- const pixelDistance = Math.abs(cursorPixelY - dataPointPixel[1]);
- isNearby = pixelDistance <= yBufferPixels;
- } else {
- // Fallback to data-space comparison for primary axis
- isNearby = cursorY <= yValue + yBuffer && cursorY >= yValue - yBuffer;
- }
- } else {
- // Fallback to original data-space comparison
- isNearby = cursorY <= yValue + yBuffer && cursorY >= yValue - yBuffer;
- }
-
- if (isNearby) {
- // show fewer bold series in tooltip when many total series
- const minPercentRange = totalSeries > SHOW_FEWER_SERIES_LIMIT ? 2 : 5;
- const percentRangeToCheck = Math.max(minPercentRange, 100 / totalSeries);
-
- // For isClosestToCursor, also use pixel space for multi-axis
- let isClosestToCursor = false;
- if (cursorPixelY !== undefined) {
- const dataPointPixel = chart.convertToPixel({ seriesIndex: seriesIdx }, [datumIdx, yValue]);
- if (dataPointPixel && dataPointPixel[1] !== undefined) {
- const pixelDistance = Math.abs(cursorPixelY - dataPointPixel[1]);
- // Use percentage of buffer for "closest" determination
- const tightBufferPixels = (yBufferPixels ?? 50) * (percentRangeToCheck / 100);
- isClosestToCursor = pixelDistance <= Math.max(tightBufferPixels, 5);
- } else {
- isClosestToCursor = isWithinPercentageRange({
- valueToCheck: cursorY,
- baseValue: yValue,
- percentage: percentRangeToCheck,
- });
- }
- } else {
- isClosestToCursor = isWithinPercentageRange({
- valueToCheck: cursorY,
- baseValue: yValue,
- percentage: percentRangeToCheck,
- });
- }
-
- if (isClosestToCursor) {
- // shows as bold in tooltip, customize 'emphasis' options in getTimeSeries util
- emphasizedSeriesIndexes.push(seriesIdx);
+ const resolvedCursorXPixel = cursorXPixel ?? getPixelXFromGrid(closestTimestamp, chart);
+
+ const candidates = gatherCandidates(
+ data,
+ seriesMapping,
+ closestTimestamp,
+ cursorX,
+ cursorY,
+ resolvedCursorXPixel,
+ cursorPixelY,
+ yBuffer,
+ yBufferPixels,
+ chart
+ );
- // Used to determine which datapoint to apply select styles to.
- // Accounts for cases where lines may be rendered directly on top of eachother.
- const duplicateValuesCount = yValueCounts.get(yValue) ?? 0;
- yValueCounts.set(yValue, duplicateValuesCount + 1);
- if (duplicateValuesCount > 0) {
- duplicateDatapoints.push({
- seriesIndex: seriesIdx,
- dataIndex: datumIdx,
- seriesName: currentSeriesName,
- yValue: yValue,
- });
- }
+ const winner = findClosestCandidate(candidates);
- // keep track of all bold datapoints in tooltip so that 'select' state only applied to topmost
- emphasizedDatapoints.push({
- seriesIndex: seriesIdx,
- dataIndex: datumIdx,
- seriesName: currentSeriesName,
- yValue: yValue,
- });
- } else {
- nonEmphasizedSeriesIndexes.push(seriesIdx);
- // ensure series far away from cursor are not highlighted
- chart.dispatchAction({
- type: 'downplay',
- seriesIndex: seriesIdx,
- });
- }
- const formattedY = formatValue(yValue, seriesFormat);
- currentNearbySeriesData.push({
- seriesIdx: seriesIdx,
- datumIdx: datumIdx,
- seriesName: currentSeriesName,
- date: closestTimestamp,
- x: xValue,
- y: yValue,
- formattedY: formattedY,
- markerColor: markerColor.toString(),
- isClosestToCursor,
- });
- nearbySeriesIndexes.push(seriesIdx);
- }
- }
- }
- }
- }
+ const candidateIndexes = new Set();
+ for (const candidate of candidates) candidateIndexes.add(candidate.seriesIdx);
+ const nonCandidateSeriesIndexes: number[] = [];
+ for (let idx = 0; idx < data.length; idx++) {
+ if (!candidateIndexes.has(idx)) nonCandidateSeriesIndexes.push(idx);
}
- batchDispatchNearbySeriesActions(
- chart,
- nearbySeriesIndexes,
- emphasizedSeriesIndexes,
- nonEmphasizedSeriesIndexes,
- emphasizedDatapoints,
- duplicateDatapoints
- );
-
- return currentNearbySeriesData;
+ return processCandidates(candidates, winner, format, seriesFormatMap, chart, nonCandidateSeriesIndexes);
}
/**
@@ -396,31 +515,28 @@ export function getNearbySeriesData({
if (cursorTargetMatchesChart === false || data === null || chart['_model'] === undefined) return EMPTY_TOOLTIP_DATA;
- // mousemove position undefined when not hovering over chart canvas
if (mousePos.plotCanvas.x === undefined || mousePos.plotCanvas.y === undefined) return EMPTY_TOOLTIP_DATA;
const cursorPixelY = mousePos.plotCanvas.y;
- const pointInGrid = getPointInGrid(mousePos.plotCanvas.x, cursorPixelY, chart);
+ const cursorXPixel = mousePos.plotCanvas.x;
+ const pointInGrid = getPointInGrid(cursorXPixel, cursorPixelY, chart);
if (pointInGrid !== null) {
const chartModel = chart['_model'];
const yAxisScale = chartModel.getComponent('yAxis').axis.scale;
const isLogScale = yAxisScale.type === 'log';
let yInterval = yAxisScale._interval;
- // For logarithmic scales, convert the log interval to actual data range
+ // For log scales, convert from log-space extent to actual data range and use 1% as the interval.
if (isLogScale && yAxisScale.base) {
const logBase = yAxisScale.base;
const extent = yAxisScale._extent;
- // Calculate actual data range from log extent
- // extent is in log space (e.g., [0, 2] for 10^0 to 10^2)
+ // e.g. extent [0, 2] → 10^0..10^2
const actualMin = logBase ** extent[0];
const actualMax = logBase ** extent[1];
- // Use a fraction of the actual range as the interval
yInterval = (actualMax - actualMin) / 100;
}
const totalSeries = data.length;
const yBuffer = getYBuffer({ yInterval, totalSeries, showAllSeries });
- // Detect if chart has multiple Y-axes by checking if any series uses yAxisIndex > 0
const hasMultipleYAxes = seriesMapping.some((series) => series.yAxisIndex !== undefined && series.yAxisIndex > 0);
return checkforNearbyTimeSeries(
@@ -431,7 +547,8 @@ export function getNearbySeriesData({
chart,
format,
seriesFormatMap,
- hasMultipleYAxes ? cursorPixelY : undefined
+ hasMultipleYAxes ? cursorPixelY : undefined,
+ cursorXPixel
);
}
@@ -446,11 +563,7 @@ export function isWithinPercentageRange({
valueToCheck,
baseValue,
percentage,
-}: {
- valueToCheck: number;
- baseValue: number;
- percentage: number;
-}): boolean {
+}: IsWithinPercentageRangeParams): boolean {
const range = (percentage / 100) * baseValue;
const lowerBound = baseValue - range;
const upperBound = baseValue + range;
@@ -460,15 +573,7 @@ export function isWithinPercentageRange({
/*
* Get range to check within for nearby series to show in tooltip.
*/
-export function getYBuffer({
- yInterval,
- totalSeries,
- showAllSeries = false,
-}: {
- yInterval: number;
- totalSeries: number;
- showAllSeries?: boolean;
-}): number {
+export function getYBuffer({ yInterval, totalSeries, showAllSeries = false }: GetYBufferParams): number {
if (showAllSeries) {
return yInterval * 10; // roughly correlates with grid so entire canvas is searched
}
diff --git a/components/src/TimeSeriesTooltip/tooltip-model.ts b/components/src/TimeSeriesTooltip/tooltip-model.ts
index 9b819328..d10ab3d6 100644
--- a/components/src/TimeSeriesTooltip/tooltip-model.ts
+++ b/components/src/TimeSeriesTooltip/tooltip-model.ts
@@ -12,7 +12,7 @@
// limitations under the License.
import { useEffect, useState } from 'react';
-import { NearbySeriesArray } from './nearby-series';
+import { NearbySeriesArray } from './types';
export const TOOLTIP_MIN_WIDTH = 375;
export const TOOLTIP_MAX_WIDTH = 650;
diff --git a/components/src/TimeSeriesTooltip/types.ts b/components/src/TimeSeriesTooltip/types.ts
new file mode 100644
index 00000000..d3c75154
--- /dev/null
+++ b/components/src/TimeSeriesTooltip/types.ts
@@ -0,0 +1,46 @@
+// Copyright The Perses Authors
+// Licensed 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 interface NearbySeriesInfo {
+ seriesIdx: number | null;
+ datumIdx: number | null;
+ seriesName: string;
+ date: number;
+ markerColor: string;
+ x: number;
+ y: number;
+ formattedY: string;
+ isClosestToCursor: boolean;
+}
+
+export type NearbySeriesArray = NearbySeriesInfo[];
+
+export type Candidate = Omit & {
+ seriesIdx: number;
+ datumIdx: number;
+ seriesId: string;
+ visualY: number;
+ distance: number;
+};
+
+export type IsWithinPercentageRangeParams = {
+ valueToCheck: number;
+ baseValue: number;
+ percentage: number;
+};
+
+export type GetYBufferParams = {
+ yInterval: number;
+ totalSeries: number;
+ showAllSeries?: boolean;
+};
diff --git a/components/src/TimeSeriesTooltip/utils.test.ts b/components/src/TimeSeriesTooltip/utils.test.ts
new file mode 100644
index 00000000..29661f58
--- /dev/null
+++ b/components/src/TimeSeriesTooltip/utils.test.ts
@@ -0,0 +1,158 @@
+// Copyright The Perses Authors
+// Licensed 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 { assembleTransform } from './utils';
+import { CursorData, TOOLTIP_MAX_HEIGHT, TOOLTIP_MAX_WIDTH } from './tooltip-model';
+
+const VIEWPORT_WIDTH = 1600;
+const VIEWPORT_HEIGHT = 720;
+
+function makeMousePos(pageX: number, pageY: number): CursorData['coords'] {
+ return {
+ page: { x: pageX, y: pageY },
+ client: { x: pageX, y: pageY },
+ plotCanvas: { x: pageX, y: pageY },
+ target: null,
+ };
+}
+
+function parseTransform(transform: string | undefined): { x: number; y: number } | null {
+ if (!transform) return null;
+ const match = transform.match(/translate3d\((-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px,\s*0(?:px)?\)/);
+ if (!match || match[1] === undefined || match[2] === undefined) return null;
+ return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
+}
+
+describe('assembleTransform', () => {
+ const originalInnerWidth = window.innerWidth;
+ const originalInnerHeight = window.innerHeight;
+ const originalScrollY = window.scrollY;
+
+ beforeAll(() => {
+ Object.defineProperty(window, 'innerWidth', { configurable: true, value: VIEWPORT_WIDTH });
+ Object.defineProperty(window, 'innerHeight', { configurable: true, value: VIEWPORT_HEIGHT });
+ Object.defineProperty(window, 'scrollY', { configurable: true, value: 0 });
+ });
+
+ afterAll(() => {
+ Object.defineProperty(window, 'innerWidth', { configurable: true, value: originalInnerWidth });
+ Object.defineProperty(window, 'innerHeight', { configurable: true, value: originalInnerHeight });
+ Object.defineProperty(window, 'scrollY', { configurable: true, value: originalScrollY });
+ });
+
+ it('returns undefined when mousePos is null', () => {
+ expect(assembleTransform(null, null, 200, 400)).toBeUndefined();
+ });
+
+ describe('when tooltip size is unknown (first render)', () => {
+ it('uses TOOLTIP_MAX_HEIGHT as fallback and clamps against the viewport bottom', () => {
+ const mousePos = makeMousePos(800, 400);
+ const result = parseTransform(assembleTransform(mousePos, null, 0, 0));
+ expect(result).not.toBeNull();
+ expect(result!.y).toBe(VIEWPORT_HEIGHT - TOOLTIP_MAX_HEIGHT - 16);
+ // Worst-case bottom (y + max height) must still fit inside the viewport.
+ expect(result!.y + TOOLTIP_MAX_HEIGHT).toBeLessThanOrEqual(VIEWPORT_HEIGHT);
+ });
+
+ it('uses TOOLTIP_MAX_WIDTH as fallback and flips to the left of the cursor when needed', () => {
+ const mousePos = makeMousePos(VIEWPORT_WIDTH - 100, 100);
+ const result = parseTransform(assembleTransform(mousePos, null, 0, 0));
+ expect(result).not.toBeNull();
+ expect(result!.x).toBe(VIEWPORT_WIDTH - 100 - TOOLTIP_MAX_WIDTH - 32);
+ });
+ });
+
+ describe('when tooltip size is known', () => {
+ it('places the tooltip to the right and below the cursor when it fits', () => {
+ const mousePos = makeMousePos(400, 100);
+ const result = parseTransform(assembleTransform(mousePos, null, 200, 300));
+ expect(result).toEqual({ x: 400 + 32, y: 100 + 16 });
+ });
+
+ it('clamps y so the tooltip does not extend past the viewport bottom', () => {
+ const mousePos = makeMousePos(400, 600);
+ const result = parseTransform(assembleTransform(mousePos, null, 400, 300));
+ expect(result).not.toBeNull();
+ expect(result!.y).toBe(VIEWPORT_HEIGHT - 400 - 16);
+ expect(result!.y + 400).toBeLessThanOrEqual(VIEWPORT_HEIGHT);
+ });
+
+ it('flips the tooltip to the left of the cursor when it would overflow the right edge', () => {
+ const mousePos = makeMousePos(VIEWPORT_WIDTH - 100, 100);
+ const result = parseTransform(assembleTransform(mousePos, null, 200, 400));
+ expect(result).not.toBeNull();
+ expect(result!.x).toBe(VIEWPORT_WIDTH - 100 - 400 - 32);
+ });
+
+ it('never places the tooltip past the left edge of the viewport', () => {
+ const mousePos = makeMousePos(0, 100);
+ const result = parseTransform(assembleTransform(mousePos, null, 200, 3000));
+ expect(result).not.toBeNull();
+ expect(result!.x).toBe(32);
+ });
+
+ it('never places the tooltip past the top of the viewport', () => {
+ const mousePos = makeMousePos(400, -100);
+ const result = parseTransform(assembleTransform(mousePos, null, 200, 400));
+ expect(result).not.toBeNull();
+ expect(result!.y).toBe(4);
+ });
+ });
+
+ describe('when a pinnedPos is provided', () => {
+ it('uses pinnedPos instead of the live mouse position', () => {
+ const mousePos = makeMousePos(400, 100);
+ const pinnedPos = {
+ page: { x: 800, y: 200 },
+ client: { x: 800, y: 200 },
+ plotCanvas: { x: 800, y: 200 },
+ target: null,
+ };
+ const result = parseTransform(assembleTransform(mousePos, pinnedPos, 200, 300));
+ expect(result).toEqual({ x: 832, y: 216 });
+ });
+ });
+
+ describe('when a container element is provided', () => {
+ it('adjusts coordinates relative to the container', () => {
+ const container = {
+ getBoundingClientRect: () =>
+ ({ top: 200, left: 100, width: 800, height: 400, right: 900, bottom: 600, x: 100, y: 200 }) as DOMRect,
+ scrollLeft: 0,
+ scrollTop: 0,
+ scrollHeight: 400,
+ } as unknown as Element;
+
+ // Cursor at page (500, 250) → relative (400, 50); tooltip 100 tall fits inside container.
+ const mousePos = makeMousePos(500, 250);
+ const result = parseTransform(assembleTransform(mousePos, null, 100, 200, container));
+ expect(result).not.toBeNull();
+ expect(result).toEqual({ x: 432, y: 66 });
+ });
+
+ it('falls back to TOOLTIP_MAX_HEIGHT when tooltip size is unknown inside a container', () => {
+ const container = {
+ getBoundingClientRect: () =>
+ ({ top: 0, left: 0, width: 800, height: 400, right: 800, bottom: 400, x: 0, y: 0 }) as DOMRect,
+ scrollLeft: 0,
+ scrollTop: 0,
+ scrollHeight: 400,
+ } as unknown as Element;
+
+ const mousePos = makeMousePos(400, 300);
+ const result = parseTransform(assembleTransform(mousePos, null, 0, 0, container));
+ expect(result).not.toBeNull();
+ expect(result!.y).toBe(4);
+ });
+ });
+});
diff --git a/components/src/TimeSeriesTooltip/utils.ts b/components/src/TimeSeriesTooltip/utils.ts
index 18f5cd69..40398dfe 100644
--- a/components/src/TimeSeriesTooltip/utils.ts
+++ b/components/src/TimeSeriesTooltip/utils.ts
@@ -12,6 +12,9 @@
// limitations under the License.
import { Theme } from '@mui/material';
+import { ECharts as EChartsInstance } from 'echarts/core';
+import { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
+import { TimeChartSeriesMapping } from '../model';
import {
CursorCoordinates,
CursorData,
@@ -23,7 +26,7 @@ import {
} from './tooltip-model';
/**
- * Determine position of tooltip depending on chart dimensions and the number of focused series
+ * Determine position of tooltip depending on chart dimensions and the number of focused series.
*/
export function assembleTransform(
mousePos: CursorData['coords'],
@@ -45,6 +48,10 @@ export function assembleTransform(
if (mousePos.plotCanvas.x === undefined) return undefined;
+ // Fall back to max size before the resize observer reports a real measurement.
+ const effectiveHeight = tooltipHeight > 0 ? tooltipHeight : TOOLTIP_MAX_HEIGHT;
+ const effectiveWidth = tooltipWidth > 0 ? tooltipWidth : TOOLTIP_MAX_WIDTH;
+
let x = mousePos.page.x + cursorPaddingX; // Default to right side of the cursor
let y = mousePos.page.y + cursorPaddingY;
@@ -56,19 +63,19 @@ export function assembleTransform(
// Ensure tooltip does not go out of the container's bottom
const containerBottom = containerRect.top + containerElement.scrollHeight;
- if (y + tooltipHeight > containerBottom) {
- y = Math.max(containerBottom - tooltipHeight - cursorPaddingY, TOOLTIP_PADDING / 2);
+ if (y + effectiveHeight > containerBottom) {
+ y = Math.max(containerBottom - effectiveHeight - cursorPaddingY, TOOLTIP_PADDING / 2);
}
} else {
// Ensure tooltip does not go out of the screen on the bottom
- if (y + tooltipHeight > window.innerHeight + window.scrollY) {
- y = Math.max(window.innerHeight + window.scrollY - tooltipHeight - cursorPaddingY, TOOLTIP_PADDING / 2);
+ if (y + effectiveHeight > window.innerHeight + window.scrollY) {
+ y = Math.max(window.innerHeight + window.scrollY - effectiveHeight - cursorPaddingY, TOOLTIP_PADDING / 2);
}
}
// Ensure tooltip does not go out of the screen on the right
- if (x + tooltipWidth > window.innerWidth) {
- x = mousePos.page.x - tooltipWidth - cursorPaddingX; // Move to the left of the cursor
+ if (x + effectiveWidth > window.innerWidth) {
+ x = mousePos.page.x - effectiveWidth - cursorPaddingX; // Move to the left of the cursor
}
// Ensure tooltip does not go out of the screen on the left
@@ -107,8 +114,9 @@ export function getTooltipStyles(
fontSize: '11px',
visibility: 'visible',
opacity: 1,
- transition: 'all 0.1s ease-out',
- // Ensure pinned tooltip shows behind edit panel drawer and sticky header
+ // Animating transform causes intermediate positions outside the viewport; animate opacity/visibility instead.
+ transition: 'opacity 0.1s ease-out, visibility 0.1s ease-out',
+ // Pinned tooltip should not float above the drawer/sticky header.
zIndex: pinnedPos !== null ? 'auto' : theme.zIndex.tooltip,
overflow: 'hidden',
'&:hover': {
@@ -116,3 +124,116 @@ export function getTooltipStyles(
},
};
}
+
+export function getPixelXFromGrid(timestamp: number, chart: EChartsInstance): number | null {
+ try {
+ const pixelCoords = chart.convertToPixel('grid', [timestamp, 0]);
+ return pixelCoords?.[0] ?? null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Returns the cumulative (visual) Y for a series, accumulating stack totals in-place.
+ * For non-stacked series, returns the raw yValue unchanged.
+ * Mutates `stackTotals` — pass a fresh Map for each cursor evaluation.
+ */
+export function calculateVisualYForSeries(
+ seriesIdx: number,
+ yValue: number,
+ seriesMapping: TimeChartSeriesMapping,
+ stackTotals: Map
+): number {
+ const currentSeries = seriesMapping[seriesIdx];
+ if (!currentSeries) return yValue;
+
+ const stackId = (currentSeries as LineSeriesOption | BarSeriesOption).stack;
+ if (!stackId) {
+ return yValue;
+ }
+
+ const stackIdStr = stackId.toString();
+ const currentTotal = stackTotals.get(stackIdStr) ?? 0;
+ const newTotal = currentTotal + yValue;
+ stackTotals.set(stackIdStr, newTotal);
+ return newTotal;
+}
+
+export function calculateBarBandwidth(timestamp: number, sortedTimestamps: number[], chart: EChartsInstance): number {
+ const currentIdx = sortedTimestamps.indexOf(timestamp);
+ if (currentIdx === -1) {
+ return 20;
+ }
+
+ const prevTimestamp = currentIdx > 0 ? (sortedTimestamps[currentIdx - 1] ?? null) : null;
+ const nextTimestamp = currentIdx < sortedTimestamps.length - 1 ? (sortedTimestamps[currentIdx + 1] ?? null) : null;
+
+ const currentPixelX = getPixelXFromGrid(timestamp, chart);
+ if (currentPixelX === null) return 20;
+
+ let leftBound: number;
+ let rightBound: number;
+
+ if (prevTimestamp !== null && nextTimestamp !== null) {
+ const prevPixelX = getPixelXFromGrid(prevTimestamp, chart) ?? currentPixelX;
+ const nextPixelX = getPixelXFromGrid(nextTimestamp, chart) ?? currentPixelX;
+ leftBound = (currentPixelX + prevPixelX) / 2;
+ rightBound = (currentPixelX + nextPixelX) / 2;
+ } else if (prevTimestamp !== null) {
+ const prevPixelX = getPixelXFromGrid(prevTimestamp, chart) ?? currentPixelX;
+ leftBound = (currentPixelX + prevPixelX) / 2;
+ rightBound = currentPixelX + (currentPixelX - leftBound);
+ } else if (nextTimestamp !== null) {
+ const nextPixelX = getPixelXFromGrid(nextTimestamp, chart) ?? currentPixelX;
+ rightBound = (currentPixelX + nextPixelX) / 2;
+ leftBound = currentPixelX - (rightBound - currentPixelX);
+ } else {
+ return 20;
+ }
+
+ return Math.max(1, rightBound - leftBound);
+}
+
+/**
+ * Computes the pixel left/right bounds of one bar segment within a group.
+ * @param barRelativeIdx - zero-based index among bar-only series
+ * @param bandwidth - total pixel width for the bar group
+ * @param centerPixelX - pixel X of the bar group centre
+ * @param barCount - total bar series count (lines excluded)
+ */
+export function calculateBarSegmentBounds(
+ barRelativeIdx: number,
+ bandwidth: number,
+ centerPixelX: number,
+ barCount: number
+): { left: number; right: number } {
+ const count = Math.max(1, barCount);
+ const segmentWidth = bandwidth / count;
+ const segmentLeft = centerPixelX - bandwidth / 2 + barRelativeIdx * segmentWidth;
+ return {
+ left: segmentLeft,
+ right: segmentLeft + segmentWidth,
+ };
+}
+
+export function calculateBarYBounds(
+ visualYBottom: number,
+ visualYTop: number,
+ chart: EChartsInstance
+): { top: number; bottom: number } | null {
+ try {
+ const bottomPixel = chart.convertToPixel('grid', [0, visualYBottom]);
+ const topPixel = chart.convertToPixel('grid', [0, visualYTop]);
+
+ if (!bottomPixel || !topPixel || bottomPixel[1] === undefined || topPixel[1] === undefined) return null;
+
+ // Y increases downward in pixels; min/max normalizes the mapping for negative bar values.
+ return {
+ top: Math.min(topPixel[1], bottomPixel[1]),
+ bottom: Math.max(topPixel[1], bottomPixel[1]),
+ };
+ } catch {
+ return null;
+ }
+}
diff --git a/components/src/utils/chart-actions.test.ts b/components/src/utils/chart-actions.test.ts
index ca3e627d..cb555adf 100644
--- a/components/src/utils/chart-actions.test.ts
+++ b/components/src/utils/chart-actions.test.ts
@@ -12,7 +12,13 @@
// limitations under the License.
import { TimeSeries, TimeSeriesValueTuple } from '@perses-dev/spec';
-import { getClosestTimestamp, getClosestTimestampInFullDataset } from './chart-actions';
+import { ECharts as EChartsInstance } from 'echarts/core';
+import { DatapointInfo } from '../model';
+import {
+ batchDispatchNearbySeriesActions,
+ getClosestTimestamp,
+ getClosestTimestampInFullDataset,
+} from './chart-actions';
const TEST_TIME_SERIES_VALUES: TimeSeriesValueTuple[] = [
[1690381125000, 0.12],
@@ -138,3 +144,70 @@ describe('getClosestTimestampInFullDataset', () => {
expect(getClosestTimestampInFullDataset(TEST_TIME_SERIES_DATA, 1690386199722.634)).toEqual(1690386195000);
});
});
+
+describe('batchDispatchNearbySeriesActions', () => {
+ function makeChartMock(): { chart: EChartsInstance; calls: Array<{ type: string; payload: unknown }> } {
+ const calls: Array<{ type: string; payload: unknown }> = [];
+ const chart = {
+ dispatchAction: (payload: { type: string; [k: string]: unknown }) => {
+ calls.push({ type: payload.type, payload: JSON.parse(JSON.stringify(payload)) });
+ },
+ } as unknown as EChartsInstance;
+ return { chart, calls };
+ }
+
+ it('dispatches a blanket downplay to clear ECharts axis-triggered emphasis before highlighting the winner', () => {
+ const { chart, calls } = makeChartMock();
+ const winnerDatapoint: DatapointInfo = { seriesIndex: 3, dataIndex: 5, seriesName: 's3', yValue: 42 };
+
+ batchDispatchNearbySeriesActions(chart, [1, 2, 3, 4], [3], [1, 2, 4], [winnerDatapoint], []);
+
+ const blanketDownplayIdx = calls.findIndex(
+ (c) => c.type === 'downplay' && (c.payload as { seriesIndex?: unknown }).seriesIndex === undefined
+ );
+ const targetedDownplayIdx = calls.findIndex(
+ (c) => c.type === 'downplay' && Array.isArray((c.payload as { seriesIndex?: unknown }).seriesIndex)
+ );
+ const highlightIdx = calls.findIndex((c) => c.type === 'highlight');
+
+ expect(blanketDownplayIdx).toBeGreaterThanOrEqual(0);
+ expect(targetedDownplayIdx).toBeGreaterThan(blanketDownplayIdx);
+ expect(highlightIdx).toBeGreaterThan(targetedDownplayIdx);
+ expect((calls[highlightIdx]!.payload as { seriesIndex: number[] }).seriesIndex).toEqual([3]);
+ });
+
+ it('dispatches a select action on the winning datapoint', () => {
+ const { chart, calls } = makeChartMock();
+ const winnerDatapoint: DatapointInfo = { seriesIndex: 7, dataIndex: 11, seriesName: 's7', yValue: 1.5 };
+
+ batchDispatchNearbySeriesActions(chart, [7], [7], [], [winnerDatapoint], []);
+
+ const selectCall = calls.find((c) => c.type === 'select');
+ expect(selectCall).toBeDefined();
+ expect((selectCall!.payload as { seriesIndex: number; dataIndex: number }).seriesIndex).toBe(7);
+ expect((selectCall!.payload as { seriesIndex: number; dataIndex: number }).dataIndex).toBe(11);
+ });
+
+ it('uses the last duplicate datapoint for select when duplicates exist (avoids color mismatch)', () => {
+ const { chart, calls } = makeChartMock();
+ const winner: DatapointInfo = { seriesIndex: 1, dataIndex: 0, seriesName: 's1', yValue: 100 };
+ const duplicate: DatapointInfo = { seriesIndex: 2, dataIndex: 0, seriesName: 's2', yValue: 100 };
+
+ batchDispatchNearbySeriesActions(chart, [1, 2], [1, 2], [], [winner, duplicate], [duplicate]);
+
+ const selectCall = calls.find((c) => c.type === 'select');
+ expect((selectCall!.payload as { seriesIndex: number }).seriesIndex).toBe(2);
+ });
+
+ it('falls back to highlighting all nearby series when no emphasized winner exists', () => {
+ const { chart, calls } = makeChartMock();
+
+ batchDispatchNearbySeriesActions(chart, [1, 2, 3], [], [1, 2, 3], [], []);
+
+ const highlight = calls.find((c) => c.type === 'highlight');
+ expect(highlight).toBeDefined();
+ expect((highlight!.payload as { seriesIndex: number[]; notBlur: boolean }).seriesIndex).toEqual([1, 2, 3]);
+ expect((highlight!.payload as { seriesIndex: number[]; notBlur: boolean }).notBlur).toBe(true);
+ expect(calls.some((c) => c.type === 'toggleSelect')).toBe(true);
+ });
+});
diff --git a/components/src/utils/chart-actions.ts b/components/src/utils/chart-actions.ts
index 50330c0c..a05cd565 100644
--- a/components/src/utils/chart-actions.ts
+++ b/components/src/utils/chart-actions.ts
@@ -117,13 +117,22 @@ export function batchDispatchNearbySeriesActions(
});
}
- // Clears emphasis state of all lines that are not emphasized.
- // Emphasized is a subset of just the nearby series that are closest to cursor.
+ // Blanket downplay clears axis-triggered emphasis (enlarged "big point" markers) before
+ // re-applying emphasis to only the winner series.
+ // https://echarts.apache.org/en/api.html#action.downplay
chart.dispatchAction({
type: 'downplay',
- seriesIndex: nonEmphasizedSeriesIndexes,
});
+ // Clears emphasis state of all lines that are not emphasized.
+ // Emphasized is a subset of just the nearby series that are closest to cursor.
+ if (nonEmphasizedSeriesIndexes.length > 0) {
+ chart.dispatchAction({
+ type: 'downplay',
+ seriesIndex: nonEmphasizedSeriesIndexes,
+ });
+ }
+
// https://echarts.apache.org/en/api.html#action.highlight
if (emphasizedSeriesIndexes.length > 0) {
// Fadeout opacity of all series not closest to cursor.
diff --git a/components/src/utils/variable-interpolation.test.ts b/components/src/utils/variable-interpolation.test.ts
index 57a8a03e..376a2ad2 100644
--- a/components/src/utils/variable-interpolation.test.ts
+++ b/components/src/utils/variable-interpolation.test.ts
@@ -215,6 +215,14 @@ describe('replaceVariables() with custom formats', () => {
},
expected: 'hello (perses\\.|prometheus\\$) (world\\.)',
},
+ {
+ text: 'hello ${var1:regexliteral} ${var2:regexliteral}',
+ state: {
+ var1: { value: ['perses.', 'prometheus$'], loading: false },
+ var2: { value: 'world.', loading: false },
+ },
+ expected: 'hello (perses\\\\.|prometheus\\\\$) (world\\\\.)',
+ },
// singlequote
{
text: 'hello ${var1:singlequote} ${var2:singlequote}',
diff --git a/components/src/utils/variable-interpolation.ts b/components/src/utils/variable-interpolation.ts
index f536aa88..35d243e4 100644
--- a/components/src/utils/variable-interpolation.ts
+++ b/components/src/utils/variable-interpolation.ts
@@ -62,6 +62,7 @@ export enum InterpolationFormat {
SQLSTRING = 'sqlstring',
TEXT = 'text',
QUERYPARAM = 'queryparam',
+ REGEX_LITERAL = 'regexliteral',
}
function stringToFormat(val: string | undefined): InterpolationFormat | undefined {
@@ -110,6 +111,10 @@ export function interpolate(values: string[], name: string, format: Interpolatio
const escapedRegex = values.map((v) => v.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'));
return `(${escapedRegex.join('|')})`;
}
+ case InterpolationFormat.REGEX_LITERAL: {
+ const escapedRegex = values.map((v) => v.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\\\$&'));
+ return `(${escapedRegex.join('|')})`;
+ }
case InterpolationFormat.SINGLEQUOTE:
return values.map((v) => `'${v}'`).join(',');
case InterpolationFormat.SQLSTRING:
@@ -143,7 +148,6 @@ export function replaceVariable(
if (typeof variableValue === 'string') {
replaceString = interpolate([variableValue], varName, varFormat || InterpolationFormat.RAW);
}
-
text = text.replaceAll(variableSyntax, replaceString);
return text.replaceAll(alternativeVariableSyntax, replaceString);
}
diff --git a/cue/cue.mod/module.cue b/cue/cue.mod/module.cue
index 1fc7fb85..5aef98b8 100644
--- a/cue/cue.mod/module.cue
+++ b/cue/cue.mod/module.cue
@@ -7,7 +7,7 @@ source: {
}
deps: {
"github.com/perses/spec/cue@v0": {
- v: "v0.2.0-beta.6"
+ v: "v0.2.0-rc.0"
default: true
}
}
diff --git a/dashboards/package.json b/dashboards/package.json
index a605be3b..dd6508c2 100644
--- a/dashboards/package.json
+++ b/dashboards/package.json
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/dashboards",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"description": "The dashboards feature in Perses",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
@@ -29,10 +29,10 @@
"lint:fix": "eslint --fix src --ext .ts,.tsx"
},
"dependencies": {
- "@perses-dev/components": "0.54.0-beta.10",
- "@perses-dev/plugin-system": "0.54.0-beta.10",
- "@perses-dev/spec": "0.2.0-beta.6",
- "@perses-dev/client": "0.54.0-beta.10",
+ "@perses-dev/components": "0.54.0",
+ "@perses-dev/plugin-system": "0.54.0",
+ "@perses-dev/spec": "0.2.0",
+ "@perses-dev/client": "0.54.0",
"@tanstack/hotkeys": "^0.8.0",
"@tanstack/react-hotkeys": "^0.9.1",
"immer": "^10.1.1",
diff --git a/dashboards/src/components/DownloadButton/serializeDashboard.ts b/dashboards/src/components/DownloadButton/serializeDashboard.ts
index 5b0754cf..a33d5f30 100644
--- a/dashboards/src/components/DownloadButton/serializeDashboard.ts
+++ b/dashboards/src/components/DownloadButton/serializeDashboard.ts
@@ -12,6 +12,7 @@
// limitations under the License.
import { DashboardResource } from '@perses-dev/client';
+import { DashboardSpec } from '@perses-dev/spec';
import { stringify } from 'yaml';
//TODO: Although the previous comment suggests the metadata not should not be used, I keep them. Need to be discussed.
@@ -22,6 +23,20 @@ type SerializedDashboard = {
content: string;
};
+type PersesV1Alpha2CR = {
+ apiVersion: 'perses.dev/v1alpha2';
+ kind: 'PersesDashboard';
+ metadata: {
+ labels: Record;
+ annotations?: Record;
+ name: string;
+ namespace: string;
+ };
+ spec: {
+ config: DashboardSpec;
+ };
+};
+
function serializeYaml(dashboard: DashboardResource, shape?: 'cr-v1alpha1' | 'cr-v1alpha2'): SerializedDashboard {
let content: string;
@@ -46,25 +61,28 @@ function serializeYaml(dashboard: DashboardResource, shape?: 'cr-v1alpha1' | 'cr
);
} else if (shape === 'cr-v1alpha2') {
const name = dashboard.metadata.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
- content = stringify(
- {
- apiVersion: 'perses.dev/v1alpha2',
- kind: 'PersesDashboard',
- metadata: {
- labels: {
- 'app.kubernetes.io/name': 'perses-dashboard',
- 'app.kubernetes.io/instance': name,
- 'app.kubernetes.io/part-of': 'perses-operator',
- },
- name,
- namespace: dashboard.metadata.project,
- },
- spec: {
- config: dashboard.spec,
+ const crContent: PersesV1Alpha2CR = {
+ apiVersion: 'perses.dev/v1alpha2',
+ kind: 'PersesDashboard',
+ metadata: {
+ labels: {
+ 'app.kubernetes.io/name': 'perses-dashboard',
+ 'app.kubernetes.io/instance': name,
+ 'app.kubernetes.io/part-of': 'perses-operator',
},
+ name,
+ namespace: dashboard.metadata.project,
},
- { schema: 'yaml-1.1' }
- );
+ spec: {
+ config: dashboard.spec,
+ },
+ };
+
+ if (dashboard.metadata.tags && dashboard.metadata.tags.length > 0) {
+ crContent.metadata.annotations = { 'perses.dev/tags': dashboard.metadata.tags.join(',') };
+ }
+
+ content = stringify(crContent, { schema: 'yaml-1.1' });
} else {
content = stringify(dashboard, { schema: 'yaml-1.1' });
}
diff --git a/dashboards/src/components/GridLayout/GridLayout.tsx b/dashboards/src/components/GridLayout/GridLayout.tsx
index 85126fb5..39784812 100644
--- a/dashboards/src/components/GridLayout/GridLayout.tsx
+++ b/dashboards/src/components/GridLayout/GridLayout.tsx
@@ -11,14 +11,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ReactElement, useState } from 'react';
-import { Layouts, Layout } from 'react-grid-layout';
+import { Layout, Layouts } from 'react-grid-layout';
-import { PanelGroupId } from '@perses-dev/spec';
-import { useVariableValues, VariableContext } from '@perses-dev/plugin-system';
+import { useVariableValues, VariableContext, PanelGroupId } from '@perses-dev/plugin-system';
+import { PanelGroupDefinition } from '../../model';
import { useEditMode, usePanelGroup, usePanelGroupActions, useViewPanelGroup } from '../../context';
import { GRID_LAYOUT_SMALL_BREAKPOINT } from '../../constants';
import { PanelOptions } from '../Panel';
-import { PanelGroupDefinition } from '../../model';
import { Row, RowProps } from './Row';
export interface GridLayoutProps {
diff --git a/dashboards/src/components/GridLayout/GridTitle.tsx b/dashboards/src/components/GridLayout/GridTitle.tsx
index f88e8ae8..850777aa 100644
--- a/dashboards/src/components/GridLayout/GridTitle.tsx
+++ b/dashboards/src/components/GridLayout/GridTitle.tsx
@@ -20,8 +20,7 @@ import ArrowUpIcon from 'mdi-material-ui/ArrowUp';
import ArrowDownIcon from 'mdi-material-ui/ArrowDown';
import DeleteIcon from 'mdi-material-ui/DeleteOutline';
import { InfoTooltip } from '@perses-dev/components';
-import { useReplaceVariablesInString } from '@perses-dev/plugin-system';
-import { PanelGroupId } from '@perses-dev/spec';
+import { useReplaceVariablesInString, PanelGroupId } from '@perses-dev/plugin-system';
import { ReactElement } from 'react';
import { ARIA_LABEL_TEXT, TOOLTIP_TEXT } from '../../constants';
import { usePanelGroupActions, useEditMode, useDeletePanelGroupDialog } from '../../context';
diff --git a/dashboards/src/components/GridLayout/Row.tsx b/dashboards/src/components/GridLayout/Row.tsx
index 7b5fa36a..2c062b0f 100644
--- a/dashboards/src/components/GridLayout/Row.tsx
+++ b/dashboards/src/components/GridLayout/Row.tsx
@@ -12,11 +12,11 @@
// limitations under the License.
import { Collapse, useTheme } from '@mui/material';
-import { PanelGroupId } from '@perses-dev/spec';
import { PanelOptions, useViewPanelGroup } from '@perses-dev/dashboards';
import { ReactElement, useEffect, useMemo, useState } from 'react';
import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout';
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { GRID_LAYOUT_COLS, GRID_LAYOUT_SMALL_BREAKPOINT } from '../../constants';
import { PanelGroupDefinition, PanelGroupItemLayout } from '../../model';
import { GridContainer } from './GridContainer';
diff --git a/dashboards/src/components/Panel/useSelectionItemActions.tsx b/dashboards/src/components/Panel/useSelectionItemActions.tsx
index 5f5bbe6d..70c7e0c0 100644
--- a/dashboards/src/components/Panel/useSelectionItemActions.tsx
+++ b/dashboards/src/components/Panel/useSelectionItemActions.tsx
@@ -14,6 +14,7 @@
import { Box, CircularProgress } from '@mui/material';
import { Dialog, InfoTooltip, useItemActions, useSelection } from '@perses-dev/components';
import { ACTION_ICONS, executeAction, ItemAction, VariableStateMap } from '@perses-dev/plugin-system';
+import { useFetch } from '@perses-dev/client';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { HeaderIconButton } from './HeaderIconButton';
@@ -41,6 +42,7 @@ export function useSelectionItemActions({
}: UseItemActionsOptions): UseItemActionsResult {
const { selectionMap } = useSelection();
const { actionStatuses, setActionStatus } = useItemActions();
+ const { fetch } = useFetch();
const [confirmState, setConfirmState] = useState<{
open: boolean;
action?: ItemAction;
@@ -56,6 +58,7 @@ export function useSelectionItemActions({
selectionMap: new Map>([[item.id, item.data]]),
variableState,
setActionStatus,
+ fetchFn: fetch,
});
} else {
await executeAction({
@@ -63,10 +66,11 @@ export function useSelectionItemActions({
selectionMap: selectionMap as Map>,
variableState,
setActionStatus,
+ fetchFn: fetch,
});
}
},
- [selectionMap, variableState, setActionStatus]
+ [selectionMap, variableState, setActionStatus, fetch]
);
const handleActionClick = useCallback(
diff --git a/dashboards/src/components/PanelDrawer/PanelDrawer.tsx b/dashboards/src/components/PanelDrawer/PanelDrawer.tsx
index eec9b1ab..4ed1b421 100644
--- a/dashboards/src/components/PanelDrawer/PanelDrawer.tsx
+++ b/dashboards/src/components/PanelDrawer/PanelDrawer.tsx
@@ -25,10 +25,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { ReactElement, useState, useMemo, ReactNode, useCallback } from 'react';
+import { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
import { Drawer, ErrorAlert, ErrorBoundary } from '@perses-dev/components';
-import { PanelEditorValues } from '@perses-dev/spec';
-import { useVariableValues, VariableContext } from '@perses-dev/plugin-system';
+import { PanelEditorValues, useVariableValues, VariableContext } from '@perses-dev/plugin-system';
import { usePanelEditor, usePanelKey } from '../../context';
import { PanelEditorForm } from './PanelEditorForm';
diff --git a/dashboards/src/components/PanelDrawer/PanelEditorForm.tsx b/dashboards/src/components/PanelDrawer/PanelEditorForm.tsx
index 4d72e5f0..a1cfdba8 100644
--- a/dashboards/src/components/PanelDrawer/PanelEditorForm.tsx
+++ b/dashboards/src/components/PanelDrawer/PanelEditorForm.tsx
@@ -13,15 +13,15 @@
import { ReactElement, useCallback, useEffect, useState } from 'react';
import { Box, Button, Grid, MenuItem, Stack, TextField, Typography } from '@mui/material';
-import { PanelDefinition, PanelEditorValues } from '@perses-dev/spec';
+import { PanelDefinition } from '@perses-dev/spec';
+import { PanelEditorValues, PluginKindSelect, usePluginEditor, useValidationSchemas } from '@perses-dev/plugin-system';
import {
DiscardChangesConfirmationDialog,
ErrorAlert,
ErrorBoundary,
- getTitleAction,
getSubmitText,
+ getTitleAction,
} from '@perses-dev/components';
-import { PluginKindSelect, usePluginEditor, useValidationSchemas } from '@perses-dev/plugin-system';
import { Controller, FormProvider, SubmitHandler, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Action } from '@perses-dev/client';
diff --git a/dashboards/src/components/PanelDrawer/PanelPreview.tsx b/dashboards/src/components/PanelDrawer/PanelPreview.tsx
index 606b7b66..13aeb073 100644
--- a/dashboards/src/components/PanelDrawer/PanelPreview.tsx
+++ b/dashboards/src/components/PanelDrawer/PanelPreview.tsx
@@ -13,7 +13,7 @@
import { ReactElement, useContext, useEffect, useRef } from 'react';
import { Box } from '@mui/material';
-import { PanelEditorValues } from '@perses-dev/spec';
+import { PanelEditorValues } from '@perses-dev/plugin-system';
import { Panel } from '../Panel';
import { PanelEditorContext } from '../../context';
diff --git a/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx b/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx
index 65e2f506..f1e3a870 100644
--- a/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx
+++ b/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx
@@ -14,8 +14,14 @@
import { Grid, Typography } from '@mui/material';
import { ErrorAlert, ErrorBoundary } from '@perses-dev/components';
import { PanelEditorContext, PanelPreview } from '@perses-dev/dashboards';
-import { DataQueriesProvider, PanelSpecEditor, usePlugin, useSuggestedStepMs } from '@perses-dev/plugin-system';
-import { Definition, PanelDefinition, PanelEditorValues, QueryDefinition, UnknownSpec } from '@perses-dev/spec';
+import {
+ DataQueriesProvider,
+ PanelEditorValues,
+ PanelSpecEditor,
+ usePlugin,
+ useSuggestedStepMs,
+} from '@perses-dev/plugin-system';
+import { Definition, PanelDefinition, QueryDefinition, UnknownSpec } from '@perses-dev/spec';
import { Control } from 'react-hook-form';
import { ReactElement, useCallback, useContext, useMemo, useState } from 'react';
diff --git a/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts b/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts
index a650ac9c..046df30f 100644
--- a/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts
+++ b/dashboards/src/context/DashboardProvider/dashboard-provider-api.ts
@@ -12,8 +12,9 @@
// limitations under the License.
import { useCallback, useMemo } from 'react';
-import { DurationString, Link, PanelDefinition, PanelGroupId } from '@perses-dev/spec';
+import { DurationString, Link, PanelDefinition } from '@perses-dev/spec';
import { DashboardResource } from '@perses-dev/client';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { PanelGroupDefinition, PanelGroupItemId, PanelGroupItemLayout } from '../../model';
import { DashboardStoreState, useDashboardStore } from './DashboardProvider';
import { DeletePanelGroupDialogState } from './delete-panel-group-slice';
diff --git a/dashboards/src/context/DashboardProvider/delete-panel-group-slice.ts b/dashboards/src/context/DashboardProvider/delete-panel-group-slice.ts
index 33eabdff..3f1685d0 100644
--- a/dashboards/src/context/DashboardProvider/delete-panel-group-slice.ts
+++ b/dashboards/src/context/DashboardProvider/delete-panel-group-slice.ts
@@ -12,7 +12,7 @@
// limitations under the License.
import { StateCreator } from 'zustand';
-import { PanelGroupId } from '@perses-dev/spec';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { Middleware } from './common';
import { PanelGroupSlice } from './panel-group-slice';
import { PanelSlice } from './panel-slice';
diff --git a/dashboards/src/context/DashboardProvider/panel-editor-slice.ts b/dashboards/src/context/DashboardProvider/panel-editor-slice.ts
index 5040ebcb..7d413314 100644
--- a/dashboards/src/context/DashboardProvider/panel-editor-slice.ts
+++ b/dashboards/src/context/DashboardProvider/panel-editor-slice.ts
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { PanelEditorValues, PanelGroupId } from '@perses-dev/spec';
+import { PanelEditorValues, PanelGroupId } from '@perses-dev/plugin-system';
import { StateCreator } from 'zustand';
import { Action } from '@perses-dev/client';
import { generatePanelKey, getYForNewRow } from '../../utils';
diff --git a/dashboards/src/context/DashboardProvider/panel-group-editor-slice.ts b/dashboards/src/context/DashboardProvider/panel-group-editor-slice.ts
index 250c808c..4b777695 100644
--- a/dashboards/src/context/DashboardProvider/panel-group-editor-slice.ts
+++ b/dashboards/src/context/DashboardProvider/panel-group-editor-slice.ts
@@ -12,7 +12,7 @@
// limitations under the License.
import { StateCreator } from 'zustand';
-import { PanelGroupId } from '@perses-dev/spec';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { Middleware } from './common';
import { PanelGroupSlice, addPanelGroup, createEmptyPanelGroup } from './panel-group-slice';
diff --git a/dashboards/src/context/DashboardProvider/panel-group-slice.ts b/dashboards/src/context/DashboardProvider/panel-group-slice.ts
index 421e6bf8..dcb971ac 100644
--- a/dashboards/src/context/DashboardProvider/panel-group-slice.ts
+++ b/dashboards/src/context/DashboardProvider/panel-group-slice.ts
@@ -11,9 +11,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { getPanelKeyFromRef, LayoutDefinition, PanelGroupId } from '@perses-dev/spec';
+import { getPanelKeyFromRef, LayoutDefinition } from '@perses-dev/spec';
import { StateCreator } from 'zustand';
import { WritableDraft } from 'immer';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { PanelGroupDefinition } from '../../model';
import { generateId, Middleware } from './common';
diff --git a/dashboards/src/context/DashboardProvider/view-panel-slice.ts b/dashboards/src/context/DashboardProvider/view-panel-slice.ts
index aa030bde..62747108 100644
--- a/dashboards/src/context/DashboardProvider/view-panel-slice.ts
+++ b/dashboards/src/context/DashboardProvider/view-panel-slice.ts
@@ -12,7 +12,7 @@
// limitations under the License.
import { StateCreator } from 'zustand';
-import { PanelGroupId } from '@perses-dev/spec';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { PanelGroupDefinition, PanelGroupItemId } from '../../model';
import { Middleware } from './common';
import { PanelGroupSlice } from './panel-group-slice';
diff --git a/dashboards/src/context/useDashboard.tsx b/dashboards/src/context/useDashboard.tsx
index ccebf7b1..e140a45c 100644
--- a/dashboards/src/context/useDashboard.tsx
+++ b/dashboards/src/context/useDashboard.tsx
@@ -11,8 +11,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { createPanelRef, DashboardSpec, DurationString, GridDefinition, PanelGroupId } from '@perses-dev/spec';
+import { createPanelRef, DashboardSpec, DurationString, GridDefinition } from '@perses-dev/spec';
import { DashboardResource } from '@perses-dev/client';
+import { PanelGroupId } from '@perses-dev/plugin-system';
import { PanelGroupDefinition } from '../model';
import { useDashboardStore } from './DashboardProvider';
diff --git a/dashboards/src/model/PanelGroupDefinition.ts b/dashboards/src/model/PanelGroupDefinition.ts
index 82adffe7..171da4ae 100644
--- a/dashboards/src/model/PanelGroupDefinition.ts
+++ b/dashboards/src/model/PanelGroupDefinition.ts
@@ -11,8 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-export type PanelGroupId = number;
-
+import { PanelGroupId } from '@perses-dev/plugin-system';
/**
* Panel Group Item Layout ID type. String identifier for items within a panel group.
*/
diff --git a/design-tokens/.eslintrc.js b/design-tokens/.eslintrc.js
new file mode 100644
index 00000000..08ac9603
--- /dev/null
+++ b/design-tokens/.eslintrc.js
@@ -0,0 +1,14 @@
+// Copyright The Perses Authors
+// Licensed 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.
+
+module.exports = require('../.eslintrc.base.js');
diff --git a/design-tokens/jest.config.ts b/design-tokens/jest.config.ts
new file mode 100644
index 00000000..02371308
--- /dev/null
+++ b/design-tokens/jest.config.ts
@@ -0,0 +1,21 @@
+// Copyright The Perses Authors
+// Licensed 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 { Config } from '@jest/types';
+import shared from '../jest.shared';
+
+const jestConfig: Config.InitialOptions = {
+ ...shared,
+};
+
+export default jestConfig;
diff --git a/design-tokens/package.json b/design-tokens/package.json
new file mode 100644
index 00000000..fbd18540
--- /dev/null
+++ b/design-tokens/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@perses-dev/design-tokens",
+ "version": "0.54.0-beta.10",
+ "description": "Perses design tokens for ui components",
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/perses/perses/blob/main/README.md",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/perses/perses.git"
+ },
+ "bugs": {
+ "url": "https://github.com/perses/perses/issues"
+ },
+ "module": "dist/index.js",
+ "main": "dist/cjs/index.js",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/cjs/index.js"
+ },
+ "./css": "./dist/css/index.css",
+ "./css/reset": "./dist/css/reset.css",
+ "./css/tokens": "./dist/css/tokens.css",
+ "./css/semantic": "./dist/css/semantic.css"
+ },
+ "sideEffects": [
+ "*.css"
+ ],
+ "scripts": {
+ "clean": "rimraf dist/",
+ "build": "concurrently \"npm:build:*\"",
+ "build:cjs": "swc ./src -d dist/cjs --strip-leading-paths --config-file ../.cjs.swcrc --ignore '**/test/**'",
+ "build:esm": "swc ./src -d dist --strip-leading-paths --config-file ../.swcrc --ignore '**/test/**'",
+ "build:types": "tsc --project tsconfig.build.json",
+ "build:css": "mkdir -p dist/css && cp -f src/css/*.css dist/css/",
+ "type-check": "tsc --noEmit",
+ "start": "concurrently -P \"npm:build:* -- {*}\" -- --watch",
+ "test": "cross-env TZ=UTC jest",
+ "test:watch": "npm run test -- --watch",
+ "lint": "eslint src --ext .ts,.tsx",
+ "lint:fix": "eslint --fix src --ext .ts,.tsx"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/design-tokens/src/colors.ts b/design-tokens/src/colors.ts
new file mode 100644
index 00000000..87380aae
--- /dev/null
+++ b/design-tokens/src/colors.ts
@@ -0,0 +1,129 @@
+// Copyright The Perses Authors
+// Licensed 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 type HexColor = `#${string}`;
+
+export interface PersesColor {
+ 50: HexColor;
+ 100: HexColor;
+ 150: HexColor;
+ 200: HexColor;
+ 300: HexColor;
+ 400: HexColor;
+ 500: HexColor;
+ 600: HexColor;
+ 700: HexColor;
+ 800: HexColor;
+ 850: HexColor;
+ 900: HexColor;
+ 950: HexColor;
+}
+
+export const blue: PersesColor = {
+ 50: '#E7F1FC',
+ 100: '#D0E3FA',
+ 150: '#B8D5F7',
+ 200: '#A1C7F5',
+ 300: '#72ABF0',
+ 400: '#438FEB',
+ 500: '#1473E6',
+ 600: '#105CB8',
+ 700: '#0C458A',
+ 800: '#082E5C',
+ 850: '#062345',
+ 900: '#04172E',
+ 950: '#020C17',
+};
+
+export const green: PersesColor = {
+ 50: '#EAF9F1',
+ 100: '#D5F2E3',
+ 150: '#C1ECD4',
+ 200: '#ACE5C6',
+ 300: '#82D9AA',
+ 400: '#59CC8D',
+ 500: '#2FBF71',
+ 600: '#26995A',
+ 700: '#1C7344',
+ 800: '#134C2D',
+ 850: '#0E3922',
+ 900: '#092617',
+ 950: '#05130B',
+};
+
+export const gray: PersesColor = {
+ 50: '#F0F1F6',
+ 100: '#E1E3ED',
+ 150: '#D2D5E4',
+ 200: '#C3C7DB',
+ 300: '#A4ACC8',
+ 400: '#8690B6',
+ 500: '#717CA4',
+ 600: '#535D83',
+ 700: '#3E4662',
+ 800: '#2A2E42',
+ 850: '#1F2331',
+ 900: '#151721',
+ 950: '#0A0C10',
+};
+
+export const orange: PersesColor = {
+ 50: '#FFF5E8',
+ 100: '#FFECD2',
+ 150: '#FFE2BB',
+ 200: '#FFD9A4',
+ 300: '#FFC577',
+ 400: '#FFB249',
+ 500: '#FF9F1C',
+ 600: '#CC7F16',
+ 700: '#995F11',
+ 800: '#66400B',
+ 850: '#4D3008',
+ 900: '#332006',
+ 950: '#1A1003',
+};
+
+export const purple: PersesColor = {
+ 50: '#EFE9FD',
+ 100: '#E0D2FC',
+ 150: '#D0BCFA',
+ 200: '#C1A6F8',
+ 300: '#A179F5',
+ 400: '#824DF1',
+ 500: '#6320EE',
+ 600: '#4F1ABE',
+ 700: '#3B138F',
+ 800: '#280D5F',
+ 850: '#1E0A47',
+ 900: '#140630',
+ 950: '#0A0318',
+};
+
+export const red: PersesColor = {
+ 50: '#FDEDED',
+ 100: '#FBDADA',
+ 150: '#F9C8C8',
+ 200: '#F7B5B5',
+ 300: '#F29191',
+ 400: '#EE6C6C',
+ 500: '#EA4747',
+ 600: '#BD3939',
+ 700: '#902B2B',
+ 800: '#621D1D',
+ 850: '#4C1616',
+ 900: '#350F0F',
+ 950: '#1F0808',
+};
+
+export const white = '#FFFFFF' as HexColor;
+export const black = '#000000' as HexColor;
diff --git a/design-tokens/src/css/index.css b/design-tokens/src/css/index.css
new file mode 100644
index 00000000..8749d048
--- /dev/null
+++ b/design-tokens/src/css/index.css
@@ -0,0 +1,20 @@
+/*
+ * Copyright The Perses Authors
+ * Licensed 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.
+ */
+
+@layer perses.reset, perses.tokens, perses.semantic;
+
+@import './reset.css';
+@import './tokens.css';
+@import './semantic.css';
diff --git a/design-tokens/src/css/reset.css b/design-tokens/src/css/reset.css
new file mode 100644
index 00000000..d211b250
--- /dev/null
+++ b/design-tokens/src/css/reset.css
@@ -0,0 +1,28 @@
+/*
+ * Copyright The Perses Authors
+ * Licensed 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.
+ */
+
+@layer perses.reset {
+ *,
+ *::before,
+ *::after {
+ box-sizing: border-box;
+ }
+
+ body {
+ margin: 0;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+}
diff --git a/design-tokens/src/css/semantic.css b/design-tokens/src/css/semantic.css
new file mode 100644
index 00000000..2f4d37c6
--- /dev/null
+++ b/design-tokens/src/css/semantic.css
@@ -0,0 +1,162 @@
+/*
+ * Copyright The Perses Authors
+ * Licensed 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.
+ */
+
+@layer perses.semantic {
+ /*
+ * Elevation hierarchy (lowest → highest):
+ * bg-default — page/app background (base level)
+ * bg-surface — raised surfaces: cards, panels, dialogs
+ * bg-sunken — recessed areas: code blocks, inset regions
+ * bg-overlay — floating surfaces: tooltips, popovers, dropdowns
+ * bg-backdrop — semi-transparent scrim behind modals
+ *
+ * Other backgrounds:
+ * bg-navigation — sidebar/nav area (brand-tinted)
+ *
+ * Border:
+ * border-default — general-purpose border color
+ */
+
+ /* ---- Light mode (default) ---- */
+ :root {
+ /* Background: Elevation */
+ --perses-bg-default: var(--perses-color-white);
+ --perses-bg-surface: var(--perses-color-gray-50);
+ --perses-bg-sunken: var(--perses-color-gray-200);
+ --perses-bg-overlay: var(--perses-color-gray-100);
+ --perses-bg-backdrop: rgba(21, 23, 33, 0.75);
+ --perses-bg-navigation: var(--perses-color-blue-150);
+
+ /* Border */
+ --perses-border-default: var(--perses-color-gray-100);
+
+ /* Text */
+ --perses-text-primary: var(--perses-color-gray-800);
+ --perses-text-secondary: var(--perses-color-gray-700);
+ --perses-text-disabled: var(--perses-color-gray-300);
+ --perses-text-link: var(--perses-color-blue-500);
+ --perses-text-link-hover: var(--perses-color-blue-600);
+ --perses-text-navigation: var(--perses-color-gray-800);
+ --perses-text-accent: var(--perses-color-gray-300);
+
+ /* Status: Primary */
+ --perses-status-bg-primary: var(--perses-color-blue-50);
+ --perses-status-bg-primary-hover: var(--perses-color-blue-100);
+ --perses-status-text-primary: var(--perses-color-blue-700);
+ --perses-status-border-primary: var(--perses-color-blue-200);
+ --perses-status-icon-primary: var(--perses-color-blue-600);
+
+ /* Status: Secondary */
+ --perses-status-bg-secondary: var(--perses-color-gray-50);
+ --perses-status-bg-secondary-hover: var(--perses-color-gray-100);
+ --perses-status-text-secondary: var(--perses-color-gray-700);
+ --perses-status-border-secondary: var(--perses-color-gray-200);
+ --perses-status-icon-secondary: var(--perses-color-gray-600);
+
+ /* Status: Error */
+ --perses-status-bg-error: var(--perses-color-red-50);
+ --perses-status-bg-error-hover: var(--perses-color-red-100);
+ --perses-status-text-error: var(--perses-color-red-700);
+ --perses-status-border-error: var(--perses-color-red-200);
+ --perses-status-icon-error: var(--perses-color-red-600);
+
+ /* Status: Warning */
+ --perses-status-bg-warning: var(--perses-color-orange-50);
+ --perses-status-bg-warning-hover: var(--perses-color-orange-100);
+ --perses-status-text-warning: var(--perses-color-orange-700);
+ --perses-status-border-warning: var(--perses-color-orange-200);
+ --perses-status-icon-warning: var(--perses-color-orange-600);
+
+ /* Status: Success */
+ --perses-status-bg-success: var(--perses-color-green-50);
+ --perses-status-bg-success-hover: var(--perses-color-green-100);
+ --perses-status-text-success: var(--perses-color-green-700);
+ --perses-status-border-success: var(--perses-color-green-200);
+ --perses-status-icon-success: var(--perses-color-green-600);
+
+ /* Status: Info */
+ --perses-status-bg-info: var(--perses-color-blue-50);
+ --perses-status-bg-info-hover: var(--perses-color-blue-100);
+ --perses-status-text-info: var(--perses-color-blue-700);
+ --perses-status-border-info: var(--perses-color-blue-200);
+ --perses-status-icon-info: var(--perses-color-blue-600);
+ }
+
+ /* ---- Dark mode: explicit (data attribute) ---- */
+ [data-perses-mode='dark'] {
+ /* Background: Elevation */
+ --perses-bg-default: var(--perses-color-gray-900);
+ --perses-bg-surface: var(--perses-color-gray-850);
+ --perses-bg-sunken: var(--perses-color-gray-800);
+ --perses-bg-overlay: var(--perses-color-gray-600);
+ --perses-bg-backdrop: rgba(10, 12, 16, 0.85);
+ --perses-bg-navigation: var(--perses-color-gray-850);
+
+ /* Border */
+ --perses-border-default: var(--perses-color-gray-600);
+
+ /* Text */
+ --perses-text-primary: var(--perses-color-white);
+ --perses-text-secondary: var(--perses-color-gray-50);
+ --perses-text-disabled: var(--perses-color-gray-600);
+ --perses-text-link: var(--perses-color-blue-400);
+ --perses-text-link-hover: var(--perses-color-blue-500);
+ --perses-text-navigation: var(--perses-color-white);
+ --perses-text-accent: var(--perses-color-gray-400);
+
+ /* Status: Primary */
+ --perses-status-bg-primary: var(--perses-color-blue-900);
+ --perses-status-bg-primary-hover: var(--perses-color-blue-850);
+ --perses-status-text-primary: var(--perses-color-blue-300);
+ --perses-status-border-primary: var(--perses-color-blue-700);
+ --perses-status-icon-primary: var(--perses-color-blue-400);
+
+ /* Status: Secondary */
+ --perses-status-bg-secondary: var(--perses-color-gray-850);
+ --perses-status-bg-secondary-hover: var(--perses-color-gray-800);
+ --perses-status-text-secondary: var(--perses-color-gray-200);
+ --perses-status-border-secondary: var(--perses-color-gray-700);
+ --perses-status-icon-secondary: var(--perses-color-gray-400);
+
+ /* Status: Error */
+ --perses-status-bg-error: var(--perses-color-red-900);
+ --perses-status-bg-error-hover: var(--perses-color-red-850);
+ --perses-status-text-error: var(--perses-color-red-300);
+ --perses-status-border-error: var(--perses-color-red-700);
+ --perses-status-icon-error: var(--perses-color-red-400);
+
+ /* Status: Warning */
+ --perses-status-bg-warning: var(--perses-color-orange-900);
+ --perses-status-bg-warning-hover: var(--perses-color-orange-850);
+ --perses-status-text-warning: var(--perses-color-orange-300);
+ --perses-status-border-warning: var(--perses-color-orange-700);
+ --perses-status-icon-warning: var(--perses-color-orange-400);
+
+ /* Status: Success */
+ --perses-status-bg-success: var(--perses-color-green-900);
+ --perses-status-bg-success-hover: var(--perses-color-green-850);
+ --perses-status-text-success: var(--perses-color-green-300);
+ --perses-status-border-success: var(--perses-color-green-700);
+ --perses-status-icon-success: var(--perses-color-green-400);
+
+ /* Status: Info */
+ --perses-status-bg-info: var(--perses-color-blue-900);
+ --perses-status-bg-info-hover: var(--perses-color-blue-850);
+ --perses-status-text-info: var(--perses-color-blue-300);
+ --perses-status-border-info: var(--perses-color-blue-700);
+ --perses-status-icon-info: var(--perses-color-blue-400);
+ }
+
+}
diff --git a/design-tokens/src/css/tokens.css b/design-tokens/src/css/tokens.css
new file mode 100644
index 00000000..246b3279
--- /dev/null
+++ b/design-tokens/src/css/tokens.css
@@ -0,0 +1,155 @@
+/*
+ * Copyright The Perses Authors
+ * Licensed 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.
+ */
+
+@layer perses.tokens {
+ :root {
+ /* ---- Colors: Blue ---- */
+ --perses-color-blue-50: #e7f1fc;
+ --perses-color-blue-100: #d0e3fa;
+ --perses-color-blue-150: #b8d5f7;
+ --perses-color-blue-200: #a1c7f5;
+ --perses-color-blue-300: #72abf0;
+ --perses-color-blue-400: #438feb;
+ --perses-color-blue-500: #1473e6;
+ --perses-color-blue-600: #105cb8;
+ --perses-color-blue-700: #0c458a;
+ --perses-color-blue-800: #082e5c;
+ --perses-color-blue-850: #062345;
+ --perses-color-blue-900: #04172e;
+ --perses-color-blue-950: #020c17;
+
+ /* ---- Colors: Green ---- */
+ --perses-color-green-50: #eaf9f1;
+ --perses-color-green-100: #d5f2e3;
+ --perses-color-green-150: #c1ecd4;
+ --perses-color-green-200: #ace5c6;
+ --perses-color-green-300: #82d9aa;
+ --perses-color-green-400: #59cc8d;
+ --perses-color-green-500: #2fbf71;
+ --perses-color-green-600: #26995a;
+ --perses-color-green-700: #1c7344;
+ --perses-color-green-800: #134c2d;
+ --perses-color-green-850: #0e3922;
+ --perses-color-green-900: #092617;
+ --perses-color-green-950: #05130b;
+
+ /* ---- Colors: Gray ---- */
+ --perses-color-gray-50: #f0f1f6;
+ --perses-color-gray-100: #e1e3ed;
+ --perses-color-gray-150: #d2d5e4;
+ --perses-color-gray-200: #c3c7db;
+ --perses-color-gray-300: #a4acc8;
+ --perses-color-gray-400: #8690b6;
+ --perses-color-gray-500: #717ca4;
+ --perses-color-gray-600: #535d83;
+ --perses-color-gray-700: #3e4662;
+ --perses-color-gray-800: #2a2e42;
+ --perses-color-gray-850: #1f2331;
+ --perses-color-gray-900: #151721;
+ --perses-color-gray-950: #0a0c10;
+
+ /* ---- Colors: Orange ---- */
+ --perses-color-orange-50: #fff5e8;
+ --perses-color-orange-100: #ffecd2;
+ --perses-color-orange-150: #ffe2bb;
+ --perses-color-orange-200: #ffd9a4;
+ --perses-color-orange-300: #ffc577;
+ --perses-color-orange-400: #ffb249;
+ --perses-color-orange-500: #ff9f1c;
+ --perses-color-orange-600: #cc7f16;
+ --perses-color-orange-700: #995f11;
+ --perses-color-orange-800: #66400b;
+ --perses-color-orange-850: #4d3008;
+ --perses-color-orange-900: #332006;
+ --perses-color-orange-950: #1a1003;
+
+ /* ---- Colors: Purple ---- */
+ --perses-color-purple-50: #efe9fd;
+ --perses-color-purple-100: #e0d2fc;
+ --perses-color-purple-150: #d0bcfa;
+ --perses-color-purple-200: #c1a6f8;
+ --perses-color-purple-300: #a179f5;
+ --perses-color-purple-400: #824df1;
+ --perses-color-purple-500: #6320ee;
+ --perses-color-purple-600: #4f1abe;
+ --perses-color-purple-700: #3b138f;
+ --perses-color-purple-800: #280d5f;
+ --perses-color-purple-850: #1e0a47;
+ --perses-color-purple-900: #140630;
+ --perses-color-purple-950: #0a0318;
+
+ /* ---- Colors: Red ---- */
+ --perses-color-red-50: #fdeded;
+ --perses-color-red-100: #fbdada;
+ --perses-color-red-150: #f9c8c8;
+ --perses-color-red-200: #f7b5b5;
+ --perses-color-red-300: #f29191;
+ --perses-color-red-400: #ee6c6c;
+ --perses-color-red-500: #ea4747;
+ --perses-color-red-600: #bd3939;
+ --perses-color-red-700: #902b2b;
+ --perses-color-red-800: #621d1d;
+ --perses-color-red-850: #4c1616;
+ --perses-color-red-900: #350f0f;
+ --perses-color-red-950: #1f0808;
+
+ /* ---- Colors: Common ---- */
+ --perses-color-white: #ffffff;
+ --perses-color-black: #000000;
+
+ /* ---- Spacing (rem) ---- */
+ --perses-spacing-0: 0;
+ --perses-spacing-xs: 0.25rem;
+ --perses-spacing-sm: 0.5rem;
+ --perses-spacing-md: 0.75rem;
+ --perses-spacing-lg: 1rem;
+ --perses-spacing-xl: 1.25rem;
+ --perses-spacing-2xl: 1.5rem;
+ --perses-spacing-3xl: 2rem;
+ --perses-spacing-4xl: 3rem;
+
+ /* ---- Border Radius ---- */
+ --perses-radius-none: 0;
+ --perses-radius-sm: 2px;
+ --perses-radius-md: 4px;
+ --perses-radius-lg: 8px;
+ --perses-radius-xl: 12px;
+ --perses-radius-full: 9999px;
+
+ /* ---- Typography ---- */
+ --perses-font-family:
+ Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+
+ --perses-font-weight-light: 300;
+ --perses-font-weight-regular: 400;
+ --perses-font-weight-medium: 600;
+ --perses-font-weight-bold: 700;
+
+ --perses-font-size-xs: 0.75rem;
+ --perses-font-size-sm: 0.875rem;
+ --perses-font-size-md: 1rem;
+ --perses-font-size-lg: 1.25rem;
+ --perses-font-size-xl: 1.5rem;
+ --perses-font-size-2xl: 2rem;
+ --perses-font-size-3xl: 2.5rem;
+ --perses-font-size-4xl: 3rem;
+
+ --perses-line-height-tight: 1.2;
+ --perses-line-height-compact: 1.3;
+ --perses-line-height-normal: 1.4;
+ --perses-line-height-relaxed: 1.5;
+ }
+}
diff --git a/design-tokens/src/index.ts b/design-tokens/src/index.ts
new file mode 100644
index 00000000..d627d721
--- /dev/null
+++ b/design-tokens/src/index.ts
@@ -0,0 +1,16 @@
+// Copyright The Perses Authors
+// Licensed 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 './types';
+export { tokens } from './tokens';
+export { blue, green, gray, orange, purple, red, white, black } from './colors';
diff --git a/design-tokens/src/test/consistency.test.ts b/design-tokens/src/test/consistency.test.ts
new file mode 100644
index 00000000..2204fb85
--- /dev/null
+++ b/design-tokens/src/test/consistency.test.ts
@@ -0,0 +1,60 @@
+// Copyright The Perses Authors
+// Licensed 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 { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { tokens } from '../tokens';
+
+const cssDir = resolve(__dirname, '../css');
+const readCss = (f: string): string => readFileSync(resolve(cssDir, f), 'utf-8');
+
+function extractCssVarDefinitions(...files: string[]): Set {
+ const vars = new Set();
+ for (const file of files) {
+ const css = readCss(file);
+ for (const match of css.matchAll(/^\s*(--perses-[\w-]+)\s*:/gm)) {
+ vars.add(match[1]!);
+ }
+ }
+ return vars;
+}
+
+function extractTokenVarRefs(obj: Record): Set {
+ const vars = new Set();
+ for (const value of Object.values(obj)) {
+ if (typeof value === 'string') {
+ const match = value.match(/^var\((--perses-[\w-]+)\)$/);
+ if (match) vars.add(match[1]!);
+ } else if (typeof value === 'object' && value !== null) {
+ for (const v of extractTokenVarRefs(value as Record)) {
+ vars.add(v);
+ }
+ }
+ }
+ return vars;
+}
+
+describe('token ↔ CSS consistency', () => {
+ const cssVars = extractCssVarDefinitions('tokens.css', 'semantic.css');
+ const tokenVars = extractTokenVarRefs(tokens as unknown as Record);
+
+ it('every CSS variable has a corresponding tokens entry', () => {
+ const missing = [...cssVars].filter((v) => !tokenVars.has(v)).sort();
+ expect(missing).toEqual([]);
+ });
+
+ it('every tokens entry references a defined CSS variable', () => {
+ const missing = [...tokenVars].filter((v) => !cssVars.has(v)).sort();
+ expect(missing).toEqual([]);
+ });
+});
diff --git a/design-tokens/src/test/css.test.ts b/design-tokens/src/test/css.test.ts
new file mode 100644
index 00000000..c23f0b54
--- /dev/null
+++ b/design-tokens/src/test/css.test.ts
@@ -0,0 +1,115 @@
+// Copyright The Perses Authors
+// Licensed 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 { readFileSync } from 'fs';
+import { resolve } from 'path';
+import { blue, green, gray, orange, purple, red, white, black, type PersesColor } from '../colors';
+
+const cssDir = resolve(__dirname, '../css');
+
+const readCss = (filename: string): string => readFileSync(resolve(cssDir, filename), 'utf-8');
+
+describe('CSS layer declarations', () => {
+ it('index.css declares layer order', () => {
+ const css = readCss('index.css');
+ expect(css).toContain('@layer perses.reset, perses.tokens, perses.semantic');
+ });
+
+ it('reset.css uses @layer perses.reset', () => {
+ const css = readCss('reset.css');
+ expect(css).toContain('@layer perses.reset');
+ });
+
+ it('tokens.css uses @layer perses.tokens', () => {
+ const css = readCss('tokens.css');
+ expect(css).toContain('@layer perses.tokens');
+ });
+
+ it('semantic.css uses @layer perses.semantic', () => {
+ const css = readCss('semantic.css');
+ expect(css).toContain('@layer perses.semantic');
+ });
+});
+
+describe('CSS primitive color variables', () => {
+ const tokensCss = readCss('tokens.css');
+ const tokensCssUpper = tokensCss.toUpperCase();
+
+ const hues: Array<[string, PersesColor]> = [
+ ['blue', blue],
+ ['green', green],
+ ['gray', gray],
+ ['orange', orange],
+ ['purple', purple],
+ ['red', red],
+ ];
+
+ const stops = [50, 100, 150, 200, 300, 400, 500, 600, 700, 800, 850, 900, 950] as const;
+
+ it.each(hues)('tokens.css defines all %s color variables', (hue, colorObj) => {
+ for (const stop of stops) {
+ expect(tokensCss).toContain(`--perses-color-${hue}-${stop}`);
+ expect(tokensCssUpper).toContain(colorObj[stop].toUpperCase());
+ }
+ });
+
+ it('tokens.css defines white and black', () => {
+ expect(tokensCss).toContain('--perses-color-white');
+ expect(tokensCss).toContain('--perses-color-black');
+ expect(tokensCssUpper).toContain(white.toUpperCase());
+ expect(tokensCssUpper).toContain(black.toUpperCase());
+ });
+});
+
+describe('CSS semantic variables', () => {
+ const semanticCss = readCss('semantic.css');
+
+ it('defines light mode defaults on :root', () => {
+ expect(semanticCss).toContain(':root {');
+ expect(semanticCss).toContain('--perses-bg-default');
+ expect(semanticCss).toContain('--perses-text-primary');
+ });
+
+ it('defines dark mode via data attribute', () => {
+ expect(semanticCss).toContain(`[data-perses-mode='dark']`);
+ });
+
+ it('has all background semantic tokens', () => {
+ const bgTokens = ['default', 'surface', 'sunken', 'overlay', 'backdrop', 'navigation'];
+ for (const name of bgTokens) {
+ expect(semanticCss).toContain(`--perses-bg-${name}`);
+ }
+ });
+
+ it('has border semantic token', () => {
+ expect(semanticCss).toContain('--perses-border-default');
+ });
+
+ it('has all text semantic tokens', () => {
+ const textTokens = ['primary', 'secondary', 'disabled', 'link', 'link-hover', 'navigation', 'accent'];
+ for (const name of textTokens) {
+ expect(semanticCss).toContain(`--perses-text-${name}`);
+ }
+ });
+
+ it('has all status tokens with property-scoped naming', () => {
+ const roles = ['primary', 'secondary', 'error', 'warning', 'success', 'info'];
+ const properties = ['bg', 'text', 'border', 'icon'];
+ for (const role of roles) {
+ for (const prop of properties) {
+ expect(semanticCss).toContain(`--perses-status-${prop}-${role}`);
+ }
+ expect(semanticCss).toContain(`--perses-status-bg-${role}-hover`);
+ }
+ });
+});
diff --git a/design-tokens/src/test/tokens.test.ts b/design-tokens/src/test/tokens.test.ts
new file mode 100644
index 00000000..4caaef09
--- /dev/null
+++ b/design-tokens/src/test/tokens.test.ts
@@ -0,0 +1,132 @@
+// Copyright The Perses Authors
+// Licensed 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 { tokens } from '../tokens';
+import { blue, green, gray, orange, purple, red, white, black } from '../colors';
+
+const HEX_PATTERN = /^#[0-9A-Fa-f]{6}$/;
+
+describe('color constants', () => {
+ const colorEntries = [
+ ['blue', blue],
+ ['green', green],
+ ['gray', gray],
+ ['orange', orange],
+ ['purple', purple],
+ ['red', red],
+ ] as const;
+
+ it.each(colorEntries)('%s has valid hex values for all stops', (_name, color) => {
+ const stops = [50, 100, 150, 200, 300, 400, 500, 600, 700, 800, 850, 900, 950] as const;
+ for (const stop of stops) {
+ expect(color[stop]).toMatch(HEX_PATTERN);
+ }
+ });
+
+ it('white and black are valid hex', () => {
+ expect(white).toMatch(HEX_PATTERN);
+ expect(black).toMatch(HEX_PATTERN);
+ });
+});
+
+describe('tokens object', () => {
+ it('produces correct var() strings for primitive colors', () => {
+ expect(tokens.color.blue[500]).toBe('var(--perses-color-blue-500)');
+ expect(tokens.color.gray[100]).toBe('var(--perses-color-gray-100)');
+ expect(tokens.color.red[50]).toBe('var(--perses-color-red-50)');
+ expect(tokens.color.white).toBe('var(--perses-color-white)');
+ expect(tokens.color.black).toBe('var(--perses-color-black)');
+ });
+
+ it('produces correct var() strings for semantic background tokens', () => {
+ expect(tokens.bg.default).toBe('var(--perses-bg-default)');
+ expect(tokens.bg.surface).toBe('var(--perses-bg-surface)');
+ expect(tokens.bg.sunken).toBe('var(--perses-bg-sunken)');
+ expect(tokens.bg.overlay).toBe('var(--perses-bg-overlay)');
+ expect(tokens.bg.backdrop).toBe('var(--perses-bg-backdrop)');
+ expect(tokens.bg.navigation).toBe('var(--perses-bg-navigation)');
+ });
+
+ it('produces correct var() strings for semantic border tokens', () => {
+ expect(tokens.border.default).toBe('var(--perses-border-default)');
+ });
+
+ it('produces correct var() strings for semantic text tokens', () => {
+ expect(tokens.text.primary).toBe('var(--perses-text-primary)');
+ expect(tokens.text.link).toBe('var(--perses-text-link)');
+ expect(tokens.text.disabled).toBe('var(--perses-text-disabled)');
+ });
+
+ it('produces correct var() strings for status tokens', () => {
+ expect(tokens.status.success.bg).toBe('var(--perses-status-bg-success)');
+ expect(tokens.status.success.bgHover).toBe('var(--perses-status-bg-success-hover)');
+ expect(tokens.status.success.text).toBe('var(--perses-status-text-success)');
+ expect(tokens.status.success.border).toBe('var(--perses-status-border-success)');
+ expect(tokens.status.success.icon).toBe('var(--perses-status-icon-success)');
+
+ expect(tokens.status.error.bg).toBe('var(--perses-status-bg-error)');
+ expect(tokens.status.error.text).toBe('var(--perses-status-text-error)');
+ expect(tokens.status.warning.border).toBe('var(--perses-status-border-warning)');
+ expect(tokens.status.info.icon).toBe('var(--perses-status-icon-info)');
+ expect(tokens.status.primary.bg).toBe('var(--perses-status-bg-primary)');
+ expect(tokens.status.secondary.bgHover).toBe('var(--perses-status-bg-secondary-hover)');
+ });
+
+ it('has all 6 status roles with 5 properties each', () => {
+ const roles = ['primary', 'secondary', 'error', 'warning', 'success', 'info'] as const;
+ const properties = ['bg', 'bgHover', 'text', 'border', 'icon'] as const;
+ for (const role of roles) {
+ for (const prop of properties) {
+ expect(tokens.status[role][prop]).toBeDefined();
+ expect(tokens.status[role][prop]).toMatch(/^var\(--perses-status-/);
+ }
+ }
+ });
+
+ it('produces correct var() strings for spacing tokens', () => {
+ expect(tokens.spacing[0]).toBe('var(--perses-spacing-0)');
+ expect(tokens.spacing.xs).toBe('var(--perses-spacing-xs)');
+ expect(tokens.spacing.sm).toBe('var(--perses-spacing-sm)');
+ expect(tokens.spacing.md).toBe('var(--perses-spacing-md)');
+ expect(tokens.spacing.lg).toBe('var(--perses-spacing-lg)');
+ expect(tokens.spacing.xl).toBe('var(--perses-spacing-xl)');
+ expect(tokens.spacing['2xl']).toBe('var(--perses-spacing-2xl)');
+ expect(tokens.spacing['3xl']).toBe('var(--perses-spacing-3xl)');
+ expect(tokens.spacing['4xl']).toBe('var(--perses-spacing-4xl)');
+ });
+
+ it('produces correct var() strings for radius tokens', () => {
+ expect(tokens.radius.none).toBe('var(--perses-radius-none)');
+ expect(tokens.radius.md).toBe('var(--perses-radius-md)');
+ expect(tokens.radius.full).toBe('var(--perses-radius-full)');
+ });
+
+ it('produces correct var() strings for typography tokens', () => {
+ expect(tokens.font.family).toBe('var(--perses-font-family)');
+ expect(tokens.font.weight.bold).toBe('var(--perses-font-weight-bold)');
+ expect(tokens.font.size.xs).toBe('var(--perses-font-size-xs)');
+ expect(tokens.font.size.sm).toBe('var(--perses-font-size-sm)');
+ expect(tokens.font.size.md).toBe('var(--perses-font-size-md)');
+ expect(tokens.font.size['2xl']).toBe('var(--perses-font-size-2xl)');
+ expect(tokens.font.lineHeight.tight).toBe('var(--perses-line-height-tight)');
+ expect(tokens.font.lineHeight.compact).toBe('var(--perses-line-height-compact)');
+ expect(tokens.font.lineHeight.normal).toBe('var(--perses-line-height-normal)');
+ expect(tokens.font.lineHeight.relaxed).toBe('var(--perses-line-height-relaxed)');
+ });
+
+ it('has all expected top-level categories', () => {
+ expect(Object.keys(tokens)).toEqual(
+ expect.arrayContaining(['color', 'bg', 'border', 'text', 'status', 'spacing', 'radius', 'font'])
+ );
+ });
+});
diff --git a/design-tokens/src/test/type-assertions.ts b/design-tokens/src/test/type-assertions.ts
new file mode 100644
index 00000000..75ab9ff3
--- /dev/null
+++ b/design-tokens/src/test/type-assertions.ts
@@ -0,0 +1,62 @@
+// Copyright The Perses Authors
+// Licensed 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.
+
+// Compile-time type assertions: if a token var name is missing from
+// PersesTokenVar, `npm run type-check` will fail here.
+// This file is never executed — it only needs to pass tsc.
+
+import type { PersesTokenVar } from '../types';
+import { tokens } from '../tokens';
+
+type ExtractVar = T extends `var(${infer V})` ? V : never;
+
+type AssertAssignable = T;
+
+// Verify that the var() references in the tokens object produce variable names
+// that are assignable to PersesTokenVar. If a token references a CSS variable
+// not covered by PersesTokenVar, tsc will emit an error on this line.
+type _BgVars = AssertAssignable, PersesTokenVar>;
+type _BgSurface = AssertAssignable, PersesTokenVar>;
+type _BgSunken = AssertAssignable, PersesTokenVar>;
+type _BgOverlay = AssertAssignable, PersesTokenVar>;
+type _BgBackdrop = AssertAssignable, PersesTokenVar>;
+type _BgNav = AssertAssignable, PersesTokenVar>;
+
+type _BorderDefault = AssertAssignable, PersesTokenVar>;
+
+type _TextPrimary = AssertAssignable, PersesTokenVar>;
+type _TextSecondary = AssertAssignable, PersesTokenVar>;
+type _TextDisabled = AssertAssignable, PersesTokenVar>;
+type _TextLink = AssertAssignable, PersesTokenVar>;
+type _TextLinkHover = AssertAssignable, PersesTokenVar>;
+type _TextNav = AssertAssignable, PersesTokenVar>;
+type _TextAccent = AssertAssignable, PersesTokenVar>;
+
+type _SpacingXs = AssertAssignable, PersesTokenVar>;
+type _SpacingLg = AssertAssignable, PersesTokenVar>;
+
+type _RadiusMd = AssertAssignable, PersesTokenVar>;
+type _RadiusFull = AssertAssignable, PersesTokenVar>;
+
+type _FontFamily = AssertAssignable, PersesTokenVar>;
+type _FontWeightBold = AssertAssignable, PersesTokenVar>;
+type _FontSizeSm = AssertAssignable, PersesTokenVar>;
+type _LineHeightTight = AssertAssignable, PersesTokenVar>;
+
+type _StatusErrorBg = AssertAssignable, PersesTokenVar>;
+type _StatusSuccessText = AssertAssignable, PersesTokenVar>;
+type _StatusWarningBorder = AssertAssignable, PersesTokenVar>;
+type _StatusInfoIcon = AssertAssignable, PersesTokenVar>;
+
+type _ColorBlue500 = AssertAssignable, PersesTokenVar>;
+type _ColorWhite = AssertAssignable, PersesTokenVar>;
diff --git a/design-tokens/src/tokens.ts b/design-tokens/src/tokens.ts
new file mode 100644
index 00000000..1f68128a
--- /dev/null
+++ b/design-tokens/src/tokens.ts
@@ -0,0 +1,132 @@
+// Copyright The Perses Authors
+// Licensed 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 { ColorHue, StatusRole } from './types';
+
+const colorScale = (hue: ColorHue) =>
+ ({
+ 50: `var(--perses-color-${hue}-50)`,
+ 100: `var(--perses-color-${hue}-100)`,
+ 150: `var(--perses-color-${hue}-150)`,
+ 200: `var(--perses-color-${hue}-200)`,
+ 300: `var(--perses-color-${hue}-300)`,
+ 400: `var(--perses-color-${hue}-400)`,
+ 500: `var(--perses-color-${hue}-500)`,
+ 600: `var(--perses-color-${hue}-600)`,
+ 700: `var(--perses-color-${hue}-700)`,
+ 800: `var(--perses-color-${hue}-800)`,
+ 850: `var(--perses-color-${hue}-850)`,
+ 900: `var(--perses-color-${hue}-900)`,
+ 950: `var(--perses-color-${hue}-950)`,
+ }) as const;
+
+const statusRole = (role: StatusRole) =>
+ ({
+ bg: `var(--perses-status-bg-${role})`,
+ bgHover: `var(--perses-status-bg-${role}-hover)`,
+ text: `var(--perses-status-text-${role})`,
+ border: `var(--perses-status-border-${role})`,
+ icon: `var(--perses-status-icon-${role})`,
+ }) as const;
+
+export const tokens = {
+ color: {
+ blue: colorScale('blue'),
+ green: colorScale('green'),
+ gray: colorScale('gray'),
+ orange: colorScale('orange'),
+ purple: colorScale('purple'),
+ red: colorScale('red'),
+ white: 'var(--perses-color-white)',
+ black: 'var(--perses-color-black)',
+ },
+
+ bg: {
+ default: 'var(--perses-bg-default)',
+ surface: 'var(--perses-bg-surface)',
+ sunken: 'var(--perses-bg-sunken)',
+ overlay: 'var(--perses-bg-overlay)',
+ backdrop: 'var(--perses-bg-backdrop)',
+ navigation: 'var(--perses-bg-navigation)',
+ },
+
+ border: {
+ default: 'var(--perses-border-default)',
+ },
+
+ text: {
+ primary: 'var(--perses-text-primary)',
+ secondary: 'var(--perses-text-secondary)',
+ disabled: 'var(--perses-text-disabled)',
+ link: 'var(--perses-text-link)',
+ linkHover: 'var(--perses-text-link-hover)',
+ navigation: 'var(--perses-text-navigation)',
+ accent: 'var(--perses-text-accent)',
+ },
+
+ status: {
+ primary: statusRole('primary'),
+ secondary: statusRole('secondary'),
+ error: statusRole('error'),
+ warning: statusRole('warning'),
+ success: statusRole('success'),
+ info: statusRole('info'),
+ },
+
+ spacing: {
+ '0': 'var(--perses-spacing-0)',
+ xs: 'var(--perses-spacing-xs)',
+ sm: 'var(--perses-spacing-sm)',
+ md: 'var(--perses-spacing-md)',
+ lg: 'var(--perses-spacing-lg)',
+ xl: 'var(--perses-spacing-xl)',
+ '2xl': 'var(--perses-spacing-2xl)',
+ '3xl': 'var(--perses-spacing-3xl)',
+ '4xl': 'var(--perses-spacing-4xl)',
+ },
+
+ radius: {
+ none: 'var(--perses-radius-none)',
+ sm: 'var(--perses-radius-sm)',
+ md: 'var(--perses-radius-md)',
+ lg: 'var(--perses-radius-lg)',
+ xl: 'var(--perses-radius-xl)',
+ full: 'var(--perses-radius-full)',
+ },
+
+ font: {
+ family: 'var(--perses-font-family)',
+ weight: {
+ light: 'var(--perses-font-weight-light)',
+ regular: 'var(--perses-font-weight-regular)',
+ medium: 'var(--perses-font-weight-medium)',
+ bold: 'var(--perses-font-weight-bold)',
+ },
+ size: {
+ xs: 'var(--perses-font-size-xs)',
+ sm: 'var(--perses-font-size-sm)',
+ md: 'var(--perses-font-size-md)',
+ lg: 'var(--perses-font-size-lg)',
+ xl: 'var(--perses-font-size-xl)',
+ '2xl': 'var(--perses-font-size-2xl)',
+ '3xl': 'var(--perses-font-size-3xl)',
+ '4xl': 'var(--perses-font-size-4xl)',
+ },
+ lineHeight: {
+ tight: 'var(--perses-line-height-tight)',
+ compact: 'var(--perses-line-height-compact)',
+ normal: 'var(--perses-line-height-normal)',
+ relaxed: 'var(--perses-line-height-relaxed)',
+ },
+ },
+} as const;
diff --git a/design-tokens/src/types.ts b/design-tokens/src/types.ts
new file mode 100644
index 00000000..6e633945
--- /dev/null
+++ b/design-tokens/src/types.ts
@@ -0,0 +1,87 @@
+// Copyright The Perses Authors
+// Licensed 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 type { HexColor, PersesColor } from './colors';
+
+export type ColorStop = 50 | 100 | 150 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 850 | 900 | 950;
+
+export type ColorHue = 'blue' | 'green' | 'gray' | 'orange' | 'purple' | 'red';
+
+export type PrimitiveColorVar = `--perses-color-${ColorHue}-${ColorStop}`;
+
+export type CommonColorVar = '--perses-color-white' | '--perses-color-black';
+
+export type SemanticBgVar =
+ | '--perses-bg-default'
+ | '--perses-bg-surface'
+ | '--perses-bg-sunken'
+ | '--perses-bg-overlay'
+ | '--perses-bg-backdrop'
+ | '--perses-bg-navigation';
+
+export type SemanticBorderVar = '--perses-border-default';
+
+export type SemanticTextVar =
+ | '--perses-text-primary'
+ | '--perses-text-secondary'
+ | '--perses-text-disabled'
+ | '--perses-text-link'
+ | '--perses-text-link-hover'
+ | '--perses-text-navigation'
+ | '--perses-text-accent';
+
+export type StatusRole = 'primary' | 'secondary' | 'error' | 'warning' | 'success' | 'info';
+
+export type StatusBgVar = `--perses-status-bg-${StatusRole}` | `--perses-status-bg-${StatusRole}-hover`;
+
+export type StatusTextVar = `--perses-status-text-${StatusRole}`;
+
+export type StatusBorderVar = `--perses-status-border-${StatusRole}`;
+
+export type StatusIconVar = `--perses-status-icon-${StatusRole}`;
+
+export type SpacingScale = '0' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
+
+export type SpacingVar = `--perses-spacing-${SpacingScale}`;
+
+export type RadiusVar = `--perses-radius-${'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'}`;
+
+export type FontSizeScale = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
+
+export type FontSizeVar = `--perses-font-size-${FontSizeScale}`;
+
+export type LineHeightScale = 'tight' | 'compact' | 'normal' | 'relaxed';
+
+export type LineHeightVar = `--perses-line-height-${LineHeightScale}`;
+
+export type FontVar =
+ | '--perses-font-family'
+ | `--perses-font-weight-${'light' | 'regular' | 'medium' | 'bold'}`
+ | FontSizeVar
+ | LineHeightVar;
+
+export type PersesTokenVar =
+ | PrimitiveColorVar
+ | CommonColorVar
+ | SemanticBgVar
+ | SemanticBorderVar
+ | SemanticTextVar
+ | StatusBgVar
+ | StatusTextVar
+ | StatusBorderVar
+ | StatusIconVar
+ | SpacingVar
+ | RadiusVar
+ | FontVar;
+
+export type PersesMode = 'light' | 'dark';
diff --git a/design-tokens/tsconfig.build.json b/design-tokens/tsconfig.build.json
new file mode 100644
index 00000000..e477966e
--- /dev/null
+++ b/design-tokens/tsconfig.build.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["**/*.stories.*", "**/*.test.*", "**/stories/*", "**/test/*"],
+ "compilerOptions": {
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "preserveWatchOutput": true
+ }
+}
diff --git a/design-tokens/tsconfig.json b/design-tokens/tsconfig.json
new file mode 100644
index 00000000..806aa79a
--- /dev/null
+++ b/design-tokens/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src"
+ }
+}
diff --git a/explore/package.json b/explore/package.json
index fe0ee174..1ad193b1 100644
--- a/explore/package.json
+++ b/explore/package.json
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/explore",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"description": "The explore feature in Perses",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
@@ -28,9 +28,9 @@
},
"dependencies": {
"@nexucis/fuzzy": "^0.5.1",
- "@perses-dev/components": "0.54.0-beta.10",
- "@perses-dev/dashboards": "0.54.0-beta.10",
- "@perses-dev/plugin-system": "0.54.0-beta.10",
+ "@perses-dev/components": "0.54.0",
+ "@perses-dev/dashboards": "0.54.0",
+ "@perses-dev/plugin-system": "0.54.0",
"mdi-material-ui": "^7.9.2",
"qs": "^6.14.0",
"react-virtuoso": "^4.12.2",
diff --git a/go.mod b/go.mod
index c5bd34f0..51ec5ce6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/perses/shared
-go 1.26.0
+go 1.26.5
require (
github.com/perses/perses v0.53.1
@@ -9,5 +9,5 @@ require (
require (
github.com/perses/common v0.30.2 // indirect
- golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
)
diff --git a/go.sum b/go.sum
index 9b61ff55..35221a79 100644
--- a/go.sum
+++ b/go.sum
@@ -10,7 +10,7 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/jest.shared.ts b/jest.shared.ts
index 3cfbe332..8c6f5e48 100644
--- a/jest.shared.ts
+++ b/jest.shared.ts
@@ -29,7 +29,7 @@ const config: Config.InitialOptions = {
'^use-resize-observer$': 'use-resize-observer/polyfilled',
// Tell Jest where other Perses packages live since it doesn't know about project references
- '^@perses-dev/(client|components|dashboards|explore|plugin-system)(.*)$': '/../$1/src',
+ '^@perses-dev/(client|components|dashboards|design-tokens|explore|plugin-system)(.*)$': '/../$1/src',
// Configure Jest to handle stylesheets
'\\.(css|less)$': '/../stylesMock.js',
diff --git a/package-lock.json b/package-lock.json
index cf3a1865..bd30bd80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,16 @@
{
"name": "perses-shared",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "perses-shared",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"workspaces": [
"components",
"dashboards",
+ "design-tokens",
"plugin-system",
"explore",
"client"
@@ -55,16 +56,19 @@
},
"client": {
"name": "@perses-dev/client",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"license": "Apache-2.0",
"dependencies": {
- "@perses-dev/spec": "0.2.0-beta.6",
+ "@perses-dev/spec": "0.2.0",
"zod": "^3.21.4"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
}
},
"components": {
"name": "@perses-dev/components",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"license": "Apache-2.0",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
@@ -73,8 +77,8 @@
"@date-fns/tz": "^1.4.1",
"@fontsource/inter": "^5.0.0",
"@mui/x-date-pickers": "^7.23.1",
- "@perses-dev/client": "0.54.0-beta.10",
- "@perses-dev/spec": "0.2.0-beta.6",
+ "@perses-dev/client": "0.54.0",
+ "@perses-dev/spec": "0.2.0",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.5",
"@uiw/react-codemirror": "^4.19.1",
@@ -88,7 +92,6 @@
"numbro": "^2.3.6",
"react-colorful": "^5.6.1",
"react-error-boundary": "^3.1.4",
- "react-hook-form": "^7.51.3",
"react-virtuoso": "^4.12.2"
},
"devDependencies": {
@@ -105,13 +108,13 @@
},
"dashboards": {
"name": "@perses-dev/dashboards",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"license": "Apache-2.0",
"dependencies": {
- "@perses-dev/client": "0.54.0-beta.10",
- "@perses-dev/components": "0.54.0-beta.10",
- "@perses-dev/plugin-system": "0.54.0-beta.10",
- "@perses-dev/spec": "0.2.0-beta.6",
+ "@perses-dev/client": "0.54.0",
+ "@perses-dev/components": "0.54.0",
+ "@perses-dev/plugin-system": "0.54.0",
+ "@perses-dev/spec": "0.2.0",
"@tanstack/hotkeys": "^0.8.0",
"@tanstack/react-hotkeys": "^0.9.1",
"immer": "^10.1.1",
@@ -175,15 +178,20 @@
"url": "https://github.com/sponsors/eemeli"
}
},
+ "design-tokens": {
+ "name": "@perses-dev/design-tokens",
+ "version": "0.54.0-beta.10",
+ "license": "Apache-2.0"
+ },
"explore": {
"name": "@perses-dev/explore",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"license": "Apache-2.0",
"dependencies": {
"@nexucis/fuzzy": "^0.5.1",
- "@perses-dev/components": "0.54.0-beta.10",
- "@perses-dev/dashboards": "0.54.0-beta.10",
- "@perses-dev/plugin-system": "0.54.0-beta.10",
+ "@perses-dev/components": "0.54.0",
+ "@perses-dev/dashboards": "0.54.0",
+ "@perses-dev/plugin-system": "0.54.0",
"mdi-material-ui": "^7.9.2",
"qs": "^6.14.0",
"react-virtuoso": "^4.12.2",
@@ -1262,9 +1270,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1341,9 +1349,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1475,9 +1483,9 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
- "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
+ "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2080,36 +2088,22 @@
"license": "MIT"
},
"node_modules/@module-federation/bridge-react-webpack-plugin": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.6.0.tgz",
- "integrity": "sha512-V+4+1PUmDgAozXPJA8evIa2G+dPJGYn1JzDoHS9wMFch2blko/DO2sMq6FGuZFnT1oyjxiWeiaWXluRElOU9rQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-2.8.1.tgz",
+ "integrity": "sha512-w/+d+OjtzT6sa0b3elBcCw6uw+6l8JDOtMIyWzx1lNNuV9IPhq2pgSLXEVEHdIo77r4zgQAktm9kUfCzjO0VrQ==",
"license": "MIT",
"dependencies": {
- "@module-federation/sdk": "2.6.0",
- "@types/semver": "7.5.8",
- "semver": "7.6.3"
- }
- },
- "node_modules/@module-federation/bridge-react-webpack-plugin/node_modules/semver": {
- "version": "7.6.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
+ "@module-federation/sdk": "2.8.1"
}
},
"node_modules/@module-federation/cli": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-2.6.0.tgz",
- "integrity": "sha512-CHBsi5f8Oe9sCN6rY4R0+P/oFApvuutnqfoYNtuORdMn6FHittFDkuLFCSYlrZN2QYw/Sqir2xgLcwmY77Z7PA==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-2.8.1.tgz",
+ "integrity": "sha512-DZo0f3gTN7bKoqw/KM5Kj4qIKeF3bfQwCq7fgf2Gnd+jfZPY4otVy7nbRxFmtdienEhqzcmFFWJUPH/cOkyMqg==",
"license": "MIT",
"dependencies": {
- "@module-federation/dts-plugin": "2.6.0",
- "@module-federation/sdk": "2.6.0",
+ "@module-federation/dts-plugin": "2.8.1",
+ "@module-federation/sdk": "2.8.1",
"commander": "11.1.0",
"jiti": "2.4.2"
},
@@ -2130,24 +2124,22 @@
}
},
"node_modules/@module-federation/dts-plugin": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.6.0.tgz",
- "integrity": "sha512-0dMjLhU+2HNPyX5Txu6JqA2CAYxVLRv3c/bpRk58aNVwhDO/jqp66IdFztdMZ1XiHqOW9jB3pkOSuIpcl/yXpQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-2.8.1.tgz",
+ "integrity": "sha512-JM8g76KzhhH44kHvM2JPJ5FIlQDwUNzw0vvq5EDSo/znNUmUEuSrfTssukCKg1nqnBGWLohjIV0LOOhLdCknoQ==",
"license": "MIT",
"dependencies": {
- "@module-federation/error-codes": "2.6.0",
- "@module-federation/managers": "2.6.0",
- "@module-federation/sdk": "2.6.0",
- "@module-federation/third-party-dts-extractor": "2.6.0",
- "adm-zip": "0.5.10",
- "ansi-colors": "4.1.3",
+ "@module-federation/error-codes": "2.8.1",
+ "@module-federation/managers": "2.8.1",
+ "@module-federation/sdk": "2.8.1",
+ "@module-federation/third-party-dts-extractor": "2.8.1",
+ "adm-zip": "0.6.0",
"isomorphic-ws": "5.0.0",
- "node-schedule": "2.1.1",
"undici": "7.28.0",
"ws": "8.21.0"
},
"peerDependencies": {
- "typescript": "^4.9.0 || ^5.0.0",
+ "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"vue-tsc": ">=1.0.24"
},
"peerDependenciesMeta": {
@@ -2157,31 +2149,30 @@
}
},
"node_modules/@module-federation/enhanced": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.6.0.tgz",
- "integrity": "sha512-pQ0V93FfeHtr67Nusr6ySTQO1qeOGs1x9MMjbeZ4ObOPbXdHNX1tH19xVP8mo5k3e8iIV2teMoSayTmHT+nD0g==",
- "license": "MIT",
- "dependencies": {
- "@module-federation/bridge-react-webpack-plugin": "2.6.0",
- "@module-federation/cli": "2.6.0",
- "@module-federation/dts-plugin": "2.6.0",
- "@module-federation/error-codes": "2.6.0",
- "@module-federation/inject-external-runtime-core-plugin": "2.6.0",
- "@module-federation/managers": "2.6.0",
- "@module-federation/manifest": "2.6.0",
- "@module-federation/rspack": "2.6.0",
- "@module-federation/runtime-tools": "2.6.0",
- "@module-federation/sdk": "2.6.0",
- "@module-federation/webpack-bundler-runtime": "2.6.0",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-2.8.1.tgz",
+ "integrity": "sha512-QHlvm+poXVPkIPK0C8Ras36peZA9CxRp43deSQcArBZ6uEsZTGGm07ohDxRLzVO7kYXR6dE56gIAN9fqa9Fhgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@module-federation/bridge-react-webpack-plugin": "2.8.1",
+ "@module-federation/cli": "2.8.1",
+ "@module-federation/dts-plugin": "2.8.1",
+ "@module-federation/error-codes": "2.8.1",
+ "@module-federation/inject-external-runtime-core-plugin": "2.8.1",
+ "@module-federation/managers": "2.8.1",
+ "@module-federation/manifest": "2.8.1",
+ "@module-federation/rspack": "2.8.1",
+ "@module-federation/runtime-tools": "2.8.1",
+ "@module-federation/sdk": "2.8.1",
+ "@module-federation/webpack-bundler-runtime": "2.8.1",
"schema-utils": "4.3.0",
- "tapable": "2.3.0",
- "upath": "2.0.1"
+ "tapable": "2.3.0"
},
"bin": {
"mf": "bin/mf.js"
},
"peerDependencies": {
- "typescript": "^4.9.0 || ^5.0.0",
+ "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"vue-tsc": ">=1.0.24",
"webpack": "^5.0.0"
},
@@ -2264,59 +2255,57 @@
}
},
"node_modules/@module-federation/error-codes": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.6.0.tgz",
- "integrity": "sha512-J9opjWJJZ1JI/9EvNZF8Ps1VMHS9EsH/i0UjCkQQxFVYZxSDBX1CFunvc7awac4cY//LqJUfqBbfoQPhCfKKKw==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-2.8.1.tgz",
+ "integrity": "sha512-0mQ+bWt1LRCZyURx3g2b8G+aAlvk8iXIgrp3Jit/75blrlVda/eVqnHz1L+YOxwkP3xSrdbUb4423AoWti31ZQ==",
"license": "MIT"
},
"node_modules/@module-federation/inject-external-runtime-core-plugin": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.6.0.tgz",
- "integrity": "sha512-x5SmH33U1nSEcBXG6YzvkwIQLoKcd/CNfrAx4IJ4qBzlQKI0NvY7U4JE83LpBnwahNTQLvp27ZVX+1f7kt4Vww==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-2.8.1.tgz",
+ "integrity": "sha512-xpqjWaLw4KbPW4CMRDRiGjH08/ABdPc9z4fRuPiFYA4loNYrjFWALRe2QA6W90SAew/d5RQ6QchO/wuhrzy3dw==",
"license": "MIT",
"peerDependencies": {
- "@module-federation/runtime-tools": "2.6.0"
+ "@module-federation/runtime-tools": "2.8.1"
}
},
"node_modules/@module-federation/managers": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-2.6.0.tgz",
- "integrity": "sha512-7Y4Ri6Lh10dCHoEJvNGFmK2Gg1JKy7oJ1TEZhuU3n3mgIpZ519fDNRvU4+tiO9C49p1xyK+mQ8ewxth83waKUA==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-2.8.1.tgz",
+ "integrity": "sha512-bHRooIgplIFNLH42eM3g21b2apM/a7lMG1EwifUxT4A4AobS42H08KGdYITdKJcrgowx3D7PhYndiwtHFI03zQ==",
"license": "MIT",
"dependencies": {
- "@module-federation/sdk": "2.6.0",
- "find-pkg": "2.0.0"
+ "@module-federation/sdk": "2.8.1"
}
},
"node_modules/@module-federation/manifest": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.6.0.tgz",
- "integrity": "sha512-wIjI4wgFKBoq4l/iBOT2lva+i5sPv1+Ci1gOLOfjMAkygyGo97csUG5TaCWgLJpZzbYrpbFObCB2wZhqaLrQIw==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-2.8.1.tgz",
+ "integrity": "sha512-1NOd4J1sJrTpl7M1NYsdUtjSR2Eu3R//r+w51KC9XhAZkxWse0+uPphGdeiUqCOVgAAWjKreckY+xnEmG6Kqig==",
"license": "MIT",
"dependencies": {
- "@module-federation/dts-plugin": "2.6.0",
- "@module-federation/managers": "2.6.0",
- "@module-federation/sdk": "2.6.0",
- "find-pkg": "2.0.0"
+ "@module-federation/dts-plugin": "2.8.1",
+ "@module-federation/managers": "2.8.1",
+ "@module-federation/sdk": "2.8.1"
}
},
"node_modules/@module-federation/rspack": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.6.0.tgz",
- "integrity": "sha512-Ayh7WLxhLRMyfBfajDwEytR5StFfnqf8p4tPHq5ds+HzJMlGz/M+NIYEzdOpfOFdE5IqN9bY/mj2AFI5lCureQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-2.8.1.tgz",
+ "integrity": "sha512-jJYkARn1U1M92CbiLyoaP7+tNKh8YzzOTMSGzbdj6okTtWK0Z9ImyVoKEnAnjnLP1nbQjkPEaojkR2D2IQFhLw==",
"license": "MIT",
"dependencies": {
- "@module-federation/bridge-react-webpack-plugin": "2.6.0",
- "@module-federation/dts-plugin": "2.6.0",
- "@module-federation/inject-external-runtime-core-plugin": "2.6.0",
- "@module-federation/managers": "2.6.0",
- "@module-federation/manifest": "2.6.0",
- "@module-federation/runtime-tools": "2.6.0",
- "@module-federation/sdk": "2.6.0"
+ "@module-federation/bridge-react-webpack-plugin": "2.8.1",
+ "@module-federation/dts-plugin": "2.8.1",
+ "@module-federation/inject-external-runtime-core-plugin": "2.8.1",
+ "@module-federation/managers": "2.8.1",
+ "@module-federation/manifest": "2.8.1",
+ "@module-federation/runtime-tools": "2.8.1",
+ "@module-federation/sdk": "2.8.1"
},
"peerDependencies": {
"@rspack/core": "^0.7.0 || ^1.0.0 || ^2.0.0-0",
- "typescript": "^4.9.0 || ^5.0.0",
+ "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"vue-tsc": ">=1.0.24"
},
"peerDependenciesMeta": {
@@ -2329,86 +2318,57 @@
}
},
"node_modules/@module-federation/runtime": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.6.0.tgz",
- "integrity": "sha512-Y8KgL70RDxD8cCtGWGlGkt+TSDleIjdGmS27gnllibQAIgG/vHhPD11r8kd92x44k4dqtMIntzreOphGICl92g==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-2.8.1.tgz",
+ "integrity": "sha512-+xpq/r6Om4plbGJisZp6/rl7usEUlQszz34E+JUKgl9uW3QIRzVgxwOAzGiF7U+dqOKxMqmiq+nTJwK2wAwteA==",
"license": "MIT",
"dependencies": {
- "@module-federation/error-codes": "2.6.0",
- "@module-federation/runtime-core": "2.6.0",
- "@module-federation/sdk": "2.6.0"
+ "@module-federation/error-codes": "2.8.1",
+ "@module-federation/runtime-core": "2.8.1",
+ "@module-federation/sdk": "2.8.1"
}
},
"node_modules/@module-federation/runtime-core": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.6.0.tgz",
- "integrity": "sha512-9sL7Yj3H3COZI9JpK/J4hmJUBkIJdmowkj6phHuFiy5Hm5bHiE5U+9WBbR9KqTdyNhAVSL9FeprBW5/Io6i59A==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-2.8.1.tgz",
+ "integrity": "sha512-Dif+3u7fvq6qBATFIv5qB7ay6Rgo2HNzhaNOt1yRfpVXjiJqQ3UPnHZ+TLP9znkXiZvg6Bg5W9EV2ZX4nH2S0w==",
"license": "MIT",
"dependencies": {
- "@module-federation/error-codes": "2.6.0",
- "@module-federation/sdk": "2.6.0"
+ "@module-federation/error-codes": "2.8.1",
+ "@module-federation/sdk": "2.8.1"
}
},
"node_modules/@module-federation/runtime-tools": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.6.0.tgz",
- "integrity": "sha512-LfvihAhAWWNWlAL9zM+UDVDSSuLE2ZU0ATJ6dcbJuLstYbHYfUqq2e6IyU8TKLPXIib0VAWqPrT44TPam1VJ7g==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-2.8.1.tgz",
+ "integrity": "sha512-CIQ9dPqWOiitXKCYqgHXfr0hehtxsZDfiuFaesJAJnXtqkM5+nVkkBNkY07cDV5wOi00/aiOhEWYoCcz+cRQtQ==",
"license": "MIT",
"dependencies": {
- "@module-federation/runtime": "2.6.0",
- "@module-federation/webpack-bundler-runtime": "2.6.0"
+ "@module-federation/runtime": "2.8.1",
+ "@module-federation/webpack-bundler-runtime": "2.8.1"
}
},
"node_modules/@module-federation/sdk": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.6.0.tgz",
- "integrity": "sha512-Z3HT1uDciMrvz2at3sw6TJbAK9Fl7RmoC/8iqO35HDtQMQJ1qSsMIAjnsiCpAC0D4nAm6PIqgSqPWRO/WYgo0Q==",
- "license": "MIT",
- "peerDependencies": {
- "node-fetch": "^2.7.0 || ^3.3.2"
- },
- "peerDependenciesMeta": {
- "node-fetch": {
- "optional": true
- }
- }
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-2.8.1.tgz",
+ "integrity": "sha512-3EVljiNilY2pFIG2RO4KNCC6gIPnYc9J+p5U6Nn8D5X3PtJeEcPyBvKGttxZnSLlXCi+YXQfgexBOfnkvEuzuQ==",
+ "license": "MIT"
},
"node_modules/@module-federation/third-party-dts-extractor": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.6.0.tgz",
- "integrity": "sha512-rEC1YRC3gTafoOcFm1Htz6cAwHRoWEMQNCm1LXR1HiuayJzUkViVpK/1Pp37UZfytOyQ0qIaP6Ag81PdGvDbmw==",
- "license": "MIT",
- "dependencies": {
- "find-pkg": "2.0.0",
- "resolve": "1.22.8"
- }
- },
- "node_modules/@module-federation/third-party-dts-extractor/node_modules/resolve": {
- "version": "1.22.8",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
- "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.13.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-2.8.1.tgz",
+ "integrity": "sha512-6ulu7vqv5wyM5YWwxjUHzZ+8Sg6e+/vn+gskiUBs6EPqe/w3XMdRoUrFjAI3EW62xi0e0xbWLsjbQs9jCAj8ig==",
+ "license": "MIT"
},
"node_modules/@module-federation/webpack-bundler-runtime": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.6.0.tgz",
- "integrity": "sha512-JjuWOU3ktwDjBWhM4WCoeiErcUxcB5YwUnVjyTMfNJHC3+DMhG4m6ziCl5EJrCv+KaEQdcvXmxLliNZidBZGQg==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-2.8.1.tgz",
+ "integrity": "sha512-dGFlrTLimhxpHohysx/qPRuIRaAiutqFfX0xFzDchEkY0DXpS2sOhuJ2foNcCIQK/FKFJW1YeaaIUkZ5FWMcOg==",
"license": "MIT",
"dependencies": {
- "@module-federation/error-codes": "2.6.0",
- "@module-federation/runtime": "2.6.0",
- "@module-federation/sdk": "2.6.0"
+ "@module-federation/error-codes": "2.8.1",
+ "@module-federation/runtime": "2.8.1",
+ "@module-federation/sdk": "2.8.1"
}
},
"node_modules/@mui/core-downloads-tracker": {
@@ -3259,13 +3219,13 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
- "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"license": "MIT",
"optional": true,
"dependencies": {
- "@tybys/wasm-util": "^0.10.2"
+ "@tybys/wasm-util": "^0.10.3"
},
"funding": {
"type": "github",
@@ -3345,6 +3305,10 @@
"resolved": "dashboards",
"link": true
},
+ "node_modules/@perses-dev/design-tokens": {
+ "resolved": "design-tokens",
+ "link": true
+ },
"node_modules/@perses-dev/explore": {
"resolved": "explore",
"link": true
@@ -3354,9 +3318,9 @@
"link": true
},
"node_modules/@perses-dev/spec": {
- "version": "0.2.0-beta.6",
- "resolved": "https://registry.npmjs.org/@perses-dev/spec/-/spec-0.2.0-beta.6.tgz",
- "integrity": "sha512-J8oWuZgc/0nlT8PDlEIahA7XZ/tKITC9C0FG7e+FFhYGRzZ5oyMz2keuPkAzl3KPYB9yeczit9TFQWsoY/ot5Q==",
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@perses-dev/spec/-/spec-0.2.0.tgz",
+ "integrity": "sha512-0SukMq7kzYBni670E+uSSebr8LzQP4PdgUUli9HVU2FsDTrby5rJ3KjgoGRbCtGJNCB/fmeWGx57ZlfFohvwog==",
"license": "Apache-2.0",
"dependencies": {
"date-fns": "^4.1.0",
@@ -3455,30 +3419,30 @@
}
},
"node_modules/@rspack/binding": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.0.tgz",
- "integrity": "sha512-LsXFIOOYDutHk44SAOcVQa5iA7lhYwEbD+nZhgmCiGJvKKh0UIpBj6EAsBsB6omEK5GEXvjDeLFieKgbYW08QQ==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.7.tgz",
+ "integrity": "sha512-wYqi8TY30hsIzLry503o/Uqu7y9Ec7pEwN5TVmB7Pb3xHrR2eHsQPzdpF/GkCLUjQSgD2Es3CDVV1mr6zO/78g==",
"license": "MIT",
"peer": true,
"optionalDependencies": {
- "@rspack/binding-darwin-arm64": "2.1.0",
- "@rspack/binding-darwin-x64": "2.1.0",
- "@rspack/binding-linux-arm64-gnu": "2.1.0",
- "@rspack/binding-linux-arm64-musl": "2.1.0",
- "@rspack/binding-linux-riscv64-gnu": "2.1.0",
- "@rspack/binding-linux-riscv64-musl": "2.1.0",
- "@rspack/binding-linux-x64-gnu": "2.1.0",
- "@rspack/binding-linux-x64-musl": "2.1.0",
- "@rspack/binding-wasm32-wasi": "2.1.0",
- "@rspack/binding-win32-arm64-msvc": "2.1.0",
- "@rspack/binding-win32-ia32-msvc": "2.1.0",
- "@rspack/binding-win32-x64-msvc": "2.1.0"
+ "@rspack/binding-darwin-arm64": "2.1.7",
+ "@rspack/binding-darwin-x64": "2.1.7",
+ "@rspack/binding-linux-arm64-gnu": "2.1.7",
+ "@rspack/binding-linux-arm64-musl": "2.1.7",
+ "@rspack/binding-linux-riscv64-gnu": "2.1.7",
+ "@rspack/binding-linux-riscv64-musl": "2.1.7",
+ "@rspack/binding-linux-x64-gnu": "2.1.7",
+ "@rspack/binding-linux-x64-musl": "2.1.7",
+ "@rspack/binding-wasm32-wasi": "2.1.7",
+ "@rspack/binding-win32-arm64-msvc": "2.1.7",
+ "@rspack/binding-win32-ia32-msvc": "2.1.7",
+ "@rspack/binding-win32-x64-msvc": "2.1.7"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.1.0.tgz",
- "integrity": "sha512-1DdnXLCl4/7BydtxvFyJbqOyvo3dgeKIdukr5BrM7UUA5rJnpin0qZIq/C0Y+ZwTx7ML4zdYaJeR+WOujRQH1Q==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.1.7.tgz",
+ "integrity": "sha512-DwxzrXRctueP/3Pyom9JHcIsRShuEAlHb+mrE5OPT+4cdHI1UnJpbzEvEDLTo4IKJhDb3vjXdHLtjqtL0SYbeA==",
"cpu": [
"arm64"
],
@@ -3490,9 +3454,9 @@
"peer": true
},
"node_modules/@rspack/binding-darwin-x64": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.1.0.tgz",
- "integrity": "sha512-PJB6n/BaupvfLaErsfvC7q9W07WozkPe2Xw7sQqX6fblK+4tooBp0ZdAtKi76L+U2fR8t8/nQb0Jokco0co7Fg==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.1.7.tgz",
+ "integrity": "sha512-kPbrYvR/XUHfAMgRVq3QnC71DW/qjwsPj+3hEUuEnRmlploPNy9u8Szf1IHKSVUSrVZBTgDyMoZQdxYLfhResw==",
"cpu": [
"x64"
],
@@ -3504,9 +3468,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.1.0.tgz",
- "integrity": "sha512-TCmWIeI03ZZi8GjpIS2yl9JpaazsaA4F84zbX6a4kdZnFkrmFKRdvczZrquTNQvmggAEaJiPxkSrS8OC1LSAwA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.1.7.tgz",
+ "integrity": "sha512-VFB+YXM3kZ6IIuLV64H3vgnwqvQIIaqfR/aeGwuxYvwcZsrgblSBmXMeDULdgDjqP8Yr0VaFMBBiD9OtG5KdFw==",
"cpu": [
"arm64"
],
@@ -3518,9 +3482,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-arm64-musl": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.1.0.tgz",
- "integrity": "sha512-HJzw5gG62qjj9fRQgj948naLucwE1Vg1bfcYHAxOr1/bVVIm4I4QvWGuqvd3XOu0MfLXPWvEyMAvJL+rtgamsw==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.1.7.tgz",
+ "integrity": "sha512-Mzbxyg0aJ+ITj526Iuz0enEDYY6WxhFIwEKXqwjQh+Vpd5v/+aPzPo83sSQVX/3puBV1sbmviTURbh6N9e1fvA==",
"cpu": [
"arm64"
],
@@ -3532,9 +3496,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-riscv64-gnu": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-2.1.0.tgz",
- "integrity": "sha512-B3ENZHIBi5u1Apt6RJ62QSCabCijI5l86Sm2AEDYpQnqqBj3vIc+Br9HJHvNjK8PNWs1WfmD//UTUmQqZbYpKQ==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-2.1.7.tgz",
+ "integrity": "sha512-mpazwgT/Pse1720mvEJsoXfPkJ+enj0xUqpbe/wL6aedwjGT+9jJNB8HTJXE4XBX0UO7umGqcJMeKA6YsD2CDA==",
"cpu": [
"riscv64"
],
@@ -3546,9 +3510,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-riscv64-musl": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-2.1.0.tgz",
- "integrity": "sha512-Qho1S8bW2BKRsJjl/f39GoyPRznF8ZarIgxZdVCIkn4k+3veggKWxqR1WWKoMj/LfykQd1uG3FF6n7zy5IfxWw==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-2.1.7.tgz",
+ "integrity": "sha512-oU/l3soPRsDEWn7KZic+npyTMM2N1kRdHjoJ+L5IUBXs8bjdTXPLoyTbTdIOza5ZSoT4+UeEiEryj4BB0tQE5w==",
"cpu": [
"riscv64"
],
@@ -3560,9 +3524,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-x64-gnu": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.1.0.tgz",
- "integrity": "sha512-oE2CMALLdV3QNiA3YYDZ46tDGf+WRlqu/tQ+B79JYKVwt3sI0fpzvgwPNpx/gfRKUyA0phaeYS4kyOEnpltjpA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.1.7.tgz",
+ "integrity": "sha512-7Gtpl3h3jtnOpk1mYQE8mRndXAO2ibI8mnAbs7klevdKey+ZHneWMoMi2yOMQhhI/ifWEFxDzyGJ8bdxo0XTsA==",
"cpu": [
"x64"
],
@@ -3574,9 +3538,9 @@
"peer": true
},
"node_modules/@rspack/binding-linux-x64-musl": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.1.0.tgz",
- "integrity": "sha512-lxTFZgsfPPyyIt/DpOH5TK2u1ZROMB+gLp/LWvYBc8FSOtmR0Gl4L/AWmJdM2yqwPfy0hgSkVicf/7k80jHuVQ==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.1.7.tgz",
+ "integrity": "sha512-w+whI2Uy+DYkGN+MVkzMFWweL7B/s1gMqX+nvTE1vhOy3hGV0VyA9H6lqWjSD3I+eGkpYhN9Pr244cYnLpZOUQ==",
"cpu": [
"x64"
],
@@ -3588,9 +3552,9 @@
"peer": true
},
"node_modules/@rspack/binding-wasm32-wasi": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.1.0.tgz",
- "integrity": "sha512-ZsDDduXaEF1SpyGz2OFuEU9Tzm0pKtbtCYviymiNtQS+3lx6rXyv+FaK0oIWn+gWL+gVNamplxKnNNR2jZsp5w==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.1.7.tgz",
+ "integrity": "sha512-cDVgvzRdTgxaeM+a5Lx0+7/VAvunvwO0wNtQ3ATQGOtFCW5b7cUzhNPcytH5ZSJTnFWuxinlGwtar5yfcnkdZQ==",
"cpu": [
"wasm32"
],
@@ -3598,15 +3562,15 @@
"optional": true,
"peer": true,
"dependencies": {
- "@emnapi/core": "1.11.1",
- "@emnapi/runtime": "1.11.1",
- "@napi-rs/wasm-runtime": "1.1.5"
+ "@emnapi/core": "1.11.2",
+ "@emnapi/runtime": "1.11.2",
+ "@napi-rs/wasm-runtime": "1.1.6"
}
},
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/core": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
- "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
+ "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -3616,9 +3580,9 @@
}
},
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -3638,9 +3602,9 @@
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.1.0.tgz",
- "integrity": "sha512-0uMWAZYgwdkk0ocE4X85w/0BNWT5GaKJTEZDVYxfSYcfVAxJvIsuM0VH/cjRjsQtEeE1rcY7JvJyd0rQ6j0DqA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.1.7.tgz",
+ "integrity": "sha512-JDd85+iYwUvaG9Zrt5X7oIxRZRiTW+76FwkRakoXNy/5VAWQW32Jq4ESjSVz6l6mh0KnZxPq3TLMugacCPnLjw==",
"cpu": [
"arm64"
],
@@ -3652,9 +3616,9 @@
"peer": true
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.1.0.tgz",
- "integrity": "sha512-MRuIZwF6w1tGyZgoJZ5dnpLaD/oMx8zAYSYQfNS7l0f7qjxbnp42625wkeNB8kPqrmDfqaWUWLwiOaRqFPmumA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.1.7.tgz",
+ "integrity": "sha512-y9PKEs6v9BLHV0i/4eaIRtxpATvSgcf/VYQkMT8mp+qWlPjUwDQNwU2ueWVGpff6INO+YAa7zobzziNFRgO7Lg==",
"cpu": [
"ia32"
],
@@ -3666,9 +3630,9 @@
"peer": true
},
"node_modules/@rspack/binding-win32-x64-msvc": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.1.0.tgz",
- "integrity": "sha512-Fme2Ifa647CtD7N6we9xvK+COzfzVJREtUayxdG+VArPdijURZyRQVUKKlYSBW+2qMg+G+kF1xo7gf7svG3sNA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.1.7.tgz",
+ "integrity": "sha512-BjkOzcPY/K8YlRRvyywz0mDWk89MMxqAMhDmgBXCWorh1IjgKTsWDJ2lCGIM8M9CZXUG3khom8AfrOGwRT2I+g==",
"cpu": [
"x64"
],
@@ -3680,13 +3644,13 @@
"peer": true
},
"node_modules/@rspack/core": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.1.0.tgz",
- "integrity": "sha512-dlZRzWQi90HzLYErGh0/xnEWAEMEAtDKXvNxERCEj5uIVIOVu9+uYwpNyAkKc9cK5sPhOz05kk9MIb1EaUJ5gg==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.1.7.tgz",
+ "integrity": "sha512-d5Ju3zXzGgbqQWvlMlLUtek2eFPIzsFe2QOF4nwTAknxo/4OZ64t+kPT9nM6fr3aZX93VK0R3v02/kZYIRrV9Q==",
"license": "MIT",
"peer": true,
"dependencies": {
- "@rspack/binding": "2.1.0"
+ "@rspack/binding": "2.1.7"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -4487,9 +4451,9 @@
]
},
"node_modules/@tybys/wasm-util": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
- "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -4731,9 +4695,10 @@
}
},
"node_modules/@types/semver": {
- "version": "7.5.8",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/stack-utils": {
@@ -4966,16 +4931,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
@@ -5642,16 +5607,16 @@
}
},
"node_modules/@xhmikosr/decompress": {
- "version": "11.1.2",
- "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-11.1.2.tgz",
- "integrity": "sha512-f2hlnMN1ChbifAfdzWns6mssojjr3lgJm6MtT4ttVAClx85QEIQAdGXN22bPqy/qKxbvDf93GdqbR6+xIO/a4A==",
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-11.1.3.tgz",
+ "integrity": "sha512-NiyhJq6z7ERsYghcnXZUI6ooDXgZtoB+G9eUsYhfSM4VLp2rKx9UxhKI1NEf1PqosrNPxG3bnSsr2UBVbNurlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@xhmikosr/decompress-tar": "^9.0.1",
"@xhmikosr/decompress-tarbz2": "^9.0.1",
"@xhmikosr/decompress-targz": "^9.0.1",
- "@xhmikosr/decompress-unzip": "^8.1.0",
+ "@xhmikosr/decompress-unzip": "^8.1.1",
"graceful-fs": "^4.2.11",
"strip-dirs": "^3.0.0"
},
@@ -5821,12 +5786,12 @@
}
},
"node_modules/adm-zip": {
- "version": "0.5.10",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
- "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
+ "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
"license": "MIT",
"engines": {
- "node": ">=6.0"
+ "node": ">=14.0"
}
},
"node_modules/agent-base": {
@@ -5895,15 +5860,6 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
- "node_modules/ansi-colors": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
- "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -6567,9 +6523,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
- "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz",
+ "integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7011,15 +6967,15 @@
"license": "MIT"
},
"node_modules/concurrently": {
- "version": "10.0.3",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
- "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
+ "version": "10.0.4",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.4.tgz",
+ "integrity": "sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "5.6.2",
"rxjs": "7.8.2",
- "shell-quote": "1.8.4",
+ "shell-quote": "1.9.0",
"supports-color": "10.2.2",
"tree-kill": "1.2.2",
"yargs": "18.0.0"
@@ -7251,18 +7207,6 @@
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
- "node_modules/cron-parser": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
- "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
- "license": "MIT",
- "dependencies": {
- "luxon": "^3.2.1"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -8075,9 +8019,9 @@
}
},
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8172,9 +8116,9 @@
}
},
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8273,9 +8217,9 @@
}
},
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8374,9 +8318,9 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8543,18 +8487,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/expand-tilde": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
- "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
- "license": "MIT",
- "dependencies": {
- "homedir-polyfill": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/expect": {
"version": "30.4.1",
"resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz",
@@ -8641,9 +8573,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
- "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"funding": [
{
"type": "github",
@@ -8771,30 +8703,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/find-file-up": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-2.0.1.tgz",
- "integrity": "sha512-qVdaUhYO39zmh28/JLQM5CoYN9byEOKEH4qfa8K1eNV17W0UUMJ9WgbR/hHFH+t5rcl+6RTb5UC7ck/I+uRkpQ==",
- "license": "MIT",
- "dependencies": {
- "resolve-dir": "^1.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-pkg": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-2.0.0.tgz",
- "integrity": "sha512-WgZ+nKbELDa6N3i/9nrHeNznm+lY3z4YfhDDWgW+5P0pdmMj26bxaxU11ookgY3NyP9GC7HvZ9etp0jRFqGEeQ==",
- "license": "MIT",
- "dependencies": {
- "find-file-up": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@@ -8851,9 +8759,9 @@
}
},
"node_modules/flat-cache/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9219,54 +9127,6 @@
"license": "BSD-2-Clause",
"peer": true
},
- "node_modules/global-modules": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
- "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
- "license": "MIT",
- "dependencies": {
- "global-prefix": "^1.0.1",
- "is-windows": "^1.0.1",
- "resolve-dir": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/global-prefix": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
- "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
- "license": "MIT",
- "dependencies": {
- "expand-tilde": "^2.0.2",
- "homedir-polyfill": "^1.0.1",
- "ini": "^1.3.4",
- "is-windows": "^1.0.1",
- "which": "^1.2.14"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/global-prefix/node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/global-prefix/node_modules/which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "which": "bin/which"
- }
- },
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -9496,18 +9356,6 @@
"react-is": "^16.7.0"
}
},
- "node_modules/homedir-polyfill": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
- "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
- "license": "MIT",
- "dependencies": {
- "parse-passwd": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -9716,12 +9564,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
- },
"node_modules/inspect-with-kind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz",
@@ -10210,15 +10052,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-windows": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
- "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -11352,9 +11185,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
- "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
"funding": [
{
@@ -11603,12 +11436,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/long-timeout": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz",
- "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==",
- "license": "MIT"
- },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -11649,6 +11476,8 @@
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -11941,20 +11770,6 @@
"node": ">=18"
}
},
- "node_modules/node-schedule": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz",
- "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==",
- "license": "MIT",
- "dependencies": {
- "cron-parser": "^4.2.0",
- "long-timeout": "0.1.1",
- "sorted-array-functions": "^1.3.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -12372,15 +12187,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/parse-passwd": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
- "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -13131,19 +12937,6 @@
"node": ">=8"
}
},
- "node_modules/resolve-dir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
- "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
- "license": "MIT",
- "dependencies": {
- "expand-tilde": "^2.0.0",
- "global-modules": "^1.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -13528,9 +13321,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.8.4",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
- "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
+ "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13671,12 +13464,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/sorted-array-functions": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz",
- "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==",
- "license": "MIT"
- },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -14349,9 +14136,9 @@
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14917,16 +14704,6 @@
"@unrs/resolver-binding-win32-x64-msvc": "1.12.2"
}
},
- "node_modules/upath": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
- "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
- "license": "MIT",
- "engines": {
- "node": ">=4",
- "yarn": "*"
- }
- },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -15699,14 +15476,14 @@
},
"plugin-system": {
"name": "@perses-dev/plugin-system",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"license": "Apache-2.0",
"dependencies": {
- "@module-federation/enhanced": "^2.6.0",
- "@perses-dev/client": "0.54.0-beta.10",
- "@perses-dev/components": "0.54.0-beta.10",
+ "@module-federation/enhanced": "^2.8.0",
+ "@perses-dev/client": "0.54.0",
+ "@perses-dev/components": "0.54.0",
"@perses-dev/core": "0.53.0",
- "@perses-dev/spec": "0.2.0-beta.6",
+ "@perses-dev/spec": "0.2.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"immer": "^10.1.1",
@@ -15715,6 +15492,9 @@
"use-query-params": "^2.2.1",
"zod": "^3.25.76"
},
+ "devDependencies": {
+ "@types/semver": "^7.7.1"
+ },
"peerDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
diff --git a/package.json b/package.json
index 5b4acc81..cf6598f4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "perses-shared",
"description": "Monorepo for the Perses UI shared packages",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"private": true,
"scripts": {
"build": "turbo run build",
@@ -17,6 +17,7 @@
"workspaces": [
"components",
"dashboards",
+ "design-tokens",
"plugin-system",
"explore",
"client"
diff --git a/plugin-system/package.json b/plugin-system/package.json
index 969ef381..7554c0cb 100644
--- a/plugin-system/package.json
+++ b/plugin-system/package.json
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/plugin-system",
- "version": "0.54.0-beta.10",
+ "version": "0.54.0",
"description": "The plugin feature in Pereses",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
@@ -28,18 +28,18 @@
"lint:fix": "eslint --fix src --ext .ts,.tsx"
},
"dependencies": {
- "@module-federation/enhanced": "^2.6.0",
- "@perses-dev/components": "0.54.0-beta.10",
+ "@module-federation/enhanced": "^2.8.0",
+ "@perses-dev/client": "0.54.0",
+ "@perses-dev/components": "0.54.0",
"@perses-dev/core": "0.53.0",
- "@perses-dev/spec": "0.2.0-beta.6",
- "@perses-dev/client": "0.54.0-beta.10",
+ "@perses-dev/spec": "0.2.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"immer": "^10.1.1",
"react-hook-form": "^7.46.1",
+ "semver": "^7.8.0",
"use-query-params": "^2.2.1",
- "zod": "^3.25.76",
- "semver": "^7.8.0"
+ "zod": "^3.25.76"
},
"peerDependencies": {
"@emotion/react": "^11.14.0",
@@ -52,5 +52,8 @@
},
"files": [
"dist"
- ]
+ ],
+ "devDependencies": {
+ "@types/semver": "^7.7.1"
+ }
}
diff --git a/components/src/LinksEditor/LinksEditor.tsx b/plugin-system/src/components/LinksEditor/LinksEditor.tsx
similarity index 97%
rename from components/src/LinksEditor/LinksEditor.tsx
rename to plugin-system/src/components/LinksEditor/LinksEditor.tsx
index 9ce02ec9..93c2a0bc 100644
--- a/components/src/LinksEditor/LinksEditor.tsx
+++ b/plugin-system/src/components/LinksEditor/LinksEditor.tsx
@@ -16,8 +16,8 @@ import { Divider, IconButton, Stack, Typography } from '@mui/material';
import { Controller, useFieldArray, Control } from 'react-hook-form';
import PlusIcon from 'mdi-material-ui/Plus';
import MinusIcon from 'mdi-material-ui/Minus';
-import { PanelEditorValues } from '@perses-dev/spec';
-import { LinkEditorForm } from './LinkEditorForm';
+import { LinkEditorForm } from '@perses-dev/components';
+import { PanelEditorValues } from '../../model';
export interface LinksEditorProps extends HTMLAttributes {
control: Control;
diff --git a/plugin-system/src/components/LinksEditor/index.ts b/plugin-system/src/components/LinksEditor/index.ts
new file mode 100644
index 00000000..2dc504b3
--- /dev/null
+++ b/plugin-system/src/components/LinksEditor/index.ts
@@ -0,0 +1,14 @@
+// Copyright The Perses Authors
+// Licensed 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 './LinksEditor';
diff --git a/plugin-system/src/components/MultiQueryEditor/MultiQueryEditor.tsx b/plugin-system/src/components/MultiQueryEditor/MultiQueryEditor.tsx
index 3375bdf5..3b2bafd3 100644
--- a/plugin-system/src/components/MultiQueryEditor/MultiQueryEditor.tsx
+++ b/plugin-system/src/components/MultiQueryEditor/MultiQueryEditor.tsx
@@ -54,7 +54,7 @@ function useDefaultQueryDefinition(
defaultQueryKind = defaultPluginKinds?.[defaultQueryType] ?? queryPlugins?.[0]?.spec.name ?? '';
}
- const { data: defaultQueryPlugin } = usePlugin(defaultQueryType, defaultQueryKind, {
+ const { data: defaultQueryPlugin, isLoading: isPluginLoading } = usePlugin(defaultQueryType, defaultQueryKind, {
useErrorBoundary: true,
enabled: true,
});
@@ -67,7 +67,7 @@ function useDefaultQueryDefinition(
plugin: { kind: defaultQueryKind, spec: defaultQueryPlugin?.createInitialOptions() || {} },
},
},
- isLoading,
+ isLoading: isLoading || isPluginLoading,
};
}
diff --git a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx
index 3ff23401..37963bec 100644
--- a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx
+++ b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx
@@ -13,8 +13,8 @@
import { screen } from '@testing-library/react';
import { useForm } from 'react-hook-form';
-import { PanelEditorValues } from '@perses-dev/spec';
import { ReactElement } from 'react';
+import { PanelEditorValues } from '../../model';
import { renderWithContext } from '../../test';
import { DataQueriesContext } from '../../runtime';
import { PanelSpecEditor, PanelSpecEditorProps } from './PanelSpecEditor';
diff --git a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx
index b77a1f7d..eeb92e75 100644
--- a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx
+++ b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx
@@ -11,13 +11,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { ErrorAlert, JSONEditor, LinksEditor } from '@perses-dev/components';
-import { PanelDefinition, PanelEditorValues, QueryDefinition, UnknownSpec } from '@perses-dev/spec';
+import { ErrorAlert, JSONEditor } from '@perses-dev/components';
+import { PanelDefinition, QueryDefinition, UnknownSpec } from '@perses-dev/spec';
import { Control, Controller } from 'react-hook-form';
import { forwardRef, ReactElement } from 'react';
+import { LinksEditor } from '../LinksEditor';
+import { PanelEditorValues, PanelPlugin } from '../../model';
import { useDataQueriesContext, usePlugin } from '../../runtime';
-import { PanelPlugin } from '../../model';
-import { OptionsEditorTabsProps, OptionsEditorTabs } from '../OptionsEditorTabs';
+import { OptionsEditorTabs, OptionsEditorTabsProps } from '../OptionsEditorTabs';
import { MultiQueryEditor } from '../MultiQueryEditor';
import { PluginEditorRef } from '../PluginEditor';
diff --git a/plugin-system/src/components/PluginRegistry/PluginRegistry.tsx b/plugin-system/src/components/PluginRegistry/PluginRegistry.tsx
index 5fc6f631..fbee2fd8 100644
--- a/plugin-system/src/components/PluginRegistry/PluginRegistry.tsx
+++ b/plugin-system/src/components/PluginRegistry/PluginRegistry.tsx
@@ -77,8 +77,12 @@ export function PluginRegistry(props: PluginRegistryProps): ReactElement {
if (!resource) continue;
const pluginModule = (await loadPluginModule(resource)) as Record>;
+ // Try to get the plugin implementation from the module using the versioned export first
const plugin = pluginModule?.[resourceKey];
if (plugin) return plugin as PluginImplementation;
+ // If the plugin module doesn't have a versioned export, fallback to the plugin name
+ const versionlessPlugin = pluginModule?.[name];
+ if (versionlessPlugin) return versionlessPlugin as PluginImplementation;
}
throw new Error(`A ${name} plugin for kind '${kind}' is not installed`);
diff --git a/plugin-system/src/components/index.ts b/plugin-system/src/components/index.ts
index 460fdf25..da728a4c 100644
--- a/plugin-system/src/components/index.ts
+++ b/plugin-system/src/components/index.ts
@@ -18,6 +18,7 @@ export * from './DatasourceSelect';
export * from './HTTPSettingsEditor';
export * from './ItemSelectionActionsOptionsEditor';
export * from './LegendOptionsEditor';
+export * from './LinksEditor';
export * from './MultiQueryEditor';
export * from './OptionsEditorRadios';
export * from './OptionsEditorTabs';
diff --git a/plugin-system/src/context/ValidationProvider.tsx b/plugin-system/src/context/ValidationProvider.tsx
index 13000f84..5f9a77c5 100644
--- a/plugin-system/src/context/ValidationProvider.tsx
+++ b/plugin-system/src/context/ValidationProvider.tsx
@@ -13,20 +13,19 @@
import { createContext, ReactElement, ReactNode, useContext, useState } from 'react';
import {
- PanelEditorValues,
- VariableDefinition,
- PluginSchema,
- panelEditorSchema as defaultPanelEditorSchema,
- variableDefinitionSchema,
- buildPanelEditorSchema,
- buildVariableDefinitionSchema,
AnnotationSpec,
annotationSpecSchema,
buildAnnotationSpecSchema,
+ buildVariableDefinitionSchema,
+ PluginSchema,
+ VariableDefinition,
+ variableDefinitionSchema,
} from '@perses-dev/spec';
import { z } from 'zod';
import { buildDatasourceDefinitionSchema, DatasourceDefinition, datasourceDefinitionSchema } from '@perses-dev/client';
+import { buildPanelEditorSchema, panelEditorSchema as defaultPanelEditorSchema } from '../schema';
+import { PanelEditorValues } from '../model';
export interface ValidationSchemas {
datasourceEditorSchema: z.Schema;
diff --git a/plugin-system/src/index.ts b/plugin-system/src/index.ts
index 806a3170..e38fa3d6 100644
--- a/plugin-system/src/index.ts
+++ b/plugin-system/src/index.ts
@@ -15,6 +15,7 @@ export * from './components';
export * from './constants';
export * from './model';
export * from './runtime';
+export * from './schema';
export * from './test-utils';
export * from './utils';
export * from './context';
diff --git a/plugin-system/src/model/panels.ts b/plugin-system/src/model/panels.ts
index b28eb8ec..d75e1452 100644
--- a/plugin-system/src/model/panels.ts
+++ b/plugin-system/src/model/panels.ts
@@ -12,7 +12,7 @@
// limitations under the License.
import React from 'react';
-import { UnknownSpec, PanelDefinition, QueryPluginType, QueryDataType, QueryDefinition } from '@perses-dev/spec';
+import { PanelDefinition, QueryDataType, QueryDefinition, QueryPluginType, UnknownSpec } from '@perses-dev/spec';
import { OptionsEditorTab } from '../components';
import { QueryOptions } from '../runtime';
import { OptionsEditorProps, Plugin } from './plugin-base';
@@ -79,3 +79,13 @@ export interface PanelData {
definition: QueryDefinition;
data: SupportedQueryTypes;
}
+
+export type PanelGroupId = number;
+
+/**
+ * Panel values that can be edited in the panel editor.
+ */
+export interface PanelEditorValues {
+ groupId: PanelGroupId;
+ panelDefinition: PanelDefinition;
+}
diff --git a/plugin-system/src/remote/PluginRuntime.tsx b/plugin-system/src/remote/PluginRuntime.tsx
index 67e24385..cd1b0a11 100644
--- a/plugin-system/src/remote/PluginRuntime.tsx
+++ b/plugin-system/src/remote/PluginRuntime.tsx
@@ -91,11 +91,11 @@ const getPluginRuntime = (): ModuleFederation => {
},
},
'@perses-dev/spec': {
- version: '0.2.0-beta.2',
+ version: '0.2.0-rc.0',
lib: () => require('@perses-dev/spec'),
shareConfig: {
singleton: true,
- requiredVersion: '^0.2.0-beta.2',
+ requiredVersion: '^0.2.0-rc.0',
},
},
'@perses-dev/core': {
@@ -107,43 +107,43 @@ const getPluginRuntime = (): ModuleFederation => {
},
},
'@perses-dev/client': {
- version: '0.54.0-beta.1',
+ version: '0.54.0-rc.1',
lib: () => require('@perses-dev/client'),
shareConfig: {
singleton: true,
- requiredVersion: '^0.54.0-beta.1',
+ requiredVersion: '^0.54.0-rc.1',
},
},
'@perses-dev/components': {
- version: '0.53.1',
+ version: '0.54.0-rc.1',
lib: () => require('@perses-dev/components'),
shareConfig: {
singleton: true,
- requiredVersion: '^0.53.1',
+ requiredVersion: '^0.54.0-rc.1',
},
},
'@perses-dev/plugin-system': {
- version: '0.53.1',
+ version: '0.54.0-rc.1',
lib: () => require('@perses-dev/plugin-system'),
shareConfig: {
singleton: true,
- requiredVersion: '^0.53.1',
+ requiredVersion: '^0.54.0-rc.1',
},
},
'@perses-dev/explore': {
- version: '0.53.1',
+ version: '0.54.0-rc.1',
lib: () => require('@perses-dev/explore'),
shareConfig: {
singleton: true,
- requiredVersion: '^0.53.1',
+ requiredVersion: '^0.54.0-rc.1',
},
},
'@perses-dev/dashboards': {
- version: '0.53.1',
+ version: '0.54.0-rc.1',
lib: () => require('@perses-dev/dashboards'),
shareConfig: {
singleton: true,
- requiredVersion: '^0.53.1',
+ requiredVersion: '^0.54.0-rc.1',
},
},
// Below are the shared modules that are used by the plugins, this can be part of the SDK
diff --git a/plugin-system/src/remote/remotePluginLoader.test.ts b/plugin-system/src/remote/remotePluginLoader.test.ts
index 72f4972c..6338fedc 100644
--- a/plugin-system/src/remote/remotePluginLoader.test.ts
+++ b/plugin-system/src/remote/remotePluginLoader.test.ts
@@ -119,6 +119,19 @@ describe('remotePluginLoader', () => {
expect(result).toEqual([]);
expect(mockConsoleError).toHaveBeenCalledWith('RemotePluginLoader: No valid plugins found');
});
+
+ it('should use custom fetchFn when provided', async () => {
+ const customFetch = jest.fn().mockResolvedValue({
+ json: jest.fn().mockResolvedValue([MOCK_VALID_PLUGIN_MODULE_RESOURCE]),
+ });
+
+ const loader = remotePluginLoader({ fetchFn: customFetch });
+ const result = await loader.getInstalledPlugins();
+
+ expect(customFetch).toHaveBeenCalledWith('/api/v1/plugins');
+ expect(mockFetch).not.toHaveBeenCalled();
+ expect(result).toEqual([MOCK_VALID_PLUGIN_MODULE_RESOURCE]);
+ });
});
describe('importPluginModule', () => {
diff --git a/plugin-system/src/remote/remotePluginLoader.ts b/plugin-system/src/remote/remotePluginLoader.ts
index 752e47a7..a933582a 100644
--- a/plugin-system/src/remote/remotePluginLoader.ts
+++ b/plugin-system/src/remote/remotePluginLoader.ts
@@ -18,6 +18,7 @@ import {
PluginType,
getPluginModuleCompoundKey,
} from '@perses-dev/plugin-system';
+import { fetch as defaultFetch, type FetchFn } from '@perses-dev/client';
import { RemotePluginModule } from './PersesPlugin.types';
import { loadPlugin } from './PluginRuntime';
@@ -58,6 +59,10 @@ type RemotePluginLoaderOptions = {
* @default ''
**/
baseURL?: string;
+ /**
+ * Optional custom fetch function to use for network requests. If not provided, the default fetch implementation will be used.
+ */
+ fetchFn?: FetchFn;
};
type ParsedPluginOptions = {
@@ -88,10 +93,11 @@ const paramToOptions = (options?: RemotePluginLoaderOptions): ParsedPluginOption
*/
export function remotePluginLoader(options?: RemotePluginLoaderOptions): PluginLoader {
const { pluginsApiPath, pluginsAssetsPath } = paramToOptions(options);
+ const fetchFn = options?.fetchFn ?? defaultFetch;
return {
getInstalledPlugins: async (): Promise => {
- const pluginsResponse = await fetch(pluginsApiPath);
+ const pluginsResponse = await fetchFn(pluginsApiPath);
const plugins = await pluginsResponse.json();
let pluginModules: PluginModuleResource[] = [];
diff --git a/plugin-system/src/runtime/UsageMetricsProvider.tsx b/plugin-system/src/runtime/UsageMetricsProvider.tsx
index cf26cd2e..0eb38cd6 100644
--- a/plugin-system/src/runtime/UsageMetricsProvider.tsx
+++ b/plugin-system/src/runtime/UsageMetricsProvider.tsx
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { fetch } from '@perses-dev/client';
+import { FetchFn, useFetch } from '@perses-dev/client';
import { QueryDefinition } from '@perses-dev/spec';
import { createContext, ReactElement, ReactNode, useContext } from 'react';
@@ -25,6 +25,7 @@ interface UsageMetrics {
renderErrorCount: number;
pendingQueries: Map;
apiPrefix?: string;
+ fetchFn: FetchFn;
}
interface UsageMetricsProps {
@@ -76,7 +77,7 @@ export const useUsageMetrics = (): UseUsageMetricsResults => {
};
const submitMetrics = async (stats: UsageMetrics): Promise => {
- await fetch(`${stats.apiPrefix ?? ''}/api/v1/view`, {
+ await stats.fetchFn(`${stats.apiPrefix ?? ''}/api/v1/view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -91,6 +92,8 @@ const submitMetrics = async (stats: UsageMetrics): Promise => {
};
export const UsageMetricsProvider = ({ apiPrefix, project, dashboard, children }: UsageMetricsProps): ReactElement => {
+ const { fetch } = useFetch();
+
const ctx: UsageMetrics = {
project: project,
dashboard: dashboard,
@@ -99,6 +102,7 @@ export const UsageMetricsProvider = ({ apiPrefix, project, dashboard, children }
renderDurationMs: 0,
pendingQueries: new Map(),
apiPrefix,
+ fetchFn: fetch,
};
return {children};
diff --git a/plugin-system/src/runtime/item-actions.ts b/plugin-system/src/runtime/item-actions.ts
index fb8d7313..97154082 100644
--- a/plugin-system/src/runtime/item-actions.ts
+++ b/plugin-system/src/runtime/item-actions.ts
@@ -18,7 +18,7 @@ import {
SelectionItem,
VariableStateMap,
} from '@perses-dev/components';
-import { fetch } from '@perses-dev/client';
+import { type FetchFn } from '@perses-dev/client';
import { ItemAction, EventAction, WebhookAction } from '../components/ItemSelectionActionsOptionsEditor';
const BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);
@@ -49,6 +49,8 @@ export interface ExecuteActionParams {
variableState?: VariableStateMap;
/** Callback to update action status */
setActionStatus: (actionName: string, status: Partial, itemId?: Id) => void;
+ /** Fetch function from FetchProvider context */
+ fetchFn: FetchFn;
}
/**
@@ -163,7 +165,8 @@ async function executeWebhookIndividual(
action: WebhookAction,
selectionMap: Map,
variableState: VariableStateMap | undefined,
- setActionStatus: ExecuteActionParams['setActionStatus']
+ setActionStatus: ExecuteActionParams['setActionStatus'],
+ fetchFn: FetchFn
): Promise {
const entries = Array.from(selectionMap.entries());
const count = entries.length;
@@ -192,7 +195,7 @@ async function executeWebhookIndividual(
}
// Make the request
- const response = await fetch(urlResult.text, {
+ const response = await fetchFn(urlResult.text, {
method: action.method,
headers: buildWebhookHeaders(action),
body: body,
@@ -237,7 +240,8 @@ async function executeWebhookBatch(
action: WebhookAction,
selectionMap: Map,
variableState: VariableStateMap | undefined,
- setActionStatus: ExecuteActionParams['setActionStatus']
+ setActionStatus: ExecuteActionParams['setActionStatus'],
+ fetchFn: FetchFn
): Promise {
const items = Array.from(selectionMap.values());
@@ -257,7 +261,7 @@ async function executeWebhookBatch(
}
// Make the request
- const response = await fetch(urlResult.text, {
+ const response = await fetchFn(urlResult.text, {
method: action.method,
headers: buildWebhookHeaders(action),
body: body,
@@ -283,12 +287,13 @@ async function executeWebhookAction(
action: WebhookAction,
selectionMap: Map,
variableState: VariableStateMap | undefined,
- setActionStatus: ExecuteActionParams['setActionStatus']
+ setActionStatus: ExecuteActionParams['setActionStatus'],
+ fetchFn: FetchFn
): Promise {
if (action.batchMode === 'batch') {
- return executeWebhookBatch(action, selectionMap, variableState, setActionStatus);
+ return executeWebhookBatch(action, selectionMap, variableState, setActionStatus, fetchFn);
} else {
- return executeWebhookIndividual(action, selectionMap, variableState, setActionStatus);
+ return executeWebhookIndividual(action, selectionMap, variableState, setActionStatus, fetchFn);
}
}
@@ -315,7 +320,7 @@ async function executeEventAction(
* @returns Promise resolving to the execution result
*/
export async function executeAction(params: ExecuteActionParams): Promise {
- const { action, selectionMap, variableState, setActionStatus } = params;
+ const { action, selectionMap, variableState, setActionStatus, fetchFn } = params;
if (selectionMap.size === 0) {
return { success: true };
@@ -324,7 +329,7 @@ export async function executeAction(params: ExecuteActionParams = z.object({
+ groupId: z.number(),
+ panelDefinition: panelDefinitionSchema,
+});
+
+export function buildPanelEditorSchema(pluginSchema: PluginSchema): z.ZodSchema {
+ return z.object({
+ groupId: z.number(),
+ panelDefinition: buildPanelDefinitionSchema(pluginSchema),
+ });
+}