mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Update rule API to address folders by UID (#74600)
* Change ruler API to expect the folder UID as namespace * Update example requests * Fix tests * Update swagger * Modify FIle field in /api/prometheus/grafana/api/v1/rules * Fix ruler export * Modify folder in responses to be formatted as <parent UID>/<title> * Add alerting test with nested folders * Apply suggestion from code review * Alerting: use folder UID instead of title in rule API (#77166) Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com> * Drop a few more latent uses of namespace_id * move getNamespaceKey to models package * switch GetAlertRulesForScheduling to use folder table * update GetAlertRulesForScheduling to return folder titles in format `parent_uid/title`. * fi tests * add tests for GetAlertRulesForScheduling when parent uid * fix integration tests after merge * fix test after merge * change format of the namespace to JSON array this is needed for forward compatibility, when we migrate to full paths * update EF code to decode nested folder --------- Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com> Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com> Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com> Co-authored-by: Alex Weaver <weaver.alex.d@gmail.com> Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
committed by
GitHub
parent
ec1d4274ed
commit
d1dab5828d
@@ -5,7 +5,6 @@ import { Route } from 'react-router-dom';
|
||||
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
import { TestProvider } from '../../../../../../test/helpers/TestProvider';
|
||||
@@ -36,7 +35,6 @@ const ui = {
|
||||
form: {
|
||||
nameInput: byRole('textbox', { name: 'name' }),
|
||||
folder: byTestId('folder-picker'),
|
||||
folderContainer: byTestId(selectors.components.FolderPicker.containerV2),
|
||||
group: byTestId('group-picker'),
|
||||
annotationKey: (idx: number) => byTestId(`annotation-key-${idx}`),
|
||||
annotationValue: (idx: number) => byTestId(`annotation-value-${idx}`),
|
||||
@@ -75,7 +73,7 @@ describe('GrafanaModifyExport', () => {
|
||||
const grafanaRule = getGrafanaRule(undefined, {
|
||||
uid: 'test-rule-uid',
|
||||
title: 'cpu-usage',
|
||||
namespace_uid: 'folder-test-uid',
|
||||
namespace_uid: 'folderUID1',
|
||||
data: [
|
||||
{
|
||||
refId: 'A',
|
||||
@@ -97,21 +95,23 @@ describe('GrafanaModifyExport', () => {
|
||||
mockSearchApi(server).search([
|
||||
mockDashboardSearchItem({
|
||||
title: grafanaRule.namespace.name,
|
||||
uid: 'folder-test-uid',
|
||||
uid: 'folderUID1',
|
||||
url: '',
|
||||
tags: [],
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
}),
|
||||
]);
|
||||
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, {
|
||||
[grafanaRule.namespace.name]: [{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }],
|
||||
});
|
||||
mockAlertRuleApi(server).rulerRuleGroup(
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
grafanaRule.namespace.name,
|
||||
grafanaRule.group.name,
|
||||
{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }
|
||||
);
|
||||
mockExportApi(server).modifiedExport(grafanaRule.namespace.name, {
|
||||
mockAlertRuleApi(server).rulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, 'folderUID1', grafanaRule.group.name, {
|
||||
name: grafanaRule.group.name,
|
||||
interval: '1m',
|
||||
rules: [grafanaRule.rulerRule!],
|
||||
});
|
||||
mockExportApi(server).modifiedExport('folderUID1', {
|
||||
yaml: 'Yaml Export Content',
|
||||
json: 'Json Export Content',
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
@@ -127,6 +127,7 @@ describe('GrafanaModifyExport', () => {
|
||||
expect(drawer).toBeInTheDocument();
|
||||
|
||||
expect(ui.exportDrawer.yamlTab.get(drawer)).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(ui.exportDrawer.editor.get(drawer)).toHaveTextContent('Yaml Export Content');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { DataFrame, FieldType, toDataFrame } from '@grafana/data';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { getSeriesName, formatLabels, getSeriesValue, isEmptySeries, getSeriesLabels } from './util';
|
||||
import { mockDataSource } from '../../mocks';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import {
|
||||
decodeGrafanaNamespace,
|
||||
formatLabels,
|
||||
getSeriesLabels,
|
||||
getSeriesName,
|
||||
getSeriesValue,
|
||||
isEmptySeries,
|
||||
} from './util';
|
||||
|
||||
const EMPTY_FRAME: DataFrame = toDataFrame([]);
|
||||
const NAMED_FRAME: DataFrame = {
|
||||
@@ -34,6 +45,103 @@ describe('formatLabels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeGrafanaNamespace', () => {
|
||||
it('should work for regular Grafana namespaces', () => {
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: `my_rule_namespace`,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
groups: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('my_rule_namespace');
|
||||
});
|
||||
|
||||
it('should work for Grafana namespaces in nested folders format', () => {
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: `["parentUID","my_rule_namespace"]`,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
groups: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('my_rule_namespace');
|
||||
});
|
||||
|
||||
it('should default to name if format is invalid: invalid JSON', () => {
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: `["parentUID"`,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
groups: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe(`["parentUID"`);
|
||||
});
|
||||
|
||||
it('should default to name if format is invalid: empty array', () => {
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: `[]`,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
groups: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe(`[]`);
|
||||
});
|
||||
|
||||
it('grab folder name if format is long array', () => {
|
||||
const grafanaNamespace: CombinedRuleNamespace = {
|
||||
name: `["parentUID","my_rule_namespace","another_part"]`,
|
||||
rulesSource: GRAFANA_RULES_SOURCE_NAME,
|
||||
groups: [
|
||||
{
|
||||
name: 'group1',
|
||||
rules: [],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(decodeGrafanaNamespace(grafanaNamespace)).toBe('another_part');
|
||||
});
|
||||
|
||||
it('should not change output for cloud namespaces', () => {
|
||||
const cloudNamespace: CombinedRuleNamespace = {
|
||||
name: `["parentUID","my_rule_namespace"]`,
|
||||
rulesSource: mockDataSource(),
|
||||
groups: [
|
||||
{
|
||||
name: 'Prom group',
|
||||
rules: [],
|
||||
totals: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(decodeGrafanaNamespace(cloudNamespace)).toBe(`["parentUID","my_rule_namespace"]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmptySeries', () => {
|
||||
it('should be true for empty series', () => {
|
||||
expect(isEmptySeries([EMPTY_FRAME])).toBe(true);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { DataFrame, Labels, roundDecimals } from '@grafana/data';
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { isCloudRulesSource } from '../../utils/datasource';
|
||||
|
||||
/**
|
||||
* ⚠️ `frame.fields` could be an empty array ⚠️
|
||||
@@ -37,10 +40,29 @@ const formatLabels = (labels: Labels): string => {
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const decodeGrafanaNamespace = (namespace: CombinedRuleNamespace): string => {
|
||||
const namespaceName = namespace.name;
|
||||
|
||||
if (isCloudRulesSource(namespace.rulesSource)) {
|
||||
return namespaceName;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(namespaceName).at(-1) ?? namespaceName;
|
||||
} catch {
|
||||
return namespaceName;
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptySeries = (series: DataFrame[]): boolean => {
|
||||
const isEmpty = series.every((serie) => serie.fields.every((field) => field.values.every((value) => value == null)));
|
||||
|
||||
return isEmpty;
|
||||
};
|
||||
|
||||
export { getSeriesName, getSeriesValue, getSeriesLabels, formatLabels, isEmptySeries };
|
||||
export { decodeGrafanaNamespace, formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries };
|
||||
|
||||
@@ -39,7 +39,7 @@ import { checkForPathSeparator } from './util';
|
||||
|
||||
export const MAX_GROUP_RESULTS = 1000;
|
||||
|
||||
export const useFolderGroupOptions = (folderTitle: string, enableProvisionedGroups: boolean) => {
|
||||
export const useFolderGroupOptions = (folderUid: string, enableProvisionedGroups: boolean) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
|
||||
@@ -52,7 +52,7 @@ export const useFolderGroupOptions = (folderTitle: string, enableProvisionedGrou
|
||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
||||
|
||||
const grafanaFolders = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
|
||||
const folderGroups = grafanaFolders.find((f) => f.name === folderTitle)?.groups ?? [];
|
||||
const folderGroups = grafanaFolders.find((f) => f.uid === folderUid)?.groups ?? [];
|
||||
|
||||
const groupOptions = folderGroups
|
||||
.map<SelectableValue<string>>((group) => {
|
||||
@@ -105,7 +105,7 @@ export function FolderAndGroup({
|
||||
const folder = watch('folder');
|
||||
const group = watch('group');
|
||||
|
||||
const { groupOptions, loading } = useFolderGroupOptions(folder?.title ?? '', enableProvisionedGroups);
|
||||
const { groupOptions, loading } = useFolderGroupOptions(folder?.uid ?? '', enableProvisionedGroups);
|
||||
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
|
||||
@@ -146,55 +146,62 @@ export function FolderAndGroup({
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Stack alignItems="center">
|
||||
<Field
|
||||
label={
|
||||
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
|
||||
Folder
|
||||
</Label>
|
||||
}
|
||||
className={styles.formInput}
|
||||
error={errors.folder?.message}
|
||||
invalid={!!errors.folder?.message}
|
||||
data-testid="folder-picker"
|
||||
>
|
||||
{(!isCreatingFolder && (
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<div style={{ width: 420 }}>
|
||||
<RuleFolderPicker
|
||||
inputId="folder"
|
||||
{...field}
|
||||
enableReset={true}
|
||||
onChange={({ title, uid }) => {
|
||||
field.onChange({ title, uid });
|
||||
resetGroup();
|
||||
{
|
||||
<Field
|
||||
label={
|
||||
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
|
||||
Folder
|
||||
</Label>
|
||||
}
|
||||
className={styles.formInput}
|
||||
error={errors.folder?.message}
|
||||
data-testid="folder-picker"
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
{(!isCreatingFolder && (
|
||||
<>
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<div style={{ width: 420 }}>
|
||||
<RuleFolderPicker
|
||||
inputId="folder"
|
||||
invalid={!!errors.folder?.message}
|
||||
{...field}
|
||||
enableReset={true}
|
||||
onChange={({ title, uid }) => {
|
||||
field.onChange({ title, uid });
|
||||
resetGroup();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
name="folder"
|
||||
rules={{
|
||||
required: { value: true, message: 'Select a folder' },
|
||||
validate: {
|
||||
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.uid),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
name="folder"
|
||||
rules={{
|
||||
required: { value: true, message: 'Select a folder' },
|
||||
validate: {
|
||||
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)) || <div>Creating new folder...</div>}
|
||||
</Field>
|
||||
<Box marginTop={2.5} gap={1} display={'flex'} alignItems={'center'}>
|
||||
<Text color="secondary">or</Text>
|
||||
<Button
|
||||
onClick={onOpenFolderCreationModal}
|
||||
type="button"
|
||||
icon="plus"
|
||||
fill="outline"
|
||||
variant="secondary"
|
||||
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
|
||||
>
|
||||
New folder
|
||||
</Button>
|
||||
</Box>
|
||||
<Text color="secondary">or</Text>
|
||||
<Button
|
||||
onClick={onOpenFolderCreationModal}
|
||||
type="button"
|
||||
icon="plus"
|
||||
fill="outline"
|
||||
variant="secondary"
|
||||
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
|
||||
>
|
||||
New folder
|
||||
</Button>
|
||||
</>
|
||||
)) || <div>Creating new folder...</div>}
|
||||
</Stack>
|
||||
</Field>
|
||||
}
|
||||
{isCreatingFolder && (
|
||||
<FolderCreationModal onCreate={handleFolderCreation} onClose={() => setIsCreatingFolder(false)} />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{isCreatingFolder && (
|
||||
|
||||
@@ -92,16 +92,16 @@ function FolderGroupAndEvaluationInterval({
|
||||
const { watch, setValue, getValues } = useFormContext<RuleFormValues>();
|
||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||
|
||||
const [groupName, folderName] = watch(['group', 'folder.title']);
|
||||
const [groupName, folderUid, folderName] = watch(['group', 'folder.uid', 'folder.title']);
|
||||
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
||||
|
||||
const grafanaNamespaces = useCombinedRuleNamespaces(GRAFANA_RULES_SOURCE_NAME);
|
||||
const existingNamespace = grafanaNamespaces.find((ns) => ns.name === folderName);
|
||||
const existingNamespace = grafanaNamespaces.find((ns) => ns.uid === folderUid);
|
||||
const existingGroup = existingNamespace?.groups.find((g) => g.name === groupName);
|
||||
|
||||
const isNewGroup = useIsNewGroup(folderName ?? '', groupName);
|
||||
const isNewGroup = useIsNewGroup(folderUid ?? '', groupName);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewGroup && existingGroup?.interval) {
|
||||
@@ -118,7 +118,7 @@ function FolderGroupAndEvaluationInterval({
|
||||
|
||||
const onOpenEditGroupModal = () => setIsEditingGroup(true);
|
||||
|
||||
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderName || !groupName;
|
||||
const editGroupDisabled = groupfoldersForGrafana?.loading || isNewGroup || !folderUid || !groupName;
|
||||
|
||||
const emptyNamespace: CombinedRuleNamespace = {
|
||||
name: folderName,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
||||
import { OldFolderPicker, Props as FolderPickerProps } from 'app/core/components/Select/OldFolderPicker';
|
||||
import { Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Props as FolderPickerProps, OldFolderPicker } from 'app/core/components/Select/OldFolderPicker';
|
||||
import { PermissionLevelString, SearchQueryType } from 'app/types';
|
||||
|
||||
import { FolderWarning, CustomAdd } from '../../../../../core/components/Select/OldFolderPicker';
|
||||
import { CustomAdd, FolderWarning } from '../../../../../core/components/Select/OldFolderPicker';
|
||||
|
||||
export interface Folder {
|
||||
title: string;
|
||||
@@ -15,6 +15,7 @@ export interface Folder {
|
||||
|
||||
export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialTitle' | 'initialFolderId'> {
|
||||
value?: Folder;
|
||||
invalid?: boolean;
|
||||
}
|
||||
|
||||
const SlashesWarning = () => {
|
||||
@@ -51,7 +52,6 @@ export function RuleFolderPicker(props: RuleFolderPickerProps) {
|
||||
showRoot={false}
|
||||
rootName=""
|
||||
allowEmpty={true}
|
||||
initialTitle={value?.title}
|
||||
initialFolderUid={value?.uid}
|
||||
searchQueryType={SearchQueryType.AlertFolder}
|
||||
{...props}
|
||||
|
||||
@@ -111,14 +111,14 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
|
||||
);
|
||||
}
|
||||
|
||||
const useGetGroup = (nameSpace: string, group: string) => {
|
||||
const useGetGroup = (nameSpaceUID: string, group: string) => {
|
||||
const { dsFeatures } = useDataSourceFeatures(GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
const rulerConfig = dsFeatures?.rulerConfig;
|
||||
|
||||
const targetGroup = useAsync(async () => {
|
||||
return rulerConfig ? await fetchRulerRulesGroup(rulerConfig, nameSpace, group) : undefined;
|
||||
}, [rulerConfig, nameSpace, group]);
|
||||
return rulerConfig ? await fetchRulerRulesGroup(rulerConfig, nameSpaceUID, group) : undefined;
|
||||
}, [rulerConfig, nameSpaceUID, group]);
|
||||
|
||||
return targetGroup;
|
||||
};
|
||||
@@ -166,7 +166,7 @@ export const getPayloadToExport = (
|
||||
};
|
||||
|
||||
const useGetPayloadToExport = (values: RuleFormValues, uid: string) => {
|
||||
const rulerGroupDto = useGetGroup(values.folder?.title ?? '', values.group);
|
||||
const rulerGroupDto = useGetGroup(values.folder?.uid ?? '', values.group);
|
||||
const payload: ModifyExportPayload = useMemo(() => {
|
||||
return getPayloadToExport(uid, values, rulerGroupDto?.value);
|
||||
}, [uid, rulerGroupDto, values]);
|
||||
@@ -182,11 +182,11 @@ const GrafanaRuleDesignExportPreview = ({
|
||||
const [getExport, exportData] = alertRuleApi.endpoints.exportModifiedRuleGroup.useMutation();
|
||||
const { loadingGroup, payload } = useGetPayloadToExport(exportValues, uid);
|
||||
|
||||
const nameSpace = exportValues.folder?.title ?? '';
|
||||
const nameSpaceUID = exportValues.folder?.uid ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
!loadingGroup && getExport({ payload, format: exportFormat, nameSpace: nameSpace });
|
||||
}, [nameSpace, exportFormat, payload, getExport, loadingGroup]);
|
||||
!loadingGroup && getExport({ payload, format: exportFormat, nameSpaceUID });
|
||||
}, [nameSpaceUID, exportFormat, payload, getExport, loadingGroup]);
|
||||
|
||||
if (exportData.isLoading) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
import { decodeGrafanaNamespace } from '../expressions/util';
|
||||
import { RuleViewerLayout } from '../rule-viewer/RuleViewerLayout';
|
||||
import { RuleDetailsActionButtons } from '../rules/RuleDetailsActionButtons';
|
||||
import { RuleDetailsAnnotations } from '../rules/RuleDetailsAnnotations';
|
||||
@@ -153,7 +154,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
||||
<RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
|
||||
{isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
|
||||
<DetailsField label="Namespace / Group" className={styles.rightSideDetails}>
|
||||
{rule.namespace.name} / {rule.group.name}
|
||||
{decodeGrafanaNamespace(rule.namespace)} / {rule.group.name}
|
||||
</DetailsField>
|
||||
{isGrafanaRulerRule(rule.rulerRule) && <GrafanaRuleUID rule={rule.rulerRule.grafana_alert} />}
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { AlertingPageWrapper } from '../../AlertingPageWrapper';
|
||||
import MoreButton from '../../MoreButton';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
||||
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton';
|
||||
import { decodeGrafanaNamespace } from '../../expressions/util';
|
||||
import { Details } from '../tabs/Details';
|
||||
import { History } from '../tabs/History';
|
||||
import { InstancesList } from '../tabs/Instances';
|
||||
@@ -336,6 +337,9 @@ function usePageNav(rule: CombinedRule) {
|
||||
const isAlertType = isAlertingRule(promRule);
|
||||
const numberOfInstance = isAlertType ? (promRule.alerts ?? []).length : undefined;
|
||||
|
||||
const namespaceName = decodeGrafanaNamespace(rule.namespace);
|
||||
const groupName = rule.group.name;
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
...defaultPageNav,
|
||||
text: rule.name,
|
||||
@@ -372,14 +376,14 @@ function usePageNav(rule: CombinedRule) {
|
||||
},
|
||||
],
|
||||
parentItem: {
|
||||
text: rule.group.name,
|
||||
text: groupName,
|
||||
url: createListFilterLink([
|
||||
['namespace', rule.namespace.name],
|
||||
['group', rule.group.name],
|
||||
['namespace', namespaceName],
|
||||
['group', groupName],
|
||||
]),
|
||||
parentItem: {
|
||||
text: rule.namespace.name,
|
||||
url: createListFilterLink([['namespace', rule.namespace.name]]),
|
||||
text: namespaceName,
|
||||
url: createListFilterLink([['namespace', namespaceName]]),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -158,11 +158,12 @@ export interface ModalProps {
|
||||
onClose: (saved?: boolean) => void;
|
||||
intervalEditOnly?: boolean;
|
||||
folderUrl?: string;
|
||||
folderUid?: string;
|
||||
hideFolder?: boolean;
|
||||
}
|
||||
|
||||
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||
const { namespace, group, onClose, intervalEditOnly } = props;
|
||||
const { namespace, group, onClose, intervalEditOnly, folderUid } = props;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
@@ -201,6 +202,7 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||
namespaceName: namespace.name,
|
||||
newNamespaceName: values.namespaceName,
|
||||
groupInterval: values.groupInterval || undefined,
|
||||
folderUid,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,12 +27,13 @@ interface ModalProps {
|
||||
namespace: CombinedRuleNamespace;
|
||||
group: CombinedRuleGroup;
|
||||
onClose: () => void;
|
||||
folderUid?: string;
|
||||
}
|
||||
|
||||
type CombinedRuleWithUID = { uid: string } & CombinedRule;
|
||||
|
||||
export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||
const { group, namespace, onClose } = props;
|
||||
const { group, namespace, onClose, folderUid } = props;
|
||||
const [pending, setPending] = useState<boolean>(false);
|
||||
const [rulesList, setRulesList] = useState<CombinedRule[]>(group.rules);
|
||||
|
||||
@@ -63,6 +64,7 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||
groupName: group.name,
|
||||
rulesSourceName: rulesSourceName,
|
||||
newRules: rulerRules,
|
||||
folderUid: folderUid || namespace.name,
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
@@ -70,7 +72,7 @@ export const ReorderCloudGroupModal = (props: ModalProps) => {
|
||||
setPending(false);
|
||||
});
|
||||
},
|
||||
[group.name, namespace.name, namespace.rulesSource, rulesList]
|
||||
[group.name, namespace.name, namespace.rulesSource, rulesList, folderUid]
|
||||
);
|
||||
|
||||
// assign unique but stable identifiers to each (alerting / recording) rule
|
||||
|
||||
@@ -3,11 +3,11 @@ import pluralize from 'pluralize';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
||||
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
|
||||
import { logInfo, LogMessages } from '../../Analytics';
|
||||
import { LogMessages, logInfo } from '../../Analytics';
|
||||
import { useFolder } from '../../hooks/useFolder';
|
||||
import { useHasRuler } from '../../hooks/useHasRuler';
|
||||
import { deleteRulesGroupAction } from '../../state/actions';
|
||||
@@ -19,6 +19,7 @@ import { CollapseToggle } from '../CollapseToggle';
|
||||
import { RuleLocation } from '../RuleLocation';
|
||||
import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter';
|
||||
import { GrafanaRuleGroupExporter } from '../export/GrafanaRuleGroupExporter';
|
||||
import { decodeGrafanaNamespace } from '../expressions/util';
|
||||
|
||||
import { ActionIcon } from './ActionIcon';
|
||||
import { EditCloudGroupModal } from './EditRuleGroupModal';
|
||||
@@ -204,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 ? (
|
||||
<RuleLocation namespace={namespace.name} />
|
||||
<RuleLocation namespace={decodeGrafanaNamespace(namespace)} />
|
||||
) : (
|
||||
<RuleLocation namespace={namespace.name} group={group.name} />
|
||||
<RuleLocation namespace={decodeGrafanaNamespace(namespace)} group={group.name} />
|
||||
);
|
||||
|
||||
const closeEditModal = (saved = false) => {
|
||||
@@ -278,10 +279,16 @@ export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }:
|
||||
group={group}
|
||||
onClose={() => closeEditModal()}
|
||||
folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder) : undefined}
|
||||
folderUid={folderUID}
|
||||
/>
|
||||
)}
|
||||
{isReorderingGroup && (
|
||||
<ReorderCloudGroupModal group={group} namespace={namespace} onClose={() => setIsReorderingGroup(false)} />
|
||||
<ReorderCloudGroupModal
|
||||
group={group}
|
||||
folderUid={folderUID}
|
||||
namespace={namespace}
|
||||
onClose={() => setIsReorderingGroup(false)}
|
||||
/>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={isDeletingGroup}
|
||||
|
||||
Reference in New Issue
Block a user