Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4194d46ce0 | |||
| b1000e53ad | |||
| 5556e249a8 | |||
| 5038f697ba | |||
| 92a7a7d5cf | |||
| ced15ae5ff | |||
| d133e1dce9 | |||
| 6fc7627bec |
@@ -0,0 +1,233 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data';
|
||||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import { DrilldownExtensionPoint } from './DrilldownExtensionPoint';
|
||||
|
||||
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 mockGlobalOpen = jest.fn();
|
||||
global.open = mockGlobalOpen;
|
||||
|
||||
let usePluginLinksMock: jest.Mock;
|
||||
|
||||
beforeAll(() => {
|
||||
usePluginLinksMock = jest.fn().mockReturnValue({ links: [], isLoading: false });
|
||||
setPluginLinksHook(usePluginLinksMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
usePluginLinksMock.mockClear();
|
||||
usePluginLinksMock.mockReturnValue({ links: [], isLoading: false });
|
||||
mockGlobalOpen.mockClear();
|
||||
});
|
||||
|
||||
describe('DrilldownExtensionPoint', () => {
|
||||
const defaultQueries: DataQuery[] = [{ refId: 'A' }];
|
||||
|
||||
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: '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: '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: '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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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: '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: '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: '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,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
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[];
|
||||
onExtensionClick?: () => void;
|
||||
};
|
||||
|
||||
const QUERYLESS_APPS = [
|
||||
'grafana-pyroscope-app',
|
||||
'grafana-lokiexplore-app',
|
||||
'grafana-exploretraces-app',
|
||||
'grafana-metricsdrilldown-app',
|
||||
];
|
||||
|
||||
/**
|
||||
* Renders a button to open queryless drilldown apps.
|
||||
* Only displays when at least one queryless app extension is available.
|
||||
*/
|
||||
export function DrilldownExtensionPoint(props: Props): ReactElement | null {
|
||||
const { onExtensionClick } = props;
|
||||
const context = useExtensionPointContext(props);
|
||||
const { links } = usePluginLinks({
|
||||
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
|
||||
context: context,
|
||||
});
|
||||
const querylessLinks = useMemo(() => links.filter((link) => QUERYLESS_APPS.includes(link.pluginId)), [links]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
onExtensionClick?.();
|
||||
const firstLink = querylessLinks[0];
|
||||
if (!firstLink?.path) {
|
||||
return;
|
||||
}
|
||||
global.open(locationUtil.assureBaseUrl(firstLink.path), '_blank');
|
||||
}, [querylessLinks, onExtensionClick]);
|
||||
|
||||
if (!querylessLinks.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="secondary" onClick={onClick}>
|
||||
<Trans i18nKey="explore.queryless-apps-extensions.drilldown">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]);
|
||||
}
|
||||
@@ -7518,7 +7518,8 @@
|
||||
"loading-placeholder": "Loading..."
|
||||
},
|
||||
"queryless-apps-extensions": {
|
||||
"aria-label-go-queryless": "Go queryless"
|
||||
"aria-label-go-queryless": "Go queryless",
|
||||
"drilldown": "Drilldown"
|
||||
},
|
||||
"raw-list-container": {
|
||||
"item-count": "Result series: {{numItems}}",
|
||||
|
||||
Reference in New Issue
Block a user