From 2a2813b577b21b50bc077a04afe835ecfd98baee Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 12 Sep 2024 14:58:45 +0300 Subject: [PATCH] Scopes: Reload dashboards when on scopes change (#92804) --- .../scene/DashboardScopesFacade.ts | 22 + .../transformSaveModelToScene.ts | 8 +- .../features/dashboard/api/dashboard_api.ts | 2 +- .../scopes/tests/dashboardReload.test.ts | 38 + .../scopes/tests/dashboardsApi.test.ts | 106 +-- .../scopes/tests/dashboardsList.test.ts | 250 +++++++ .../features/scopes/tests/featureFlag.test.ts | 27 + .../app/features/scopes/tests/scopes.test.ts | 687 ------------------ .../features/scopes/tests/selector.test.ts | 83 +++ public/app/features/scopes/tests/tree.test.ts | 247 +++++++ .../app/features/scopes/tests/utils.test.ts | 664 +++++++++-------- .../features/scopes/tests/utils/actions.ts | 93 +++ .../features/scopes/tests/utils/assertions.ts | 82 +++ .../app/features/scopes/tests/utils/mocks.ts | 39 + .../features/scopes/tests/utils/render.tsx | 204 ++++-- .../features/scopes/tests/utils/selectors.ts | 32 +- .../features/scopes/tests/viewMode.test.ts | 64 ++ public/app/types/dashboard.ts | 4 + 18 files changed, 1471 insertions(+), 1181 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts create mode 100644 public/app/features/scopes/tests/dashboardReload.test.ts create mode 100644 public/app/features/scopes/tests/dashboardsList.test.ts create mode 100644 public/app/features/scopes/tests/featureFlag.test.ts delete mode 100644 public/app/features/scopes/tests/scopes.test.ts create mode 100644 public/app/features/scopes/tests/selector.test.ts create mode 100644 public/app/features/scopes/tests/tree.test.ts create mode 100644 public/app/features/scopes/tests/utils/actions.ts create mode 100644 public/app/features/scopes/tests/utils/assertions.ts create mode 100644 public/app/features/scopes/tests/viewMode.test.ts diff --git a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts new file mode 100644 index 00000000000..0b6267cde58 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts @@ -0,0 +1,22 @@ +import { locationService } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; +import { ScopesFacade } from 'app/features/scopes'; + +export interface DashboardScopesFacadeState { + reloadOnScopesChange?: boolean; + uid?: string; +} + +export class DashboardScopesFacade extends ScopesFacade { + constructor({ reloadOnScopesChange, uid }: DashboardScopesFacadeState) { + super({ + handler: (facade) => { + if (reloadOnScopesChange && uid) { + locationService.reload(); + } else { + sceneGraph.getTimeRange(facade).onRefresh(); + } + }, + }); + } +} diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 5f761ad10ab..0195625f24b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -19,10 +19,8 @@ import { SceneDataLayerProvider, SceneDataLayerControls, UserActionEvent, - sceneGraph, } from '@grafana/scenes'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { ScopesFacade } from 'app/features/scopes'; import { DashboardDTO, DashboardDataDTO } from 'app/types'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; @@ -32,6 +30,7 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem'; import { registerDashboardMacro } from '../scene/DashboardMacro'; import { DashboardScene } from '../scene/DashboardScene'; +import { DashboardScopesFacade } from '../scene/DashboardScopesFacade'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; @@ -245,8 +244,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, registerPanelInteractionsReporter, new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), preserveDashboardSceneStateInLocalStorage, - new ScopesFacade({ - handler: (facade) => sceneGraph.getTimeRange(facade).onRefresh(), + new DashboardScopesFacade({ + reloadOnScopesChange: oldModel.meta.reloadOnScopesChange, + uid: oldModel.uid, }), ], $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 661a02379a8..88ca1d3b01c 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -42,7 +42,7 @@ class LegacyDashboardAPI implements DashboardAPI { } getDashboardDTO(uid: string): Promise { - const scopes = getSelectedScopesNames(); + const scopes = config.featureToggles.passScopeToDashboardApi ? getSelectedScopesNames() : []; const queryParams = scopes.length > 0 ? { scopes } : undefined; return getBackendSrv().get(`/api/dashboards/uid/${uid}`, queryParams); diff --git a/public/app/features/scopes/tests/dashboardReload.test.ts b/public/app/features/scopes/tests/dashboardReload.test.ts new file mode 100644 index 00000000000..69e2e5ae02e --- /dev/null +++ b/public/app/features/scopes/tests/dashboardReload.test.ts @@ -0,0 +1,38 @@ +import { config } from '@grafana/runtime'; + +import { updateScopes } from './utils/actions'; +import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions'; +import { getDatasource, getInstanceSettings, getMock } from './utils/mocks'; +import { renderDashboard, resetScenes } from './utils/render'; + +jest.mock('@grafana/runtime', () => ({ + __esModule: true, + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), + getBackendSrv: () => ({ get: getMock }), + getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +describe('Dashboard reload', () => { + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.groupByVariable = true; + }); + + afterEach(async () => { + await resetScenes(); + }); + + it('Does not reload the dashboard without UID', async () => { + renderDashboard({ uid: undefined }, { reloadOnScopesChange: true }); + await updateScopes(['grafana']); + expectNotDashboardReload(); + }); + + it('Reloads the dashboard with UID', async () => { + renderDashboard({}, { reloadOnScopesChange: true }); + await updateScopes(['grafana']); + expectDashboardReload(); + }); +}); diff --git a/public/app/features/scopes/tests/dashboardsApi.test.ts b/public/app/features/scopes/tests/dashboardsApi.test.ts index 530f7b49717..8fe9448c662 100644 --- a/public/app/features/scopes/tests/dashboardsApi.test.ts +++ b/public/app/features/scopes/tests/dashboardsApi.test.ts @@ -1,84 +1,48 @@ -import { cleanup } from '@testing-library/react'; +import { config } from '@grafana/runtime'; +import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; -import { config, locationService } from '@grafana/runtime'; -import { getDashboardAPI, setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; - -import { initializeScopes } from '../instance'; - -import { getMock } from './utils/mocks'; -import { resetScenes } from './utils/render'; +import { getDashboardDTO, updateScopes } from './utils/actions'; +import { expectNewDashboardDTO, expectOldDashboardDTO } from './utils/assertions'; +import { getDatasource, getInstanceSettings, getMock } from './utils/mocks'; +import { renderDashboard, resetScenes } from './utils/render'; jest.mock('@grafana/runtime', () => ({ __esModule: true, ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => ({ - get: getMock, - }), + useChromeHeaderHeight: jest.fn(), + getBackendSrv: () => ({ get: getMock }), + getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), })); -describe('Scopes', () => { - describe('Dashboards API', () => { - describe('Feature flag off', () => { - beforeAll(() => { - config.featureToggles.scopeFilters = true; - config.featureToggles.passScopeToDashboardApi = false; - }); +const runTest = async (passScopes: boolean, kubernetesApi: boolean) => { + config.featureToggles.scopeFilters = true; + config.featureToggles.passScopeToDashboardApi = passScopes; + config.featureToggles.kubernetesDashboards = kubernetesApi; + setDashboardAPI(undefined); + renderDashboard({}, { reloadOnScopesChange: true }); + await updateScopes(['grafana', 'mimir']); + await getDashboardDTO(); - beforeEach(() => { - setDashboardAPI(undefined); - locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3'); - }); + if (kubernetesApi) { + return expectNewDashboardDTO(); + } - afterEach(() => { - resetScenes(); - cleanup(); - }); + if (passScopes) { + return expectOldDashboardDTO(['grafana', 'mimir']); + } - it('Legacy API should not pass the scopes', async () => { - config.featureToggles.kubernetesDashboards = false; - await getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', undefined); - }); + return expectOldDashboardDTO(); +}; - it('K8s API should not pass the scopes', async () => { - config.featureToggles.kubernetesDashboards = true; - await getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith( - '/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto' - ); - }); - }); - - describe('Feature flag on', () => { - beforeAll(() => { - config.featureToggles.scopeFilters = true; - config.featureToggles.passScopeToDashboardApi = true; - }); - - beforeEach(() => { - setDashboardAPI(undefined); - locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3'); - initializeScopes(); - }); - - afterEach(() => { - resetScenes(); - cleanup(); - }); - - it('Legacy API should pass the scopes', async () => { - config.featureToggles.kubernetesDashboards = false; - await getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', { scopes: ['scope1', 'scope2', 'scope3'] }); - }); - - it('K8s API should not pass the scopes', async () => { - config.featureToggles.kubernetesDashboards = true; - await getDashboardAPI().getDashboardDTO('1'); - expect(getMock).toHaveBeenCalledWith( - '/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto' - ); - }); - }); +describe('Dashboards API', () => { + afterEach(async () => { + setDashboardAPI(undefined); + await resetScenes(); }); + + it('Legacy API should not pass the scopes with feature flag off', async () => runTest(false, false)); + it('K8s API should not pass the scopes with feature flag off', async () => runTest(false, true)); + it('Legacy API should pass the scopes with feature flag on', async () => runTest(true, false)); + it('K8s API should not pass the scopes with feature flag on', async () => runTest(true, true)); }); diff --git a/public/app/features/scopes/tests/dashboardsList.test.ts b/public/app/features/scopes/tests/dashboardsList.test.ts new file mode 100644 index 00000000000..e3ff44e04cd --- /dev/null +++ b/public/app/features/scopes/tests/dashboardsList.test.ts @@ -0,0 +1,250 @@ +import { config } from '@grafana/runtime'; + +import { + clearNotFound, + expandDashboardFolder, + searchDashboards, + toggleDashboards, + updateScopes, +} from './utils/actions'; +import { + expectDashboardFolderNotInDocument, + expectDashboardInDocument, + expectDashboardLength, + expectDashboardNotInDocument, + expectDashboardSearchValue, + expectDashboardsSearch, + expectNoDashboardsForFilter, + expectNoDashboardsForScope, + expectNoDashboardsNoScopes, + expectNoDashboardsSearch, +} from './utils/assertions'; +import { fetchDashboardsSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks'; +import { renderDashboard, resetScenes } from './utils/render'; + +jest.mock('@grafana/runtime', () => ({ + __esModule: true, + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), + getBackendSrv: () => ({ get: getMock }), + getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +describe('Dashboards list', () => { + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.groupByVariable = true; + }); + + beforeEach(() => { + renderDashboard(); + }); + + afterEach(async () => { + await resetScenes(); + }); + + it('Does not fetch dashboards list when the list is not expanded', async () => { + await updateScopes(['mimir']); + expect(fetchDashboardsSpy).not.toHaveBeenCalled(); + }); + + it('Fetches dashboards list when the list is expanded', async () => { + await toggleDashboards(); + await updateScopes(['mimir']); + expect(fetchDashboardsSpy).toHaveBeenCalled(); + }); + + it('Fetches dashboards list when the list is expanded after scope selection', async () => { + await updateScopes(['mimir']); + await toggleDashboards(); + expect(fetchDashboardsSpy).toHaveBeenCalled(); + }); + + it('Shows dashboards for multiple scopes', async () => { + await toggleDashboards(); + await updateScopes(['grafana']); + await expandDashboardFolder('General'); + await expandDashboardFolder('Observability'); + await expandDashboardFolder('Usage'); + expectDashboardFolderNotInDocument('Components'); + expectDashboardFolderNotInDocument('Investigations'); + expectDashboardInDocument('general-data-sources'); + expectDashboardInDocument('general-usage'); + expectDashboardInDocument('observability-backend-errors'); + expectDashboardInDocument('observability-backend-logs'); + expectDashboardInDocument('observability-frontend-errors'); + expectDashboardInDocument('observability-frontend-logs'); + expectDashboardInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardInDocument('usage-usage-overview'); + expectDashboardInDocument('frontend'); + expectDashboardInDocument('overview'); + expectDashboardInDocument('stats'); + expectDashboardNotInDocument('multiple3-datasource-errors'); + expectDashboardNotInDocument('multiple4-datasource-logs'); + expectDashboardNotInDocument('multiple0-ingester'); + expectDashboardNotInDocument('multiple1-distributor'); + expectDashboardNotInDocument('multiple2-compacter'); + expectDashboardNotInDocument('another-stats'); + + await updateScopes(['grafana', 'mimir']); + await expandDashboardFolder('General'); + await expandDashboardFolder('Observability'); + await expandDashboardFolder('Usage'); + await expandDashboardFolder('Components'); + await expandDashboardFolder('Investigations'); + expectDashboardInDocument('general-data-sources'); + expectDashboardInDocument('general-usage'); + expectDashboardInDocument('observability-backend-errors'); + expectDashboardInDocument('observability-backend-logs'); + expectDashboardInDocument('observability-frontend-errors'); + expectDashboardInDocument('observability-frontend-logs'); + expectDashboardInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardInDocument('usage-usage-overview'); + expectDashboardInDocument('frontend'); + expectDashboardInDocument('overview'); + expectDashboardInDocument('stats'); + expectDashboardLength('multiple3-datasource-errors', 2); + expectDashboardLength('multiple4-datasource-logs', 2); + expectDashboardLength('multiple0-ingester', 2); + expectDashboardLength('multiple1-distributor', 2); + expectDashboardLength('multiple2-compacter', 2); + expectDashboardInDocument('another-stats'); + + await updateScopes(['grafana']); + await expandDashboardFolder('General'); + await expandDashboardFolder('Observability'); + await expandDashboardFolder('Usage'); + expectDashboardFolderNotInDocument('Components'); + expectDashboardFolderNotInDocument('Investigations'); + expectDashboardInDocument('general-data-sources'); + expectDashboardInDocument('general-usage'); + expectDashboardInDocument('observability-backend-errors'); + expectDashboardInDocument('observability-backend-logs'); + expectDashboardInDocument('observability-frontend-errors'); + expectDashboardInDocument('observability-frontend-logs'); + expectDashboardInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardInDocument('usage-usage-overview'); + expectDashboardInDocument('frontend'); + expectDashboardInDocument('overview'); + expectDashboardInDocument('stats'); + expectDashboardFolderNotInDocument('multiple3-datasource-errors'); + expectDashboardFolderNotInDocument('multiple4-datasource-logs'); + expectDashboardFolderNotInDocument('multiple0-ingester'); + expectDashboardFolderNotInDocument('multiple1-distributor'); + expectDashboardFolderNotInDocument('multiple2-compacter'); + expectDashboardFolderNotInDocument('another-stats'); + }); + + it('Filters the dashboards list for dashboards', async () => { + await toggleDashboards(); + await updateScopes(['grafana']); + await expandDashboardFolder('General'); + await expandDashboardFolder('Observability'); + await expandDashboardFolder('Usage'); + expectDashboardInDocument('general-data-sources'); + expectDashboardInDocument('general-usage'); + expectDashboardInDocument('observability-backend-errors'); + expectDashboardInDocument('observability-backend-logs'); + expectDashboardInDocument('observability-frontend-errors'); + expectDashboardInDocument('observability-frontend-logs'); + expectDashboardInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardInDocument('usage-usage-overview'); + expectDashboardInDocument('frontend'); + expectDashboardInDocument('overview'); + expectDashboardInDocument('stats'); + + await searchDashboards('Stats'); + expectDashboardFolderNotInDocument('general-data-sources'); + expectDashboardFolderNotInDocument('general-usage'); + expectDashboardFolderNotInDocument('observability-backend-errors'); + expectDashboardFolderNotInDocument('observability-backend-logs'); + expectDashboardFolderNotInDocument('observability-frontend-errors'); + expectDashboardFolderNotInDocument('observability-frontend-logs'); + expectDashboardFolderNotInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardFolderNotInDocument('usage-usage-overview'); + expectDashboardFolderNotInDocument('frontend'); + expectDashboardFolderNotInDocument('overview'); + expectDashboardInDocument('stats'); + }); + + it('Filters the dashboards list for folders', async () => { + await toggleDashboards(); + await updateScopes(['grafana']); + await expandDashboardFolder('General'); + await expandDashboardFolder('Observability'); + await expandDashboardFolder('Usage'); + expectDashboardInDocument('general-data-sources'); + expectDashboardInDocument('general-usage'); + expectDashboardInDocument('observability-backend-errors'); + expectDashboardInDocument('observability-backend-logs'); + expectDashboardInDocument('observability-frontend-errors'); + expectDashboardInDocument('observability-frontend-logs'); + expectDashboardInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardInDocument('usage-usage-overview'); + expectDashboardInDocument('frontend'); + expectDashboardInDocument('overview'); + expectDashboardInDocument('stats'); + + await searchDashboards('Usage'); + expectDashboardFolderNotInDocument('general-data-sources'); + expectDashboardInDocument('general-usage'); + expectDashboardFolderNotInDocument('observability-backend-errors'); + expectDashboardFolderNotInDocument('observability-backend-logs'); + expectDashboardFolderNotInDocument('observability-frontend-errors'); + expectDashboardFolderNotInDocument('observability-frontend-logs'); + expectDashboardInDocument('usage-data-sources'); + expectDashboardInDocument('usage-stats'); + expectDashboardInDocument('usage-usage-overview'); + expectDashboardFolderNotInDocument('frontend'); + expectDashboardFolderNotInDocument('overview'); + expectDashboardFolderNotInDocument('stats'); + }); + + it('Deduplicates the dashboards list', async () => { + await toggleDashboards(); + await updateScopes(['dev', 'ops']); + await expandDashboardFolder('Cardinality Management'); + await expandDashboardFolder('Usage Insights'); + expectDashboardLength('cardinality-management-labels', 1); + expectDashboardLength('cardinality-management-metrics', 1); + expectDashboardLength('cardinality-management-overview', 1); + expectDashboardLength('usage-insights-alertmanager', 1); + expectDashboardLength('usage-insights-data-sources', 1); + expectDashboardLength('usage-insights-metrics-ingestion', 1); + expectDashboardLength('usage-insights-overview', 1); + expectDashboardLength('usage-insights-query-errors', 1); + expectDashboardLength('billing-usage', 1); + }); + + it('Shows a proper message when no scopes are selected', async () => { + await toggleDashboards(); + expectNoDashboardsNoScopes(); + expectNoDashboardsSearch(); + }); + + it('Does not show the input when there are no dashboards found for scope', async () => { + await toggleDashboards(); + await updateScopes(['cloud']); + 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(); + expectNoDashboardsForFilter(); + + await clearNotFound(); + expectDashboardSearchValue(''); + }); +}); diff --git a/public/app/features/scopes/tests/featureFlag.test.ts b/public/app/features/scopes/tests/featureFlag.test.ts new file mode 100644 index 00000000000..af4a7a8daf9 --- /dev/null +++ b/public/app/features/scopes/tests/featureFlag.test.ts @@ -0,0 +1,27 @@ +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 }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +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/scopes.test.ts b/public/app/features/scopes/tests/scopes.test.ts deleted file mode 100644 index 5d886a73b5b..00000000000 --- a/public/app/features/scopes/tests/scopes.test.ts +++ /dev/null @@ -1,687 +0,0 @@ -import { act, cleanup, waitFor } from '@testing-library/react'; -import userEvents from '@testing-library/user-event'; - -import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { config, setPluginImportUtils } from '@grafana/runtime'; -import { sceneGraph } from '@grafana/scenes'; -import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; - -import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../instance'; -import { getClosestScopesFacade } from '../utils'; - -import { - fetchDashboardsSpy, - fetchNodesSpy, - fetchScopeSpy, - fetchSelectedScopesSpy, - getMock, - mocksScopes, -} from './utils/mocks'; -import { buildTestScene, renderDashboard, resetScenes } from './utils/render'; -import { - getDashboard, - getDashboardFolderExpand, - getDashboardsExpand, - getDashboardsSearch, - getNotFoundForFilter, - getNotFoundForFilterClear, - getNotFoundForScope, - getNotFoundNoScopes, - getPersistedApplicationsMimirSelect, - getPersistedApplicationsMimirTitle, - getResultApplicationsCloudDevSelect, - getResultApplicationsCloudExpand, - getResultApplicationsCloudOpsSelect, - getResultApplicationsCloudSelect, - getResultApplicationsExpand, - getResultApplicationsGrafanaSelect, - getResultApplicationsGrafanaTitle, - getResultApplicationsMimirSelect, - getResultApplicationsMimirTitle, - getResultCloudDevRadio, - getResultCloudExpand, - getResultCloudOpsRadio, - getResultCloudSelect, - getSelectorApply, - getSelectorCancel, - getSelectorInput, - getTreeHeadline, - getTreeSearch, - queryAllDashboard, - queryDashboard, - queryDashboardFolderExpand, - queryDashboardsContainer, - queryDashboardsSearch, - queryPersistedApplicationsGrafanaTitle, - queryPersistedApplicationsMimirTitle, - queryResultApplicationsCloudTitle, - queryResultApplicationsGrafanaTitle, - queryResultApplicationsMimirTitle, - querySelectorApply, -} from './utils/selectors'; - -jest.mock('@grafana/runtime', () => ({ - __esModule: true, - ...jest.requireActual('@grafana/runtime'), - useChromeHeaderHeight: jest.fn(), - getBackendSrv: () => ({ - get: getMock, - }), - usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), -})); - -const panelPlugin = getPanelPlugin({ - id: 'table', - skipDataQuery: true, -}); - -config.panels['table'] = panelPlugin.meta; - -setPluginImportUtils({ - importPanelPlugin: () => Promise.resolve(panelPlugin), - getPanelPluginFromCache: () => undefined, -}); - -describe('Scopes', () => { - describe('Feature flag off', () => { - beforeAll(() => { - config.featureToggles.scopeFilters = false; - config.featureToggles.groupByVariable = true; - - initializeScopes(); - }); - - it('Does not initialize', () => { - const dashboardScene = buildTestScene(); - dashboardScene.activate(); - expect(scopesSelectorScene).toBeNull(); - }); - }); - - describe('Feature flag on', () => { - let dashboardScene: DashboardScene; - - beforeAll(() => { - config.featureToggles.scopeFilters = true; - config.featureToggles.groupByVariable = true; - }); - - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(jest.fn()); - - fetchNodesSpy.mockClear(); - fetchScopeSpy.mockClear(); - fetchSelectedScopesSpy.mockClear(); - fetchDashboardsSpy.mockClear(); - getMock.mockClear(); - - initializeScopes(); - - dashboardScene = buildTestScene(); - - renderDashboard(dashboardScene); - }); - - afterEach(() => { - resetScenes(); - cleanup(); - }); - - describe('Tree', () => { - it('Navigates through scopes nodes', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsCloudExpand()); - await userEvents.click(getResultApplicationsExpand()); - }); - - it('Fetches scope details on select', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1)); - }); - - it('Selects the proper scopes', async () => { - await act(async () => - scopesSelectorScene?.updateScopes([ - { scopeName: 'grafana', path: [] }, - { scopeName: 'mimir', path: [] }, - ]) - ); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - expect(getResultApplicationsGrafanaSelect()).toBeChecked(); - expect(getResultApplicationsMimirSelect()).toBeChecked(); - }); - - it('Can select scopes from same level', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getResultApplicationsCloudSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toBe('Grafana, Mimir, Cloud'); - }); - - it('Can select a node from an inner level', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getResultApplicationsCloudExpand()); - await userEvents.click(getResultApplicationsCloudDevSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toBe('Dev'); - }); - - it('Can select a node from an upper level', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultCloudSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toBe('Cloud'); - }); - - it('Respects only one select per container', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultCloudExpand()); - await userEvents.click(getResultCloudDevRadio()); - expect(getResultCloudDevRadio().checked).toBe(true); - expect(getResultCloudOpsRadio().checked).toBe(false); - await userEvents.click(getResultCloudOpsRadio()); - expect(getResultCloudDevRadio().checked).toBe(false); - expect(getResultCloudOpsRadio().checked).toBe(true); - }); - - it('Search works', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.type(getTreeSearch(), 'Cloud'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - expect(queryResultApplicationsGrafanaTitle()).not.toBeInTheDocument(); - expect(queryResultApplicationsMimirTitle()).not.toBeInTheDocument(); - expect(getResultApplicationsCloudSelect()).toBeInTheDocument(); - await userEvents.clear(getTreeSearch()); - await userEvents.type(getTreeSearch(), 'Grafana'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4)); - expect(getResultApplicationsGrafanaSelect()).toBeInTheDocument(); - expect(queryResultApplicationsCloudTitle()).not.toBeInTheDocument(); - }); - - it('Opens to a selected scope', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultCloudExpand()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getSelectorInput()); - expect(queryResultApplicationsMimirTitle()).toBeInTheDocument(); - }); - - it('Persists a scope', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.type(getTreeSearch(), 'grafana'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - expect(getPersistedApplicationsMimirTitle()).toBeInTheDocument(); - expect(queryPersistedApplicationsGrafanaTitle()).not.toBeInTheDocument(); - expect(queryResultApplicationsMimirTitle()).not.toBeInTheDocument(); - expect(getResultApplicationsGrafanaTitle()).toBeInTheDocument(); - }); - - it('Does not persist a retrieved scope', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.type(getTreeSearch(), 'mimir'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - expect(queryPersistedApplicationsMimirTitle()).not.toBeInTheDocument(); - expect(getResultApplicationsMimirTitle()).toBeInTheDocument(); - }); - - it('Removes persisted nodes', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.type(getTreeSearch(), 'grafana'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - await userEvents.clear(getTreeSearch()); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4)); - expect(queryPersistedApplicationsMimirTitle()).not.toBeInTheDocument(); - expect(queryPersistedApplicationsGrafanaTitle()).not.toBeInTheDocument(); - expect(getResultApplicationsMimirTitle()).toBeInTheDocument(); - expect(getResultApplicationsGrafanaTitle()).toBeInTheDocument(); - }); - - it('Persists nodes from search', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.type(getTreeSearch(), 'mimir'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.type(getTreeSearch(), 'unknown'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4)); - expect(getPersistedApplicationsMimirTitle()).toBeInTheDocument(); - await userEvents.clear(getTreeSearch()); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(5)); - expect(getResultApplicationsMimirTitle()).toBeInTheDocument(); - expect(getResultApplicationsGrafanaTitle()).toBeInTheDocument(); - }); - - it('Selects a persisted scope', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.type(getTreeSearch(), 'grafana'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toBe('Mimir, Grafana'); - }); - - it('Deselects a persisted scope', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.type(getTreeSearch(), 'grafana'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toBe('Mimir, Grafana'); - await userEvents.click(getSelectorInput()); - await userEvents.click(getPersistedApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toBe('Grafana'); - }); - - it('Shows the proper headline', async () => { - await userEvents.click(getSelectorInput()); - expect(getTreeHeadline()).toHaveTextContent('Recommended'); - await userEvents.type(getTreeSearch(), 'Applications'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(2)); - expect(getTreeHeadline()).toHaveTextContent('Results'); - await userEvents.type(getTreeSearch(), 'unknown'); - await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3)); - expect(getTreeHeadline()).toHaveTextContent('No results found for your query'); - }); - }); - - describe('Selector', () => { - it('Opens', async () => { - await userEvents.click(getSelectorInput()); - expect(getSelectorApply()).toBeInTheDocument(); - }); - - it('Fetches scope details on save', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultCloudSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => expect(fetchSelectedScopesSpy).toHaveBeenCalled()); - expect(getClosestScopesFacade(dashboardScene)?.value).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'cloud') - ); - }); - - it('Does not save the scopes on close', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultCloudSelect()); - await userEvents.click(getSelectorCancel()); - await waitFor(() => expect(fetchSelectedScopesSpy).not.toHaveBeenCalled()); - expect(getClosestScopesFacade(dashboardScene)?.value).toEqual([]); - }); - - it('Shows selected scopes', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultCloudSelect()); - await userEvents.click(getSelectorApply()); - expect(getSelectorInput().value).toEqual('Cloud'); - }); - }); - - describe('Dashboards list', () => { - it('Toggles expanded state', async () => { - await userEvents.click(getDashboardsExpand()); - expect(getNotFoundNoScopes()).toBeInTheDocument(); - }); - - it('Does not fetch dashboards list when the list is not expanded', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => expect(fetchDashboardsSpy).not.toHaveBeenCalled()); - }); - - it('Fetches dashboards list when the list is expanded', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled()); - }); - - it('Fetches dashboards list when the list is expanded after scope selection', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardsExpand()); - await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled()); - }); - - it('Shows dashboards for multiple scopes', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardFolderExpand('General')); - await userEvents.click(getDashboardFolderExpand('Observability')); - await userEvents.click(getDashboardFolderExpand('Usage')); - expect(queryDashboardFolderExpand('Components')).not.toBeInTheDocument(); - expect(queryDashboardFolderExpand('Investigations')).not.toBeInTheDocument(); - expect(getDashboard('general-data-sources')).toBeInTheDocument(); - expect(getDashboard('general-usage')).toBeInTheDocument(); - expect(getDashboard('observability-backend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-backend-logs')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-logs')).toBeInTheDocument(); - expect(getDashboard('usage-data-sources')).toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(getDashboard('usage-usage-overview')).toBeInTheDocument(); - expect(getDashboard('frontend')).toBeInTheDocument(); - expect(getDashboard('overview')).toBeInTheDocument(); - expect(getDashboard('stats')).toBeInTheDocument(); - expect(queryDashboard('multiple3-datasource-errors')).not.toBeInTheDocument(); - expect(queryDashboard('multiple4-datasource-logs')).not.toBeInTheDocument(); - expect(queryDashboard('multiple0-ingester')).not.toBeInTheDocument(); - expect(queryDashboard('multiple1-distributor')).not.toBeInTheDocument(); - expect(queryDashboard('multiple2-compacter')).not.toBeInTheDocument(); - expect(queryDashboard('another-stats')).not.toBeInTheDocument(); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardFolderExpand('General')); - await userEvents.click(getDashboardFolderExpand('Observability')); - await userEvents.click(getDashboardFolderExpand('Usage')); - await userEvents.click(getDashboardFolderExpand('Components')); - await userEvents.click(getDashboardFolderExpand('Investigations')); - expect(getDashboard('general-data-sources')).toBeInTheDocument(); - expect(getDashboard('general-usage')).toBeInTheDocument(); - expect(getDashboard('observability-backend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-backend-logs')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-logs')).toBeInTheDocument(); - expect(getDashboard('usage-data-sources')).toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(getDashboard('usage-usage-overview')).toBeInTheDocument(); - expect(getDashboard('frontend')).toBeInTheDocument(); - expect(getDashboard('overview')).toBeInTheDocument(); - expect(getDashboard('stats')).toBeInTheDocument(); - expect(queryAllDashboard('multiple3-datasource-errors')).toHaveLength(2); - expect(queryAllDashboard('multiple4-datasource-logs')).toHaveLength(2); - expect(queryAllDashboard('multiple0-ingester')).toHaveLength(2); - expect(queryAllDashboard('multiple1-distributor')).toHaveLength(2); - expect(queryAllDashboard('multiple2-compacter')).toHaveLength(2); - expect(getDashboard('another-stats')).toBeInTheDocument(); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardFolderExpand('General')); - await userEvents.click(getDashboardFolderExpand('Observability')); - await userEvents.click(getDashboardFolderExpand('Usage')); - expect(queryDashboardFolderExpand('Components')).not.toBeInTheDocument(); - expect(queryDashboardFolderExpand('Investigations')).not.toBeInTheDocument(); - expect(getDashboard('general-data-sources')).toBeInTheDocument(); - expect(getDashboard('general-usage')).toBeInTheDocument(); - expect(getDashboard('observability-backend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-backend-logs')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-logs')).toBeInTheDocument(); - expect(getDashboard('usage-data-sources')).toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(getDashboard('usage-usage-overview')).toBeInTheDocument(); - expect(getDashboard('frontend')).toBeInTheDocument(); - expect(getDashboard('overview')).toBeInTheDocument(); - expect(getDashboard('stats')).toBeInTheDocument(); - expect(queryDashboard('multiple3-datasource-errors')).not.toBeInTheDocument(); - expect(queryDashboard('multiple4-datasource-logs')).not.toBeInTheDocument(); - expect(queryDashboard('multiple0-ingester')).not.toBeInTheDocument(); - expect(queryDashboard('multiple1-distributor')).not.toBeInTheDocument(); - expect(queryDashboard('multiple2-compacter')).not.toBeInTheDocument(); - expect(queryDashboard('another-stats')).not.toBeInTheDocument(); - }); - - it('Filters the dashboards list for dashboards', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardFolderExpand('General')); - await userEvents.click(getDashboardFolderExpand('Observability')); - await userEvents.click(getDashboardFolderExpand('Usage')); - expect(getDashboard('general-data-sources')).toBeInTheDocument(); - expect(getDashboard('general-usage')).toBeInTheDocument(); - expect(getDashboard('observability-backend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-backend-logs')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-logs')).toBeInTheDocument(); - expect(getDashboard('usage-data-sources')).toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(getDashboard('usage-usage-overview')).toBeInTheDocument(); - expect(getDashboard('frontend')).toBeInTheDocument(); - expect(getDashboard('overview')).toBeInTheDocument(); - expect(getDashboard('stats')).toBeInTheDocument(); - await userEvents.type(getDashboardsSearch(), 'Stats'); - await waitFor(() => { - expect(queryDashboard('general-data-sources')).not.toBeInTheDocument(); - expect(queryDashboard('general-usage')).not.toBeInTheDocument(); - expect(queryDashboard('observability-backend-errors')).not.toBeInTheDocument(); - expect(queryDashboard('observability-backend-logs')).not.toBeInTheDocument(); - expect(queryDashboard('observability-frontend-errors')).not.toBeInTheDocument(); - expect(queryDashboard('observability-frontend-logs')).not.toBeInTheDocument(); - expect(queryDashboard('usage-data-sources')).not.toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(queryDashboard('usage-usage-overview')).not.toBeInTheDocument(); - expect(queryDashboard('frontend')).not.toBeInTheDocument(); - expect(queryDashboard('overview')).not.toBeInTheDocument(); - expect(getDashboard('stats')).toBeInTheDocument(); - }); - }); - - it('Filters the dashboards list for folders', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardFolderExpand('General')); - await userEvents.click(getDashboardFolderExpand('Observability')); - await userEvents.click(getDashboardFolderExpand('Usage')); - expect(getDashboard('general-data-sources')).toBeInTheDocument(); - expect(getDashboard('general-usage')).toBeInTheDocument(); - expect(getDashboard('observability-backend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-backend-logs')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-errors')).toBeInTheDocument(); - expect(getDashboard('observability-frontend-logs')).toBeInTheDocument(); - expect(getDashboard('usage-data-sources')).toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(getDashboard('usage-usage-overview')).toBeInTheDocument(); - expect(getDashboard('frontend')).toBeInTheDocument(); - expect(getDashboard('overview')).toBeInTheDocument(); - expect(getDashboard('stats')).toBeInTheDocument(); - await userEvents.type(getDashboardsSearch(), 'Usage'); - await waitFor(() => { - expect(queryDashboard('general-data-sources')).not.toBeInTheDocument(); - expect(getDashboard('general-usage')).toBeInTheDocument(); - expect(queryDashboard('observability-backend-errors')).not.toBeInTheDocument(); - expect(queryDashboard('observability-backend-logs')).not.toBeInTheDocument(); - expect(queryDashboard('observability-frontend-errors')).not.toBeInTheDocument(); - expect(queryDashboard('observability-frontend-logs')).not.toBeInTheDocument(); - expect(getDashboard('usage-data-sources')).toBeInTheDocument(); - expect(getDashboard('usage-stats')).toBeInTheDocument(); - expect(getDashboard('usage-usage-overview')).toBeInTheDocument(); - expect(queryDashboard('frontend')).not.toBeInTheDocument(); - expect(queryDashboard('overview')).not.toBeInTheDocument(); - expect(queryDashboard('stats')).not.toBeInTheDocument(); - }); - }); - - it('Deduplicates the dashboards list', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsCloudExpand()); - await userEvents.click(getResultApplicationsCloudDevSelect()); - await userEvents.click(getResultApplicationsCloudOpsSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.click(getDashboardFolderExpand('Cardinality Management')); - await userEvents.click(getDashboardFolderExpand('Usage Insights')); - expect(queryAllDashboard('cardinality-management-labels')).toHaveLength(1); - expect(queryAllDashboard('cardinality-management-metrics')).toHaveLength(1); - expect(queryAllDashboard('cardinality-management-overview')).toHaveLength(1); - expect(queryAllDashboard('usage-insights-alertmanager')).toHaveLength(1); - expect(queryAllDashboard('usage-insights-data-sources')).toHaveLength(1); - expect(queryAllDashboard('usage-insights-metrics-ingestion')).toHaveLength(1); - expect(queryAllDashboard('usage-insights-overview')).toHaveLength(1); - expect(queryAllDashboard('usage-insights-query-errors')).toHaveLength(1); - expect(queryAllDashboard('billing-usage')).toHaveLength(1); - }); - - it('Shows a proper message when no scopes are selected', async () => { - await userEvents.click(getDashboardsExpand()); - expect(getNotFoundNoScopes()).toBeInTheDocument(); - expect(queryDashboardsSearch()).not.toBeInTheDocument(); - }); - - it('Does not show the input when there are no dashboards found for scope', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultCloudSelect()); - await userEvents.click(getSelectorApply()); - expect(getNotFoundForScope()).toBeInTheDocument(); - expect(queryDashboardsSearch()).not.toBeInTheDocument(); - }); - - it('Shows the input and a message when there are no dashboards found for filter', async () => { - await userEvents.click(getDashboardsExpand()); - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await userEvents.type(getDashboardsSearch(), 'unknown'); - await waitFor(() => { - expect(queryDashboardsSearch()).toBeInTheDocument(); - expect(getNotFoundForFilter()).toBeInTheDocument(); - }); - await userEvents.click(getNotFoundForFilterClear()); - await waitFor(() => { - expect(getDashboardsSearch().value).toBe(''); - }); - }); - }); - - describe('View mode', () => { - it('Enters view mode', async () => { - await act(async () => dashboardScene.onEnterEditMode()); - expect(scopesSelectorScene?.state?.isReadOnly).toEqual(true); - expect(scopesDashboardsScene?.state?.isPanelOpened).toEqual(false); - }); - - it('Closes selector on enter', async () => { - await userEvents.click(getSelectorInput()); - await act(async () => dashboardScene.onEnterEditMode()); - expect(querySelectorApply()).not.toBeInTheDocument(); - }); - - it('Closes dashboards list on enter', async () => { - await userEvents.click(getDashboardsExpand()); - await act(async () => dashboardScene.onEnterEditMode()); - expect(queryDashboardsContainer()).not.toBeInTheDocument(); - }); - - it('Does not open selector when view mode is active', async () => { - await act(async () => dashboardScene.onEnterEditMode()); - await userEvents.click(getSelectorInput()); - expect(querySelectorApply()).not.toBeInTheDocument(); - }); - - it('Disables the expand button when view mode is active', async () => { - await act(async () => dashboardScene.onEnterEditMode()); - expect(getDashboardsExpand()).toBeDisabled(); - }); - }); - - describe('Enrichers', () => { - it('Data requests', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => { - const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!; - expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'grafana') - ); - }); - - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => { - const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!; - expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'grafana' || name === 'mimir') - ); - }); - - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => { - const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!; - expect(dashboardScene.enrichDataRequest(queryRunner).scopes).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'mimir') - ); - }); - }); - - it('Filters requests', async () => { - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsExpand()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => { - expect(dashboardScene.enrichFiltersRequest().scopes).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'grafana') - ); - }); - - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsMimirSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => { - expect(dashboardScene.enrichFiltersRequest().scopes).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'grafana' || name === 'mimir') - ); - }); - - await userEvents.click(getSelectorInput()); - await userEvents.click(getResultApplicationsGrafanaSelect()); - await userEvents.click(getSelectorApply()); - await waitFor(() => { - expect(dashboardScene.enrichFiltersRequest().scopes).toEqual( - mocksScopes.filter(({ metadata: { name } }) => name === 'mimir') - ); - }); - }); - }); - }); -}); diff --git a/public/app/features/scopes/tests/selector.test.ts b/public/app/features/scopes/tests/selector.test.ts new file mode 100644 index 00000000000..78c002df870 --- /dev/null +++ b/public/app/features/scopes/tests/selector.test.ts @@ -0,0 +1,83 @@ +import { config } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; +import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; + +import { getClosestScopesFacade } from '../utils'; + +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 { renderDashboard, resetScenes } from './utils/render'; + +jest.mock('@grafana/runtime', () => ({ + __esModule: true, + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), + getBackendSrv: () => ({ get: getMock }), + getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +describe('Selector', () => { + let dashboardScene: DashboardScene; + + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.groupByVariable = true; + }); + + beforeEach(() => { + dashboardScene = renderDashboard(); + }); + + afterEach(async () => { + await resetScenes(); + }); + + it('Fetches scope details on save', async () => { + await openSelector(); + await selectResultCloud(); + await applyScopes(); + expect(fetchSelectedScopesSpy).toHaveBeenCalled(); + expect(getClosestScopesFacade(dashboardScene)?.value).toEqual( + mocksScopes.filter(({ metadata: { name } }) => name === 'cloud') + ); + }); + + it('Does not save the scopes on close', async () => { + await openSelector(); + await selectResultCloud(); + await cancelScopes(); + expect(fetchSelectedScopesSpy).not.toHaveBeenCalled(); + expect(getClosestScopesFacade(dashboardScene)?.value).toEqual([]); + }); + + it('Shows selected scopes', async () => { + await updateScopes(['grafana']); + expectScopesSelectorValue('Grafana'); + }); + + 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); + }); +}); diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts new file mode 100644 index 00000000000..a305da2eb43 --- /dev/null +++ b/public/app/features/scopes/tests/tree.test.ts @@ -0,0 +1,247 @@ +import { config } from '@grafana/runtime'; + +import { + applyScopes, + clearScopesSearch, + expandResultApplications, + expandResultApplicationsCloud, + expandResultCloud, + openSelector, + searchScopes, + selectPersistedApplicationsMimir, + selectResultApplicationsCloud, + selectResultApplicationsCloudDev, + selectResultApplicationsGrafana, + selectResultApplicationsMimir, + selectResultCloud, + selectResultCloudDev, + selectResultCloudOps, + updateScopes, +} from './utils/actions'; +import { + expectPersistedApplicationsGrafanaNotPresent, + expectPersistedApplicationsMimirNotPresent, + expectPersistedApplicationsMimirPresent, + expectResultApplicationsCloudNotPresent, + expectResultApplicationsCloudPresent, + expectResultApplicationsGrafanaNotPresent, + expectResultApplicationsGrafanaPresent, + expectResultApplicationsGrafanaSelected, + expectResultApplicationsMimirNotPresent, + expectResultApplicationsMimirPresent, + expectResultApplicationsMimirSelected, + expectResultCloudDevNotSelected, + expectResultCloudDevSelected, + expectResultCloudOpsNotSelected, + expectResultCloudOpsSelected, + expectScopesHeadline, + expectScopesSelectorValue, +} from './utils/assertions'; +import { fetchNodesSpy, fetchScopeSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks'; +import { renderDashboard, resetScenes } from './utils/render'; + +jest.mock('@grafana/runtime', () => ({ + __esModule: true, + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), + getBackendSrv: () => ({ get: getMock }), + getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +describe('Tree', () => { + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.groupByVariable = true; + }); + + beforeEach(() => { + renderDashboard(); + }); + + afterEach(async () => { + await resetScenes(); + }); + + it('Fetches scope details on select', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsGrafana(); + expect(fetchScopeSpy).toHaveBeenCalledTimes(1); + }); + + it('Selects the proper scopes', async () => { + await updateScopes(['grafana', 'mimir']); + await openSelector(); + await expandResultApplications(); + expectResultApplicationsGrafanaSelected(); + expectResultApplicationsMimirSelected(); + }); + + it('Can select scopes from same level', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsGrafana(); + await selectResultApplicationsMimir(); + await selectResultApplicationsCloud(); + await applyScopes(); + expectScopesSelectorValue('Grafana, Mimir, Cloud'); + }); + + it('Can select a node from an inner level', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsGrafana(); + await expandResultApplicationsCloud(); + await selectResultApplicationsCloudDev(); + await applyScopes(); + expectScopesSelectorValue('Dev'); + }); + + it('Can select a node from an upper level', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsGrafana(); + await expandResultApplications(); + await selectResultCloud(); + await applyScopes(); + expectScopesSelectorValue('Cloud'); + }); + + it('Respects only one select per container', async () => { + await openSelector(); + await expandResultCloud(); + await selectResultCloudDev(); + expectResultCloudDevSelected(); + expectResultCloudOpsNotSelected(); + + await selectResultCloudOps(); + expectResultCloudDevNotSelected(); + expectResultCloudOpsSelected(); + }); + + it('Search works', async () => { + await openSelector(); + await expandResultApplications(); + await searchScopes('Cloud'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + expectResultApplicationsGrafanaNotPresent(); + expectResultApplicationsMimirNotPresent(); + expectResultApplicationsCloudPresent(); + + await clearScopesSearch(); + expect(fetchNodesSpy).toHaveBeenCalledTimes(4); + + await searchScopes('Grafana'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(5); + expectResultApplicationsGrafanaPresent(); + expectResultApplicationsCloudNotPresent(); + }); + + it('Opens to a selected scope', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsMimir(); + await expandResultApplications(); + await expandResultCloud(); + await applyScopes(); + await openSelector(); + expectResultApplicationsMimirPresent(); + }); + + it('Persists a scope', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsMimir(); + await searchScopes('grafana'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + expectPersistedApplicationsMimirPresent(); + expectPersistedApplicationsGrafanaNotPresent(); + expectResultApplicationsMimirNotPresent(); + expectResultApplicationsGrafanaPresent(); + }); + + it('Does not persist a retrieved scope', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsMimir(); + await searchScopes('mimir'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + expectPersistedApplicationsMimirNotPresent(); + expectResultApplicationsMimirPresent(); + }); + + it('Removes persisted nodes', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsMimir(); + await searchScopes('grafana'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + + await clearScopesSearch(); + expect(fetchNodesSpy).toHaveBeenCalledTimes(4); + expectPersistedApplicationsMimirNotPresent(); + expectPersistedApplicationsGrafanaNotPresent(); + expectResultApplicationsMimirPresent(); + expectResultApplicationsGrafanaPresent(); + }); + + it('Persists nodes from search', async () => { + await openSelector(); + await expandResultApplications(); + await searchScopes('mimir'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + + await selectResultApplicationsMimir(); + await searchScopes('unknown'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(4); + expectPersistedApplicationsMimirPresent(); + + await clearScopesSearch(); + expect(fetchNodesSpy).toHaveBeenCalledTimes(5); + expectResultApplicationsMimirPresent(); + expectResultApplicationsGrafanaPresent(); + }); + + it('Selects a persisted scope', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsMimir(); + await searchScopes('grafana'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + + await selectResultApplicationsGrafana(); + await applyScopes(); + expectScopesSelectorValue('Mimir, Grafana'); + }); + + it('Deselects a persisted scope', async () => { + await openSelector(); + await expandResultApplications(); + await selectResultApplicationsMimir(); + await searchScopes('grafana'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(3); + + await selectResultApplicationsGrafana(); + await applyScopes(); + expectScopesSelectorValue('Mimir, Grafana'); + + await openSelector(); + await selectPersistedApplicationsMimir(); + await applyScopes(); + expectScopesSelectorValue('Grafana'); + }); + + it('Shows the proper headline', async () => { + await openSelector(); + expectScopesHeadline('Recommended'); + + await searchScopes('Applications'); + expect(fetchNodesSpy).toHaveBeenCalledTimes(2); + expectScopesHeadline('Results'); + + await searchScopes('unknown'); + 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 index 256a687b863..7b37541fb05 100644 --- a/public/app/features/scopes/tests/utils.test.ts +++ b/public/app/features/scopes/tests/utils.test.ts @@ -10,356 +10,354 @@ import { dashboardWithTwoFolders, } from './utils/mocks'; -describe('Scopes', () => { - 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], - }, +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: [], - }, - }, - }, + 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], }, - '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: [], - }, - }, - }, + 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], }, - dashboards: { - 'Dashboard ID': { - dashboard: 'Dashboard ID', - dashboardTitle: 'Dashboard Title', - items: [], - }, - 'Random ID': { - dashboard: 'Random ID', - dashboardTitle: 'Random Title', - items: [], - }, + [dashboardWithTwoFolders.spec.dashboard]: { + dashboard: dashboardWithTwoFolders.spec.dashboard, + dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle, + items: [dashboardWithTwoFolders], }, }, }, - 'dash' - ) - ).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: false, + folders: {}, + dashboards: { + [dashboardWithTwoFolders.spec.dashboard]: { + dashboard: dashboardWithTwoFolders.spec.dashboard, + dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle, + items: [dashboardWithTwoFolders], }, }, }, - dashboards: { - 'Dashboard ID': { - dashboard: 'Dashboard ID', - dashboardTitle: 'Dashboard Title', - items: [], - }, - }, }, - }); + 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 new file mode 100644 index 00000000000..77ae7fb0086 --- /dev/null +++ b/public/app/features/scopes/tests/utils/actions.ts @@ -0,0 +1,93 @@ +import { act, fireEvent } from '@testing-library/react'; + +import { getDashboardAPI, setDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; +import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; + +import { scopesSelectorScene } from '../../instance'; + +import { + fetchDashboardsSpy, + fetchNodesSpy, + fetchScopeSpy, + fetchSelectedScopesSpy, + getMock, + locationReloadSpy, +} from './mocks'; +import { + getDashboardFolderExpand, + getDashboardsExpand, + getDashboardsSearch, + getNotFoundForFilterClear, + getPersistedApplicationsMimirSelect, + getResultApplicationsCloudDevSelect, + getResultApplicationsCloudExpand, + getResultApplicationsCloudSelect, + getResultApplicationsExpand, + getResultApplicationsGrafanaSelect, + getResultApplicationsMimirSelect, + getResultCloudDevRadio, + getResultCloudExpand, + getResultCloudOpsRadio, + getResultCloudSelect, + getSelectorApply, + getSelectorCancel, + getSelectorInput, + getTreeSearch, +} from './selectors'; + +export const clearMocks = () => { + fetchNodesSpy.mockClear(); + fetchScopeSpy.mockClear(); + fetchSelectedScopesSpy.mockClear(); + fetchDashboardsSpy.mockClear(); + locationReloadSpy.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 openSelector = async () => click(getSelectorInput); +export const applyScopes = async () => { + await click(getSelectorApply); + await jest.runOnlyPendingTimersAsync(); +}; +export const cancelScopes = async () => click(getSelectorCancel); +export const searchScopes = async (value: string) => type(getTreeSearch, value); +export const clearScopesSearch = async () => type(getTreeSearch, ''); +export const expandResultApplications = async () => click(getResultApplicationsExpand); +export const expandResultApplicationsCloud = async () => click(getResultApplicationsCloudExpand); +export const expandResultCloud = async () => click(getResultCloudExpand); +export const selectResultApplicationsGrafana = async () => click(getResultApplicationsGrafanaSelect); +export const selectPersistedApplicationsMimir = async () => click(getPersistedApplicationsMimirSelect); +export const selectResultApplicationsMimir = async () => click(getResultApplicationsMimirSelect); +export const selectResultApplicationsCloud = async () => click(getResultApplicationsCloudSelect); +export const selectResultApplicationsCloudDev = async () => click(getResultApplicationsCloudDevSelect); +export const selectResultCloud = async () => click(getResultCloudSelect); +export const selectResultCloudDev = async () => click(getResultCloudDevRadio); +export const selectResultCloudOps = async () => click(getResultCloudOpsRadio); + +export const toggleDashboards = async () => click(getDashboardsExpand); +export const searchDashboards = async (value: string) => type(getDashboardsSearch, value); +export const clearNotFound = async () => click(getNotFoundForFilterClear); +export const expandDashboardFolder = (folder: string) => click(() => getDashboardFolderExpand(folder)); + +export const enterEditMode = async (dashboardScene: DashboardScene) => + act(async () => dashboardScene.onEnterEditMode()); + +export const getDashboardDTO = async () => { + setDashboardAPI(undefined); + await getDashboardAPI().getDashboardDTO('1'); +}; diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts new file mode 100644 index 00000000000..7ee5d4d1c58 --- /dev/null +++ b/public/app/features/scopes/tests/utils/assertions.ts @@ -0,0 +1,82 @@ +import { getMock, locationReloadSpy } from './mocks'; +import { + getDashboard, + getDashboardsExpand, + getDashboardsSearch, + getNotFoundForFilter, + getNotFoundForScope, + getNotFoundNoScopes, + getPersistedApplicationsMimirSelect, + getResultApplicationsCloudSelect, + getResultApplicationsGrafanaSelect, + getResultApplicationsMimirSelect, + getResultCloudDevRadio, + getResultCloudOpsRadio, + getSelectorInput, + getTreeHeadline, + queryAllDashboard, + queryDashboard, + queryDashboardFolderExpand, + queryDashboardsContainer, + queryDashboardsSearch, + queryPersistedApplicationsGrafanaSelect, + queryPersistedApplicationsMimirSelect, + queryResultApplicationsCloudSelect, + queryResultApplicationsGrafanaSelect, + queryResultApplicationsMimirSelect, + querySelectorApply, +} from './selectors'; + +const expectInDocument = (selector: () => HTMLElement) => expect(selector()).toBeInTheDocument(); +const expectNotInDocument = (selector: () => HTMLElement | null) => expect(selector()).not.toBeInTheDocument(); +const expectChecked = (selector: () => HTMLInputElement) => expect(selector()).toBeChecked(); +const expectRadioChecked = (selector: () => HTMLInputElement) => expect(selector().checked).toBe(true); +const expectRadioNotChecked = (selector: () => HTMLInputElement) => expect(selector().checked).toBe(false); +const expectValue = (selector: () => HTMLInputElement, value: string) => expect(selector().value).toBe(value); +const expectTextContent = (selector: () => HTMLElement, text: string) => expect(selector()).toHaveTextContent(text); +const expectDisabled = (selector: () => HTMLElement) => expect(selector()).toBeDisabled(); + +export const expectScopesSelectorClosed = () => expectNotInDocument(querySelectorApply); +export const expectScopesSelectorValue = (value: string) => expectValue(getSelectorInput, value); +export const expectScopesHeadline = (value: string) => expectTextContent(getTreeHeadline, value); +export const expectPersistedApplicationsGrafanaNotPresent = () => + expectNotInDocument(queryPersistedApplicationsGrafanaSelect); +export const expectResultApplicationsGrafanaSelected = () => expectChecked(getResultApplicationsGrafanaSelect); +export const expectResultApplicationsGrafanaPresent = () => expectInDocument(getResultApplicationsGrafanaSelect); +export const expectResultApplicationsGrafanaNotPresent = () => + expectNotInDocument(queryResultApplicationsGrafanaSelect); +export const expectPersistedApplicationsMimirPresent = () => expectInDocument(getPersistedApplicationsMimirSelect); +export const expectPersistedApplicationsMimirNotPresent = () => + expectNotInDocument(queryPersistedApplicationsMimirSelect); +export const expectResultApplicationsMimirSelected = () => expectChecked(getResultApplicationsMimirSelect); +export const expectResultApplicationsMimirPresent = () => expectInDocument(getResultApplicationsMimirSelect); +export const expectResultApplicationsMimirNotPresent = () => expectNotInDocument(queryResultApplicationsMimirSelect); +export const expectResultApplicationsCloudPresent = () => expectInDocument(getResultApplicationsCloudSelect); +export const expectResultApplicationsCloudNotPresent = () => expectNotInDocument(queryResultApplicationsCloudSelect); +export const expectResultCloudDevSelected = () => expectRadioChecked(getResultCloudDevRadio); +export const expectResultCloudDevNotSelected = () => expectRadioNotChecked(getResultCloudDevRadio); +export const expectResultCloudOpsSelected = () => expectRadioChecked(getResultCloudOpsRadio); +export const expectResultCloudOpsNotSelected = () => expectRadioNotChecked(getResultCloudOpsRadio); + +export const expectDashboardsDisabled = () => expectDisabled(getDashboardsExpand); +export const expectDashboardsClosed = () => expectNotInDocument(queryDashboardsContainer); +export const expectNoDashboardsSearch = () => expectNotInDocument(queryDashboardsSearch); +export const expectDashboardsSearch = () => expectInDocument(getDashboardsSearch); +export const expectNoDashboardsNoScopes = () => expectInDocument(getNotFoundNoScopes); +export const expectNoDashboardsForScope = () => expectInDocument(getNotFoundForScope); +export const expectNoDashboardsForFilter = () => expectInDocument(getNotFoundForFilter); +export const expectDashboardSearchValue = (value: string) => expectValue(getDashboardsSearch, value); +export const expectDashboardFolderNotInDocument = (uid: string) => + expectNotInDocument(() => queryDashboardFolderExpand(uid)); +export const expectDashboardInDocument = (uid: string) => expectInDocument(() => getDashboard(uid)); +export const expectDashboardNotInDocument = (uid: string) => expectNotInDocument(() => queryDashboard(uid)); +export const expectDashboardLength = (uid: string, length: number) => + expect(queryAllDashboard(uid)).toHaveLength(length); + +export const expectNotDashboardReload = () => expect(locationReloadSpy).not.toHaveBeenCalled(); +export const expectDashboardReload = () => expect(locationReloadSpy).toHaveBeenCalled(); + +export const expectOldDashboardDTO = (scopes?: string[]) => + expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', scopes ? { scopes } : undefined); +export const expectNewDashboardDTO = () => + expect(getMock).toHaveBeenCalledWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto'); diff --git a/public/app/features/scopes/tests/utils/mocks.ts b/public/app/features/scopes/tests/utils/mocks.ts index 2d67c87e0ae..924729c07cc 100644 --- a/public/app/features/scopes/tests/utils/mocks.ts +++ b/public/app/features/scopes/tests/utils/mocks.ts @@ -1,4 +1,6 @@ import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { DataSourceRef } from '@grafana/schema/dist/esm/common/common.gen'; import * as api from '../../internal/api'; @@ -371,6 +373,7 @@ 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 locationReloadSpy = jest.spyOn(locationService, 'reload'); export const getMock = jest .fn() @@ -448,3 +451,39 @@ export const dashboardWithRootFolderAndOtherFolder: ScopeDashboardBinding = gene 'With root folder and other folder', ['', 'Folder 3'] ); + +export const getDatasource = async (ref: DataSourceRef) => { + if (ref.uid === '-- Grafana --') { + return { + id: 1, + uid: '-- Grafana --', + name: 'grafana', + type: 'grafana', + meta: { + id: 'grafana', + }, + }; + } + + return { + meta: { + id: 'grafana-testdata-datasource', + }, + name: 'grafana-testdata-datasource', + type: 'grafana-testdata-datasource', + uid: 'gdev-testdata', + getRef: () => { + return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }; + }, + }; +}; + +export const getInstanceSettings = () => ({ + id: 1, + uid: 'gdev-testdata', + name: 'testDs1', + type: 'grafana-testdata-datasource', + meta: { + id: 'grafana-testdata-datasource', + }, +}); diff --git a/public/app/features/scopes/tests/utils/render.tsx b/public/app/features/scopes/tests/utils/render.tsx index 51986d242e1..538ed293770 100644 --- a/public/app/features/scopes/tests/utils/render.tsx +++ b/public/app/features/scopes/tests/utils/render.tsx @@ -1,92 +1,166 @@ +import { cleanup } from '@testing-library/react'; import { KBarProvider } from 'kbar'; import { render } from 'test/test-utils'; -import { - AdHocFiltersVariable, - behaviors, - GroupByVariable, - sceneGraph, - SceneGridItem, - SceneGridLayout, - SceneQueryRunner, - SceneTimeRange, - SceneVariableSet, - VizPanel, -} from '@grafana/scenes'; +import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; +import { config, setPluginImportUtils } from '@grafana/runtime'; +import { defaultDashboard } from '@grafana/schema'; import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; -import { DashboardControls } from 'app/features/dashboard-scene/scene//DashboardControls'; -import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; +import { transformSaveModelToScene } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene'; +import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types'; -import { ScopesFacade } from '../../ScopesFacadeScene'; -import { scopesDashboardsScene, scopesSelectorScene } from '../../instance'; +import { initializeScopes, scopesDashboardsScene, scopesSelectorScene } from '../../instance'; import { getInitialDashboardsState } from '../../internal/ScopesDashboardsScene'; import { initialSelectorState } from '../../internal/ScopesSelectorScene'; import { DASHBOARDS_OPENED_KEY } from '../../internal/const'; -export function buildTestScene(overrides: Partial = {}) { - return new DashboardScene({ +import { clearMocks } from './actions'; + +const getDashboardDTO: ( + overrideDashboard: Partial, + overrideMeta: Partial +) => DashboardDTO = (overrideDashboard, overrideMeta) => ({ + dashboard: { + ...defaultDashboard, title: 'hello', uid: 'dash-1', description: 'hello description', - tags: ['tag1', 'tag2'], - editable: true, - $timeRange: new SceneTimeRange({ - timeZone: 'browser', - }), - controls: new DashboardControls({}), - $behaviors: [ - new behaviors.CursorSync({}), - new ScopesFacade({ - handler: (facade) => sceneGraph.getTimeRange(facade).onRefresh(), - }), - ], - $variables: new SceneVariableSet({ - variables: [ - new AdHocFiltersVariable({ - name: 'adhoc', - datasource: { uid: 'my-ds-uid' }, - }), - new GroupByVariable({ - name: 'groupby', - datasource: { uid: 'my-ds-uid' }, - }), + templating: { + list: [ + { + datasource: { + type: 'datasource', + uid: 'grafana', + }, + filters: [], + name: 'Filters', + type: 'adhoc', + }, + { + current: { + text: [], + value: [], + }, + datasource: { + type: 'datasource', + uid: 'grafana', + }, + description: '', + label: 'Group By', + name: 'groupBy', + type: 'groupby', + }, ], - }), - body: new SceneGridLayout({ - children: [ - new SceneGridItem({ - key: 'griditem-1', + }, + panels: [ + { + datasource: { + type: 'datasource', + uid: 'grafana', + }, + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + align: 'auto', + cellOptions: { + type: 'auto', + }, + inspect: false, + }, + mappings: [], + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + gridPos: { + h: 8, + w: 12, x: 0, y: 0, - width: 300, - height: 300, - body: new VizPanel({ - title: 'Panel A', - key: 'panel-1', - pluginId: 'table', - $data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }), - }), - }), - ], - }), - ...overrides, - }); -} + }, + id: 1, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + pluginVersion: '11.3.0-pre', + targets: [ + { + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'table', + }, + ], + ...overrideDashboard, + }, + meta: { + ...overrideMeta, + }, +}); -export function renderDashboard(dashboardScene: DashboardScene) { - return render( +const panelPlugin = getPanelPlugin({ + id: 'table', + skipDataQuery: true, +}); + +config.panels['table'] = panelPlugin.meta; + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(panelPlugin), + getPanelPluginFromCache: () => undefined, +}); + +export 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( - + ); + + return scene; } -export function resetScenes() { +export async function resetScenes() { + await jest.runOnlyPendingTimersAsync(); + jest.useRealTimers(); scopesSelectorScene?.setState(initialSelectorState); - localStorage.removeItem(DASHBOARDS_OPENED_KEY); - scopesDashboardsScene?.setState(getInitialDashboardsState()); + cleanup(); } diff --git a/public/app/features/scopes/tests/utils/selectors.ts b/public/app/features/scopes/tests/utils/selectors.ts index 5ca61c056f6..655fe257420 100644 --- a/public/app/features/scopes/tests/utils/selectors.ts +++ b/public/app/features/scopes/tests/utils/selectors.ts @@ -53,36 +53,28 @@ export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dash export const getTreeSearch = () => screen.getByTestId(selectors.tree.search); export const getTreeHeadline = () => screen.getByTestId(selectors.tree.headline); export const getResultApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications', 'result')); -export const queryResultApplicationsGrafanaTitle = () => - screen.queryByTestId(selectors.tree.title('applications-grafana', 'result')); -export const getResultApplicationsGrafanaTitle = () => - screen.getByTestId(selectors.tree.title('applications-grafana', 'result')); +export const queryResultApplicationsGrafanaSelect = () => + screen.queryByTestId(selectors.tree.select('applications-grafana', 'result')); export const getResultApplicationsGrafanaSelect = () => - screen.getByTestId(selectors.tree.select('applications-grafana', 'result')); -export const queryPersistedApplicationsGrafanaTitle = () => - screen.queryByTestId(selectors.tree.title('applications-grafana', 'persisted')); -export const queryResultApplicationsMimirTitle = () => - screen.queryByTestId(selectors.tree.title('applications-mimir', 'result')); -export const getResultApplicationsMimirTitle = () => - screen.getByTestId(selectors.tree.title('applications-mimir', 'result')); + screen.getByTestId(selectors.tree.select('applications-grafana', 'result')); +export const queryPersistedApplicationsGrafanaSelect = () => + screen.queryByTestId(selectors.tree.select('applications-grafana', 'persisted')); +export const queryResultApplicationsMimirSelect = () => + screen.queryByTestId(selectors.tree.select('applications-mimir', 'result')); export const getResultApplicationsMimirSelect = () => - screen.getByTestId(selectors.tree.select('applications-mimir', 'result')); -export const queryPersistedApplicationsMimirTitle = () => - screen.queryByTestId(selectors.tree.title('applications-mimir', 'persisted')); -export const getPersistedApplicationsMimirTitle = () => - screen.getByTestId(selectors.tree.title('applications-mimir', 'persisted')); + screen.getByTestId(selectors.tree.select('applications-mimir', 'result')); +export const queryPersistedApplicationsMimirSelect = () => + screen.queryByTestId(selectors.tree.select('applications-mimir', 'persisted')); export const getPersistedApplicationsMimirSelect = () => screen.getByTestId(selectors.tree.select('applications-mimir', 'persisted')); -export const queryResultApplicationsCloudTitle = () => - screen.queryByTestId(selectors.tree.title('applications-cloud', 'result')); +export const queryResultApplicationsCloudSelect = () => + screen.queryByTestId(selectors.tree.select('applications-cloud', 'result')); export const getResultApplicationsCloudSelect = () => screen.getByTestId(selectors.tree.select('applications-cloud', 'result')); export const getResultApplicationsCloudExpand = () => screen.getByTestId(selectors.tree.expand('applications-cloud', 'result')); export const getResultApplicationsCloudDevSelect = () => screen.getByTestId(selectors.tree.select('applications-cloud-dev', 'result')); -export const getResultApplicationsCloudOpsSelect = () => - screen.getByTestId(selectors.tree.select('applications-cloud-ops', 'result')); export const getResultCloudSelect = () => screen.getByTestId(selectors.tree.select('cloud', 'result')); export const getResultCloudExpand = () => screen.getByTestId(selectors.tree.expand('cloud', 'result')); diff --git a/public/app/features/scopes/tests/viewMode.test.ts b/public/app/features/scopes/tests/viewMode.test.ts new file mode 100644 index 00000000000..4c39cadfdd9 --- /dev/null +++ b/public/app/features/scopes/tests/viewMode.test.ts @@ -0,0 +1,64 @@ +import { config } from '@grafana/runtime'; +import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; + +import { scopesDashboardsScene, scopesSelectorScene } from '../instance'; + +import { enterEditMode, openSelector, toggleDashboards } from './utils/actions'; +import { expectDashboardsClosed, expectDashboardsDisabled, expectScopesSelectorClosed } from './utils/assertions'; +import { getDatasource, getInstanceSettings, getMock } from './utils/mocks'; +import { renderDashboard, resetScenes } from './utils/render'; + +jest.mock('@grafana/runtime', () => ({ + __esModule: true, + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn(), + getBackendSrv: () => ({ get: getMock }), + getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }), + usePluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +describe('View mode', () => { + let dashboardScene: DashboardScene; + + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.groupByVariable = true; + }); + + beforeEach(() => { + dashboardScene = renderDashboard(); + }); + + afterEach(async () => { + await resetScenes(); + }); + + it('Enters view mode', async () => { + await enterEditMode(dashboardScene); + expect(scopesSelectorScene?.state?.isReadOnly).toEqual(true); + expect(scopesDashboardsScene?.state?.isPanelOpened).toEqual(false); + }); + + it('Closes selector on enter', async () => { + await openSelector(); + await enterEditMode(dashboardScene); + expectScopesSelectorClosed(); + }); + + it('Closes dashboards list on enter', async () => { + await toggleDashboards(); + await enterEditMode(dashboardScene); + expectDashboardsClosed(); + }); + + it('Does not open selector when view mode is active', async () => { + await enterEditMode(dashboardScene); + await openSelector(); + expectScopesSelectorClosed(); + }); + + it('Disables the expand button when view mode is active', async () => { + await enterEditMode(dashboardScene); + expectDashboardsDisabled(); + }); +}); diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 395255e2784..731ad1e9d49 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -73,6 +73,10 @@ export interface DashboardMeta { // yes weird, but this means all the editor structures can exist unchanged // until we use the resource as the main container k8s?: Partial; + + // This is a property added specifically for edge cases where dashboards should be reloaded on scopes changes + // This property is not persisted in the DB but its existence is controlled by the API + reloadOnScopesChange?: boolean; } export interface AnnotationActions {