DashboardScene: Rerender dashboard links on timerange change (#94570)

* fix

* refactor

* refactor
This commit is contained in:
Victor Marin 2024-10-15 12:39:36 +03:00 committed by GitHub
parent ef805f271e
commit aaba5a43bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 57 deletions

View File

@ -1,36 +1,22 @@
import { act, render } from '@testing-library/react';
import { render } from '@testing-library/react';
import { toUtc } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { SceneDataLayerControls, SceneVariableSet, TextBoxVariable, VariableValueSelectors } from '@grafana/scenes';
import { DashboardControls, DashboardControlsState } from './DashboardControls';
import { DashboardScene } from './DashboardScene';
const mockGetAnchorInfo = jest.fn((link) => ({
href: `/dashboard/${link.title}`,
title: link.title,
tooltip: link.tooltip || null,
}));
// Mock the getLinkSrv function
jest.mock('app/features/panel/panellinks/link_srv', () => ({
getLinkSrv: jest.fn(() => ({
getAnchorInfo: mockGetAnchorInfo,
})),
}));
describe('DashboardControls', () => {
describe('Given a standard scene', () => {
it('should initialize with default values', () => {
const { controls: scene } = buildTestScene();
const scene = buildTestScene();
expect(scene.state.variableControls).toEqual([]);
expect(scene.state.timePicker).toBeDefined();
expect(scene.state.refreshPicker).toBeDefined();
});
it('should return if time controls are hidden', () => {
const { controls: scene } = buildTestScene({
const scene = buildTestScene({
hideTimeControls: false,
hideVariableControls: false,
hideLinksControls: false,
@ -45,14 +31,14 @@ describe('DashboardControls', () => {
describe('Component', () => {
it('should render', () => {
const { controls: scene } = buildTestScene();
const scene = buildTestScene();
expect(() => {
render(<scene.Component model={scene} />);
}).not.toThrow();
});
it('should render visible controls', async () => {
const { controls: scene } = buildTestScene({
const scene = buildTestScene({
variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()],
});
const renderer = render(<scene.Component model={scene} />);
@ -65,7 +51,7 @@ describe('DashboardControls', () => {
});
it('should render with hidden controls', async () => {
const { controls: scene } = buildTestScene({
const scene = buildTestScene({
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
@ -79,13 +65,13 @@ describe('DashboardControls', () => {
describe('UrlSync', () => {
it('should return keys', () => {
const { controls: scene } = buildTestScene();
const scene = buildTestScene();
// @ts-expect-error
expect(scene._urlSync.getKeys()).toEqual(['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks']);
});
it('should not return url state for hide flags', () => {
const { controls: scene } = buildTestScene();
const scene = buildTestScene();
expect(scene.getUrlState()).toEqual({});
scene.setState({
hideTimeControls: true,
@ -96,7 +82,7 @@ describe('DashboardControls', () => {
});
it('should update from url', () => {
const { controls: scene } = buildTestScene();
const scene = buildTestScene();
scene.updateFromUrl({
'_dash.hideTimePicker': 'true',
'_dash.hideVariables': 'true',
@ -116,7 +102,7 @@ describe('DashboardControls', () => {
});
it('should not override state if no new state comes from url', () => {
const { controls: scene } = buildTestScene({
const scene = buildTestScene({
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
@ -128,7 +114,7 @@ describe('DashboardControls', () => {
});
it('should not call setState if no changes', () => {
const { controls: scene } = buildTestScene({
const scene = buildTestScene({
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
@ -144,34 +130,9 @@ describe('DashboardControls', () => {
expect(setState).toHaveBeenCalledTimes(0);
});
});
it('Should update link hrefs when time range changes', () => {
const { controls, dashboard } = buildTestScene();
render(<controls.Component model={controls} />);
//clear initial calls to getAnchorInfo
mockGetAnchorInfo.mockClear();
act(() => {
// Update time range
dashboard.state.$timeRange?.setState({
value: {
from: toUtc('2021-01-01'),
to: toUtc('2021-01-02'),
raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') },
},
});
});
//expect getAnchorInfo to be called after time range change
expect(mockGetAnchorInfo).toHaveBeenCalledTimes(1);
});
});
function buildTestScene(state?: Partial<DashboardControlsState>): {
dashboard: DashboardScene;
controls: DashboardControls;
} {
function buildTestScene(state?: Partial<DashboardControlsState>): DashboardControls {
const variable = new TextBoxVariable({
name: 'A',
label: 'A',
@ -206,5 +167,5 @@ function buildTestScene(state?: Partial<DashboardControlsState>): {
dashboard.activate();
variable.activate();
return { dashboard, controls: dashboard.state.controls as DashboardControls };
return dashboard.state.controls as DashboardControls;
}

View File

@ -122,10 +122,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
const { variableControls, refreshPicker, timePicker, hideTimeControls, hideVariableControls, hideLinksControls } =
model.useState();
const dashboard = getDashboardSceneFor(model);
const { links, editPanel, $timeRange } = dashboard.useState();
const { links, editPanel } = dashboard.useState();
const styles = useStyles2(getStyles);
const showDebugger = location.search.includes('scene-debugger');
$timeRange!.useState();
if (!model.hasControls()) {
return null;
@ -136,7 +135,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
<Stack grow={1} wrap={'wrap'}>
{!hideVariableControls && variableControls.map((c) => <c.Component model={c} key={c.state.key} />)}
<Box grow={1} />
{!hideLinksControls && !editPanel && <DashboardLinksControls links={links} uid={dashboard.state.uid} />}
{!hideLinksControls && !editPanel && <DashboardLinksControls links={links} dashboard={dashboard} />}
{editPanel && <PanelEditControls panelEditor={editPanel} />}
</Stack>
{!hideTimeControls && (

View File

@ -0,0 +1,100 @@
import { act, render } from '@testing-library/react';
import { toUtc } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { SceneTimeRange } from '@grafana/scenes';
import { DashboardControls } from './DashboardControls';
import { DashboardScene } from './DashboardScene';
const mockGetAnchorInfo = jest.fn((link) => ({
href: `/dashboard/${link.title}`,
title: link.title,
tooltip: link.tooltip || null,
}));
// Mock the getLinkSrv function
jest.mock('app/features/panel/panellinks/link_srv', () => ({
getLinkSrv: jest.fn(() => ({
getAnchorInfo: mockGetAnchorInfo,
})),
}));
describe('DashboardLinksControls', () => {
it('renders dashboard links correctly', () => {
const { controls } = buildTestScene();
const renderer = render(<controls.Component model={controls} />);
// // Expect two dashboard link containers to be rendered
const linkContainers = renderer.getAllByTestId(selectors.components.DashboardLinks.container);
expect(linkContainers).toHaveLength(2);
// Check link titles and hrefs
const links = renderer.getAllByTestId(selectors.components.DashboardLinks.link);
expect(links[0]).toHaveTextContent('Link 1');
expect(links[1]).toHaveTextContent('Link 2');
});
it('updates link hrefs when time range changes', () => {
const { controls, dashboard } = buildTestScene();
render(<controls.Component model={controls} />);
//clear initial calls to getAnchorInfo
mockGetAnchorInfo.mockClear();
act(() => {
// Update time range
dashboard.state.$timeRange?.setState({
value: {
from: toUtc('2021-01-01'),
to: toUtc('2021-01-02'),
raw: { from: toUtc('2020-01-01'), to: toUtc('2020-01-02') },
},
});
});
//expect getAnchorInfo to be called twice, once for each link, after time range change
expect(mockGetAnchorInfo).toHaveBeenCalledTimes(2);
});
});
function buildTestScene(): { controls: DashboardControls; dashboard: DashboardScene } {
const dashboard = new DashboardScene({
uid: 'A',
links: [
{
title: 'Link 1',
url: 'http://localhost:3000/$A',
type: 'link',
asDropdown: false,
icon: '',
includeVars: true,
keepTime: true,
tags: [],
targetBlank: false,
tooltip: 'Link 1',
},
{
title: 'Link 2',
url: 'http://localhost:3000/$A',
type: 'link',
asDropdown: false,
icon: '',
includeVars: true,
keepTime: true,
tags: [],
targetBlank: false,
tooltip: 'Link 2',
},
],
controls: new DashboardControls({}),
$timeRange: new SceneTimeRange({
from: 'now-1',
to: 'now',
}),
});
dashboard.activate();
return { controls: dashboard.state.controls as DashboardControls, dashboard };
}

View File

@ -1,5 +1,6 @@
import { sanitizeUrl } from '@grafana/data/src/text/sanitize';
import { selectors } from '@grafana/e2e-selectors';
import { sceneGraph } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema';
import { Tooltip } from '@grafana/ui';
import {
@ -10,12 +11,17 @@ import { getLinkSrv } from 'app/features/panel/panellinks/link_srv';
import { LINK_ICON_MAP } from '../settings/links/utils';
import { DashboardScene } from './DashboardScene';
export interface Props {
links: DashboardLink[];
uid?: string;
dashboard: DashboardScene;
}
export function DashboardLinksControls({ links, uid }: Props) {
export function DashboardLinksControls({ links, dashboard }: Props) {
sceneGraph.getTimeRange(dashboard).useState();
const uid = dashboard.state.uid;
if (!links || !uid) {
return null;
}