From c39755a8a1b9f0ae82460641719959b7f13eb059 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Fri, 24 Jul 2026 18:06:38 +0100 Subject: [PATCH] [FEATURE] Add usePanelAnnotationsWithData hook for panel-level annotations Add a panel-scoped consumer hook in @perses-dev/dashboards. It returns the dashboard-level annotations from the store unless the panel toggle disables them (annotations.enabled === false; a nil or true value keeps them on), and merges in the panel-local definitions resolved through the existing useAnnotations runtime hook. The merge is a plain concatenation of both sets. Reuses AnnotationSpecWithData and the AnnotationProvider store; no fetching logic is duplicated. Includes unit tests for the toggle and merge behavior. Depends on the panel-level annotations data model in perses/spec#61. Signed-off-by: Nicolas Takashi --- .../src/context/AnnotationProvider/index.ts | 1 + .../usePanelAnnotationsWithData.test.tsx | 71 +++++++++++++++++++ .../usePanelAnnotationsWithData.tsx | 47 ++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.test.tsx create mode 100644 dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.tsx diff --git a/dashboards/src/context/AnnotationProvider/index.ts b/dashboards/src/context/AnnotationProvider/index.ts index a79b8ba7..d92447bf 100644 --- a/dashboards/src/context/AnnotationProvider/index.ts +++ b/dashboards/src/context/AnnotationProvider/index.ts @@ -12,3 +12,4 @@ // limitations under the License. export * from './AnnotationProvider'; +export * from './usePanelAnnotationsWithData'; diff --git a/dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.test.tsx b/dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.test.tsx new file mode 100644 index 00000000..073cc80d --- /dev/null +++ b/dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.test.tsx @@ -0,0 +1,71 @@ +// 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 { ReactElement, ReactNode } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { AnnotationSpec, PanelAnnotations } from '@perses-dev/spec'; +import { AnnotationProvider, usePanelAnnotationsWithData } from '@perses-dev/dashboards'; + +// Resolve every annotation definition to a single data point derived from its name, so both the +// dashboard hydration and the panel-local resolution go through the same predictable stub. +jest.mock('@perses-dev/plugin-system', () => { + const actual = jest.requireActual('@perses-dev/plugin-system'); + return { + ...actual, + useAnnotations: (definitions: AnnotationSpec[]) => + definitions.map((d) => ({ data: [{ start: 1, title: d.display.name }] })), + }; +}); + +const dashboardDefinition: AnnotationSpec = { + display: { name: 'Deploys' }, + plugin: { kind: 'FirstAnnotation', spec: {} }, +}; + +const panelDefinition: AnnotationSpec = { + display: { name: 'Incidents' }, + plugin: { kind: 'FirstAnnotation', spec: {} }, +}; + +function wrapper({ children }: { children: ReactNode }): ReactElement { + return {children}; +} + +function renderPanelHook(panelAnnotations?: PanelAnnotations): { current: string[] } { + const { result } = renderHook(() => usePanelAnnotationsWithData(panelAnnotations).map((a) => a.definition.display.name), { + wrapper, + }); + return result; +} + +describe('usePanelAnnotationsWithData', () => { + it('returns dashboard annotations when the panel has no annotations block', async () => { + const result = renderPanelHook(undefined); + await waitFor(() => expect(result.current).toEqual(['Deploys'])); + }); + + it('suppresses dashboard annotations when the toggle is disabled', async () => { + const result = renderPanelHook({ enabled: false }); + await waitFor(() => expect(result.current).toEqual([])); + }); + + it('merges dashboard annotations with panel-local definitions when the toggle is on', async () => { + const result = renderPanelHook({ definitions: [panelDefinition] }); + await waitFor(() => expect(result.current).toEqual(['Deploys', 'Incidents'])); + }); + + it('keeps only panel-local definitions when the toggle is disabled', async () => { + const result = renderPanelHook({ enabled: false, definitions: [panelDefinition] }); + await waitFor(() => expect(result.current).toEqual(['Incidents'])); + }); +}); diff --git a/dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.tsx b/dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.tsx new file mode 100644 index 00000000..e3c3411b --- /dev/null +++ b/dashboards/src/context/AnnotationProvider/usePanelAnnotationsWithData.tsx @@ -0,0 +1,47 @@ +// 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 { useMemo } from 'react'; +import { PanelAnnotations } from '@perses-dev/spec'; +import { useAnnotations } from '@perses-dev/plugin-system'; +import { AnnotationSpecWithData, useAnnotationsWithData } from './AnnotationProvider'; + +/** + * Returns the annotations to display on a single panel: + * - the dashboard-level annotations from the store, unless the panel toggle turns them off + * (annotations.enabled === false; a nil or true value keeps them on, matching the default-on + * behavior of the two-level annotation design) + * - the panel-local annotation definitions, resolved on the fly through the same runtime hook that + * hydrates dashboard annotations + */ +export function usePanelAnnotationsWithData(panelAnnotations?: PanelAnnotations): AnnotationSpecWithData[] { + const dashboardAnnotations = useAnnotationsWithData(); + const dashboardEnabled = panelAnnotations?.enabled !== false; + + const localDefinitions = useMemo(() => panelAnnotations?.definitions ?? [], [panelAnnotations?.definitions]); + const localResults = useAnnotations(localDefinitions); + + return useMemo(() => { + const result: AnnotationSpecWithData[] = []; + if (dashboardEnabled) { + result.push(...dashboardAnnotations); + } + localDefinitions.forEach((definition, index) => { + const data = localResults[index]?.data; + if (data) { + result.push({ definition, data }); + } + }); + return result; + }, [dashboardEnabled, dashboardAnnotations, localDefinitions, localResults]); +}