mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Template variables are now correctly persisted when clicking breadcrumb links (#46790)
* Add history listener to update titleHref/parentHref when location changes * Convert to functional component and use useLocation * Wrap component in React.memo * Add new `getUrlForPartial` method, deprecate `updateSearchParams`
This commit is contained in:
parent
0e682397ab
commit
c13f4542d8
@ -53,6 +53,7 @@
|
|||||||
"@testing-library/react-hooks": "7.0.2",
|
"@testing-library/react-hooks": "7.0.2",
|
||||||
"@testing-library/user-event": "13.5.0",
|
"@testing-library/user-event": "13.5.0",
|
||||||
"@types/braintree__sanitize-url": "4.1.0",
|
"@types/braintree__sanitize-url": "4.1.0",
|
||||||
|
"@types/history": "4.7.11",
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "27.4.1",
|
||||||
"@types/jquery": "3.5.14",
|
"@types/jquery": "3.5.14",
|
||||||
"@types/lodash": "4.14.149",
|
"@types/lodash": "4.14.149",
|
||||||
@ -65,6 +66,7 @@
|
|||||||
"@types/testing-library__jest-dom": "5.14.3",
|
"@types/testing-library__jest-dom": "5.14.3",
|
||||||
"@types/testing-library__react-hooks": "^3.2.0",
|
"@types/testing-library__react-hooks": "^3.2.0",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
|
"history": "4.10.1",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"rollup": "2.70.1",
|
"rollup": "2.70.1",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Location } from 'history';
|
||||||
import { GrafanaConfig, RawTimeRange, ScopedVars } from '../types';
|
import { GrafanaConfig, RawTimeRange, ScopedVars } from '../types';
|
||||||
import { UrlQueryMap, urlUtil } from './url';
|
import { UrlQueryMap, urlUtil } from './url';
|
||||||
import { textUtil } from '../text';
|
import { textUtil } from '../text';
|
||||||
@ -37,6 +38,28 @@ const assureBaseUrl = (url: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
|
* @param location
|
||||||
|
* @param searchParamsToUpdate
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getUrlForPartial = (location: Location<any>, searchParamsToUpdate: Record<string, any>) => {
|
||||||
|
const searchParams = urlUtil.parseKeyValue(
|
||||||
|
location.search.startsWith('?') ? location.search.substring(1) : location.search
|
||||||
|
);
|
||||||
|
for (const key of Object.keys(searchParamsToUpdate)) {
|
||||||
|
// removing params with null | undefined
|
||||||
|
if (searchParamsToUpdate[key] === null || searchParamsToUpdate[key] === undefined) {
|
||||||
|
delete searchParams[key];
|
||||||
|
} else {
|
||||||
|
searchParams[key] = searchParamsToUpdate[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlUtil.renderUrl(location.pathname, searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `getUrlForPartial` instead
|
||||||
* Update URL or search param string `init` with new params `partial`.
|
* Update URL or search param string `init` with new params `partial`.
|
||||||
*/
|
*/
|
||||||
const updateSearchParams = (init: string, partial: string) => {
|
const updateSearchParams = (init: string, partial: string) => {
|
||||||
@ -92,6 +115,7 @@ export const locationUtil = {
|
|||||||
const params = getVariablesUrlParams(scopedVars);
|
const params = getVariablesUrlParams(scopedVars);
|
||||||
return urlUtil.toUrlParams(params);
|
return urlUtil.toUrlParams(params);
|
||||||
},
|
},
|
||||||
|
getUrlForPartial,
|
||||||
processUrl: (url: string) => {
|
processUrl: (url: string) => {
|
||||||
return grafanaConfig.disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
return grafanaConfig.disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
// Libaries
|
// Libaries
|
||||||
import React, { PureComponent, FC, ReactNode } from 'react';
|
import React, { FC, ReactNode } from 'react';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||||
// Components
|
// Components
|
||||||
import { DashNavButton } from './DashNavButton';
|
import { DashNavButton } from './DashNavButton';
|
||||||
import { DashNavTimeControls } from './DashNavTimeControls';
|
import { DashNavTimeControls } from './DashNavTimeControls';
|
||||||
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar } from '@grafana/ui';
|
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar, useForceUpdate } from '@grafana/ui';
|
||||||
import { locationUtil, textUtil } from '@grafana/data';
|
import { locationUtil, textUtil } from '@grafana/data';
|
||||||
// State
|
// State
|
||||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||||
@ -56,64 +57,62 @@ export function addCustomRightAction(content: DashNavButtonModel) {
|
|||||||
|
|
||||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
class DashNav extends PureComponent<Props> {
|
export const DashNav = React.memo<Props>((props) => {
|
||||||
constructor(props: Props) {
|
const forceUpdate = useForceUpdate();
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
const onStarDashboard = () => {
|
||||||
locationService.partial({ viewPanel: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
onToggleTVMode = () => {
|
|
||||||
toggleKioskMode();
|
|
||||||
};
|
|
||||||
|
|
||||||
onOpenSettings = () => {
|
|
||||||
locationService.partial({ editview: 'settings' });
|
|
||||||
};
|
|
||||||
|
|
||||||
onStarDashboard = () => {
|
|
||||||
const { dashboard } = this.props;
|
|
||||||
const dashboardSrv = getDashboardSrv();
|
const dashboardSrv = getDashboardSrv();
|
||||||
|
const { dashboard } = props;
|
||||||
|
|
||||||
dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then((newState: any) => {
|
dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then((newState: any) => {
|
||||||
dashboard.meta.isStarred = newState;
|
dashboard.meta.isStarred = newState;
|
||||||
this.forceUpdate();
|
forceUpdate();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPlaylistPrev = () => {
|
const onClose = () => {
|
||||||
|
locationService.partial({ viewPanel: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleTVMode = () => {
|
||||||
|
toggleKioskMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenSettings = () => {
|
||||||
|
locationService.partial({ editview: 'settings' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPlaylistPrev = () => {
|
||||||
playlistSrv.prev();
|
playlistSrv.prev();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPlaylistNext = () => {
|
const onPlaylistNext = () => {
|
||||||
playlistSrv.next();
|
playlistSrv.next();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPlaylistStop = () => {
|
const onPlaylistStop = () => {
|
||||||
playlistSrv.stop();
|
playlistSrv.stop();
|
||||||
this.forceUpdate();
|
forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
addCustomContent(actions: DashNavButtonModel[], buttons: ReactNode[]) {
|
const addCustomContent = (actions: DashNavButtonModel[], buttons: ReactNode[]) => {
|
||||||
actions.map((action, index) => {
|
actions.map((action, index) => {
|
||||||
const Component = action.component;
|
const Component = action.component;
|
||||||
const element = <Component {...this.props} key={`button-custom-${index}`} />;
|
const element = <Component {...props} key={`button-custom-${index}`} />;
|
||||||
typeof action.index === 'number' ? buttons.splice(action.index, 0, element) : buttons.push(element);
|
typeof action.index === 'number' ? buttons.splice(action.index, 0, element) : buttons.push(element);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
isPlaylistRunning() {
|
const isPlaylistRunning = () => {
|
||||||
return playlistSrv.isPlaying;
|
return playlistSrv.isPlaying;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderLeftActionsButton() {
|
const renderLeftActionsButton = () => {
|
||||||
const { dashboard, kioskMode } = this.props;
|
const { dashboard, kioskMode } = props;
|
||||||
const { canStar, canShare, isStarred } = dashboard.meta;
|
const { canStar, canShare, isStarred } = dashboard.meta;
|
||||||
const buttons: ReactNode[] = [];
|
const buttons: ReactNode[] = [];
|
||||||
|
|
||||||
if (kioskMode !== KioskMode.Off || this.isPlaylistRunning()) {
|
if (kioskMode !== KioskMode.Off || isPlaylistRunning()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +124,7 @@ class DashNav extends PureComponent<Props> {
|
|||||||
icon={isStarred ? 'favorite' : 'star'}
|
icon={isStarred ? 'favorite' : 'star'}
|
||||||
iconType={isStarred ? 'mono' : 'default'}
|
iconType={isStarred ? 'mono' : 'default'}
|
||||||
iconSize="lg"
|
iconSize="lg"
|
||||||
onClick={this.onStarDashboard}
|
onClick={onStarDashboard}
|
||||||
key="button-star"
|
key="button-star"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -172,22 +171,22 @@ class DashNav extends PureComponent<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCustomContent(customLeftActions, buttons);
|
addCustomContent(customLeftActions, buttons);
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderPlaylistControls() {
|
const renderPlaylistControls = () => {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup key="playlist-buttons">
|
<ButtonGroup key="playlist-buttons">
|
||||||
<ToolbarButton tooltip="Go to previous dashboard" icon="backward" onClick={this.onPlaylistPrev} narrow />
|
<ToolbarButton tooltip="Go to previous dashboard" icon="backward" onClick={onPlaylistPrev} narrow />
|
||||||
<ToolbarButton onClick={this.onPlaylistStop}>Stop playlist</ToolbarButton>
|
<ToolbarButton onClick={onPlaylistStop}>Stop playlist</ToolbarButton>
|
||||||
<ToolbarButton tooltip="Go to next dashboard" icon="forward" onClick={this.onPlaylistNext} narrow />
|
<ToolbarButton tooltip="Go to next dashboard" icon="forward" onClick={onPlaylistNext} narrow />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderTimeControls() {
|
const renderTimeControls = () => {
|
||||||
const { dashboard, updateTimeZoneForSession, hideTimePicker } = this.props;
|
const { dashboard, updateTimeZoneForSession, hideTimePicker } = props;
|
||||||
|
|
||||||
if (hideTimePicker) {
|
if (hideTimePicker) {
|
||||||
return null;
|
return null;
|
||||||
@ -196,24 +195,24 @@ class DashNav extends PureComponent<Props> {
|
|||||||
return (
|
return (
|
||||||
<DashNavTimeControls dashboard={dashboard} onChangeTimeZone={updateTimeZoneForSession} key="time-controls" />
|
<DashNavTimeControls dashboard={dashboard} onChangeTimeZone={updateTimeZoneForSession} key="time-controls" />
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderRightActionsButton() {
|
const renderRightActionsButton = () => {
|
||||||
const { dashboard, onAddPanel, isFullscreen, kioskMode } = this.props;
|
const { dashboard, onAddPanel, isFullscreen, kioskMode } = props;
|
||||||
const { canSave, canEdit, showSettings } = dashboard.meta;
|
const { canSave, canEdit, showSettings } = dashboard.meta;
|
||||||
const { snapshot } = dashboard;
|
const { snapshot } = dashboard;
|
||||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||||
const buttons: ReactNode[] = [];
|
const buttons: ReactNode[] = [];
|
||||||
const tvButton = (
|
const tvButton = (
|
||||||
<ToolbarButton tooltip="Cycle view mode" icon="monitor" onClick={this.onToggleTVMode} key="tv-button" />
|
<ToolbarButton tooltip="Cycle view mode" icon="monitor" onClick={onToggleTVMode} key="tv-button" />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.isPlaylistRunning()) {
|
if (isPlaylistRunning()) {
|
||||||
return [this.renderPlaylistControls(), this.renderTimeControls()];
|
return [renderPlaylistControls(), renderTimeControls()];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kioskMode === KioskMode.TV) {
|
if (kioskMode === KioskMode.TV) {
|
||||||
return [this.renderTimeControls(), tvButton];
|
return [renderTimeControls(), tvButton];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canEdit && !isFullscreen) {
|
if (canEdit && !isFullscreen) {
|
||||||
@ -243,7 +242,7 @@ class DashNav extends PureComponent<Props> {
|
|||||||
buttons.push(
|
buttons.push(
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
tooltip="Open original dashboard"
|
tooltip="Open original dashboard"
|
||||||
onClick={() => this.gotoSnapshotOrigin(snapshotUrl)}
|
onClick={() => gotoSnapshotOrigin(snapshotUrl)}
|
||||||
icon="link"
|
icon="link"
|
||||||
key="button-snapshot"
|
key="button-snapshot"
|
||||||
/>
|
/>
|
||||||
@ -252,27 +251,27 @@ class DashNav extends PureComponent<Props> {
|
|||||||
|
|
||||||
if (showSettings) {
|
if (showSettings) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<ToolbarButton tooltip="Dashboard settings" icon="cog" onClick={this.onOpenSettings} key="button-settings" />
|
<ToolbarButton tooltip="Dashboard settings" icon="cog" onClick={onOpenSettings} key="button-settings" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCustomContent(customRightActions, buttons);
|
addCustomContent(customRightActions, buttons);
|
||||||
|
|
||||||
buttons.push(this.renderTimeControls());
|
buttons.push(renderTimeControls());
|
||||||
buttons.push(tvButton);
|
buttons.push(tvButton);
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
};
|
||||||
|
|
||||||
gotoSnapshotOrigin(snapshotUrl: string) {
|
const gotoSnapshotOrigin = (snapshotUrl: string) => {
|
||||||
window.location.href = textUtil.sanitizeUrl(snapshotUrl);
|
window.location.href = textUtil.sanitizeUrl(snapshotUrl);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const { isFullscreen, title, folderTitle } = props;
|
||||||
const { isFullscreen, title, folderTitle } = this.props;
|
// this ensures the component rerenders when the location changes
|
||||||
const onGoBack = isFullscreen ? this.onClose : undefined;
|
const location = useLocation();
|
||||||
|
const titleHref = locationUtil.getUrlForPartial(location, { search: 'open' });
|
||||||
const titleHref = locationUtil.updateSearchParams(window.location.href, '?search=open');
|
const parentHref = locationUtil.getUrlForPartial(location, { search: 'open', folder: 'current' });
|
||||||
const parentHref = locationUtil.updateSearchParams(window.location.href, '?search=open&folder=current');
|
const onGoBack = isFullscreen ? onClose : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageToolbar
|
<PageToolbar
|
||||||
@ -282,12 +281,13 @@ class DashNav extends PureComponent<Props> {
|
|||||||
titleHref={titleHref}
|
titleHref={titleHref}
|
||||||
parentHref={parentHref}
|
parentHref={parentHref}
|
||||||
onGoBack={onGoBack}
|
onGoBack={onGoBack}
|
||||||
leftItems={this.renderLeftActionsButton()}
|
leftItems={renderLeftActionsButton()}
|
||||||
>
|
>
|
||||||
{this.renderRightActionsButton()}
|
{renderRightActionsButton()}
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
DashNav.displayName = 'DashNav';
|
||||||
|
|
||||||
export default connector(DashNav);
|
export default connector(DashNav);
|
||||||
|
@ -150,7 +150,7 @@ export function DashboardSettings({ dashboard, editview }: Props) {
|
|||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<Link
|
<Link
|
||||||
onClick={() => reportInteraction(`Dashboard settings navigation to ${page.id}`)}
|
onClick={() => reportInteraction(`Dashboard settings navigation to ${page.id}`)}
|
||||||
to={(loc) => locationUtil.updateSearchParams(loc.search, `editview=${page.id}`)}
|
to={(loc) => locationUtil.getUrlForPartial(loc, { editview: page.id })}
|
||||||
className={cx('dashboard-settings__nav-item', { active: page.id === editview })}
|
className={cx('dashboard-settings__nav-item', { active: page.id === editview })}
|
||||||
key={page.id}
|
key={page.id}
|
||||||
>
|
>
|
||||||
|
@ -4106,6 +4106,7 @@ __metadata:
|
|||||||
"@testing-library/user-event": 13.5.0
|
"@testing-library/user-event": 13.5.0
|
||||||
"@types/braintree__sanitize-url": 4.1.0
|
"@types/braintree__sanitize-url": 4.1.0
|
||||||
"@types/d3-interpolate": ^1.4.0
|
"@types/d3-interpolate": ^1.4.0
|
||||||
|
"@types/history": 4.7.11
|
||||||
"@types/jest": 27.4.1
|
"@types/jest": 27.4.1
|
||||||
"@types/jquery": 3.5.14
|
"@types/jquery": 3.5.14
|
||||||
"@types/lodash": 4.14.149
|
"@types/lodash": 4.14.149
|
||||||
@ -4121,6 +4122,7 @@ __metadata:
|
|||||||
d3-interpolate: 1.4.0
|
d3-interpolate: 1.4.0
|
||||||
date-fns: 2.28.0
|
date-fns: 2.28.0
|
||||||
eventemitter3: 4.0.7
|
eventemitter3: 4.0.7
|
||||||
|
history: 4.10.1
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
marked: 4.0.12
|
marked: 4.0.12
|
||||||
moment: 2.29.1
|
moment: 2.29.1
|
||||||
|
Loading…
Reference in New Issue
Block a user