mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes: Upgrade to latest URL sync system (#88836)
* Urlsync updates * Update * Fixing tests * Update to latest canary * fix * Update * Update * Update * Fix data trails issue * Data trails fixes * Update * correctly sync scene object graph with url state * Update
This commit is contained in:
parent
dd3c3b5857
commit
e3da5ed35d
@ -94,6 +94,7 @@
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@testing-library/react": "15.0.2",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/add": "^2",
|
||||
"@types/angular": "1.8.9",
|
||||
"@types/angular-route": "1.7.6",
|
||||
"@types/babel__core": "^7",
|
||||
@ -258,7 +259,7 @@
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/saga-icons": "workspace:*",
|
||||
"@grafana/scenes": "4.29.0",
|
||||
"@grafana/scenes": "^5.0.2",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
@ -398,7 +399,8 @@
|
||||
"uuid": "9.0.1",
|
||||
"visjs-network": "4.25.0",
|
||||
"whatwg-fetch": "3.6.20",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz",
|
||||
"yarn": "^1.22.22"
|
||||
},
|
||||
"resolutions": {
|
||||
"underscore": "1.13.6",
|
||||
|
@ -2,6 +2,7 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import { UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
@ -78,10 +79,10 @@ export function DashboardScenePage({ match, route, queryParams, history }: Props
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UrlSyncContextProvider scene={dashboard}>
|
||||
<dashboard.Component model={dashboard} key={dashboard.state.key} />
|
||||
<DashboardPrompt dashboard={dashboard} />
|
||||
</>
|
||||
</UrlSyncContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { advanceBy } from 'jest-date-mock';
|
||||
|
||||
import { BackendSrv, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { getUrlSyncManager } from '@grafana/scenes';
|
||||
import { BackendSrv, setBackendSrv } from '@grafana/runtime';
|
||||
import store from 'app/core/store';
|
||||
import { DASHBOARD_FROM_LS_KEY } from 'app/features/dashboard/state/initDashboard';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
@ -95,40 +94,6 @@ describe('DashboardScenePageStateManager', () => {
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize url sync', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
||||
|
||||
getUrlSyncManager().cleanUp(dash!);
|
||||
|
||||
// try loading again (and hitting cache)
|
||||
locationService.partial({ from: 'now-10m', to: 'now' });
|
||||
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
const dash2 = loader.state.dashboard;
|
||||
|
||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||
});
|
||||
|
||||
it('should not initialize url sync for embedded dashboards', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Embedded });
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-6h');
|
||||
});
|
||||
|
||||
describe('New dashboards', () => {
|
||||
it('Should have new empty model with meta.isNew and should not be cached', async () => {
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
|
@ -182,10 +182,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
restoreDashboardStateFromLocalStorage(dashboard);
|
||||
}
|
||||
|
||||
if (!(config.publicDashboardAccessToken && dashboard.state.controls?.state.hideTimeControls)) {
|
||||
dashboard.startUrlSync();
|
||||
}
|
||||
|
||||
this.setState({ dashboard: dashboard, isLoading: false });
|
||||
const measure = stopMeasure(LOAD_SCENE_MEASUREMENT);
|
||||
trackDashboardSceneLoaded(dashboard, measure?.duration);
|
||||
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { SceneComponentProps, UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
@ -55,7 +55,16 @@ export function PublicDashboardScenePage({ match, route }: Props) {
|
||||
return <PublicDashboardNotAvailable />;
|
||||
}
|
||||
|
||||
return <PublicDashboardSceneRenderer model={dashboard} />;
|
||||
// if no time picker render without url sync
|
||||
if (dashboard.state.controls?.state.hideTimeControls) {
|
||||
return <PublicDashboardSceneRenderer model={dashboard} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<UrlSyncContextProvider scene={dashboard}>
|
||||
<PublicDashboardSceneRenderer model={dashboard} />
|
||||
</UrlSyncContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function PublicDashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||
|
@ -61,10 +61,7 @@ export function useSaveDashboard(isCopy = false) {
|
||||
|
||||
if (newUrl !== currentLocation.pathname) {
|
||||
setTimeout(() => {
|
||||
// Because the path changes we need to stop and restart url sync
|
||||
scene.stopUrlSync();
|
||||
locationService.push({ pathname: newUrl, search: currentLocation.search });
|
||||
scene.startUrlSync();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
getUrlSyncManager,
|
||||
SceneFlexLayout,
|
||||
sceneGraph,
|
||||
SceneGridLayout,
|
||||
@ -212,27 +211,15 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
window.__grafanaSceneContext = prevSceneContext;
|
||||
clearKeyBindings();
|
||||
this._changeTracker.terminate();
|
||||
this.stopUrlSync();
|
||||
oldDashboardWrapper.destroy();
|
||||
dashboardWatcher.leave();
|
||||
};
|
||||
}
|
||||
|
||||
public startUrlSync() {
|
||||
if (!this.state.meta.isEmbedded) {
|
||||
getUrlSyncManager().initSync(this);
|
||||
}
|
||||
}
|
||||
|
||||
public stopUrlSync() {
|
||||
getUrlSyncManager().cleanUp(this);
|
||||
}
|
||||
|
||||
public onEnterEditMode = (fromExplore = false) => {
|
||||
this._fromExplore = fromExplore;
|
||||
// Save this state
|
||||
this._initialState = sceneUtils.cloneSceneObjectState(this.state);
|
||||
|
||||
this._initialUrlState = locationService.getLocation();
|
||||
|
||||
// Switch to edit mode
|
||||
@ -303,10 +290,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
private exitEditModeConfirmed(restoreInitialState = true) {
|
||||
// No need to listen to changes anymore
|
||||
this._changeTracker.stopTrackingChanges();
|
||||
// Stop url sync before updating url
|
||||
this.stopUrlSync();
|
||||
|
||||
// Now we can update urls
|
||||
// We are updating url and removing editview and editPanel.
|
||||
// The initial url may be including edit view, edit panel or inspect query params if the user pasted the url,
|
||||
// hence we need to cleanup those query params to get back to the dashboard view. Otherwise url sync can trigger overlays.
|
||||
@ -330,8 +314,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
// Do not restore
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
// and start url sync again
|
||||
this.startUrlSync();
|
||||
|
||||
// Disable grid dragging
|
||||
this.propagateEditModeChange();
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
@ -103,9 +103,8 @@ describe('NavToolbarActions', () => {
|
||||
});
|
||||
|
||||
it('Should show correct buttons when in settings menu', async () => {
|
||||
const { dashboard } = setup();
|
||||
setup();
|
||||
|
||||
dashboard.startUrlSync();
|
||||
await userEvent.click(await screen.findByText('Edit'));
|
||||
await userEvent.click(await screen.findByText('Settings'));
|
||||
|
||||
@ -118,6 +117,7 @@ describe('NavToolbarActions', () => {
|
||||
|
||||
it('Should show correct buttons when editing a new panel', async () => {
|
||||
const { dashboard } = setup();
|
||||
|
||||
await act(() => {
|
||||
dashboard.onEnterEditMode();
|
||||
const editingPanel = ((dashboard.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state
|
||||
@ -205,9 +205,13 @@ function setup() {
|
||||
|
||||
const context = getGrafanaContextMock();
|
||||
|
||||
locationService.push('/');
|
||||
|
||||
render(
|
||||
<TestProvider grafanaContext={context}>
|
||||
<ToolbarActions dashboard={dashboard} />
|
||||
<UrlSyncContextProvider scene={dashboard}>
|
||||
<ToolbarActions dashboard={dashboard} />
|
||||
</UrlSyncContextProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { CustomVariable } from '@grafana/scenes';
|
||||
import { CustomVariable, getUrlSyncManager } from '@grafana/scenes';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
@ -50,7 +50,8 @@ describe('dashboardSessionState', () => {
|
||||
restoreDashboardStateFromLocalStorage(scene);
|
||||
const variable = scene.state.$variables!.getByName('customVar') as CustomVariable;
|
||||
const timeRange = scene.state.$timeRange;
|
||||
scene.startUrlSync();
|
||||
|
||||
getUrlSyncManager().initSync(scene);
|
||||
|
||||
expect(variable!.state!.value).toEqual(['b']);
|
||||
expect(variable!.state!.text).toEqual(['b']);
|
||||
@ -63,11 +64,12 @@ describe('dashboardSessionState', () => {
|
||||
PRESERVED_SCENE_STATE_KEY,
|
||||
'?var-customVar=b&var-nonApplicableVar=b&from=now-5m&to=now&timezone=browser'
|
||||
);
|
||||
|
||||
const scene = buildTestScene();
|
||||
|
||||
restoreDashboardStateFromLocalStorage(scene);
|
||||
|
||||
expect(locationService.getSearch().toString()).toBe('var-customVar=b&from=now-5m&to=now&timezone=browser');
|
||||
expect(locationService.getLocation().search).toBe('?var-customVar=b&from=now-5m&to=now&timezone=browser');
|
||||
});
|
||||
|
||||
// handles case when user navigates back to a dashboard with the same state, i.e. using back button
|
||||
@ -79,7 +81,7 @@ describe('dashboardSessionState', () => {
|
||||
|
||||
restoreDashboardStateFromLocalStorage(scene);
|
||||
|
||||
expect(locationService.getSearch().toString()).toBe('var-customVar=b&from=now-6h&to=now&timezone=browser');
|
||||
expect(locationService.getLocation().search).toBe('?var-customVar=b&from=now-6h&to=now&timezone=browser');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -116,6 +118,7 @@ function buildTestScene() {
|
||||
version: 24,
|
||||
weekStart: '',
|
||||
};
|
||||
|
||||
const scene = transformSaveModelToScene({ dashboard: testDashboard, meta: {} });
|
||||
|
||||
// Removing data layers to avoid mocking built-in Grafana data source
|
||||
|
@ -17,10 +17,6 @@ export function restoreDashboardStateFromLocalStorage(dashboard: DashboardScene)
|
||||
preservedQueryParams.forEach((value, key) => {
|
||||
if (!currentQueryParams.has(key)) {
|
||||
currentQueryParams.append(key, value);
|
||||
} else {
|
||||
if (!currentQueryParams.getAll(key).includes(value)) {
|
||||
currentQueryParams.append(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -38,9 +34,7 @@ export function restoreDashboardStateFromLocalStorage(dashboard: DashboardScene)
|
||||
|
||||
const finalParams = currentQueryParams.toString();
|
||||
if (finalParams) {
|
||||
locationService.replace({
|
||||
search: finalParams,
|
||||
});
|
||||
locationService.replace({ search: finalParams });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ describe('DataTrail', () => {
|
||||
});
|
||||
|
||||
it('should sync state with url', () => {
|
||||
expect(locationService.getSearchObject().metric).toBe('metric_bucket');
|
||||
expect(trail.getUrlState().metric).toBe('metric_bucket');
|
||||
});
|
||||
|
||||
it('should add history step', () => {
|
||||
@ -104,10 +104,6 @@ describe('DataTrail', () => {
|
||||
trail.state.$timeRange?.setState({ from: 'now-1h' });
|
||||
});
|
||||
|
||||
it('should sync state with url', () => {
|
||||
expect(locationService.getSearchObject().from).toBe('now-1h');
|
||||
});
|
||||
|
||||
it('should add history step', () => {
|
||||
expect(trail.state.history.state.steps[2].type).toBe('time');
|
||||
});
|
||||
@ -154,10 +150,6 @@ describe('DataTrail', () => {
|
||||
trail.state.$timeRange?.setState({ from: 'now-15m' });
|
||||
});
|
||||
|
||||
it('should sync state with url', () => {
|
||||
expect(locationService.getSearchObject().from).toBe('now-15m');
|
||||
});
|
||||
|
||||
it('should add history step', () => {
|
||||
expect(trail.state.history.state.steps[3].type).toBe('time');
|
||||
});
|
||||
@ -224,10 +216,6 @@ describe('DataTrail', () => {
|
||||
getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'a' }] });
|
||||
});
|
||||
|
||||
it('should sync state with url', () => {
|
||||
expect(decodeURIComponent(locationService.getSearchObject()['var-filters']?.toString()!)).toBe('zone|=|a');
|
||||
});
|
||||
|
||||
it('should add history step', () => {
|
||||
expect(trail.state.history.state.steps[2].type).toBe('filters');
|
||||
});
|
||||
@ -276,12 +264,6 @@ describe('DataTrail', () => {
|
||||
getFilterVar().setState({ filters: [{ key: 'zone', operator: '=', value: 'b' }] });
|
||||
});
|
||||
|
||||
it('should sync state with url', () => {
|
||||
expect(decodeURIComponent(locationService.getSearchObject()['var-filters']?.toString()!)).toBe(
|
||||
'zone|=|b'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add history step', () => {
|
||||
expect(trail.state.history.state.steps[3].type).toBe('filters');
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { AdHocVariableFilter, GrafanaTheme2, VariableHide, urlUtil } from '@grafana/data';
|
||||
import { AdHocVariableFilter, GrafanaTheme2, PageLayoutType, VariableHide, urlUtil } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
VariableValueSelectors,
|
||||
} from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DataTrailSettings } from './DataTrailSettings';
|
||||
import { DataTrailHistory } from './DataTrailsHistory';
|
||||
@ -35,6 +36,7 @@ import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper';
|
||||
import { reportChangeInLabelFilters } from './interactions';
|
||||
import { MetricSelectedEvent, trailDS, VAR_DATASOURCE, VAR_FILTERS } from './shared';
|
||||
import { getMetricName } from './utils';
|
||||
|
||||
export interface DataTrailState extends SceneObjectState {
|
||||
topScene?: SceneObject;
|
||||
@ -93,21 +95,11 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
);
|
||||
}
|
||||
|
||||
// Disconnects the current step history state from the current state, to prevent changes affecting history state
|
||||
const currentState = this.state.history.state.steps[this.state.history.state.currentStep]?.trailState;
|
||||
if (currentState) {
|
||||
this.restoreFromHistoryStep(currentState);
|
||||
}
|
||||
|
||||
this.enableUrlSync();
|
||||
|
||||
// Save the current trail as a recent if the browser closes or reloads
|
||||
const saveRecentTrail = () => getTrailStore().setRecentTrail(this);
|
||||
window.addEventListener('unload', saveRecentTrail);
|
||||
|
||||
return () => {
|
||||
this.disableUrlSync();
|
||||
|
||||
if (!this.state.embedded) {
|
||||
saveRecentTrail();
|
||||
}
|
||||
@ -115,18 +107,6 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
};
|
||||
}
|
||||
|
||||
private enableUrlSync() {
|
||||
if (!this.state.embedded) {
|
||||
getUrlSyncManager().initSync(this);
|
||||
}
|
||||
}
|
||||
|
||||
private disableUrlSync() {
|
||||
if (!this.state.embedded) {
|
||||
getUrlSyncManager().cleanUp(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
variableNames: [VAR_DATASOURCE],
|
||||
onReferencedVariableValueChanged: (variable: SceneVariable) => {
|
||||
@ -167,8 +147,6 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
}
|
||||
|
||||
public restoreFromHistoryStep(state: DataTrailState) {
|
||||
this.disableUrlSync();
|
||||
|
||||
if (!state.topScene && !state.metric) {
|
||||
// If the top scene for an is missing, correct it.
|
||||
state.topScene = new MetricSelectScene({});
|
||||
@ -184,8 +162,6 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
const urlState = getUrlSyncManager().getUrlState(this);
|
||||
const fullUrl = urlUtil.renderUrl(locationService.getLocation().pathname, urlState);
|
||||
locationService.replace(fullUrl);
|
||||
|
||||
this.enableUrlSync();
|
||||
}
|
||||
|
||||
private _handleMetricSelectedEvent(evt: MetricSelectedEvent) {
|
||||
@ -227,24 +203,26 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
||||
const { controls, topScene, history, settings } = model.useState();
|
||||
const { controls, topScene, history, settings, metric } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{showHeaderForFirstTimeUsers && <MetricsHeader />}
|
||||
<history.Component model={history} />
|
||||
{controls && (
|
||||
<div className={styles.controls}>
|
||||
{controls.map((control) => (
|
||||
<control.Component key={control.state.key} model={control} />
|
||||
))}
|
||||
<settings.Component model={settings} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>
|
||||
</div>
|
||||
<Page navId="explore/metrics" pageNav={{ text: getMetricName(metric) }} layout={PageLayoutType.Custom}>
|
||||
<div className={styles.container}>
|
||||
{showHeaderForFirstTimeUsers && <MetricsHeader />}
|
||||
<history.Component model={history} />
|
||||
{controls && (
|
||||
<div className={styles.controls}>
|
||||
{controls.map((control) => (
|
||||
<control.Component key={control.state.key} model={control} />
|
||||
))}
|
||||
<settings.Component model={settings} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -288,6 +266,8 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
gap: theme.spacing(1),
|
||||
minHeight: '100%',
|
||||
flexDirection: 'column',
|
||||
background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas,
|
||||
padding: theme.spacing(2, 3, 2, 3),
|
||||
}),
|
||||
body: css({
|
||||
flexGrow: 1,
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, getUrlSyncManager } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DataTrail } from './DataTrail';
|
||||
@ -13,7 +11,7 @@ import { DataTrailsHome } from './DataTrailsHome';
|
||||
import { MetricsHeader } from './MetricsHeader';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { HOME_ROUTE, TRAILS_ROUTE } from './shared';
|
||||
import { getMetricName, getUrlForTrail, newMetricsTrail } from './utils';
|
||||
import { getUrlForTrail, newMetricsTrail } from './utils';
|
||||
|
||||
export interface DataTrailsAppState extends SceneObjectState {
|
||||
trail: DataTrail;
|
||||
@ -26,13 +24,12 @@ export class DataTrailsApp extends SceneObjectBase<DataTrailsAppState> {
|
||||
}
|
||||
|
||||
goToUrlForTrail(trail: DataTrail) {
|
||||
this.setState({ trail });
|
||||
locationService.push(getUrlForTrail(trail));
|
||||
this.setState({ trail });
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrailsApp>) => {
|
||||
const { trail, home } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@ -50,21 +47,7 @@ export class DataTrailsApp extends SceneObjectBase<DataTrailsAppState> {
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact={true}
|
||||
path={TRAILS_ROUTE}
|
||||
render={() => (
|
||||
<Page
|
||||
navId="explore/metrics"
|
||||
pageNav={{ text: getMetricName(trail.state.metric) }}
|
||||
layout={PageLayoutType.Custom}
|
||||
>
|
||||
<div className={styles.customPage}>
|
||||
<DataTrailView trail={trail} />
|
||||
</div>
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
<Route exact={true} path={TRAILS_ROUTE} render={() => <DataTrailView trail={trail} />} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
@ -84,7 +67,11 @@ function DataTrailView({ trail }: { trail: DataTrail }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <trail.Component model={trail} />;
|
||||
return (
|
||||
<UrlSyncContextProvider scene={trail}>
|
||||
<trail.Component model={trail} />
|
||||
</UrlSyncContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
let dataTrailsApp: DataTrailsApp;
|
||||
@ -92,50 +79,10 @@ let dataTrailsApp: DataTrailsApp;
|
||||
export function getDataTrailsApp() {
|
||||
if (!dataTrailsApp) {
|
||||
dataTrailsApp = new DataTrailsApp({
|
||||
trail: getInitialTrail(),
|
||||
trail: newMetricsTrail(),
|
||||
home: new DataTrailsHome({}),
|
||||
});
|
||||
}
|
||||
|
||||
return dataTrailsApp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initial trail for the app to work with based on the current URL
|
||||
*
|
||||
* It will either be a new trail that will be started based on the state represented
|
||||
* in the URL parameters, or it will be the most recently used trail (according to the trail store)
|
||||
* which has its current history step matching the URL parameters.
|
||||
*
|
||||
* The reason for trying to reinitialize from the recent trail is to resolve an issue
|
||||
* where refreshing the browser would wipe the step history. This allows you to preserve
|
||||
* it between browser refreshes, or when reaccessing the same URL.
|
||||
*/
|
||||
function getInitialTrail() {
|
||||
const newTrail = newMetricsTrail();
|
||||
|
||||
// Set the initial state of the newTrail based on the URL,
|
||||
// In case we are initializing from an externally created URL or a page reload
|
||||
getUrlSyncManager().initSync(newTrail);
|
||||
// Remove the URL sync for now. It will be restored on the trail if it is activated.
|
||||
getUrlSyncManager().cleanUp(newTrail);
|
||||
|
||||
// If one of the recent trails is a match to the newTrail derived from the current URL,
|
||||
// let's restore that trail so that a page refresh doesn't create a new trail.
|
||||
const recentMatchingTrail = getTrailStore().findMatchingRecentTrail(newTrail)?.resolve();
|
||||
|
||||
// If there is a matching trail, initialize with that. Otherwise, use the new trail.
|
||||
return recentMatchingTrail || newTrail;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
customPage: css({
|
||||
padding: theme.spacing(2, 3, 2, 3),
|
||||
background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -349,6 +349,7 @@ describe('TrailStore', () => {
|
||||
|
||||
describe('And time range is changed to now-15m to now', () => {
|
||||
let trail: DataTrail;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
localStorage.setItem(RECENT_TRAILS_KEY, JSON.stringify([{ history, currentStep: 1 }]));
|
||||
@ -357,6 +358,7 @@ describe('TrailStore', () => {
|
||||
trail = store.recent[0].resolve();
|
||||
const urlState = getUrlSyncManager().getUrlState(trail);
|
||||
locationService.partial(urlState);
|
||||
|
||||
trail.activate();
|
||||
trail.state.history.activate();
|
||||
trail.state.$timeRange?.setState({ from: 'now-15m' });
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { debounce, isEqual } from 'lodash';
|
||||
|
||||
import { urlUtil } from '@grafana/data';
|
||||
import { getUrlSyncManager, SceneObject, SceneObjectRef, SceneObjectUrlValues, sceneUtils } from '@grafana/scenes';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
@ -77,9 +78,14 @@ export class TrailStore {
|
||||
});
|
||||
|
||||
const currentStep = t.currentStep ?? trail.state.history.state.steps.length - 1;
|
||||
|
||||
trail.state.history.setState({ currentStep });
|
||||
// The state change listeners aren't activated yet, so maually change to the current step state
|
||||
trail.setState(trail.state.history.state.steps[currentStep].trailState);
|
||||
|
||||
trail.setState(
|
||||
sceneUtils.cloneSceneObjectState(trail.state.history.state.steps[currentStep].trailState, {
|
||||
history: trail.state.history,
|
||||
})
|
||||
);
|
||||
|
||||
return trail;
|
||||
}
|
||||
@ -102,8 +108,8 @@ export class TrailStore {
|
||||
}
|
||||
|
||||
private _loadFromUrl(node: SceneObject, urlValues: SceneObjectUrlValues) {
|
||||
node.urlSync?.updateFromUrl(urlValues);
|
||||
node.forEachChild((child) => this._loadFromUrl(child, urlValues));
|
||||
const urlState = urlUtil.renderUrl('', urlValues);
|
||||
sceneUtils.syncStateFromSearchParams(node, new URLSearchParams(urlState));
|
||||
}
|
||||
|
||||
// Recent Trails
|
||||
@ -140,14 +146,6 @@ export class TrailStore {
|
||||
this._save();
|
||||
}
|
||||
|
||||
findMatchingRecentTrail(trail: DataTrail) {
|
||||
const matchUrlState = getUrlStateForComparison(trail);
|
||||
return this._recent.find((t) => {
|
||||
const urlState = getUrlStateForComparison(t.resolve());
|
||||
return isEqual(matchUrlState, urlState);
|
||||
});
|
||||
}
|
||||
|
||||
// Bookmarked Trails
|
||||
get bookmarks() {
|
||||
return this._bookmarks;
|
||||
|
Loading…
Reference in New Issue
Block a user