- {childNode.isSelectable && !childNode.isExpanded ? (
+
+ {childNode.selectable && !childNode.expanded ? (
node.disableMultiSelect ? (
{
@@ -80,7 +80,7 @@ export function ScopesTreeItem({
/>
) : (
{
onNodeSelectToggle(childNodePath);
@@ -89,18 +89,18 @@ export function ScopesTreeItem({
)
) : null}
- {childNode.isExpandable ? (
+ {childNode.expandable ? (
@@ -110,7 +110,7 @@ export function ScopesTreeItem({
- {childNode.isExpanded && (
+ {childNode.expanded && (
);
- if (isLastExpandedNode) {
+ if (lastExpandedNode) {
return (
;
}
diff --git a/public/app/features/scopes/internal/ScopesTreeSearch.tsx b/public/app/features/scopes/selector/ScopesTreeSearch.tsx
similarity index 82%
rename from public/app/features/scopes/internal/ScopesTreeSearch.tsx
rename to public/app/features/scopes/selector/ScopesTreeSearch.tsx
index 273d2fa24b0..f8bc5c071ec 100644
--- a/public/app/features/scopes/internal/ScopesTreeSearch.tsx
+++ b/public/app/features/scopes/selector/ScopesTreeSearch.tsx
@@ -18,22 +18,22 @@ export interface ScopesTreeSearchProps {
export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpdate }: ScopesTreeSearchProps) {
const styles = useStyles2(getStyles);
- const [inputState, setInputState] = useState<{ value: string; isDirty: boolean }>({ value: query, isDirty: false });
+ const [inputState, setInputState] = useState<{ value: string; dirty: boolean }>({ value: query, dirty: false });
useEffect(() => {
- if (!inputState.isDirty && inputState.value !== query) {
- setInputState({ value: query, isDirty: false });
+ if (!inputState.dirty && inputState.value !== query) {
+ setInputState({ value: query, dirty: false });
}
}, [inputState, query]);
useDebounce(
() => {
- if (inputState.isDirty) {
+ if (inputState.dirty) {
onNodeUpdate(nodePath, true, inputState.value);
}
},
500,
- [inputState.isDirty, inputState.value]
+ [inputState.dirty, inputState.value]
);
if (anyChildExpanded) {
@@ -48,7 +48,7 @@ export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpda
data-testid="scopes-tree-search"
escapeRegex={false}
onChange={(value) => {
- setInputState({ value, isDirty: true });
+ setInputState({ value, dirty: true });
}}
/>
);
diff --git a/public/app/features/scopes/selector/types.ts b/public/app/features/scopes/selector/types.ts
new file mode 100644
index 00000000000..672a6f158b4
--- /dev/null
+++ b/public/app/features/scopes/selector/types.ts
@@ -0,0 +1,31 @@
+import { Scope, ScopeNodeSpec } from '@grafana/data';
+
+export enum NodeReason {
+ Persisted,
+ Result,
+}
+
+export interface Node extends ScopeNodeSpec {
+ name: string;
+ reason: NodeReason;
+ expandable: boolean;
+ selectable: boolean;
+ expanded: boolean;
+ query: string;
+ nodes: NodesMap;
+}
+
+export type NodesMap = Record;
+
+export interface SelectedScope {
+ scope: Scope;
+ path: string[];
+}
+
+export interface TreeScope {
+ scopeName: string;
+ path: string[];
+}
+
+export type OnNodeUpdate = (path: string[], expanded: boolean, query: string) => void;
+export type OnNodeSelectToggle = (path: string[]) => void;
diff --git a/public/app/features/scopes/tests/dashboardReload.test.ts b/public/app/features/scopes/tests/dashboardReload.test.ts
index 24b07bca51a..7973842c33e 100644
--- a/public/app/features/scopes/tests/dashboardReload.test.ts
+++ b/public/app/features/scopes/tests/dashboardReload.test.ts
@@ -2,8 +2,7 @@ import { config } from '@grafana/runtime';
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
-import { clearMocks, enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
-import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions';
+import { enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@@ -17,6 +16,8 @@ jest.mock('@grafana/runtime', () => ({
}));
describe('Dashboard reload', () => {
+ let dashboardReloadSpy: jest.SpyInstance;
+
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
@@ -39,43 +40,45 @@ describe('Dashboard reload', () => {
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
setDashboardAPI(undefined);
- const dashboardScene = renderDashboard({ uid: withUid ? 'dash-1' : undefined }, { reloadOnParamsChange });
+ const dashboardScene = await renderDashboard({ uid: withUid ? 'dash-1' : undefined }, { reloadOnParamsChange });
+
+ dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
if (editMode) {
await enterEditMode(dashboardScene);
}
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
+ dashboardReloadSpy.mockClear();
await updateTimeRange(dashboardScene);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
- expectNotDashboardReload();
+ expect(dashboardReloadSpy).not.toHaveBeenCalled();
} else {
- expectDashboardReload();
+ expect(dashboardReloadSpy).toHaveBeenCalled();
}
await updateMyVar(dashboardScene, '2');
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
- expectNotDashboardReload();
+ expect(dashboardReloadSpy).not.toHaveBeenCalled();
} else {
- expectDashboardReload();
+ expect(dashboardReloadSpy).toHaveBeenCalled();
}
await updateScopes(['grafana']);
await jest.advanceTimersToNextTimerAsync();
if (!shouldReload) {
- expectNotDashboardReload();
+ expect(dashboardReloadSpy).not.toHaveBeenCalled();
} else {
- expectDashboardReload();
+ expect(dashboardReloadSpy).toHaveBeenCalled();
}
getDashboardScenePageStateManager().clearDashboardCache();
getDashboardScenePageStateManager().clearSceneCache();
setDashboardAPI(undefined);
- await resetScenes();
- clearMocks();
+ await resetScenes([dashboardReloadSpy]);
}
);
});
diff --git a/public/app/features/scopes/tests/dashboardsList.test.ts b/public/app/features/scopes/tests/dashboardsList.test.ts
index 3b5f2fe1885..895337f69d2 100644
--- a/public/app/features/scopes/tests/dashboardsList.test.ts
+++ b/public/app/features/scopes/tests/dashboardsList.test.ts
@@ -1,5 +1,7 @@
import { config } from '@grafana/runtime';
+import { ScopesDashboardsService } from '../dashboards/ScopesDashboardsService';
+
import {
clearNotFound,
expandDashboardFolder,
@@ -21,7 +23,18 @@ import {
expectNoDashboardsNoScopes,
expectNoDashboardsSearch,
} from './utils/assertions';
-import { fetchDashboardsSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks';
+import {
+ alternativeDashboardWithRootFolder,
+ alternativeDashboardWithTwoFolders,
+ dashboardWithOneFolder,
+ dashboardWithoutFolder,
+ dashboardWithRootFolder,
+ dashboardWithRootFolderAndOtherFolder,
+ dashboardWithTwoFolders,
+ getDatasource,
+ getInstanceSettings,
+ getMock,
+} from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
jest.mock('@grafana/runtime', () => ({
@@ -34,17 +47,20 @@ jest.mock('@grafana/runtime', () => ({
}));
describe('Dashboards list', () => {
+ let fetchDashboardsSpy: jest.SpyInstance;
+
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
- beforeEach(() => {
- renderDashboard();
+ beforeEach(async () => {
+ await renderDashboard();
+ fetchDashboardsSpy = jest.spyOn(ScopesDashboardsService.instance!, 'fetchDashboardsApi');
});
afterEach(async () => {
- await resetScenes();
+ await resetScenes([fetchDashboardsSpy]);
});
it('Opens container and fetches dashboards list when a scope is selected', async () => {
@@ -244,14 +260,13 @@ describe('Dashboards list', () => {
});
it('Does not show the input when there are no dashboards found for scope', async () => {
- await toggleDashboards();
await updateScopes(['cloud']);
+ await toggleDashboards();
expectNoDashboardsForScope();
expectNoDashboardsSearch();
});
it('Shows the input and a message when there are no dashboards found for filter', async () => {
- await toggleDashboards();
await updateScopes(['mimir']);
await searchDashboards('unknown');
expectDashboardsSearch();
@@ -260,4 +275,359 @@ describe('Dashboards list', () => {
await clearNotFound();
expectDashboardSearchValue('');
});
+
+ describe('groupDashboards', () => {
+ it('Assigns dashboards without groups to root folder', () => {
+ expect(ScopesDashboardsService.instance?.groupDashboards([dashboardWithoutFolder])).toEqual({
+ '': {
+ title: '',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ [dashboardWithoutFolder.spec.dashboard]: {
+ dashboard: dashboardWithoutFolder.spec.dashboard,
+ dashboardTitle: dashboardWithoutFolder.status.dashboardTitle,
+ items: [dashboardWithoutFolder],
+ },
+ },
+ },
+ });
+ });
+
+ it('Assigns dashboards with root group to root folder', () => {
+ expect(ScopesDashboardsService.instance?.groupDashboards([dashboardWithRootFolder])).toEqual({
+ '': {
+ title: '',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ [dashboardWithRootFolder.spec.dashboard]: {
+ dashboard: dashboardWithRootFolder.spec.dashboard,
+ dashboardTitle: dashboardWithRootFolder.status.dashboardTitle,
+ items: [dashboardWithRootFolder],
+ },
+ },
+ },
+ });
+ });
+
+ it('Merges folders from multiple dashboards', () => {
+ expect(
+ ScopesDashboardsService.instance?.groupDashboards([dashboardWithOneFolder, dashboardWithTwoFolders])
+ ).toEqual({
+ '': {
+ title: '',
+ expanded: true,
+ folders: {
+ 'Folder 1': {
+ title: 'Folder 1',
+ expanded: false,
+ folders: {},
+ dashboards: {
+ [dashboardWithOneFolder.spec.dashboard]: {
+ dashboard: dashboardWithOneFolder.spec.dashboard,
+ dashboardTitle: dashboardWithOneFolder.status.dashboardTitle,
+ items: [dashboardWithOneFolder],
+ },
+ [dashboardWithTwoFolders.spec.dashboard]: {
+ dashboard: dashboardWithTwoFolders.spec.dashboard,
+ dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
+ items: [dashboardWithTwoFolders],
+ },
+ },
+ },
+ 'Folder 2': {
+ title: 'Folder 2',
+ expanded: false,
+ folders: {},
+ dashboards: {
+ [dashboardWithTwoFolders.spec.dashboard]: {
+ dashboard: dashboardWithTwoFolders.spec.dashboard,
+ dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
+ items: [dashboardWithTwoFolders],
+ },
+ },
+ },
+ },
+ dashboards: {},
+ },
+ });
+ });
+
+ it('Merges scopes from multiple dashboards', () => {
+ expect(
+ ScopesDashboardsService.instance?.groupDashboards([dashboardWithTwoFolders, alternativeDashboardWithTwoFolders])
+ ).toEqual({
+ '': {
+ title: '',
+ expanded: true,
+ folders: {
+ 'Folder 1': {
+ title: 'Folder 1',
+ expanded: false,
+ folders: {},
+ dashboards: {
+ [dashboardWithTwoFolders.spec.dashboard]: {
+ dashboard: dashboardWithTwoFolders.spec.dashboard,
+ dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
+ items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
+ },
+ },
+ },
+ 'Folder 2': {
+ title: 'Folder 2',
+ expanded: false,
+ folders: {},
+ dashboards: {
+ [dashboardWithTwoFolders.spec.dashboard]: {
+ dashboard: dashboardWithTwoFolders.spec.dashboard,
+ dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
+ items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
+ },
+ },
+ },
+ },
+ dashboards: {},
+ },
+ });
+ });
+
+ it('Matches snapshot', () => {
+ expect(
+ ScopesDashboardsService.instance?.groupDashboards([
+ dashboardWithoutFolder,
+ dashboardWithOneFolder,
+ dashboardWithTwoFolders,
+ alternativeDashboardWithTwoFolders,
+ dashboardWithRootFolder,
+ alternativeDashboardWithRootFolder,
+ dashboardWithRootFolderAndOtherFolder,
+ ])
+ ).toEqual({
+ '': {
+ dashboards: {
+ [dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
+ dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
+ dashboardTitle: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
+ items: [dashboardWithRootFolderAndOtherFolder],
+ },
+ [dashboardWithRootFolder.spec.dashboard]: {
+ dashboard: dashboardWithRootFolder.spec.dashboard,
+ dashboardTitle: dashboardWithRootFolder.status.dashboardTitle,
+ items: [dashboardWithRootFolder, alternativeDashboardWithRootFolder],
+ },
+ [dashboardWithoutFolder.spec.dashboard]: {
+ dashboard: dashboardWithoutFolder.spec.dashboard,
+ dashboardTitle: dashboardWithoutFolder.status.dashboardTitle,
+ items: [dashboardWithoutFolder],
+ },
+ },
+ folders: {
+ 'Folder 1': {
+ dashboards: {
+ [dashboardWithOneFolder.spec.dashboard]: {
+ dashboard: dashboardWithOneFolder.spec.dashboard,
+ dashboardTitle: dashboardWithOneFolder.status.dashboardTitle,
+ items: [dashboardWithOneFolder],
+ },
+ [dashboardWithTwoFolders.spec.dashboard]: {
+ dashboard: dashboardWithTwoFolders.spec.dashboard,
+ dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
+ items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
+ },
+ },
+ folders: {},
+ expanded: false,
+ title: 'Folder 1',
+ },
+ 'Folder 2': {
+ dashboards: {
+ [dashboardWithTwoFolders.spec.dashboard]: {
+ dashboard: dashboardWithTwoFolders.spec.dashboard,
+ dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
+ items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
+ },
+ },
+ folders: {},
+ expanded: false,
+ title: 'Folder 2',
+ },
+ 'Folder 3': {
+ dashboards: {
+ [dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
+ dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
+ dashboardTitle: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
+ items: [dashboardWithRootFolderAndOtherFolder],
+ },
+ },
+ folders: {},
+ expanded: false,
+ title: 'Folder 3',
+ },
+ },
+ expanded: true,
+ title: '',
+ },
+ });
+ });
+ });
+
+ describe('filterFolders', () => {
+ it('Shows folders matching criteria', () => {
+ expect(
+ ScopesDashboardsService.instance?.filterFolders(
+ {
+ '': {
+ title: '',
+ expanded: true,
+ folders: {
+ 'Folder 1': {
+ title: 'Folder 1',
+ expanded: false,
+ folders: {},
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ 'Folder 2': {
+ title: 'Folder 2',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ },
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ },
+ 'Folder'
+ )
+ ).toEqual({
+ '': {
+ title: '',
+ expanded: true,
+ folders: {
+ 'Folder 1': {
+ title: 'Folder 1',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ 'Folder 2': {
+ title: 'Folder 2',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ },
+ dashboards: {},
+ },
+ });
+ });
+
+ it('Shows dashboards matching criteria', () => {
+ expect(
+ ScopesDashboardsService.instance?.filterFolders(
+ {
+ '': {
+ title: '',
+ expanded: true,
+ folders: {
+ 'Folder 1': {
+ title: 'Folder 1',
+ expanded: false,
+ folders: {},
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ 'Folder 2': {
+ title: 'Folder 2',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ 'Random ID': {
+ dashboard: 'Random ID',
+ dashboardTitle: 'Random Title',
+ items: [],
+ },
+ },
+ },
+ },
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ 'Random ID': {
+ dashboard: 'Random ID',
+ dashboardTitle: 'Random Title',
+ items: [],
+ },
+ },
+ },
+ },
+ 'dash'
+ )
+ ).toEqual({
+ '': {
+ title: '',
+ expanded: true,
+ folders: {
+ 'Folder 1': {
+ title: 'Folder 1',
+ expanded: true,
+ folders: {},
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ },
+ dashboards: {
+ 'Dashboard ID': {
+ dashboard: 'Dashboard ID',
+ dashboardTitle: 'Dashboard Title',
+ items: [],
+ },
+ },
+ },
+ });
+ });
+ });
});
diff --git a/public/app/features/scopes/tests/featureFlag.test.ts b/public/app/features/scopes/tests/featureFlag.test.ts
deleted file mode 100644
index efe209a6f5b..00000000000
--- a/public/app/features/scopes/tests/featureFlag.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { config } from '@grafana/runtime';
-
-import { scopesSelectorScene } from '../instance';
-
-import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
-import { renderDashboard } from './utils/render';
-
-jest.mock('@grafana/runtime', () => ({
- __esModule: true,
- ...jest.requireActual('@grafana/runtime'),
- useChromeHeaderHeight: jest.fn(),
- getBackendSrv: () => ({ get: getMock }),
- getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }),
- usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
-}));
-
-describe('Feature flag off', () => {
- beforeAll(() => {
- config.featureToggles.scopeFilters = false;
- config.featureToggles.groupByVariable = true;
- });
-
- it('Does not initialize', () => {
- renderDashboard();
- expect(scopesSelectorScene).toBeNull();
- });
-});
diff --git a/public/app/features/scopes/tests/selector.test.ts b/public/app/features/scopes/tests/selector.test.ts
index 6435de62803..951c6f54b2e 100644
--- a/public/app/features/scopes/tests/selector.test.ts
+++ b/public/app/features/scopes/tests/selector.test.ts
@@ -1,13 +1,13 @@
import { config } from '@grafana/runtime';
-import { sceneGraph } from '@grafana/scenes';
-import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
-import { getClosestScopesFacade } from '../utils';
+import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager';
+import { ScopesSelectorService } from '../selector/ScopesSelectorService';
import { applyScopes, cancelScopes, openSelector, selectResultCloud, updateScopes } from './utils/actions';
-import { expectNotDashboardReload, expectScopesSelectorValue } from './utils/assertions';
-import { fetchSelectedScopesSpy, getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks';
+import { expectScopesSelectorValue } from './utils/assertions';
+import { getDatasource, getInstanceSettings, getMock, mocksScopes } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
+import { getListOfScopes } from './utils/selectors';
jest.mock('@grafana/runtime', () => ({
__esModule: true,
@@ -19,19 +19,22 @@ jest.mock('@grafana/runtime', () => ({
}));
describe('Selector', () => {
- let dashboardScene: DashboardScene;
+ let fetchSelectedScopesSpy: jest.SpyInstance;
+ let dashboardReloadSpy: jest.SpyInstance;
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
- beforeEach(() => {
- dashboardScene = renderDashboard();
+ beforeEach(async () => {
+ await renderDashboard();
+ fetchSelectedScopesSpy = jest.spyOn(ScopesSelectorService.instance!, 'fetchScopesApi');
+ dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
});
afterEach(async () => {
- await resetScenes();
+ await resetScenes([fetchSelectedScopesSpy, dashboardReloadSpy]);
});
it('Fetches scope details on save', async () => {
@@ -39,9 +42,7 @@ describe('Selector', () => {
await selectResultCloud();
await applyScopes();
expect(fetchSelectedScopesSpy).toHaveBeenCalled();
- expect(getClosestScopesFacade(dashboardScene)?.value).toEqual(
- mocksScopes.filter(({ metadata: { name } }) => name === 'cloud')
- );
+ expect(getListOfScopes()).toEqual(mocksScopes.filter(({ metadata: { name } }) => name === 'cloud'));
});
it('Does not save the scopes on close', async () => {
@@ -49,7 +50,7 @@ describe('Selector', () => {
await selectResultCloud();
await cancelScopes();
expect(fetchSelectedScopesSpy).not.toHaveBeenCalled();
- expect(getClosestScopesFacade(dashboardScene)?.value).toEqual([]);
+ expect(getListOfScopes()).toEqual([]);
});
it('Shows selected scopes', async () => {
@@ -59,25 +60,6 @@ describe('Selector', () => {
it('Does not reload the dashboard on scope change', async () => {
await updateScopes(['grafana']);
- expectNotDashboardReload();
- });
-
- it('Adds scopes to enrichers', async () => {
- const queryRunner = sceneGraph.getQueryController(dashboardScene)!;
-
- await updateScopes(['grafana']);
- let scopes = mocksScopes.filter(({ metadata: { name } }) => name === 'grafana');
- expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(scopes);
- expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(scopes);
-
- await updateScopes(['grafana', 'mimir']);
- scopes = mocksScopes.filter(({ metadata: { name } }) => name === 'grafana' || name === 'mimir');
- expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(scopes);
- expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(scopes);
-
- await updateScopes(['mimir']);
- scopes = mocksScopes.filter(({ metadata: { name } }) => name === 'mimir');
- expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual(scopes);
- expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(scopes);
+ expect(dashboardReloadSpy).not.toHaveBeenCalled();
});
});
diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts
index 983393ca204..186e6fe06f0 100644
--- a/public/app/features/scopes/tests/tree.test.ts
+++ b/public/app/features/scopes/tests/tree.test.ts
@@ -1,5 +1,7 @@
import { config } from '@grafana/runtime';
+import { ScopesSelectorService } from '../selector/ScopesSelectorService';
+
import {
applyScopes,
clearScopesSearch,
@@ -39,7 +41,7 @@ import {
expectSelectedScopePath,
expectTreeScopePath,
} from './utils/assertions';
-import { fetchNodesSpy, fetchScopeSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks';
+import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
jest.mock('@grafana/runtime', () => ({
@@ -52,17 +54,22 @@ jest.mock('@grafana/runtime', () => ({
}));
describe('Tree', () => {
+ let fetchNodesSpy: jest.SpyInstance;
+ let fetchScopeSpy: jest.SpyInstance;
+
beforeAll(() => {
config.featureToggles.scopeFilters = true;
config.featureToggles.groupByVariable = true;
});
- beforeEach(() => {
- renderDashboard();
+ beforeEach(async () => {
+ await renderDashboard();
+ fetchNodesSpy = jest.spyOn(ScopesSelectorService.instance!, 'fetchNodeApi');
+ fetchScopeSpy = jest.spyOn(ScopesSelectorService.instance!, 'fetchScopeApi');
});
afterEach(async () => {
- await resetScenes();
+ await resetScenes([fetchNodesSpy, fetchScopeSpy]);
});
it('Fetches scope details on select', async () => {
@@ -126,16 +133,16 @@ describe('Tree', () => {
await openSelector();
await expandResultApplications();
await searchScopes('Cloud');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectResultApplicationsGrafanaNotPresent();
expectResultApplicationsMimirNotPresent();
expectResultApplicationsCloudPresent();
await clearScopesSearch();
- expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
await searchScopes('Grafana');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(5);
expectResultApplicationsGrafanaPresent();
expectResultApplicationsCloudNotPresent();
});
@@ -156,7 +163,7 @@ describe('Tree', () => {
await expandResultApplications();
await selectResultApplicationsMimir();
await searchScopes('grafana');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectPersistedApplicationsMimirPresent();
expectPersistedApplicationsGrafanaNotPresent();
expectResultApplicationsMimirNotPresent();
@@ -168,7 +175,7 @@ describe('Tree', () => {
await expandResultApplications();
await selectResultApplicationsMimir();
await searchScopes('mimir');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectPersistedApplicationsMimirNotPresent();
expectResultApplicationsMimirPresent();
});
@@ -178,10 +185,10 @@ describe('Tree', () => {
await expandResultApplications();
await selectResultApplicationsMimir();
await searchScopes('grafana');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
await clearScopesSearch();
- expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
expectPersistedApplicationsMimirNotPresent();
expectPersistedApplicationsGrafanaNotPresent();
expectResultApplicationsMimirPresent();
@@ -192,15 +199,15 @@ describe('Tree', () => {
await openSelector();
await expandResultApplications();
await searchScopes('mimir');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
await selectResultApplicationsMimir();
await searchScopes('unknown');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
expectPersistedApplicationsMimirPresent();
await clearScopesSearch();
- expect(fetchNodesSpy).toHaveBeenCalledTimes(4);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(5);
expectResultApplicationsMimirPresent();
expectResultApplicationsGrafanaPresent();
});
@@ -210,7 +217,7 @@ describe('Tree', () => {
await expandResultApplications();
await selectResultApplicationsMimir();
await searchScopes('grafana');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
await selectResultApplicationsGrafana();
await applyScopes();
@@ -222,7 +229,7 @@ describe('Tree', () => {
await expandResultApplications();
await selectResultApplicationsMimir();
await searchScopes('grafana');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
await selectResultApplicationsGrafana();
await applyScopes();
@@ -239,11 +246,11 @@ describe('Tree', () => {
expectScopesHeadline('Recommended');
await searchScopes('Applications');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(1);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
expectScopesHeadline('Results');
await searchScopes('unknown');
- expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
+ expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectScopesHeadline('No results found for your query');
});
diff --git a/public/app/features/scopes/tests/utils.test.ts b/public/app/features/scopes/tests/utils.test.ts
deleted file mode 100644
index 7b37541fb05..00000000000
--- a/public/app/features/scopes/tests/utils.test.ts
+++ /dev/null
@@ -1,364 +0,0 @@
-import { filterFolders, groupDashboards } from '../internal/utils';
-
-import {
- alternativeDashboardWithRootFolder,
- alternativeDashboardWithTwoFolders,
- dashboardWithOneFolder,
- dashboardWithoutFolder,
- dashboardWithRootFolder,
- dashboardWithRootFolderAndOtherFolder,
- dashboardWithTwoFolders,
-} from './utils/mocks';
-
-describe('Utils', () => {
- describe('groupDashboards', () => {
- it('Assigns dashboards without groups to root folder', () => {
- expect(groupDashboards([dashboardWithoutFolder])).toEqual({
- '': {
- title: '',
- isExpanded: true,
- folders: {},
- dashboards: {
- [dashboardWithoutFolder.spec.dashboard]: {
- dashboard: dashboardWithoutFolder.spec.dashboard,
- dashboardTitle: dashboardWithoutFolder.status.dashboardTitle,
- items: [dashboardWithoutFolder],
- },
- },
- },
- });
- });
-
- it('Assigns dashboards with root group to root folder', () => {
- expect(groupDashboards([dashboardWithRootFolder])).toEqual({
- '': {
- title: '',
- isExpanded: true,
- folders: {},
- dashboards: {
- [dashboardWithRootFolder.spec.dashboard]: {
- dashboard: dashboardWithRootFolder.spec.dashboard,
- dashboardTitle: dashboardWithRootFolder.status.dashboardTitle,
- items: [dashboardWithRootFolder],
- },
- },
- },
- });
- });
-
- it('Merges folders from multiple dashboards', () => {
- expect(groupDashboards([dashboardWithOneFolder, dashboardWithTwoFolders])).toEqual({
- '': {
- title: '',
- isExpanded: true,
- folders: {
- 'Folder 1': {
- title: 'Folder 1',
- isExpanded: false,
- folders: {},
- dashboards: {
- [dashboardWithOneFolder.spec.dashboard]: {
- dashboard: dashboardWithOneFolder.spec.dashboard,
- dashboardTitle: dashboardWithOneFolder.status.dashboardTitle,
- items: [dashboardWithOneFolder],
- },
- [dashboardWithTwoFolders.spec.dashboard]: {
- dashboard: dashboardWithTwoFolders.spec.dashboard,
- dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
- items: [dashboardWithTwoFolders],
- },
- },
- },
- 'Folder 2': {
- title: 'Folder 2',
- isExpanded: false,
- folders: {},
- dashboards: {
- [dashboardWithTwoFolders.spec.dashboard]: {
- dashboard: dashboardWithTwoFolders.spec.dashboard,
- dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
- items: [dashboardWithTwoFolders],
- },
- },
- },
- },
- dashboards: {},
- },
- });
- });
-
- it('Merges scopes from multiple dashboards', () => {
- expect(groupDashboards([dashboardWithTwoFolders, alternativeDashboardWithTwoFolders])).toEqual({
- '': {
- title: '',
- isExpanded: true,
- folders: {
- 'Folder 1': {
- title: 'Folder 1',
- isExpanded: false,
- folders: {},
- dashboards: {
- [dashboardWithTwoFolders.spec.dashboard]: {
- dashboard: dashboardWithTwoFolders.spec.dashboard,
- dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
- items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
- },
- },
- },
- 'Folder 2': {
- title: 'Folder 2',
- isExpanded: false,
- folders: {},
- dashboards: {
- [dashboardWithTwoFolders.spec.dashboard]: {
- dashboard: dashboardWithTwoFolders.spec.dashboard,
- dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
- items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
- },
- },
- },
- },
- dashboards: {},
- },
- });
- });
-
- it('Matches snapshot', () => {
- expect(
- groupDashboards([
- dashboardWithoutFolder,
- dashboardWithOneFolder,
- dashboardWithTwoFolders,
- alternativeDashboardWithTwoFolders,
- dashboardWithRootFolder,
- alternativeDashboardWithRootFolder,
- dashboardWithRootFolderAndOtherFolder,
- ])
- ).toEqual({
- '': {
- dashboards: {
- [dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
- dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
- dashboardTitle: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
- items: [dashboardWithRootFolderAndOtherFolder],
- },
- [dashboardWithRootFolder.spec.dashboard]: {
- dashboard: dashboardWithRootFolder.spec.dashboard,
- dashboardTitle: dashboardWithRootFolder.status.dashboardTitle,
- items: [dashboardWithRootFolder, alternativeDashboardWithRootFolder],
- },
- [dashboardWithoutFolder.spec.dashboard]: {
- dashboard: dashboardWithoutFolder.spec.dashboard,
- dashboardTitle: dashboardWithoutFolder.status.dashboardTitle,
- items: [dashboardWithoutFolder],
- },
- },
- folders: {
- 'Folder 1': {
- dashboards: {
- [dashboardWithOneFolder.spec.dashboard]: {
- dashboard: dashboardWithOneFolder.spec.dashboard,
- dashboardTitle: dashboardWithOneFolder.status.dashboardTitle,
- items: [dashboardWithOneFolder],
- },
- [dashboardWithTwoFolders.spec.dashboard]: {
- dashboard: dashboardWithTwoFolders.spec.dashboard,
- dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
- items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
- },
- },
- folders: {},
- isExpanded: false,
- title: 'Folder 1',
- },
- 'Folder 2': {
- dashboards: {
- [dashboardWithTwoFolders.spec.dashboard]: {
- dashboard: dashboardWithTwoFolders.spec.dashboard,
- dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
- items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
- },
- },
- folders: {},
- isExpanded: false,
- title: 'Folder 2',
- },
- 'Folder 3': {
- dashboards: {
- [dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
- dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
- dashboardTitle: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
- items: [dashboardWithRootFolderAndOtherFolder],
- },
- },
- folders: {},
- isExpanded: false,
- title: 'Folder 3',
- },
- },
- isExpanded: true,
- title: '',
- },
- });
- });
- });
-
- describe('filterFolders', () => {
- it('Shows folders matching criteria', () => {
- expect(
- filterFolders(
- {
- '': {
- title: '',
- isExpanded: true,
- folders: {
- 'Folder 1': {
- title: 'Folder 1',
- isExpanded: false,
- folders: {},
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- 'Folder 2': {
- title: 'Folder 2',
- isExpanded: true,
- folders: {},
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- },
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- },
- 'Folder'
- )
- ).toEqual({
- '': {
- title: '',
- isExpanded: true,
- folders: {
- 'Folder 1': {
- title: 'Folder 1',
- isExpanded: true,
- folders: {},
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- 'Folder 2': {
- title: 'Folder 2',
- isExpanded: true,
- folders: {},
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- },
- dashboards: {},
- },
- });
- });
-
- it('Shows dashboards matching criteria', () => {
- expect(
- filterFolders(
- {
- '': {
- title: '',
- isExpanded: true,
- folders: {
- 'Folder 1': {
- title: 'Folder 1',
- isExpanded: false,
- folders: {},
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- 'Folder 2': {
- title: 'Folder 2',
- isExpanded: true,
- folders: {},
- dashboards: {
- 'Random ID': {
- dashboard: 'Random ID',
- dashboardTitle: 'Random Title',
- items: [],
- },
- },
- },
- },
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- 'Random ID': {
- dashboard: 'Random ID',
- dashboardTitle: 'Random Title',
- items: [],
- },
- },
- },
- },
- 'dash'
- )
- ).toEqual({
- '': {
- title: '',
- isExpanded: true,
- folders: {
- 'Folder 1': {
- title: 'Folder 1',
- isExpanded: true,
- folders: {},
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- },
- dashboards: {
- 'Dashboard ID': {
- dashboard: 'Dashboard ID',
- dashboardTitle: 'Dashboard Title',
- items: [],
- },
- },
- },
- });
- });
- });
-});
diff --git a/public/app/features/scopes/tests/utils/actions.ts b/public/app/features/scopes/tests/utils/actions.ts
index 9781fee2b6d..5972a430fc5 100644
--- a/public/app/features/scopes/tests/utils/actions.ts
+++ b/public/app/features/scopes/tests/utils/actions.ts
@@ -5,16 +5,8 @@ import { MultiValueVariable, sceneGraph, VariableValue } from '@grafana/scenes';
import { defaultTimeZone, TimeZone } from '@grafana/schema';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
-import { scopesSelectorScene } from '../../instance';
+import { ScopesService } from '../../ScopesService';
-import {
- dashboardReloadSpy,
- fetchDashboardsSpy,
- fetchNodesSpy,
- fetchScopeSpy,
- fetchSelectedScopesSpy,
- getMock,
-} from './mocks';
import {
getDashboardFolderExpand,
getDashboardsExpand,
@@ -37,30 +29,13 @@ import {
getTreeSearch,
} from './selectors';
-export const clearMocks = () => {
- fetchNodesSpy.mockClear();
- fetchScopeSpy.mockClear();
- fetchSelectedScopesSpy.mockClear();
- fetchDashboardsSpy.mockClear();
- dashboardReloadSpy.mockClear();
- getMock.mockClear();
-};
-
const click = async (selector: () => HTMLElement) => act(() => fireEvent.click(selector()));
const type = async (selector: () => HTMLInputElement, value: string) => {
await act(() => fireEvent.input(selector(), { target: { value } }));
await jest.runOnlyPendingTimersAsync();
};
-export const updateScopes = async (scopes: string[]) =>
- act(async () =>
- scopesSelectorScene?.updateScopes(
- scopes.map((scopeName) => ({
- scopeName,
- path: [],
- }))
- )
- );
+export const updateScopes = async (scopes: string[]) => act(async () => ScopesService.instance?.changeScopes(scopes));
export const openSelector = async () => click(getSelectorInput);
export const applyScopes = async () => {
await click(getSelectorApply);
diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts
index f58d73de50f..c842e81b3fa 100644
--- a/public/app/features/scopes/tests/utils/assertions.ts
+++ b/public/app/features/scopes/tests/utils/assertions.ts
@@ -1,4 +1,3 @@
-import { dashboardReloadSpy } from './mocks';
import {
getDashboard,
getDashboardsContainer,
@@ -21,7 +20,6 @@ import {
queryDashboard,
queryDashboardFolderExpand,
queryDashboardsContainer,
- queryDashboardsExpand,
queryDashboardsSearch,
queryPersistedApplicationsGrafanaSelect,
queryPersistedApplicationsMimirSelect,
@@ -29,7 +27,6 @@ import {
queryResultApplicationsGrafanaSelect,
queryResultApplicationsMimirSelect,
querySelectorApply,
- querySelectorInput,
} from './selectors';
const expectInDocument = (selector: () => HTMLElement) => expect(selector()).toBeInTheDocument();
@@ -42,7 +39,7 @@ const expectTextContent = (selector: () => HTMLElement, text: string) => expect(
const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled();
export const expectScopesSelectorClosed = () => expectNotInDocument(querySelectorApply);
-export const expectScopesSelectorNotInDocument = () => expectNotInDocument(querySelectorInput);
+export const expectScopesSelectorDisabled = () => expectDisabled(getSelectorInput);
export const expectScopesSelectorValue = (value: string) => expectValue(getSelectorInput, value);
export const expectScopesHeadline = (value: string) => expectTextContent(getTreeHeadline, value);
export const expectPersistedApplicationsGrafanaNotPresent = () =>
@@ -65,7 +62,6 @@ export const expectResultCloudOpsSelected = () => expectRadioChecked(getResultCl
export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getResultCloudOpsRadio);
export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand);
-export const expectDashboardsNotInDocument = () => expectNotInDocument(queryDashboardsExpand);
export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer);
export const expectDashboardsOpen = () => expectInDocument(getDashboardsContainer);
export const expectNoDashboardsSearch = () => expectNotInDocument(queryDashboardsSearch);
@@ -81,9 +77,6 @@ export const expectDashboardNotInDocument = (uid: string) => expectNotInDocument
export const expectDashboardLength = (uid: string, length: number) =>
expect(queryAllDashboard(uid)).toHaveLength(length);
-export const expectNotDashboardReload = () => expect(dashboardReloadSpy).not.toHaveBeenCalled();
-export const expectDashboardReload = () => expect(dashboardReloadSpy).toHaveBeenCalled();
-
export const expectSelectedScopePath = (name: string, path: string[] | undefined) =>
expect(getSelectedScope(name)?.path).toEqual(path);
export const expectTreeScopePath = (name: string, path: string[] | undefined) =>
diff --git a/public/app/features/scopes/tests/utils/mocks.ts b/public/app/features/scopes/tests/utils/mocks.ts
index 0ac0a785237..f61e251a8bb 100644
--- a/public/app/features/scopes/tests/utils/mocks.ts
+++ b/public/app/features/scopes/tests/utils/mocks.ts
@@ -2,8 +2,6 @@ import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
import { DataSourceRef } from '@grafana/schema/dist/esm/common/common.gen';
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
-import * as api from '../../internal/api';
-
export const mocksScopes: Scope[] = [
{
metadata: { name: 'cloud' },
@@ -369,10 +367,6 @@ export const mocksNodes: Array = [
},
] as const;
-export const fetchNodesSpy = jest.spyOn(api, 'fetchNodes');
-export const fetchScopeSpy = jest.spyOn(api, 'fetchScope');
-export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes');
-export const fetchDashboardsSpy = jest.spyOn(api, 'fetchDashboards');
export const dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
export const getMock = jest
diff --git a/public/app/features/scopes/tests/utils/render.tsx b/public/app/features/scopes/tests/utils/render.tsx
index 32ff0010d48..66072bd43b2 100644
--- a/public/app/features/scopes/tests/utils/render.tsx
+++ b/public/app/features/scopes/tests/utils/render.tsx
@@ -1,19 +1,21 @@
-import { cleanup } from '@testing-library/react';
+import { cleanup, waitFor } from '@testing-library/react';
import { KBarProvider } from 'kbar';
import { render } from 'test/test-utils';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { config, setPluginImportUtils } from '@grafana/runtime';
+import { sceneGraph } from '@grafana/scenes';
import { defaultDashboard } from '@grafana/schema';
import { AppChrome } from 'app/core/components/AppChrome/AppChrome';
import { transformSaveModelToScene } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene';
import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types';
-import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../../instance';
-import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene';
-import { initialSelectorState } from '../../internal/ScopesSelectorScene';
+import { ScopesContextProvider } from '../../ScopesContextProvider';
+import { ScopesService } from '../../ScopesService';
+import { ScopesDashboardsService } from '../../dashboards/ScopesDashboardsService';
+import { ScopesSelectorService } from '../../selector/ScopesSelectorService';
-import { clearMocks } from './actions';
+import { getMock } from './mocks';
const getDashboardDTO: (
overrideDashboard: Partial,
@@ -176,33 +178,38 @@ setPluginImportUtils({
getPanelPluginFromCache: () => undefined,
});
-export function renderDashboard(
+export async function renderDashboard(
overrideDashboard: Partial = {},
overrideMeta: Partial = {}
) {
jest.useFakeTimers({ advanceTimers: true });
jest.spyOn(console, 'error').mockImplementation(jest.fn());
- clearMocks();
- initializeScopes();
const dto: DashboardDTO = getDashboardDTO(overrideDashboard, overrideMeta);
const scene = transformSaveModelToScene(dto);
render(
-
-
-
+
+
+
+
+
);
+ await waitFor(() => expect(sceneGraph.getScopesBridge(scene)).toBeDefined());
+
return scene;
}
-export async function resetScenes() {
+export async function resetScenes(spies: jest.SpyInstance[] = []) {
await jest.runOnlyPendingTimersAsync();
jest.useRealTimers();
- scopesSelectorScene?.setState(initialSelectorState);
- scopesDashboardsScene?.setState(getInitialDashboardsState());
+ getMock.mockClear();
+ spies.forEach((spy) => spy.mockClear());
+ ScopesService.instance?.reset();
+ ScopesSelectorService.instance?.reset();
+ ScopesDashboardsService.instance?.reset();
cleanup();
}
diff --git a/public/app/features/scopes/tests/utils/selectors.ts b/public/app/features/scopes/tests/utils/selectors.ts
index 689e82ca87f..e5ef25e26bb 100644
--- a/public/app/features/scopes/tests/utils/selectors.ts
+++ b/public/app/features/scopes/tests/utils/selectors.ts
@@ -1,6 +1,7 @@
import { screen } from '@testing-library/react';
-import { scopesSelectorScene } from '../../instance';
+import { ScopesService } from '../../ScopesService';
+import { ScopesSelectorService } from '../../selector/ScopesSelectorService';
const selectors = {
tree: {
@@ -33,14 +34,12 @@ const selectors = {
};
export const getSelectorInput = () => screen.getByTestId(selectors.selector.input);
-export const querySelectorInput = () => screen.queryByTestId(selectors.selector.input);
export const querySelectorApply = () => screen.queryByTestId(selectors.selector.apply);
export const getSelectorApply = () => screen.getByTestId(selectors.selector.apply);
export const getSelectorCancel = () => screen.getByTestId(selectors.selector.cancel);
export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards.expand);
export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container);
-export const queryDashboardsExpand = () => screen.queryByTestId(selectors.dashboards.expand);
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
export const queryDashboardsSearch = () => screen.queryByTestId(selectors.dashboards.search);
export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search);
@@ -88,8 +87,9 @@ export const getResultCloudDevRadio = () =>
export const getResultCloudOpsRadio = () =>
screen.getByTestId(selectors.tree.radio('cloud-ops', 'result'));
-export const getListOfSelectedScopes = () => scopesSelectorScene?.state.scopes;
-export const getListOfTreeScopes = () => scopesSelectorScene?.state.treeScopes;
+export const getListOfScopes = () => ScopesService.instance?.state.value;
+export const getListOfSelectedScopes = () => ScopesSelectorService.instance?.state.selectedScopes;
+export const getListOfTreeScopes = () => ScopesSelectorService.instance?.state.treeScopes;
export const getSelectedScope = (name: string) =>
getListOfSelectedScopes()?.find((selectedScope) => selectedScope.scope.metadata.name === name);
export const getTreeScope = (name: string) => getListOfTreeScopes()?.find((treeScope) => treeScope.scopeName === name);
diff --git a/public/app/features/scopes/tests/viewMode.test.ts b/public/app/features/scopes/tests/viewMode.test.ts
index 89d698b9b0f..ba115534f98 100644
--- a/public/app/features/scopes/tests/viewMode.test.ts
+++ b/public/app/features/scopes/tests/viewMode.test.ts
@@ -1,14 +1,14 @@
import { config } from '@grafana/runtime';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
-import { scopesDashboardsScene, scopesSelectorScene } from '../instance';
+import { ScopesService } from '../ScopesService';
import { enterEditMode, openSelector, toggleDashboards } from './utils/actions';
import {
expectDashboardsClosed,
- expectDashboardsNotInDocument,
+ expectDashboardsDisabled,
expectScopesSelectorClosed,
- expectScopesSelectorNotInDocument,
+ expectScopesSelectorDisabled,
} from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@@ -30,8 +30,8 @@ describe('View mode', () => {
config.featureToggles.groupByVariable = true;
});
- beforeEach(() => {
- dashboardScene = renderDashboard();
+ beforeEach(async () => {
+ dashboardScene = await renderDashboard();
});
afterEach(async () => {
@@ -40,8 +40,8 @@ describe('View mode', () => {
it('Enters view mode', async () => {
await enterEditMode(dashboardScene);
- expect(scopesSelectorScene?.state?.isReadOnly).toEqual(true);
- expect(scopesDashboardsScene?.state?.isPanelOpened).toEqual(false);
+ expect(ScopesService.instance?.state.readOnly).toEqual(true);
+ expect(ScopesService.instance?.state.drawerOpened).toEqual(false);
});
it('Closes selector on enter', async () => {
@@ -58,11 +58,11 @@ describe('View mode', () => {
it('Does not show selector when view mode is active', async () => {
await enterEditMode(dashboardScene);
- expectScopesSelectorNotInDocument();
+ expectScopesSelectorDisabled();
});
it('Does not show the expand button when view mode is active', async () => {
await enterEditMode(dashboardScene);
- expectDashboardsNotInDocument();
+ expectDashboardsDisabled();
});
});
diff --git a/public/app/features/scopes/useScopesDashboardsState.ts b/public/app/features/scopes/useScopesDashboardsState.ts
deleted file mode 100644
index 7fc2e7d385a..00000000000
--- a/public/app/features/scopes/useScopesDashboardsState.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { scopesDashboardsScene } from './instance';
-
-export const useScopesDashboardsState = () => {
- return scopesDashboardsScene?.useState();
-};
diff --git a/public/app/features/scopes/utils.ts b/public/app/features/scopes/utils.ts
deleted file mode 100644
index 29ddd616382..00000000000
--- a/public/app/features/scopes/utils.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Scope } from '@grafana/data';
-import { sceneGraph, SceneObject } from '@grafana/scenes';
-
-import { ScopesFacade } from './ScopesFacadeScene';
-import { scopesDashboardsScene, scopesSelectorScene } from './instance';
-import { getScopesFromSelectedScopes } from './internal/utils';
-
-export function getSelectedScopes(): Scope[] {
- return getScopesFromSelectedScopes(scopesSelectorScene?.state.scopes ?? []);
-}
-
-export function getSelectedScopesNames(): string[] {
- return getSelectedScopes().map((scope) => scope.metadata.name);
-}
-
-export function enableScopes() {
- scopesSelectorScene?.enable();
- scopesDashboardsScene?.enable();
-}
-
-export function disableScopes() {
- scopesSelectorScene?.disable();
- scopesDashboardsScene?.disable();
-}
-
-export function exitScopesReadOnly() {
- scopesSelectorScene?.exitReadOnly();
- scopesDashboardsScene?.exitReadOnly();
-}
-
-export function enterScopesReadOnly() {
- scopesSelectorScene?.enterReadOnly();
- scopesDashboardsScene?.enterReadOnly();
-}
-
-export function getClosestScopesFacade(scene: SceneObject): ScopesFacade | null {
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- return sceneGraph.findObject(scene, (obj) => obj instanceof ScopesFacade) as ScopesFacade | null;
-}
diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx
index aef53bad053..3a6502f894f 100644
--- a/public/app/features/trails/DataTrail.tsx
+++ b/public/app/features/trails/DataTrail.tsx
@@ -31,7 +31,6 @@ import {
VariableValueSelectors,
} from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
-import { getSelectedScopes } from 'app/features/scopes';
import { DataTrailSettings } from './DataTrailSettings';
import { DataTrailHistory } from './DataTrailsHistory';
@@ -426,7 +425,11 @@ export class DataTrail extends SceneObjectBase implements SceneO
if (timeRange) {
const datasourceUid = sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR);
const otelTargets = await totalOtelResources(datasourceUid, timeRange);
- const deploymentEnvironments = await getDeploymentEnvironments(datasourceUid, timeRange, getSelectedScopes());
+ const deploymentEnvironments = await getDeploymentEnvironments(
+ datasourceUid,
+ timeRange,
+ sceneGraph.getScopesBridge(trail)?.getValue() ?? []
+ );
const hasOtelResources = otelTargets.jobs.length > 0 && otelTargets.instances.length > 0;
// loading from the url with otel resources selected will result in turning on OTel experience
const otelResourcesVariable = sceneGraph.lookupVariable(VAR_OTEL_AND_METRIC_FILTERS, this);
diff --git a/public/app/features/trails/DataTrailsApp.tsx b/public/app/features/trails/DataTrailsApp.tsx
index b1fdf17a467..156a77e3e47 100644
--- a/public/app/features/trails/DataTrailsApp.tsx
+++ b/public/app/features/trails/DataTrailsApp.tsx
@@ -1,20 +1,16 @@
-import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { Routes, Route } from 'react-router-dom-v5-compat';
-import {
- DataQueryRequest,
- DataSourceGetTagKeysOptions,
- DataSourceGetTagValuesOptions,
- PageLayoutType,
-} from '@grafana/data';
+import { PageLayoutType } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
-import { SceneComponentProps, SceneObjectBase, SceneObjectState, UrlSyncContextProvider } from '@grafana/scenes';
-import { useStyles2 } from '@grafana/ui/';
+import {
+ SceneComponentProps,
+ SceneObjectBase,
+ SceneObjectState,
+ SceneScopesBridge,
+ UrlSyncContextProvider,
+} from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page';
-import { getClosestScopesFacade, ScopesFacade, ScopesSelector } from 'app/features/scopes';
-
-import { AppChromeUpdate } from '../../core/components/AppChrome/AppChromeUpdate';
import { DataTrail } from './DataTrail';
import { DataTrailsHome } from './DataTrailsHome';
@@ -25,35 +21,14 @@ import { getMetricName, getUrlForTrail, newMetricsTrail } from './utils';
export interface DataTrailsAppState extends SceneObjectState {
trail: DataTrail;
home: DataTrailsHome;
+ scopesBridge?: SceneScopesBridge | undefined;
}
export class DataTrailsApp extends SceneObjectBase {
- private _scopesFacade: ScopesFacade | null;
+ protected _renderBeforeActivation = true;
public constructor(state: DataTrailsAppState) {
super(state);
-
- this._scopesFacade = getClosestScopesFacade(this);
- }
-
- public enrichDataRequest(): Partial {
- if (!config.featureToggles.promQLScope) {
- return {};
- }
-
- return {
- scopes: this._scopesFacade?.value,
- };
- }
-
- public enrichFiltersRequest(): Partial {
- if (!config.featureToggles.promQLScope) {
- return {};
- }
-
- return {
- scopes: this._scopesFacade?.value,
- };
}
goToUrlForTrail(trail: DataTrail) {
@@ -62,33 +37,35 @@ export class DataTrailsApp extends SceneObjectBase {
}
static Component = ({ model }: SceneComponentProps) => {
- const { trail, home } = model.useState();
+ const { trail, home, scopesBridge } = model.useState();
return (
-
- {/* The routes are relative to the HOME_ROUTE */}
- null}
- subTitle=""
- >
-
-
- }
- />
- } />
-
+ <>
+ {scopesBridge && }
+
+ {/* The routes are relative to the HOME_ROUTE */}
+ null}
+ subTitle=""
+ >
+
+
+ }
+ />
+ } />
+
+ >
);
};
}
function DataTrailView({ trail }: { trail: DataTrail }) {
- const styles = useStyles2(getStyles);
const [isInitialized, setIsInitialized] = useState(false);
const { metric } = trail.useState();
@@ -108,15 +85,6 @@ function DataTrailView({ trail }: { trail: DataTrail }) {
return (
- {config.featureToggles.enableScopesInMetricsExplore && (
-
-
-
- }
- />
- )}
@@ -127,37 +95,32 @@ let dataTrailsApp: DataTrailsApp;
export function getDataTrailsApp() {
if (!dataTrailsApp) {
- const $behaviors = config.featureToggles.enableScopesInMetricsExplore
- ? [
- new ScopesFacade({
- handler: (facade) => {
- const trail = facade.parent && 'trail' in facade.parent.state ? facade.parent.state.trail : undefined;
-
- if (trail instanceof DataTrail) {
- trail.publishEvent(new RefreshMetricsEvent());
- trail.checkDataSourceForOTelResources();
- }
- },
- }),
- ]
- : undefined;
+ const scopesBridge =
+ config.featureToggles.scopeFilters && config.featureToggles.enableScopesInMetricsExplore
+ ? new SceneScopesBridge({})
+ : undefined;
dataTrailsApp = new DataTrailsApp({
trail: newMetricsTrail(),
home: new DataTrailsHome({}),
- $behaviors,
+ scopesBridge,
+ $behaviors: [
+ () => {
+ scopesBridge?.setEnabled(true);
+
+ const sub = scopesBridge?.subscribeToValue(() => {
+ dataTrailsApp.state.trail.publishEvent(new RefreshMetricsEvent());
+ dataTrailsApp.state.trail.checkDataSourceForOTelResources();
+ });
+
+ return () => {
+ scopesBridge?.setEnabled(false);
+ sub?.unsubscribe();
+ };
+ },
+ ],
});
}
return dataTrailsApp;
}
-
-const getStyles = () => ({
- topNavContainer: css({
- width: '100%',
- height: '100%',
- display: 'flex',
- flexDirection: 'row',
- justifyItems: 'flex-start',
- }),
-});
diff --git a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
index 8a3fcd462e3..9cea458adaa 100644
--- a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
+++ b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
@@ -27,7 +27,6 @@ import {
} from '@grafana/scenes';
import { Alert, Badge, Field, Icon, IconButton, InlineSwitch, Input, Select, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
-import { getSelectedScopes } from 'app/features/scopes';
import { MetricScene } from '../MetricScene';
import { StatusWrapper } from '../StatusWrapper';
@@ -257,7 +256,7 @@ export class MetricSelectScene extends SceneObjectBase
i
const response = await getMetricNames(
datasourceUid,
timeRange,
- getSelectedScopes(),
+ sceneGraph.getScopesBridge(this)?.getValue() ?? [],
filters,
jobsList,
instancesList,
diff --git a/public/app/features/trails/utils.test.ts b/public/app/features/trails/utils.test.ts
index 04b15995982..9552b691641 100644
--- a/public/app/features/trails/utils.test.ts
+++ b/public/app/features/trails/utils.test.ts
@@ -50,6 +50,7 @@ describe('limitAdhocProviders', () => {
} as unknown as MetricDatasourceHelper;
dataTrail = {
+ forEachChild: jest.fn(),
getQueries: jest.fn().mockReturnValue([]),
} as unknown as DataTrail;
});
diff --git a/public/app/features/trails/utils.ts b/public/app/features/trails/utils.ts
index 860dc630a30..2b721161203 100644
--- a/public/app/features/trails/utils.ts
+++ b/public/app/features/trails/utils.ts
@@ -20,12 +20,12 @@ import {
SceneObject,
SceneObjectState,
SceneObjectUrlValues,
+ SceneScopesBridge,
SceneTimeRange,
sceneUtils,
SceneVariable,
SceneVariableState,
} from '@grafana/scenes';
-import { getClosestScopesFacade } from 'app/features/scopes';
import { getDatasourceSrv } from '../plugins/datasource_srv';
@@ -53,6 +53,10 @@ export function getTrailFor(model: SceneObject): DataTrail {
return sceneGraph.getAncestor(model, DataTrail);
}
+export function getScopesBridgeFor(model: SceneObject): SceneScopesBridge | undefined {
+ return sceneGraph.getScopesBridge(getTrailFor(model));
+}
+
export function getTrailSettings(model: SceneObject): DataTrailSettings {
return sceneGraph.getAncestor(model, DataTrail).state.settings;
}
@@ -193,7 +197,7 @@ export function limitAdhocProviders(
const opts = {
filters,
- scopes: getClosestScopesFacade(variable)?.value,
+ scopes: sceneGraph.getScopesBridge(dataTrail)?.getValue(),
queries: dataTrail.getQueries(),
};
@@ -237,7 +241,7 @@ export function limitAdhocProviders(
const opts = {
key: filter.key,
filters,
- scopes: getClosestScopesFacade(variable)?.value,
+ scopes: sceneGraph.getScopesBridge(dataTrail)?.getValue(),
queries: dataTrail.getQueries(),
};
diff --git a/yarn.lock b/yarn.lock
index 3171da05a1d..a65a3d0f5dc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -81,7 +81,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.26.2":
+"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.3, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.2":
version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2"
dependencies:
@@ -362,6 +362,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/parser@npm:^7.25.9":
+ version: 7.26.7
+ resolution: "@babel/parser@npm:7.26.7"
+ dependencies:
+ "@babel/types": "npm:^7.26.7"
+ bin:
+ parser: ./bin/babel-parser.js
+ checksum: 10/3ccc384366ca9a9b49c54f5b24c9d8cff9a505f2fbdd1cfc04941c8e1897084cc32f100e77900c12bc14a176cf88daa3c155faad680d9a23491b997fd2a59ffc
+ languageName: node
+ linkType: hard
+
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9"
@@ -1425,7 +1436,18 @@ __metadata:
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":
+"@babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3":
+ version: 7.25.9
+ resolution: "@babel/template@npm:7.25.9"
+ dependencies:
+ "@babel/code-frame": "npm:^7.25.9"
+ "@babel/parser": "npm:^7.25.9"
+ "@babel/types": "npm:^7.25.9"
+ checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2
+ languageName: node
+ linkType: hard
+
+"@babel/template@npm:^7.26.9":
version: 7.26.9
resolution: "@babel/template@npm:7.26.9"
dependencies:
@@ -1461,6 +1483,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/types@npm:^7.26.7":
+ version: 7.26.7
+ resolution: "@babel/types@npm:7.26.7"
+ dependencies:
+ "@babel/helper-string-parser": "npm:^7.25.9"
+ "@babel/helper-validator-identifier": "npm:^7.25.9"
+ checksum: 10/2264efd02cc261ca5d1c5bc94497c8995238f28afd2b7483b24ea64dd694cf46b00d51815bf0c87f0d0061ea221569c77893aeecb0d4b4bb254e9c2f938d7669
+ languageName: node
+ linkType: hard
+
"@bcoe/v8-coverage@npm:^0.2.3":
version: 0.2.3
resolution: "@bcoe/v8-coverage@npm:0.2.3"
@@ -3597,11 +3629,11 @@ __metadata:
languageName: unknown
linkType: soft
-"@grafana/scenes-react@npm:6.2.1":
- version: 6.2.1
- resolution: "@grafana/scenes-react@npm:6.2.1"
+"@grafana/scenes-react@npm:6.3.1":
+ version: 6.3.1
+ resolution: "@grafana/scenes-react@npm:6.3.1"
dependencies:
- "@grafana/scenes": "npm:6.2.1"
+ "@grafana/scenes": "npm:6.3.1"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3613,13 +3645,13 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
- checksum: 10/b8f44f087999fd6074233090a005de03e9d0ed1ba965a05659773cf48afb3c11ed508371fcf0c4cdc3c9a360b3f33e5126ed9082e2f4ee306459d768bd0a5a34
+ checksum: 10/77dd6f7bbe3699ece25435623a78f2ef5d831ab83b59612baa362581f7c3f6c02cb420b5acfff4e3d266872a694e08b98340370468bb246d102b20e9fcc79438
languageName: node
linkType: hard
-"@grafana/scenes@npm:6.2.1":
- version: 6.2.1
- resolution: "@grafana/scenes@npm:6.2.1"
+"@grafana/scenes@npm:6.3.1":
+ version: 6.3.1
+ resolution: "@grafana/scenes@npm:6.3.1"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3637,7 +3669,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
- checksum: 10/44e9a0386dd09a1a7a45bcdcbf285a00996e885cf56640179cbc35deb1f26af37ce34744321193f98aa078a7e02fc8a06b8f07c03b392229cc42ed56733bab05
+ checksum: 10/98e3e96b9ce12ae67aa458819dc9eed1eabd1b7421e768074bd97a0fa2a0b1e080d4121b1ddd357ce41ccaf9252439163aae7e612f12cba878ee6dca34f73831
languageName: node
linkType: hard
@@ -18071,8 +18103,8 @@ __metadata:
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/saga-icons": "workspace:*"
- "@grafana/scenes": "npm:6.2.1"
- "@grafana/scenes-react": "npm:6.2.1"
+ "@grafana/scenes": "npm:6.3.1"
+ "@grafana/scenes-react": "npm:6.3.1"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/tsconfig": "npm:^2.0.0"