Updated POC to original funcitonal component
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user