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/user-event": "13.5.0",
|
||||
"@types/braintree__sanitize-url": "4.1.0",
|
||||
"@types/history": "4.7.11",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/jquery": "3.5.14",
|
||||
"@types/lodash": "4.14.149",
|
||||
@ -65,6 +66,7 @@
|
||||
"@types/testing-library__jest-dom": "5.14.3",
|
||||
"@types/testing-library__react-hooks": "^3.2.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"history": "4.10.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"rimraf": "3.0.2",
|
||||
"rollup": "2.70.1",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Location } from 'history';
|
||||
import { GrafanaConfig, RawTimeRange, ScopedVars } from '../types';
|
||||
import { UrlQueryMap, urlUtil } from './url';
|
||||
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`.
|
||||
*/
|
||||
const updateSearchParams = (init: string, partial: string) => {
|
||||
@ -92,6 +115,7 @@ export const locationUtil = {
|
||||
const params = getVariablesUrlParams(scopedVars);
|
||||
return urlUtil.toUrlParams(params);
|
||||
},
|
||||
getUrlForPartial,
|
||||
processUrl: (url: string) => {
|
||||
return grafanaConfig.disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
||||
},
|
||||
|
@ -1,12 +1,13 @@
|
||||
// Libaries
|
||||
import React, { PureComponent, FC, ReactNode } from 'react';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
// Utils & Services
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
// Components
|
||||
import { DashNavButton } from './DashNavButton';
|
||||
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';
|
||||
// State
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
@ -56,64 +57,62 @@ export function addCustomRightAction(content: DashNavButtonModel) {
|
||||
|
||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
class DashNav extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
export const DashNav = React.memo<Props>((props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
onClose = () => {
|
||||
locationService.partial({ viewPanel: null });
|
||||
};
|
||||
|
||||
onToggleTVMode = () => {
|
||||
toggleKioskMode();
|
||||
};
|
||||
|
||||
onOpenSettings = () => {
|
||||
locationService.partial({ editview: 'settings' });
|
||||
};
|
||||
|
||||
onStarDashboard = () => {
|
||||
const { dashboard } = this.props;
|
||||
const onStarDashboard = () => {
|
||||
const dashboardSrv = getDashboardSrv();
|
||||
const { dashboard } = props;
|
||||
|
||||
dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then((newState: any) => {
|
||||
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();
|
||||
};
|
||||
|
||||
onPlaylistNext = () => {
|
||||
const onPlaylistNext = () => {
|
||||
playlistSrv.next();
|
||||
};
|
||||
|
||||
onPlaylistStop = () => {
|
||||
const onPlaylistStop = () => {
|
||||
playlistSrv.stop();
|
||||
this.forceUpdate();
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
addCustomContent(actions: DashNavButtonModel[], buttons: ReactNode[]) {
|
||||
const addCustomContent = (actions: DashNavButtonModel[], buttons: ReactNode[]) => {
|
||||
actions.map((action, index) => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
isPlaylistRunning() {
|
||||
const isPlaylistRunning = () => {
|
||||
return playlistSrv.isPlaying;
|
||||
}
|
||||
};
|
||||
|
||||
renderLeftActionsButton() {
|
||||
const { dashboard, kioskMode } = this.props;
|
||||
const renderLeftActionsButton = () => {
|
||||
const { dashboard, kioskMode } = props;
|
||||
const { canStar, canShare, isStarred } = dashboard.meta;
|
||||
const buttons: ReactNode[] = [];
|
||||
|
||||
if (kioskMode !== KioskMode.Off || this.isPlaylistRunning()) {
|
||||
if (kioskMode !== KioskMode.Off || isPlaylistRunning()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -125,7 +124,7 @@ class DashNav extends PureComponent<Props> {
|
||||
icon={isStarred ? 'favorite' : 'star'}
|
||||
iconType={isStarred ? 'mono' : 'default'}
|
||||
iconSize="lg"
|
||||
onClick={this.onStarDashboard}
|
||||
onClick={onStarDashboard}
|
||||
key="button-star"
|
||||
/>
|
||||
);
|
||||
@ -172,22 +171,22 @@ class DashNav extends PureComponent<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
this.addCustomContent(customLeftActions, buttons);
|
||||
addCustomContent(customLeftActions, buttons);
|
||||
return buttons;
|
||||
}
|
||||
};
|
||||
|
||||
renderPlaylistControls() {
|
||||
const renderPlaylistControls = () => {
|
||||
return (
|
||||
<ButtonGroup key="playlist-buttons">
|
||||
<ToolbarButton tooltip="Go to previous dashboard" icon="backward" onClick={this.onPlaylistPrev} narrow />
|
||||
<ToolbarButton onClick={this.onPlaylistStop}>Stop playlist</ToolbarButton>
|
||||
<ToolbarButton tooltip="Go to next dashboard" icon="forward" onClick={this.onPlaylistNext} narrow />
|
||||
<ToolbarButton tooltip="Go to previous dashboard" icon="backward" onClick={onPlaylistPrev} narrow />
|
||||
<ToolbarButton onClick={onPlaylistStop}>Stop playlist</ToolbarButton>
|
||||
<ToolbarButton tooltip="Go to next dashboard" icon="forward" onClick={onPlaylistNext} narrow />
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderTimeControls() {
|
||||
const { dashboard, updateTimeZoneForSession, hideTimePicker } = this.props;
|
||||
const renderTimeControls = () => {
|
||||
const { dashboard, updateTimeZoneForSession, hideTimePicker } = props;
|
||||
|
||||
if (hideTimePicker) {
|
||||
return null;
|
||||
@ -196,24 +195,24 @@ class DashNav extends PureComponent<Props> {
|
||||
return (
|
||||
<DashNavTimeControls dashboard={dashboard} onChangeTimeZone={updateTimeZoneForSession} key="time-controls" />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderRightActionsButton() {
|
||||
const { dashboard, onAddPanel, isFullscreen, kioskMode } = this.props;
|
||||
const renderRightActionsButton = () => {
|
||||
const { dashboard, onAddPanel, isFullscreen, kioskMode } = props;
|
||||
const { canSave, canEdit, showSettings } = dashboard.meta;
|
||||
const { snapshot } = dashboard;
|
||||
const snapshotUrl = snapshot && snapshot.originalUrl;
|
||||
const buttons: ReactNode[] = [];
|
||||
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()) {
|
||||
return [this.renderPlaylistControls(), this.renderTimeControls()];
|
||||
if (isPlaylistRunning()) {
|
||||
return [renderPlaylistControls(), renderTimeControls()];
|
||||
}
|
||||
|
||||
if (kioskMode === KioskMode.TV) {
|
||||
return [this.renderTimeControls(), tvButton];
|
||||
return [renderTimeControls(), tvButton];
|
||||
}
|
||||
|
||||
if (canEdit && !isFullscreen) {
|
||||
@ -243,7 +242,7 @@ class DashNav extends PureComponent<Props> {
|
||||
buttons.push(
|
||||
<ToolbarButton
|
||||
tooltip="Open original dashboard"
|
||||
onClick={() => this.gotoSnapshotOrigin(snapshotUrl)}
|
||||
onClick={() => gotoSnapshotOrigin(snapshotUrl)}
|
||||
icon="link"
|
||||
key="button-snapshot"
|
||||
/>
|
||||
@ -252,27 +251,27 @@ class DashNav extends PureComponent<Props> {
|
||||
|
||||
if (showSettings) {
|
||||
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);
|
||||
return buttons;
|
||||
}
|
||||
};
|
||||
|
||||
gotoSnapshotOrigin(snapshotUrl: string) {
|
||||
const gotoSnapshotOrigin = (snapshotUrl: string) => {
|
||||
window.location.href = textUtil.sanitizeUrl(snapshotUrl);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isFullscreen, title, folderTitle } = this.props;
|
||||
const onGoBack = isFullscreen ? this.onClose : undefined;
|
||||
|
||||
const titleHref = locationUtil.updateSearchParams(window.location.href, '?search=open');
|
||||
const parentHref = locationUtil.updateSearchParams(window.location.href, '?search=open&folder=current');
|
||||
const { isFullscreen, title, folderTitle } = props;
|
||||
// this ensures the component rerenders when the location changes
|
||||
const location = useLocation();
|
||||
const titleHref = locationUtil.getUrlForPartial(location, { search: 'open' });
|
||||
const parentHref = locationUtil.getUrlForPartial(location, { search: 'open', folder: 'current' });
|
||||
const onGoBack = isFullscreen ? onClose : undefined;
|
||||
|
||||
return (
|
||||
<PageToolbar
|
||||
@ -282,12 +281,13 @@ class DashNav extends PureComponent<Props> {
|
||||
titleHref={titleHref}
|
||||
parentHref={parentHref}
|
||||
onGoBack={onGoBack}
|
||||
leftItems={this.renderLeftActionsButton()}
|
||||
leftItems={renderLeftActionsButton()}
|
||||
>
|
||||
{this.renderRightActionsButton()}
|
||||
{renderRightActionsButton()}
|
||||
</PageToolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DashNav.displayName = 'DashNav';
|
||||
|
||||
export default connector(DashNav);
|
||||
|
@ -150,7 +150,7 @@ export function DashboardSettings({ dashboard, editview }: Props) {
|
||||
{pages.map((page) => (
|
||||
<Link
|
||||
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 })}
|
||||
key={page.id}
|
||||
>
|
||||
|
@ -4106,6 +4106,7 @@ __metadata:
|
||||
"@testing-library/user-event": 13.5.0
|
||||
"@types/braintree__sanitize-url": 4.1.0
|
||||
"@types/d3-interpolate": ^1.4.0
|
||||
"@types/history": 4.7.11
|
||||
"@types/jest": 27.4.1
|
||||
"@types/jquery": 3.5.14
|
||||
"@types/lodash": 4.14.149
|
||||
@ -4121,6 +4122,7 @@ __metadata:
|
||||
d3-interpolate: 1.4.0
|
||||
date-fns: 2.28.0
|
||||
eventemitter3: 4.0.7
|
||||
history: 4.10.1
|
||||
lodash: 4.17.21
|
||||
marked: 4.0.12
|
||||
moment: 2.29.1
|
||||
|
Loading…
Reference in New Issue
Block a user