diff --git a/package.json b/package.json index 60d2e724c58..2d04f81fdc0 100644 --- a/package.json +++ b/package.json @@ -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:*", diff --git a/public/app/features/scopes/ScopesApiClient.ts b/public/app/features/scopes/ScopesApiClient.ts index 443a7cad8c6..358d1b97e4d 100644 --- a/public/app/features/scopes/ScopesApiClient.ts +++ b/public/app/features/scopes/ScopesApiClient.ts @@ -62,6 +62,10 @@ export class ScopesApiClient { }); } + /** + * @param parent + * @param query Filters by title substring + */ async fetchNode(parent: string, query: string): Promise { try { const nodes = diff --git a/public/app/features/scopes/ScopesContextProvider.tsx b/public/app/features/scopes/ScopesContextProvider.tsx index 8e47b0e4b3a..9519b10be3b 100644 --- a/public/app/features/scopes/ScopesContextProvider.tsx +++ b/public/app/features/scopes/ScopesContextProvider.tsx @@ -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 ( diff --git a/public/app/features/scopes/ScopesService.ts b/public/app/features/scopes/ScopesService.ts index 2c836470194..21969652552 100644 --- a/public/app/features/scopes/ScopesService.ts +++ b/public/app/features/scopes/ScopesService.ts @@ -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; + private subscriptions: Subscription[] = []; + constructor( private selectorService: ScopesSelectorService, - private dashboardsService: ScopesDashboardsService + private dashboardsService: ScopesDashboardsService, + private locationService: LocationService ) { this._state = new BehaviorSubject({ 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(); + } + } } diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 158be33d5f3..54b3aa6e5fb 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -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 { - 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 { let treeScopes = [...this.state.treeScopes]; @@ -110,6 +140,8 @@ export class ScopesSelectorService extends ScopesServiceBase 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 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 scope.metadata.name)); selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes); @@ -151,6 +192,9 @@ export class ScopesSelectorService extends ScopesServiceBase 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 { - 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((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((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>((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>((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; +} diff --git a/public/app/features/scopes/tests/dashboardsList.test.ts b/public/app/features/scopes/tests/dashboardsList.test.ts index 8c89619b062..eec2829b144 100644 --- a/public/app/features/scopes/tests/dashboardsList.test.ts +++ b/public/app/features/scopes/tests/dashboardsList.test.ts @@ -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]); }); diff --git a/public/app/features/scopes/tests/selector.test.ts b/public/app/features/scopes/tests/selector.test.ts index 251d19c0db7..e430a95ab4f 100644 --- a/public/app/features/scopes/tests/selector.test.ts +++ b/public/app/features/scopes/tests/selector.test.ts @@ -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]); }); diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts index 84f047a0768..51ebbc35219 100644 --- a/public/app/features/scopes/tests/tree.test.ts +++ b/public/app/features/scopes/tests/tree.test.ts @@ -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]); }); diff --git a/yarn.lock b/yarn.lock index 284b4ebf097..fe969bf0465 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: