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:
Ashley Harrison 2022-03-24 16:01:43 +00:00 committed by GitHub
parent 0e682397ab
commit c13f4542d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 78 deletions

View File

@ -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",

View File

@ -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);
}, },

View File

@ -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);

View File

@ -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}
> >

View File

@ -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