Scopes: Move url sync from scenes to the ScopesSelectorService (#102469)
* Add url sync * Move the URL sync handling to ScopesService to keep persistence * Reset location after tests * Prettier * Do url replace on enabling scopes * Remove comment * Update scenes version
This commit is contained in:
+2
-2
@@ -277,8 +277,8 @@
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/saga-icons": "workspace:*",
|
||||
"@grafana/scenes": "6.5.2",
|
||||
"@grafana/scenes-react": "6.5.2",
|
||||
"@grafana/scenes": "6.5.3",
|
||||
"@grafana/scenes-react": "6.5.3",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -62,6 +62,10 @@ export class ScopesApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parent
|
||||
* @param query Filters by title substring
|
||||
*/
|
||||
async fetchNode(parent: string, query: string): Promise<NodesMap> {
|
||||
try {
|
||||
const nodes =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, ReactNode, useMemo, useContext } from 'react';
|
||||
import { createContext, ReactNode, useMemo, useContext, useEffect } from 'react';
|
||||
|
||||
import { config, ScopesContext } from '@grafana/runtime';
|
||||
import { config, locationService, ScopesContext } from '@grafana/runtime';
|
||||
|
||||
import { ScopesApiClient } from './ScopesApiClient';
|
||||
import { ScopesService } from './ScopesService';
|
||||
@@ -36,7 +36,7 @@ export function defaultScopesServices() {
|
||||
const dashboardService = new ScopesDashboardsService(client);
|
||||
const selectorService = new ScopesSelectorService(client, dashboardService);
|
||||
return {
|
||||
scopesService: new ScopesService(selectorService, dashboardService),
|
||||
scopesService: new ScopesService(selectorService, dashboardService, locationService),
|
||||
scopesSelectorService: selectorService,
|
||||
scopesDashboardsService: dashboardService,
|
||||
client,
|
||||
@@ -48,6 +48,12 @@ export const ScopesContextProvider = ({ children, services }: ScopesContextProvi
|
||||
return services ?? defaultScopesServices();
|
||||
}, [services]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
memoizedServices.scopesService.cleanUp();
|
||||
};
|
||||
}, [memoizedServices]);
|
||||
|
||||
return (
|
||||
<ScopesContext.Provider value={config.featureToggles.scopeFilters ? memoizedServices.scopesService : undefined}>
|
||||
<ScopesServicesContext.Provider value={config.featureToggles.scopeFilters ? memoizedServices : undefined}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, combineLatest, Subscription } from 'rxjs';
|
||||
import { map, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
import { ScopesContextValue, ScopesContextValueState } from '@grafana/runtime';
|
||||
import { LocationService, ScopesContextValue, ScopesContextValueState } from '@grafana/runtime';
|
||||
|
||||
import { ScopesDashboardsService } from './dashboards/ScopesDashboardsService';
|
||||
import { ScopesSelectorService } from './selector/ScopesSelectorService';
|
||||
@@ -24,9 +24,12 @@ export class ScopesService implements ScopesContextValue {
|
||||
// This will contain the combined state that will be public.
|
||||
private readonly _stateObservable: BehaviorSubject<ScopesContextValueState>;
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private selectorService: ScopesSelectorService,
|
||||
private dashboardsService: ScopesDashboardsService
|
||||
private dashboardsService: ScopesDashboardsService,
|
||||
private locationService: LocationService
|
||||
) {
|
||||
this._state = new BehaviorSubject<State>({
|
||||
enabled: false,
|
||||
@@ -41,24 +44,64 @@ export class ScopesService implements ScopesContextValue {
|
||||
});
|
||||
|
||||
// We combine the latest emissions from this state + selectorService + dashboardsService.
|
||||
combineLatest([
|
||||
this._state.asObservable(),
|
||||
this.getSelectorServiceStateObservable(),
|
||||
this.getDashboardsServiceStateObservable(),
|
||||
])
|
||||
.pipe(
|
||||
// Map the 3 states into single ScopesContextValueState object
|
||||
map(
|
||||
([thisState, selectorState, dashboardsState]): ScopesContextValueState => ({
|
||||
...thisState,
|
||||
value: selectorState.selectedScopes,
|
||||
loading: selectorState.loading,
|
||||
drawerOpened: dashboardsState.drawerOpened,
|
||||
})
|
||||
this.subscriptions.push(
|
||||
combineLatest([
|
||||
this._state.asObservable(),
|
||||
this.getSelectorServiceStateObservable(),
|
||||
this.getDashboardsServiceStateObservable(),
|
||||
])
|
||||
.pipe(
|
||||
// Map the 3 states into single ScopesContextValueState object
|
||||
map(
|
||||
([thisState, selectorState, dashboardsState]): ScopesContextValueState => ({
|
||||
...thisState,
|
||||
value: selectorState.selectedScopes,
|
||||
loading: selectorState.loading,
|
||||
drawerOpened: dashboardsState.drawerOpened,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
// We pass this into behaviourSubject so we get the 1 event buffer and we can access latest value.
|
||||
.subscribe(this._stateObservable);
|
||||
// We pass this into behaviourSubject so we get the 1 event buffer and we can access latest value.
|
||||
.subscribe(this._stateObservable)
|
||||
);
|
||||
|
||||
// Init from the URL when we first load
|
||||
const queryParams = new URLSearchParams(locationService.getLocation().search);
|
||||
this.changeScopes(queryParams.getAll('scopes'));
|
||||
|
||||
// Update scopes state based on URL.
|
||||
this.subscriptions.push(
|
||||
locationService.getLocationObservable().subscribe((location) => {
|
||||
if (!this.state.enabled) {
|
||||
// We don't need to react on pages that don't interact with scopes.
|
||||
return;
|
||||
}
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const scopes = queryParams.getAll('scopes');
|
||||
if (scopes.length) {
|
||||
// We only update scopes but never delete them. This is to keep the scopes in memory if user navigates to
|
||||
// page that does not use scopes (like from dashboard to dashboard list back to dashboard). If user
|
||||
// changes the URL directly, it would trigger a reload so scopes would still be reset.
|
||||
this.changeScopes(scopes);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Update the URL based on change in the scopes state
|
||||
this.subscriptions.push(
|
||||
selectorService.subscribeToState((state, prev) => {
|
||||
const oldScopeNames = prev.selectedScopes.map((scope) => scope.scope.metadata.name);
|
||||
const newScopeNames = state.selectedScopes.map((scope) => scope.scope.metadata.name);
|
||||
if (!isEqual(oldScopeNames, newScopeNames)) {
|
||||
this.locationService.partial(
|
||||
{
|
||||
scopes: newScopeNames,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +140,14 @@ export class ScopesService implements ScopesContextValue {
|
||||
public setEnabled = (enabled: boolean) => {
|
||||
if (this.state.enabled !== enabled) {
|
||||
this.updateState({ enabled });
|
||||
if (enabled) {
|
||||
this.locationService.partial(
|
||||
{
|
||||
scopes: this.selectorService.state.selectedScopes.map(({ scope }) => scope.metadata.name),
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,4 +178,13 @@ export class ScopesService implements ScopesContextValue {
|
||||
distinctUntilChanged((prev, curr) => prev.drawerOpened === curr.drawerOpened)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup subscriptions so this can be garbage collected.
|
||||
*/
|
||||
public cleanUp() {
|
||||
for (const sub of this.subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, last } from 'lodash';
|
||||
|
||||
import { ScopesApiClient } from '../ScopesApiClient';
|
||||
import { ScopesServiceBase } from '../ScopesServiceBase';
|
||||
@@ -14,7 +14,11 @@ export interface ScopesSelectorServiceState {
|
||||
opened: boolean;
|
||||
loadingNodeName: string | undefined;
|
||||
nodes: NodesMap;
|
||||
|
||||
// Scopes that are selected and applied.
|
||||
selectedScopes: SelectedScope[];
|
||||
|
||||
// Representation of what is selected in the tree in the UI. This state may not be yet applied to the selectedScopes.
|
||||
treeScopes: TreeScope[];
|
||||
}
|
||||
|
||||
@@ -45,29 +49,47 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a node at a path with the new expanded state and query. If we expand a node or change the query we will
|
||||
* load its children. The expectation is that this is used to expand or filter nodes that are already loaded, not
|
||||
* load a deep nested node which parents weren't loaded yet.
|
||||
* @param path Path to the nodes. Each element in the path is a node name. Has to have at least one element.
|
||||
* @param expanded
|
||||
* @param query Substring of the title to filter by.
|
||||
*/
|
||||
public updateNode = async (path: string[], expanded: boolean, query: string) => {
|
||||
let nodes = { ...this.state.nodes };
|
||||
let currentLevel: NodesMap = nodes;
|
||||
|
||||
for (let idx = 0; idx < path.length - 1; idx++) {
|
||||
currentLevel = currentLevel[path[idx]].nodes;
|
||||
if (path.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingNodeName = path[path.length - 1];
|
||||
const currentNode = currentLevel[loadingNodeName];
|
||||
// Making a copy as we will be changing this in place and then updating state later.
|
||||
// This though does not make a deep copy so you cannot rely on reference of nested nodes changing.
|
||||
const nodes = { ...this.state.nodes };
|
||||
let currentLevel: NodesMap = nodes;
|
||||
let loadingNodeName = path[0];
|
||||
|
||||
if (path.length > 1) {
|
||||
const pathToParent = path.slice(0, path.length - 1);
|
||||
currentLevel = getNodesAtPath(nodes, pathToParent);
|
||||
loadingNodeName = last(path)!;
|
||||
}
|
||||
|
||||
const currentNode = currentLevel[loadingNodeName];
|
||||
const differentQuery = currentNode.query !== query;
|
||||
|
||||
currentNode.expanded = expanded;
|
||||
currentNode.query = query;
|
||||
|
||||
if (expanded || differentQuery) {
|
||||
// Means we have to fetch the children of the node
|
||||
|
||||
this.updateState({ nodes, loadingNodeName });
|
||||
|
||||
// fetchNodeApi does not throw just return empty object
|
||||
// fetchNodeApi does not throw just returns empty object.
|
||||
// Load all the children of the loadingNodeName
|
||||
const childNodes = await this.apiClient.fetchNode(loadingNodeName, query);
|
||||
if (loadingNodeName === this.state.loadingNodeName) {
|
||||
const [selectedScopes, treeScopes] = this.getScopesAndTreeScopesWithPaths(
|
||||
const [selectedScopes, treeScopes] = getScopesAndTreeScopesWithPaths(
|
||||
this.state.selectedScopes,
|
||||
this.state.treeScopes,
|
||||
path,
|
||||
@@ -95,6 +117,14 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle a selection of a scope node. Only leaf nodes representing an actual scope can be toggled so the path should
|
||||
* represent such node.
|
||||
*
|
||||
* The main function of this method is to update the treeScopes state which is a representation of what is selected in
|
||||
* the UI and to prefetch the scope data from the server.
|
||||
* @param path
|
||||
*/
|
||||
public toggleNodeSelect = (path: string[]) => {
|
||||
let treeScopes = [...this.state.treeScopes];
|
||||
|
||||
@@ -110,6 +140,8 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
const selectedIdx = treeScopes.findIndex(({ scopeName }) => scopeName === linkId);
|
||||
|
||||
if (selectedIdx === -1) {
|
||||
// We prefetch the scope when clicking on it. This will mean that once the selection is applied in closeAndApply()
|
||||
// we already have all the scopes in cache and don't need to fetch all of them again is multiple requests.
|
||||
this.apiClient.fetchScope(linkId!);
|
||||
|
||||
const selectedFromSameNode =
|
||||
@@ -133,8 +165,14 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
|
||||
changeScopes = (scopeNames: string[]) => this.setNewScopes(scopeNames.map((scopeName) => ({ scopeName, path: [] })));
|
||||
|
||||
/**
|
||||
* Apply the selected scopes. Apart from setting the scopes it also fetches the scope metadata and also loads the
|
||||
* related dashboards.
|
||||
* @param treeScopes The scopes to be applied. If not provided the treeScopes state is used which was populated
|
||||
* before for example by toggling the scopes in the scoped tree UI.
|
||||
*/
|
||||
private setNewScopes = async (treeScopes = this.state.treeScopes) => {
|
||||
if (isEqual(treeScopes, this.getTreeScopesFromSelectedScopes(this.state.selectedScopes))) {
|
||||
if (isEqual(treeScopes, getTreeScopesFromSelectedScopes(this.state.selectedScopes))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +180,10 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
scope: getEmptyScopeObject(scopeName),
|
||||
path,
|
||||
}));
|
||||
|
||||
// Apply the scopes right away even though we don't have the metadata yet.
|
||||
this.updateState({ selectedScopes, treeScopes, loading: true });
|
||||
|
||||
this.dashboardsService.fetchDashboards(selectedScopes.map(({ scope }) => scope.metadata.name));
|
||||
|
||||
selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes);
|
||||
@@ -151,6 +192,9 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
|
||||
public removeAllScopes = () => this.setNewScopes([]);
|
||||
|
||||
/**
|
||||
* Opens the scopes selector drawer and loads the root nodes if they are not loaded yet.
|
||||
*/
|
||||
public open = async () => {
|
||||
if (Object.keys(this.state.nodes[''].nodes).length === 0) {
|
||||
await this.updateNode([''], true, '');
|
||||
@@ -159,7 +203,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
let nodes = { ...this.state.nodes };
|
||||
|
||||
// First close all nodes
|
||||
nodes = this.closeNodes(nodes);
|
||||
nodes = closeNodes(nodes);
|
||||
|
||||
// Extract the path of a scope
|
||||
let path = [...(this.state.selectedScopes[0]?.path ?? ['', ''])];
|
||||
@@ -172,85 +216,90 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
|
||||
};
|
||||
|
||||
public closeAndReset = () => {
|
||||
this.updateState({ opened: false, treeScopes: this.getTreeScopesFromSelectedScopes(this.state.selectedScopes) });
|
||||
// Reset the treeScopes if we don't want them actually applied.
|
||||
this.updateState({ opened: false, treeScopes: getTreeScopesFromSelectedScopes(this.state.selectedScopes) });
|
||||
};
|
||||
|
||||
public closeAndApply = () => {
|
||||
this.updateState({ opened: false });
|
||||
this.setNewScopes();
|
||||
};
|
||||
}
|
||||
|
||||
private closeNodes = (nodes: NodesMap): NodesMap => {
|
||||
return Object.entries(nodes).reduce<NodesMap>((acc, [id, node]) => {
|
||||
acc[id] = {
|
||||
...node,
|
||||
expanded: false,
|
||||
nodes: this.closeNodes(node.nodes),
|
||||
};
|
||||
/**
|
||||
* Creates a deep copy of the node tree with expanded prop set to false.
|
||||
* @param nodes
|
||||
*/
|
||||
function closeNodes(nodes: NodesMap): NodesMap {
|
||||
return Object.entries(nodes).reduce<NodesMap>((acc, [id, node]) => {
|
||||
acc[id] = {
|
||||
...node,
|
||||
expanded: false,
|
||||
nodes: closeNodes(node.nodes),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private getTreeScopesFromSelectedScopes = (scopes: SelectedScope[]): TreeScope[] => {
|
||||
return scopes.map(({ scope, path }) => ({
|
||||
scopeName: scope.metadata.name,
|
||||
path,
|
||||
}));
|
||||
};
|
||||
function getTreeScopesFromSelectedScopes(scopes: SelectedScope[]): TreeScope[] {
|
||||
return scopes.map(({ scope, path }) => ({
|
||||
scopeName: scope.metadata.name,
|
||||
path,
|
||||
}));
|
||||
}
|
||||
|
||||
// helper func to get the selected/tree scopes together with their paths
|
||||
// needed to maintain selected scopes in tree for example when navigating
|
||||
// between categories or when loading scopes from URL to find the scope's path
|
||||
private getScopesAndTreeScopesWithPaths = (
|
||||
selectedScopes: SelectedScope[],
|
||||
treeScopes: TreeScope[],
|
||||
path: string[],
|
||||
childNodes: NodesMap
|
||||
): [SelectedScope[], TreeScope[]] => {
|
||||
const childNodesArr = Object.values(childNodes);
|
||||
// helper func to get the selected/tree scopes together with their paths
|
||||
// needed to maintain selected scopes in tree for example when navigating
|
||||
// between categories or when loading scopes from URL to find the scope's path
|
||||
function getScopesAndTreeScopesWithPaths(
|
||||
selectedScopes: SelectedScope[],
|
||||
treeScopes: TreeScope[],
|
||||
path: string[],
|
||||
childNodes: NodesMap
|
||||
): [SelectedScope[], TreeScope[]] {
|
||||
const childNodesArr = Object.values(childNodes);
|
||||
|
||||
// Get all scopes without paths
|
||||
// We use tree scopes as the list is always up to date as opposed to selected scopes which can be outdated
|
||||
const scopeNamesWithoutPaths = treeScopes.filter(({ path }) => path.length === 0).map(({ scopeName }) => scopeName);
|
||||
// Get all scopes without paths
|
||||
// We use tree scopes as the list is always up to date as opposed to selected scopes which can be outdated
|
||||
const scopeNamesWithoutPaths = treeScopes.filter(({ path }) => path.length === 0).map(({ scopeName }) => scopeName);
|
||||
|
||||
// We search for the path of each scope name without a path
|
||||
const scopeNamesWithPaths = scopeNamesWithoutPaths.reduce<Record<string, string[]>>((acc, scopeName) => {
|
||||
const possibleParent = childNodesArr.find((childNode) => childNode.selectable && childNode.linkId === scopeName);
|
||||
// We search for the path of each scope name without a path
|
||||
const scopeNamesWithPaths = scopeNamesWithoutPaths.reduce<Record<string, string[]>>((acc, scopeName) => {
|
||||
const possibleParent = childNodesArr.find((childNode) => childNode.selectable && childNode.linkId === scopeName);
|
||||
|
||||
if (possibleParent) {
|
||||
acc[scopeName] = [...path, possibleParent.name];
|
||||
}
|
||||
if (possibleParent) {
|
||||
acc[scopeName] = [...path, possibleParent.name];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Update the paths of the selected scopes based on what we found
|
||||
const newSelectedScopes = selectedScopes.map((selectedScope) => {
|
||||
if (selectedScope.path.length > 0) {
|
||||
return selectedScope;
|
||||
}
|
||||
// Update the paths of the selected scopes based on what we found
|
||||
const newSelectedScopes = selectedScopes.map((selectedScope) => {
|
||||
if (selectedScope.path.length > 0) {
|
||||
return selectedScope;
|
||||
}
|
||||
|
||||
return {
|
||||
...selectedScope,
|
||||
path: scopeNamesWithPaths[selectedScope.scope.metadata.name] ?? [],
|
||||
};
|
||||
});
|
||||
return {
|
||||
...selectedScope,
|
||||
path: scopeNamesWithPaths[selectedScope.scope.metadata.name] ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
// Update the paths of the tree scopes based on what we found
|
||||
const newTreeScopes = treeScopes.map((treeScope) => {
|
||||
if (treeScope.path.length > 0) {
|
||||
return treeScope;
|
||||
}
|
||||
// Update the paths of the tree scopes based on what we found
|
||||
const newTreeScopes = treeScopes.map((treeScope) => {
|
||||
if (treeScope.path.length > 0) {
|
||||
return treeScope;
|
||||
}
|
||||
|
||||
return {
|
||||
...treeScope,
|
||||
path: scopeNamesWithPaths[treeScope.scopeName] ?? [],
|
||||
};
|
||||
});
|
||||
return {
|
||||
...treeScope,
|
||||
path: scopeNamesWithPaths[treeScope.scopeName] ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
return [newSelectedScopes, newTreeScopes];
|
||||
};
|
||||
return [newSelectedScopes, newTreeScopes];
|
||||
}
|
||||
|
||||
function expandNodes(nodes: NodesMap, path: string[]): NodesMap {
|
||||
@@ -269,3 +318,13 @@ function expandNodes(nodes: NodesMap, path: string[]): NodesMap {
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function getNodesAtPath(nodes: NodesMap, path: string[]): NodesMap {
|
||||
let currentNodes = nodes;
|
||||
|
||||
for (const section of path) {
|
||||
currentNodes = currentNodes[section].nodes;
|
||||
}
|
||||
|
||||
return currentNodes;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
|
||||
import { ScopesService } from '../ScopesService';
|
||||
import { ScopesDashboardsService } from '../dashboards/ScopesDashboardsService';
|
||||
@@ -65,6 +65,7 @@ describe('Dashboards list', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
locationService.replace('');
|
||||
await resetScenes([fetchDashboardsSpy]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
|
||||
import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { ScopesService } from '../ScopesService';
|
||||
@@ -36,6 +36,7 @@ describe('Selector', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
locationService.replace('');
|
||||
await resetScenes([fetchSelectedScopesSpy, dashboardReloadSpy]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
|
||||
import { ScopesService } from '../ScopesService';
|
||||
import { ScopesSelectorService } from '../selector/ScopesSelectorService';
|
||||
@@ -74,6 +74,7 @@ describe('Tree', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
locationService.replace('');
|
||||
await resetScenes([fetchNodesSpy, fetchScopeSpy]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1416,7 +1416,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:7.26.10, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
|
||||
"@babel/runtime@npm:7.26.10":
|
||||
version: 7.26.10
|
||||
resolution: "@babel/runtime@npm:7.26.10"
|
||||
dependencies:
|
||||
@@ -1425,6 +1425,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7":
|
||||
version: 7.26.9
|
||||
resolution: "@babel/runtime@npm:7.26.9"
|
||||
dependencies:
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
checksum: 10/08edd07d774eafbf157fdc8450ed6ddd22416fdd8e2a53e4a00349daba1b502c03ab7f7ad3ad3a7c46b9a24d99b5697591d0f852ee2f84642082ef7dda90b83d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.3.3":
|
||||
version: 7.26.9
|
||||
resolution: "@babel/template@npm:7.26.9"
|
||||
@@ -3471,11 +3480,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes-react@npm:6.5.2":
|
||||
version: 6.5.2
|
||||
resolution: "@grafana/scenes-react@npm:6.5.2"
|
||||
"@grafana/scenes-react@npm:6.5.3":
|
||||
version: 6.5.3
|
||||
resolution: "@grafana/scenes-react@npm:6.5.3"
|
||||
dependencies:
|
||||
"@grafana/scenes": "npm:6.5.2"
|
||||
"@grafana/scenes": "npm:6.5.3"
|
||||
lru-cache: "npm:^10.2.2"
|
||||
react-use: "npm:^17.4.0"
|
||||
peerDependencies:
|
||||
@@ -3487,13 +3496,13 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/c922fea8ca86db49651a818640f32a7a35eeabcf886b52309ccd515a17bd071d19814e578158540299a4d01824ff3b9a05667bb86c91d87885c8ba7d0810c353
|
||||
checksum: 10/42630d527e3d9db2e2ae6728ad551b7cc9d36d2c3cdad3a6fdd5c8a959e2f88503767d257674db48480dc676bc83833a030a00dacd3bbd7058a51256f2c3b816
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/scenes@npm:6.5.2":
|
||||
version: 6.5.2
|
||||
resolution: "@grafana/scenes@npm:6.5.2"
|
||||
"@grafana/scenes@npm:6.5.3":
|
||||
version: 6.5.3
|
||||
resolution: "@grafana/scenes@npm:6.5.3"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.16"
|
||||
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
||||
@@ -3511,7 +3520,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/267af84b2cc86acc547afbacb661aeffdd67388146bbf0f682ddd6577992973766daf7f8b35fb79ef5170ed10808f73294bfc8565b90fc4dd044dd738a34eba8
|
||||
checksum: 10/25a2041ba2e6a6bcac0e08d0493575ab26d3331c27db21d8398e9bdc32f6eb117f6620a4d07c2d20b0fbb02782e6b921932d134298f6765062f2872f6a9b972b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17683,8 +17692,8 @@ __metadata:
|
||||
"@grafana/prometheus": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/saga-icons": "workspace:*"
|
||||
"@grafana/scenes": "npm:6.5.2"
|
||||
"@grafana/scenes-react": "npm:6.5.2"
|
||||
"@grafana/scenes": "npm:6.5.3"
|
||||
"@grafana/scenes-react": "npm:6.5.3"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/sql": "workspace:*"
|
||||
"@grafana/tsconfig": "npm:^2.0.0"
|
||||
@@ -30524,7 +30533,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:11.0.5":
|
||||
"uuid@npm:11.0.5, uuid@npm:^11.0.0, uuid@npm:^11.0.2":
|
||||
version: 11.0.5
|
||||
resolution: "uuid@npm:11.0.5"
|
||||
bin:
|
||||
@@ -30542,7 +30551,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uuid@npm:^11.0.0, uuid@npm:^11.0.2, uuid@npm:^11.0.5":
|
||||
"uuid@npm:^11.0.5":
|
||||
version: 11.1.0
|
||||
resolution: "uuid@npm:11.1.0"
|
||||
bin:
|
||||
|
||||
Reference in New Issue
Block a user