diff --git a/public/app/features/alerting/unified/components/expressions/util.test.ts b/public/app/features/alerting/unified/components/expressions/util.test.ts
index 18600e64d85..f8cb2d39d23 100644
--- a/public/app/features/alerting/unified/components/expressions/util.test.ts
+++ b/public/app/features/alerting/unified/components/expressions/util.test.ts
@@ -6,6 +6,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import {
decodeGrafanaNamespace,
+ encodeGrafanaNamespace,
formatLabels,
getSeriesLabels,
getSeriesName,
@@ -58,7 +59,8 @@ describe('decodeGrafanaNamespace', () => {
},
],
};
- expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('my_rule_namespace');
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('name', 'my_rule_namespace');
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('parents', []);
});
it('should work for Grafana namespaces in nested folders format', () => {
@@ -74,7 +76,8 @@ describe('decodeGrafanaNamespace', () => {
],
};
- expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('my_rule_namespace');
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('name', 'my_rule_namespace');
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('parents', ['parentUID']);
});
it('should default to name if format is invalid: invalid JSON', () => {
@@ -90,7 +93,8 @@ describe('decodeGrafanaNamespace', () => {
],
};
- expect(decodeGrafanaNamespace(grafanaNamespace)).toBe(`["parentUID"`);
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('name', `["parentUID"`);
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('parents', []);
});
it('should default to name if format is invalid: empty array', () => {
@@ -106,7 +110,8 @@ describe('decodeGrafanaNamespace', () => {
],
};
- expect(decodeGrafanaNamespace(grafanaNamespace)).toBe(`[]`);
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('name', `[]`);
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('parents', []);
});
it('grab folder name if format is long array', () => {
@@ -122,7 +127,8 @@ describe('decodeGrafanaNamespace', () => {
],
};
- expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('another_part');
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('name', 'another_part');
+ expect(decodeGrafanaNamespace(grafanaNamespace)).toHaveProperty('parents', ['parentUID', 'my_rule_namespace']);
});
it('should not change output for cloud namespaces', () => {
@@ -138,7 +144,23 @@ describe('decodeGrafanaNamespace', () => {
],
};
- expect(decodeGrafanaNamespace(cloudNamespace)).toBe(`["parentUID","my_rule_namespace"]`);
+ expect(decodeGrafanaNamespace(cloudNamespace)).toHaveProperty('name', `["parentUID","my_rule_namespace"]`);
+ expect(decodeGrafanaNamespace(cloudNamespace)).toHaveProperty('parents', []);
+ });
+});
+
+describe('encodeGrafanaNamespace', () => {
+ it('should encode with parents', () => {
+ const name = 'folder';
+ const parents = ['1', '2', '3'];
+
+ expect(encodeGrafanaNamespace(name, parents)).toBe(`["1","2","3","folder"]`);
+ });
+
+ it('should encode without parents', () => {
+ const name = 'folder';
+
+ expect(encodeGrafanaNamespace(name)).toBe(`["folder"]`);
});
});
diff --git a/public/app/features/alerting/unified/components/expressions/util.ts b/public/app/features/alerting/unified/components/expressions/util.ts
index e6794e4ab26..07d732eb27b 100644
--- a/public/app/features/alerting/unified/components/expressions/util.ts
+++ b/public/app/features/alerting/unified/components/expressions/util.ts
@@ -1,3 +1,5 @@
+import { dropRight, last } from 'lodash';
+
import { DataFrame, Labels, roundDecimals } from '@grafana/data';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
@@ -40,29 +42,66 @@ const formatLabels = (labels: Labels): string => {
.join(', ');
};
+interface DecodedNamespace {
+ name: string;
+ parents: string[];
+}
+
/**
* After https://github.com/grafana/grafana/pull/74600,
* Grafana folder names will be returned from the API as a combination of the folder name and parent UID in a format of JSON array,
* where first element is parent UID and the second element is Title.
+ *
+ * Here we parse this to return the name of the last folder and the array of parent folders
*/
-const decodeGrafanaNamespace = (namespace: CombinedRuleNamespace): string => {
+const decodeGrafanaNamespace = (namespace: CombinedRuleNamespace): DecodedNamespace => {
const namespaceName = namespace.name;
if (isCloudRulesSource(namespace.rulesSource)) {
- return namespaceName;
+ return {
+ name: namespaceName,
+ parents: [],
+ };
}
+ // try to parse the folder as a nested folder, if it fails fall back to returning the folder name as-is.
try {
- return JSON.parse(namespaceName).at(-1) ?? namespaceName;
+ const folderParts: string[] = JSON.parse(namespaceName);
+ if (!Array.isArray(folderParts)) {
+ throw new Error('not a nested Grafana folder');
+ }
+
+ const name = last(folderParts) ?? namespaceName;
+ const parents = dropRight(folderParts, 1);
+
+ return {
+ name,
+ parents,
+ };
} catch {
- return namespaceName;
+ return {
+ name: namespace.name,
+ parents: [],
+ };
}
};
+const encodeGrafanaNamespace = (name: string, parents: string[] | undefined = []) => {
+ return JSON.stringify(parents.concat(name));
+};
+
const isEmptySeries = (series: DataFrame[]): boolean => {
const isEmpty = series.every((serie) => serie.fields.every((field) => field.values.every((value) => value == null)));
return isEmpty;
};
-export { decodeGrafanaNamespace, formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries };
+export {
+ decodeGrafanaNamespace,
+ encodeGrafanaNamespace,
+ formatLabels,
+ getSeriesLabels,
+ getSeriesName,
+ getSeriesValue,
+ isEmptySeries,
+};
diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx
index 78d793b1a31..d1ff060bdd7 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx
@@ -154,7 +154,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
{isFederatedRule && }
- {decodeGrafanaNamespace(rule.namespace)} / {rule.group.name}
+ {decodeGrafanaNamespace(rule.namespace).name} / {rule.group.name}
{isGrafanaRulerRule(rule.rulerRule) && }
diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx b/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx
index 29d4c0607c4..0088ddd0833 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx
@@ -259,7 +259,7 @@ function usePageNav(rule: CombinedRule) {
const isAlertType = isAlertingRule(promRule);
const numberOfInstance = isAlertType ? (promRule.alerts ?? []).length : undefined;
- const namespaceName = decodeGrafanaNamespace(rule.namespace);
+ const namespaceName = decodeGrafanaNamespace(rule.namespace).name;
const groupName = rule.group.name;
const isGrafanaAlertRule = isGrafanaRulerRule(rule.rulerRule) && isAlertType;
@@ -309,6 +309,7 @@ function usePageNav(rule: CombinedRule) {
['namespace', namespaceName],
['group', groupName],
]),
+ // @TODO support nested folders here
parentItem: {
text: namespaceName,
url: createListFilterLink([['namespace', namespaceName]]),
diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx
index bc24dd8be88..4864237c17c 100644
--- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx
+++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx
@@ -20,6 +20,7 @@ import { AlertInfo, getAlertInfo, isRecordingRulerRule } from '../../utils/rules
import { parsePrometheusDuration, safeParseDurationstr } from '../../utils/time';
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
+import { decodeGrafanaNamespace, encodeGrafanaNamespace } from '../expressions/util';
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
const ITEMS_PER_PAGE = 10;
@@ -173,7 +174,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
const defaultValues = useMemo(
(): FormValues => ({
- namespaceName: namespace.name,
+ namespaceName: decodeGrafanaNamespace(namespace).name,
groupName: group.name,
groupInterval: group.interval ?? '',
}),
@@ -183,6 +184,9 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
const rulesSourceName = getRulesSourceName(namespace.rulesSource);
const isGrafanaManagedGroup = rulesSourceName === GRAFANA_RULES_SOURCE_NAME;
+ // parse any parent folders the alert rule might be stored in
+ const nestedFolderParents = decodeGrafanaNamespace(namespace).parents;
+
const nameSpaceLabel = isGrafanaManagedGroup ? 'Folder' : 'Namespace';
// close modal if successfully saved
@@ -194,13 +198,18 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
useCleanup((state) => (state.unifiedAlerting.updateLotexNamespaceAndGroup = initialAsyncRequestState));
const onSubmit = (values: FormValues) => {
+ // make sure that when dealing with a nested folder for Grafana managed rules we encode the folder properly
+ const newNamespaceName = isGrafanaManagedGroup
+ ? encodeGrafanaNamespace(values.namespaceName, nestedFolderParents)
+ : values.namespaceName;
+
dispatch(
updateLotexNamespaceAndGroupAction({
rulesSourceName: rulesSourceName,
groupName: group.name,
newGroupName: values.groupName,
namespaceName: namespace.name,
- newNamespaceName: values.namespaceName,
+ newNamespaceName: newNamespaceName,
groupInterval: values.groupInterval || undefined,
folderUid,
})
@@ -212,6 +221,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
defaultValues,
shouldFocusError: true,
});
+
const {
handleSubmit,
register,
diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
index f98ad2b7424..fd6072e359f 100644
--- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx
@@ -205,9 +205,9 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
// ungrouped rules are rules that are in the "default" group name
const groupName = isListView ? (
-
+
) : (
-
+
);
const closeEditModal = (saved = false) => {
diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts
index cb41bea1e15..21115d42ec7 100644
--- a/public/app/features/alerting/unified/utils/misc.ts
+++ b/public/app/features/alerting/unified/utils/misc.ts
@@ -150,7 +150,7 @@ export function makeFolderAlertsLink(folderUID: string, title: string): string {
}
export function makeFolderSettingsLink(folder: FolderDTO): string {
- return createUrl(`/dashboards/f/${folder.uid}/${folder.title}/settings`);
+ return createUrl(`/dashboards/f/${folder.uid}/settings`);
}
export function makeDashboardLink(dashboardUID: string): string {