diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index ee8fe2bbb17..da52a55447f 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -72,7 +72,10 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
AccessControlAction.AlertingSilenceRead,
]),
component: importAlertingComponent(
- () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
+ () =>
+ import(
+ /* webpackChunkName: "SilencesTablePage" */ 'app/features/alerting/unified/components/silences/SilencesTable'
+ )
),
},
{
@@ -84,13 +87,16 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
AccessControlAction.AlertingSilenceUpdate,
]),
component: importAlertingComponent(
- () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
+ () => import(/* webpackChunkName: "NewSilencePage" */ 'app/features/alerting/unified/NewSilencePage')
),
},
{
path: '/alerting/silence/:id/edit',
component: importAlertingComponent(
- () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
+ () =>
+ import(
+ /* webpackChunkName: "ExistingSilenceEditorPage" */ 'app/features/alerting/unified/components/silences/SilencesEditor'
+ )
),
},
{
diff --git a/public/app/features/alerting/unified/NewSilencePage.tsx b/public/app/features/alerting/unified/NewSilencePage.tsx
new file mode 100644
index 00000000000..859b9c3875e
--- /dev/null
+++ b/public/app/features/alerting/unified/NewSilencePage.tsx
@@ -0,0 +1,51 @@
+import { useLocation } from 'react-router-dom-v5-compat';
+
+import { withErrorBoundary } from '@grafana/ui';
+import {
+ defaultsFromQuery,
+ getDefaultSilenceFormValues,
+} from 'app/features/alerting/unified/components/silences/utils';
+import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/constants';
+import { parseQueryParamMatchers } from 'app/features/alerting/unified/utils/matchers';
+
+import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
+import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
+import { SilencesEditor } from './components/silences/SilencesEditor';
+import { useAlertmanager } from './state/AlertmanagerContext';
+
+const SilencesEditorComponent = () => {
+ const location = useLocation();
+ const queryParams = new URLSearchParams(location.search);
+ const { selectedAlertmanager = '' } = useAlertmanager();
+ const potentialAlertRuleMatcher = parseQueryParamMatchers(queryParams.getAll('matcher')).find(
+ (m) => m.name === MATCHER_ALERT_RULE_UID
+ );
+
+ const potentialRuleUid = potentialAlertRuleMatcher?.value;
+ const formValues = getDefaultSilenceFormValues(defaultsFromQuery(queryParams));
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+function NewSilencePage() {
+ const pageNav = {
+ id: 'silence-new',
+ text: 'Silence alert rule',
+ subTitle: 'Configure silences to stop notifications from a particular alert rule',
+ };
+ return (
+
+
+
+ );
+}
+export default withErrorBoundary(NewSilencePage, { style: 'page' });
diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx
index 009f39a8b7c..e2b2f134ca2 100644
--- a/public/app/features/alerting/unified/Silences.test.tsx
+++ b/public/app/features/alerting/unified/Silences.test.tsx
@@ -1,4 +1,4 @@
-import { useParams } from 'react-router-dom-v5-compat';
+import { Route, Routes } from 'react-router-dom-v5-compat';
import { render, screen, userEvent, waitFor, within } from 'test/test-utils';
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
@@ -17,7 +17,9 @@ import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/cons
import { MatcherOperator, SilenceState } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
-import Silences from './Silences';
+import NewSilencePage from './NewSilencePage';
+import ExistingSilenceEditorPage from './components/silences/SilencesEditor';
+import SilencesTablePage from './components/silences/SilencesTable';
import {
MOCK_SILENCE_ID_EXISTING,
MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID,
@@ -30,21 +32,21 @@ import { grafanaRulerRule } from './mocks/grafanaRulerApi';
import { setupDataSources } from './testSetup/datasources';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
-jest.mock('app/core/services/context_srv');
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useParams: jest.fn(),
-}));
-
const TEST_TIMEOUT = 60000;
const renderSilences = (location = '/alerting/silences/') => {
- return render(, {
- historyOptions: {
- initialEntries: [location],
- },
- });
+ return render(
+
+ } />
+ } />
+ } />
+ ,
+ {
+ historyOptions: {
+ initialEntries: [location],
+ },
+ }
+ );
};
const dataSources = {
@@ -124,8 +126,7 @@ describe('Silences', () => {
it(
'loads and shows silences',
async () => {
- const user = userEvent.setup();
- renderSilences();
+ const { user } = renderSilences();
expect(await ui.notExpiredTable.find()).toBeInTheDocument();
@@ -174,8 +175,7 @@ describe('Silences', () => {
it(
'filters silences by matchers',
async () => {
- const user = userEvent.setup();
- renderSilences();
+ const { user } = renderSilences();
const queryBar = await ui.queryBar.find();
await user.type(queryBar, 'foo=bar');
@@ -260,8 +260,7 @@ describe('Silence create/edit', () => {
it(
'creates a new silence',
async () => {
- const user = userEvent.setup();
- renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
+ const { user } = renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
expect(await ui.editor.durationField.find()).toBeInTheDocument();
const postRequest = waitForServerRequest(silenceCreateHandler());
@@ -320,20 +319,17 @@ describe('Silence create/edit', () => {
});
it('shows an error when existing silence cannot be found', async () => {
- (useParams as jest.Mock).mockReturnValue({ id: 'foo-bar' });
renderSilences('/alerting/silence/foo-bar/edit');
expect(await ui.existingSilenceNotFound.find()).toBeInTheDocument();
});
it('shows an error when user cannot edit/recreate silence', async () => {
- (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_LACKING_PERMISSIONS });
renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_LACKING_PERMISSIONS}/edit`);
expect(await ui.noPermissionToEdit.find()).toBeInTheDocument();
});
it('populates form with existing silence information', async () => {
- (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_EXISTING });
renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING}/edit`);
// Await the first value to be populated, after which we can expect that all of the other
@@ -344,7 +340,6 @@ describe('Silence create/edit', () => {
});
it('populates form with existing silence information that has __alert_rule_uid__', async () => {
- (useParams as jest.Mock).mockReturnValue({ id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID });
mockAlertRuleApi(server).getAlertRule(MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, grafanaRulerRule);
renderSilences(`/alerting/silence/${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}/edit`);
expect(await screen.findByLabelText(/alert rule/i)).toHaveValue(grafanaRulerRule.grafana_alert.title);
@@ -358,11 +353,9 @@ describe('Silence create/edit', () => {
it(
'silences page should contain alertmanager parameter after creating a silence',
async () => {
- const user = userEvent.setup();
-
const postRequest = waitForServerRequest(silenceCreateHandler());
- renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
+ const { user } = renderSilences(`${baseUrlPath}?alertmanager=${GRAFANA_RULES_SOURCE_NAME}`);
await waitFor(() => expect(ui.editor.durationField.query()).not.toBeNull());
await enterSilenceLabel(0, 'foo', MatcherOperator.equal, 'bar');
diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx
deleted file mode 100644
index bc257e65a2e..00000000000
--- a/public/app/features/alerting/unified/Silences.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { Route, Switch } from 'react-router-dom';
-import { useLocation } from 'react-router-dom-v5-compat';
-
-import { withErrorBoundary } from '@grafana/ui';
-import {
- defaultsFromQuery,
- getDefaultSilenceFormValues,
-} from 'app/features/alerting/unified/components/silences/utils';
-import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/constants';
-import { parseQueryParamMatchers } from 'app/features/alerting/unified/utils/matchers';
-
-import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
-import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
-import ExistingSilenceEditor, { SilencesEditor } from './components/silences/SilencesEditor';
-import SilencesTable from './components/silences/SilencesTable';
-import { useSilenceNavData } from './hooks/useSilenceNavData';
-import { useAlertmanager } from './state/AlertmanagerContext';
-
-const Silences = () => {
- const { selectedAlertmanager } = useAlertmanager();
-
- if (!selectedAlertmanager) {
- return null;
- }
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-function SilencesPage() {
- const pageNav = useSilenceNavData();
-
- return (
-
-
-
- );
-}
-
-export default withErrorBoundary(SilencesPage, { style: 'page' });
-
-type SilencesEditorComponentProps = {
- selectedAlertmanager: string;
-};
-const SilencesEditorComponent = ({ selectedAlertmanager }: SilencesEditorComponentProps) => {
- const location = useLocation();
- const queryParams = new URLSearchParams(location.search);
-
- const potentialAlertRuleMatcher = parseQueryParamMatchers(queryParams.getAll('matcher')).find(
- (m) => m.name === MATCHER_ALERT_RULE_UID
- );
-
- const potentialRuleUid = potentialAlertRuleMatcher?.value;
-
- const formValues = getDefaultSilenceFormValues(defaultsFromQuery(queryParams));
-
- return (
-
- );
-};
diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
index e9ce5661d3c..f4164badfad 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
@@ -25,6 +25,7 @@ import {
Stack,
TextArea,
useStyles2,
+ withErrorBoundary,
} from '@grafana/ui';
import { alertSilencesApi, SilenceCreatedResponse } from 'app/features/alerting/unified/api/alertSilencesApi';
import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/constants';
@@ -32,26 +33,26 @@ import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from 'app/features/ale
import { MatcherOperator, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
+import { useAlertmanager } from '../../state/AlertmanagerContext';
import { SilenceFormFields } from '../../types/silence-form';
import { matcherFieldToMatcher } from '../../utils/alertmanager';
import { makeAMLink } from '../../utils/misc';
+import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
+import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import MatchersField from './MatchersField';
import { SilencePeriod } from './SilencePeriod';
import { SilencedInstancesPreview } from './SilencedInstancesPreview';
import { getDefaultSilenceFormValues, getFormFieldsForSilence } from './utils';
-interface Props {
- alertManagerSourceName: string;
-}
-
/**
* Silences editor for editing an existing silence.
*
* Fetches silence details from API, based on `silenceId`
*/
-const ExistingSilenceEditor = ({ alertManagerSourceName }: Props) => {
+const ExistingSilenceEditor = () => {
const { id: silenceId = '' } = useParams();
+ const { selectedAlertmanager: alertManagerSourceName = '' } = useAlertmanager();
const {
data: silence,
isLoading: getSilenceIsLoading,
@@ -91,7 +92,10 @@ const ExistingSilenceEditor = ({ alertManagerSourceName }: Props) => {
}
return (
-
+ <>
+
+
+ >
);
};
@@ -279,4 +283,16 @@ const getStyles = (theme: GrafanaTheme2) => ({
}),
});
-export default ExistingSilenceEditor;
+function ExistingSilenceEditorPage() {
+ const pageNav = {
+ id: 'silence-edit',
+ text: 'Edit silence',
+ subTitle: 'Recreate existing silence to stop notifications from a particular alert rule',
+ };
+ return (
+
+
+
+ );
+}
+export default withErrorBoundary(ExistingSilenceEditorPage, { style: 'page' });
diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
index 01b92427a8e..6eaf819d11a 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx
@@ -12,6 +12,7 @@ import {
LoadingPlaceholder,
Stack,
useStyles2,
+ withErrorBoundary,
} from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { Trans } from 'app/core/internationalization';
@@ -23,10 +24,13 @@ import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
+import { useAlertmanager } from '../../state/AlertmanagerContext';
import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers';
import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc';
+import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import { Authorize } from '../Authorize';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
+import { GrafanaAlertmanagerDeliveryWarning } from '../GrafanaAlertmanagerDeliveryWarning';
import { Matchers } from './Matchers';
import { NoSilencesSplash } from './NoSilencesCTA';
@@ -40,13 +44,11 @@ export interface SilenceTableItem extends Silence {
type SilenceTableColumnProps = DynamicTableColumnProps;
type SilenceTableItemProps = DynamicTableItemProps;
-interface Props {
- alertManagerSourceName: string;
-}
const API_QUERY_OPTIONS = { pollingInterval: SILENCES_POLL_INTERVAL_MS, refetchOnFocus: true };
-const SilencesTable = ({ alertManagerSourceName }: Props) => {
+const SilencesTable = () => {
+ const { selectedAlertmanager: alertManagerSourceName = '' } = useAlertmanager();
const [previewAlertsSupported, previewAlertsAllowed] = useAlertmanagerAbility(
AlertmanagerAction.PreviewSilencedInstances
);
@@ -135,6 +137,7 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => {
return (
+
{!!silences.length && (
@@ -382,4 +385,12 @@ function useColumns(alertManagerSourceName: string) {
return columns;
}, [alertManagerSourceName, expireSilence, isGrafanaFlavoredAlertmanager, updateAllowed, updateSupported]);
}
-export default SilencesTable;
+
+function SilencesTablePage() {
+ return (
+
+
+
+ );
+}
+export default withErrorBoundary(SilencesTablePage, { style: 'page' });
diff --git a/public/app/features/alerting/unified/hooks/useSilenceNavData.test.tsx b/public/app/features/alerting/unified/hooks/useSilenceNavData.test.tsx
deleted file mode 100644
index f06c47805d1..00000000000
--- a/public/app/features/alerting/unified/hooks/useSilenceNavData.test.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { render } from '@testing-library/react';
-import { useMatch } from 'react-router-dom-v5-compat';
-
-import { useSilenceNavData } from './useSilenceNavData';
-
-jest.mock('react-router-dom-v5-compat', () => ({
- ...jest.requireActual('react-router-dom-v5-compat'),
- useMatch: jest.fn(),
-}));
-
-const setup = () => {
- let result: ReturnType;
- function TestComponent() {
- result = useSilenceNavData();
- return null;
- }
-
- render();
-
- return { result };
-};
-describe('useSilenceNavData', () => {
- it('should return correct nav data when route is "/alerting/silence/new"', () => {
- (useMatch as jest.Mock).mockImplementation((param) => param === '/alerting/silence/new');
- const { result } = setup();
-
- expect(result).toMatchObject({
- text: 'Silence alert rule',
- });
- });
-
- it('should return correct nav data when route is "/alerting/silence/:id/edit"', () => {
- (useMatch as jest.Mock).mockImplementation((param) => param === '/alerting/silence/:id/edit');
- const { result } = setup();
-
- expect(result).toMatchObject({
- text: 'Edit silence',
- });
- });
-});
diff --git a/public/app/features/alerting/unified/hooks/useSilenceNavData.ts b/public/app/features/alerting/unified/hooks/useSilenceNavData.ts
deleted file mode 100644
index 9df734e8aa2..00000000000
--- a/public/app/features/alerting/unified/hooks/useSilenceNavData.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useMatch } from 'react-router-dom-v5-compat';
-
-import { NavModelItem } from '@grafana/data';
-
-const defaultPageNav: Partial = {
- icon: 'bell-slash',
-};
-
-export function useSilenceNavData() {
- const [pageNav, setPageNav] = useState();
- const isNewPath = useMatch('/alerting/silence/new');
- const isEditPath = useMatch('/alerting/silence/:id/edit');
-
- useEffect(() => {
- if (isNewPath) {
- setPageNav({
- ...defaultPageNav,
- id: 'silence-new',
- text: 'Silence alert rule',
- subTitle: 'Configure silences to stop notifications from a particular alert rule',
- });
- } else if (isEditPath) {
- setPageNav({
- ...defaultPageNav,
- id: 'silence-edit',
- text: 'Edit silence',
- subTitle: 'Recreate existing silence to stop notifications from a particular alert rule',
- });
- }
- }, [isEditPath, isNewPath]);
-
- return pageNav;
-}
diff --git a/public/app/features/scopes/internal/ScopesSelectorScene.tsx b/public/app/features/scopes/internal/ScopesSelectorScene.tsx
index 1ca1237b545..e02ef57e17b 100644
--- a/public/app/features/scopes/internal/ScopesSelectorScene.tsx
+++ b/public/app/features/scopes/internal/ScopesSelectorScene.tsx
@@ -22,7 +22,12 @@ import { ScopesInput } from './ScopesInput';
import { ScopesTree } from './ScopesTree';
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
-import { getBasicScope, getScopeNamesFromSelectedScopes, getTreeScopesFromSelectedScopes } from './utils';
+import {
+ getBasicScope,
+ getScopeNamesFromSelectedScopes,
+ getScopesAndTreeScopesWithPaths,
+ getTreeScopesFromSelectedScopes,
+} from './utils';
export interface ScopesSelectorSceneState extends SceneObjectState {
dashboards: SceneObjectRef | null;
@@ -126,7 +131,14 @@ export class ScopesSelectorScene extends SceneObjectBase {
- const persistedNodes = this.state.treeScopes
+ const [scopes, treeScopes] = getScopesAndTreeScopesWithPaths(
+ this.state.scopes,
+ this.state.treeScopes,
+ path,
+ childNodes
+ );
+
+ const persistedNodes = treeScopes
.map(({ path }) => path[path.length - 1])
.filter((nodeName) => nodeName in currentNode.nodes && !(nodeName in childNodes))
.reduce((acc, nodeName) => {
@@ -140,7 +152,7 @@ export class ScopesSelectorScene extends SceneObjectBase scope.metadata.name);
}
+// helper func to get the selected/tree scopes together with their paths
+// needed to maintain selected scopes in tree for example when navigating
+// between categories or when loading scopes from URL to find the scope's path
+export function getScopesAndTreeScopesWithPaths(
+ selectedScopes: SelectedScope[],
+ treeScopes: TreeScope[],
+ path: string[],
+ childNodes: NodesMap
+): [SelectedScope[], TreeScope[]] {
+ const childNodesArr = Object.values(childNodes);
+
+ // Get all scopes without paths
+ // We use tree scopes as the list is always up to date as opposed to selected scopes which can be outdated
+ const scopeNamesWithoutPaths = treeScopes.filter(({ path }) => path.length === 0).map(({ scopeName }) => scopeName);
+
+ // We search for the path of each scope name without a path
+ const scopeNamesWithPaths = scopeNamesWithoutPaths.reduce>((acc, scopeName) => {
+ const possibleParent = childNodesArr.find((childNode) => childNode.isSelectable && childNode.linkId === scopeName);
+
+ if (possibleParent) {
+ acc[scopeName] = [...path, possibleParent.name];
+ }
+
+ return acc;
+ }, {});
+
+ // Update the paths of the selected scopes based on what we found
+ const newSelectedScopes = selectedScopes.map((selectedScope) => {
+ if (selectedScope.path.length > 0) {
+ return selectedScope;
+ }
+
+ return {
+ ...selectedScope,
+ path: scopeNamesWithPaths[selectedScope.scope.metadata.name] ?? [],
+ };
+ });
+
+ // Update the paths of the tree scopes based on what we found
+ const newTreeScopes = treeScopes.map((treeScope) => {
+ if (treeScope.path.length > 0) {
+ return treeScope;
+ }
+
+ return {
+ ...treeScope,
+ path: scopeNamesWithPaths[treeScope.scopeName] ?? [],
+ };
+ });
+
+ return [newSelectedScopes, newTreeScopes];
+}
+
export function groupDashboards(dashboards: ScopeDashboardBinding[]): SuggestedDashboardsFoldersMap {
return dashboards.reduce(
(acc, dashboard) => {
diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts
index 2a456d73f6b..fab6a707baa 100644
--- a/public/app/features/scopes/tests/tree.test.ts
+++ b/public/app/features/scopes/tests/tree.test.ts
@@ -36,6 +36,8 @@ import {
expectResultCloudOpsSelected,
expectScopesHeadline,
expectScopesSelectorValue,
+ expectSelectedScopePath,
+ expectTreeScopePath,
} from './utils/assertions';
import { fetchNodesSpy, fetchScopeSpy, getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@@ -244,4 +246,28 @@ describe('Tree', () => {
expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectScopesHeadline('No results found for your query');
});
+
+ it('Updates the paths for scopes without paths on nodes fetching', async () => {
+ const selectedScopeName = 'grafana';
+ const unselectedScopeName = 'mimir';
+ const selectedScopeNameFromOtherGroup = 'dev';
+
+ await updateScopes([selectedScopeName, selectedScopeNameFromOtherGroup]);
+ expectSelectedScopePath(selectedScopeName, []);
+ expectTreeScopePath(selectedScopeName, []);
+ expectSelectedScopePath(unselectedScopeName, undefined);
+ expectTreeScopePath(unselectedScopeName, undefined);
+ expectSelectedScopePath(selectedScopeNameFromOtherGroup, []);
+ expectTreeScopePath(selectedScopeNameFromOtherGroup, []);
+
+ await openSelector();
+ await expandResultApplications();
+ const expectedPath = ['', 'applications', 'applications-grafana'];
+ expectSelectedScopePath(selectedScopeName, expectedPath);
+ expectTreeScopePath(selectedScopeName, expectedPath);
+ expectSelectedScopePath(unselectedScopeName, undefined);
+ expectTreeScopePath(unselectedScopeName, undefined);
+ expectSelectedScopePath(selectedScopeNameFromOtherGroup, []);
+ expectTreeScopePath(selectedScopeNameFromOtherGroup, []);
+ });
});
diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts
index 7ee5d4d1c58..785f72e005f 100644
--- a/public/app/features/scopes/tests/utils/assertions.ts
+++ b/public/app/features/scopes/tests/utils/assertions.ts
@@ -12,8 +12,10 @@ import {
getResultApplicationsMimirSelect,
getResultCloudDevRadio,
getResultCloudOpsRadio,
+ getSelectedScope,
getSelectorInput,
getTreeHeadline,
+ getTreeScope,
queryAllDashboard,
queryDashboard,
queryDashboardFolderExpand,
@@ -80,3 +82,8 @@ 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');
+
+export const expectSelectedScopePath = (name: string, path: string[] | undefined) =>
+ expect(getSelectedScope(name)?.path).toEqual(path);
+export const expectTreeScopePath = (name: string, path: string[] | undefined) =>
+ expect(getTreeScope(name)?.path).toEqual(path);
diff --git a/public/app/features/scopes/tests/utils/selectors.ts b/public/app/features/scopes/tests/utils/selectors.ts
index 655fe257420..44f24b5fbac 100644
--- a/public/app/features/scopes/tests/utils/selectors.ts
+++ b/public/app/features/scopes/tests/utils/selectors.ts
@@ -1,5 +1,7 @@
import { screen } from '@testing-library/react';
+import { scopesSelectorScene } from '../../instance';
+
const selectors = {
tree: {
search: 'scopes-tree-search',
@@ -82,3 +84,9 @@ export const getResultCloudDevRadio = () =>
screen.getByTestId(selectors.tree.radio('cloud-dev', 'result'));
export const getResultCloudOpsRadio = () =>
screen.getByTestId(selectors.tree.radio('cloud-ops', 'result'));
+
+export const getListOfSelectedScopes = () => scopesSelectorScene?.state.scopes;
+export const getListOfTreeScopes = () => scopesSelectorScene?.state.treeScopes;
+export const getSelectedScope = (name: string) =>
+ getListOfSelectedScopes()?.find((selectedScope) => selectedScope.scope.metadata.name === name);
+export const getTreeScope = (name: string) => getListOfTreeScopes()?.find((treeScope) => treeScope.scopeName === name);
diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json
index a2be614eb02..ad79affc6b5 100644
--- a/public/locales/de-DE/grafana.json
+++ b/public/locales/de-DE/grafana.json
@@ -2218,7 +2218,9 @@
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
- "search": ""
+ "search": "",
+ "user-info-get-error": "",
+ "user-names": ""
},
"query-operation": {
"header": {
diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json
index 76a3bd0426b..9ab396e02e5 100644
--- a/public/locales/es-ES/grafana.json
+++ b/public/locales/es-ES/grafana.json
@@ -2218,7 +2218,9 @@
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
- "search": ""
+ "search": "",
+ "user-info-get-error": "",
+ "user-names": ""
},
"query-operation": {
"header": {
diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json
index 711c3eb893d..da3b6c03325 100644
--- a/public/locales/fr-FR/grafana.json
+++ b/public/locales/fr-FR/grafana.json
@@ -2218,7 +2218,9 @@
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
- "search": ""
+ "search": "",
+ "user-info-get-error": "",
+ "user-names": ""
},
"query-operation": {
"header": {
diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json
index 0c02ca59833..edb0a1a470e 100644
--- a/public/locales/pt-BR/grafana.json
+++ b/public/locales/pt-BR/grafana.json
@@ -2218,7 +2218,9 @@
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
- "search": ""
+ "search": "",
+ "user-info-get-error": "",
+ "user-names": ""
},
"query-operation": {
"header": {
diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json
index b4c7740e3ab..20bc710c828 100644
--- a/public/locales/zh-Hans/grafana.json
+++ b/public/locales/zh-Hans/grafana.json
@@ -2208,7 +2208,9 @@
"datasource-names": "",
"delete-query-button": "",
"query-template-get-error": "",
- "search": ""
+ "search": "",
+ "user-info-get-error": "",
+ "user-names": ""
},
"query-operation": {
"header": {