Updated POC to original funcitonal component

This commit is contained in:
Collin Fingar
2026-01-06 16:42:44 -05:00
parent 6fc7627bec
commit d133e1dce9
2 changed files with 268 additions and 33 deletions
@@ -0,0 +1,232 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data';
import { usePluginLinks } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { DrilldownExtensionPoint } from './DrilldownExtensionPoint';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
usePluginLinks: jest.fn(),
}));
jest.mock('@grafana/data', () => ({
...jest.requireActual('@grafana/data'),
getDefaultTimeRange: jest.fn(() => ({
raw: { from: 'now-1h', to: 'now' },
})),
getTimeZone: jest.fn(() => 'browser'),
locationUtil: {
assureBaseUrl: jest.fn((path: string) => `http://localhost${path}`),
},
}));
const usePluginLinksMock = jest.mocked(usePluginLinks);
const mockGlobalOpen = jest.fn();
global.open = mockGlobalOpen;
describe('DrilldownExtensionPoint', () => {
const defaultQueries: DataQuery[] = [{ refId: 'A' }];
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the button when queryless app links are available', () => {
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-pyroscope-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Explore Profiles',
description: 'Explore Profiles',
path: '/a/grafana-pyroscope-app',
},
],
isLoading: false,
});
render(<DrilldownExtensionPoint queries={defaultQueries} />);
expect(screen.getByRole('button', { name: 'Open in Drilldown' })).toBeVisible();
});
it('should open the first queryless app link when button is clicked', async () => {
const user = userEvent.setup();
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-pyroscope-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Explore Profiles',
description: 'Explore Profiles',
path: '/a/grafana-pyroscope-app',
},
],
isLoading: false,
});
render(<DrilldownExtensionPoint queries={defaultQueries} />);
await user.click(screen.getByRole('button', { name: 'Open in Drilldown' }));
expect(mockGlobalOpen).toHaveBeenCalledTimes(1);
expect(mockGlobalOpen).toHaveBeenCalledWith('http://localhost/a/grafana-pyroscope-app', '_blank');
});
it('should open the first link when multiple queryless app links are available', async () => {
const user = userEvent.setup();
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-pyroscope-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Explore Profiles',
path: '/a/grafana-pyroscope-app',
description: 'Explore Profiles',
},
{
pluginId: 'grafana-lokiexplore-app',
id: '2',
type: PluginExtensionTypes.link,
title: 'Explore Logs',
path: '/a/grafana-lokiexplore-app',
description: 'Explore Logs',
},
],
isLoading: false,
});
render(<DrilldownExtensionPoint queries={defaultQueries} />);
await user.click(screen.getByRole('button', { name: 'Open in Drilldown' }));
expect(mockGlobalOpen).toHaveBeenCalledWith('http://localhost/a/grafana-pyroscope-app', '_blank');
expect(mockGlobalOpen).toHaveBeenCalledTimes(1);
});
it('should pass correct context to usePluginLinks', () => {
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-pyroscope-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Explore Profiles',
description: 'Explore Profiles',
path: '/a/grafana-pyroscope-app',
},
],
isLoading: false,
});
render(<DrilldownExtensionPoint queries={defaultQueries} />);
expect(usePluginLinksMock).toHaveBeenCalledWith({
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
context: {
targets: defaultQueries,
timeRange: { from: 'now-1h', to: 'now' },
timeZone: 'browser',
},
limitPerPlugin: 3,
});
});
it('should not render the button when no queryless app links are available', () => {
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'other-plugin',
id: '1',
type: PluginExtensionTypes.link,
title: 'Other Extension',
path: '/a/other-plugin',
description: 'Other Extension',
},
],
isLoading: false,
});
const { container } = render(<DrilldownExtensionPoint queries={defaultQueries} />);
expect(screen.queryByRole('button', { name: 'Open in Drilldown' })).not.toBeInTheDocument();
expect(container.firstChild).toBeNull();
});
it('should not render the button when links array is empty', () => {
usePluginLinksMock.mockReturnValue({
links: [],
isLoading: false,
});
const { container } = render(<DrilldownExtensionPoint queries={defaultQueries} />);
expect(screen.queryByRole('button', { name: 'Open in Drilldown' })).not.toBeInTheDocument();
expect(container.firstChild).toBeNull();
});
it('should not call global.open when link has no path', async () => {
const user = userEvent.setup();
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-pyroscope-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Explore Profiles',
description: 'Explore Profiles',
},
],
isLoading: false,
});
render(<DrilldownExtensionPoint queries={defaultQueries} />);
await user.click(screen.getByRole('button', { name: 'Open in Drilldown' }));
expect(mockGlobalOpen).not.toHaveBeenCalled();
});
it('should update context when queries change', () => {
const queries1: DataQuery[] = [{ refId: 'A' }];
const queries2: DataQuery[] = [{ refId: 'B' }];
usePluginLinksMock.mockReturnValue({
links: [
{
pluginId: 'grafana-pyroscope-app',
id: '1',
type: PluginExtensionTypes.link,
title: 'Explore Profiles',
description: 'Explore Profiles',
path: '/a/grafana-pyroscope-app',
},
],
isLoading: false,
});
const { rerender } = render(<DrilldownExtensionPoint queries={queries1} />);
expect(usePluginLinksMock).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({
targets: queries1,
}),
})
);
rerender(<DrilldownExtensionPoint queries={queries2} />);
expect(usePluginLinksMock).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({
targets: queries2,
}),
})
);
});
});
@@ -1,8 +1,14 @@
import { useCallback, useMemo } from 'react';
import { ReactElement, useCallback, useMemo } from 'react';
import { PluginExtensionPoints, RawTimeRange, getDefaultTimeRange, getTimeZone, locationUtil } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { usePluginLinks } from '@grafana/runtime';
import { DataQuery, TimeZone } from '@grafana/schema';
import { Button } from '@grafana/ui';
type Props = {
queries: DataQuery[];
};
const QUERYLESS_APPS = [
'grafana-pyroscope-app',
@@ -11,33 +17,12 @@ const QUERYLESS_APPS = [
'grafana-metricsdrilldown-app',
];
export type PluginExtensionExploreContext = {
targets: DataQuery[];
timeRange: RawTimeRange;
timeZone: TimeZone;
};
/**
* Creates the extension point context for drilldown actions.
* @param queries - The data queries to include in the context
* @returns The extension point context
* Renders a button to open queryless drilldown apps.
* Only displays when at least one queryless app extension is available.
*/
export function createExtensionPointContext(queries: DataQuery[]): PluginExtensionExploreContext {
const range = getDefaultTimeRange();
return {
targets: queries,
timeRange: range.raw,
timeZone: getTimeZone(),
};
}
/**
* Hook that returns queryless drilldown links and handlers.
* @param queries - The data queries to use for the extension point context
* @returns An object containing the queryless links and an onClick handler, or null if no links are available
*/
export function useDrilldownExtensionPoint(queries: DataQuery[]) {
const context = useMemo(() => createExtensionPointContext(queries), [queries]);
export function DrilldownExtensionPoint(props: Props): ReactElement | null {
const context = useExtensionPointContext(props);
const { links } = usePluginLinks({
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
context: context,
@@ -46,19 +31,37 @@ export function useDrilldownExtensionPoint(queries: DataQuery[]) {
const querylessLinks = useMemo(() => links.filter((link) => QUERYLESS_APPS.includes(link.pluginId)), [links]);
const onClick = useCallback(() => {
if (!querylessLinks.length || !querylessLinks[0].path) {
const firstLink = querylessLinks[0];
if (!firstLink?.path) {
return;
}
global.open(locationUtil.assureBaseUrl(querylessLinks[0].path), '_blank');
global.open(locationUtil.assureBaseUrl(firstLink.path), '_blank');
}, [querylessLinks]);
if (!querylessLinks.length) {
return null;
}
return {
links: querylessLinks,
onClick,
primaryLink: querylessLinks[0],
};
return (
<Button variant="secondary" onClick={onClick}>
<Trans i18nKey="explore.queryless-apps-extensions.open-in-drilldown">Open in Drilldown</Trans>
</Button>
);
}
export type PluginExtensionExploreContext = {
targets: DataQuery[];
timeRange: RawTimeRange;
timeZone: TimeZone;
};
function useExtensionPointContext({ queries }: Props): PluginExtensionExploreContext {
return useMemo(() => {
const range = getDefaultTimeRange();
return {
targets: queries,
timeRange: range.raw,
timeZone: getTimeZone(),
};
}, [queries]);
}