mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 09:26:43 -06:00
Alerting: Add Modify export feature for Grafana-managed alert rules (#75114)
* Initial POC for modified rule expor * Add rule and group export options to modified export * Add feature toggle for modifier export * Rename GrafanaRuleDesigner to ModifyExportRuleForm to identify it easily as a rule form * Refactor naming and folder for RuleDesigner => ModifyExport * Don't render more action drop-down button when no more actions are allowed * Redirect cancel button to alert list view * Fix modify export page being reloaded correctly without errors * Fix test * Protect modify-export route when toggle-feature is not enabled * Fix css betterer error * Address pr review coments --------- Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
parent
b0b2158dea
commit
169c5262a5
@ -2333,12 +2333,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rule-editor/AnnotationKeyInput.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
|
@ -141,6 +141,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `externalCorePlugins` | Allow core plugins to be loaded as external |
|
||||
| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins |
|
||||
| `httpSLOLevels` | Adds SLO level to http request metrics |
|
||||
| `alertingModifiedExport` | Enables using UI for provisioned rules modification and export |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
@ -134,4 +134,5 @@ export interface FeatureToggles {
|
||||
idForwarding?: boolean;
|
||||
cloudWatchWildCardDimensionValues?: boolean;
|
||||
externalServiceAccounts?: boolean;
|
||||
alertingModifiedExport?: boolean;
|
||||
}
|
||||
|
@ -808,5 +808,12 @@ var (
|
||||
RequiresDevMode: true,
|
||||
Owner: grafanaAuthnzSquad,
|
||||
},
|
||||
{
|
||||
Name: "alertingModifiedExport",
|
||||
Description: "Enables using UI for provisioned rules modification and export",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaAlertingSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -115,3 +115,4 @@ httpSLOLevels,experimental,@grafana/hosted-grafana-team,false,false,true,false
|
||||
idForwarding,experimental,@grafana/grafana-authnz-team,true,false,false,false
|
||||
cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false,false
|
||||
externalServiceAccounts,experimental,@grafana/grafana-authnz-team,true,false,false,false
|
||||
alertingModifiedExport,experimental,@grafana/alerting-squad,false,false,false,false
|
||||
|
|
@ -470,4 +470,8 @@ const (
|
||||
// FlagExternalServiceAccounts
|
||||
// Automatic service account and token setup for plugins
|
||||
FlagExternalServiceAccounts = "externalServiceAccounts"
|
||||
|
||||
// FlagAlertingModifiedExport
|
||||
// Enables using UI for provisioned rules modification and export
|
||||
FlagAlertingModifiedExport = "alertingModifiedExport"
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { uniq } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||
import { NavLandingPage } from 'app/core/components/NavLandingPage/NavLandingPage';
|
||||
@ -243,6 +244,19 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/:id/modify-export',
|
||||
pageClass: 'page-alerting',
|
||||
roles: evaluateAccess([AccessControlAction.AlertingRuleUpdate]),
|
||||
component: config.featureToggles.alertingModifiedExport
|
||||
? SafeDynamicImport(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/components/export/GrafanaModifyExport'
|
||||
)
|
||||
)
|
||||
: () => <Redirect to="/alerting/List" />,
|
||||
},
|
||||
{
|
||||
path: '/alerting/:sourceName/:id/view',
|
||||
pageClass: 'page-alerting',
|
||||
|
@ -9,7 +9,7 @@ import { useDispatch } from '../../../types';
|
||||
import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting';
|
||||
import { RulerRuleDTO } from '../../../types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
|
||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||
import { fetchEditableRuleAction } from './state/actions';
|
||||
import { generateCopiedName } from './utils/duplicate';
|
||||
import { rulerRuleToFormValues } from './utils/rule-form';
|
||||
|
@ -6,7 +6,7 @@ import { useDispatch } from 'app/types';
|
||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertWarning } from './AlertWarning';
|
||||
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
|
||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
||||
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||
import { fetchEditableRuleAction } from './state/actions';
|
||||
|
@ -11,7 +11,7 @@ import { AlertWarning } from './AlertWarning';
|
||||
import { CloneRuleEditor } from './CloneRuleEditor';
|
||||
import { ExistingRuleEditor } from './ExistingRuleEditor';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
|
||||
import { AlertRuleForm } from './components/rule-editor/alert-rule-form/AlertRuleForm';
|
||||
import { useURLSearchParams } from './hooks/useURLSearchParams';
|
||||
import { fetchRulesSourceBuildInfoAction } from './state/actions';
|
||||
import { useRulesAccess } from './utils/accessControlHooks';
|
||||
|
@ -12,6 +12,7 @@ interface GrafanaExportDrawerProps {
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
formatProviders: Array<ExportProvider<ExportFormats>>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function GrafanaExportDrawer({
|
||||
@ -20,15 +21,15 @@ export function GrafanaExportDrawer({
|
||||
children,
|
||||
onClose,
|
||||
formatProviders,
|
||||
title = 'Export',
|
||||
}: GrafanaExportDrawerProps) {
|
||||
const grafanaRulesTabs = Object.values(formatProviders).map((provider) => ({
|
||||
label: provider.name,
|
||||
value: provider.exportFormat,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="Export"
|
||||
title={title}
|
||||
subtitle="Select the format and download the file or copy the contents to clipboard"
|
||||
tabs={
|
||||
<RuleInspectorTabs<ExportFormats> tabs={grafanaRulesTabs} setActiveTab={onTabChange} activeTab={activeTab} />
|
||||
|
@ -0,0 +1,112 @@
|
||||
import { omit } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui';
|
||||
|
||||
import { GrafanaRouteComponentProps } from '../../../../../core/navigation/types';
|
||||
import { useDispatch } from '../../../../../types';
|
||||
import { RuleIdentifier, RuleWithLocation } from '../../../../../types/unified-alerting';
|
||||
import { RulerRuleDTO } from '../../../../../types/unified-alerting-dto';
|
||||
import { fetchEditableRuleAction, fetchRulesSourceBuildInfoAction } from '../../state/actions';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { rulerRuleToFormValues } from '../../utils/rule-form';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm';
|
||||
|
||||
interface GrafanaModifyExportProps extends GrafanaRouteComponentProps<{ id?: string }> {}
|
||||
|
||||
// TODO Duplicated in AlertRuleForm
|
||||
const ignoreHiddenQueries = (ruleDefinition: RuleFormValues): RuleFormValues => {
|
||||
return {
|
||||
...ruleDefinition,
|
||||
queries: ruleDefinition.queries?.map((query) => omit(query, 'model.hide')),
|
||||
};
|
||||
};
|
||||
|
||||
function formValuesFromExistingRule(rule: RuleWithLocation<RulerRuleDTO>) {
|
||||
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
|
||||
}
|
||||
|
||||
export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get rule source build info
|
||||
const [ruleIdentifier, setRuleIdentifier] = useState<RuleIdentifier | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const identifier = ruleId.tryParse(match.params.id, true);
|
||||
setRuleIdentifier(identifier);
|
||||
}, [match.params.id]);
|
||||
|
||||
const { loading: loadingBuildInfo = true } = useAsync(async () => {
|
||||
if (ruleIdentifier) {
|
||||
await dispatch(fetchRulesSourceBuildInfoAction({ rulesSourceName: ruleIdentifier.ruleSourceName }));
|
||||
}
|
||||
}, [dispatch, ruleIdentifier]);
|
||||
|
||||
// Get rule
|
||||
const {
|
||||
loading,
|
||||
value: alertRule,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
if (!ruleIdentifier) {
|
||||
return;
|
||||
}
|
||||
return await dispatch(fetchEditableRuleAction(ruleIdentifier)).unwrap();
|
||||
}, [ruleIdentifier, loadingBuildInfo]);
|
||||
|
||||
if (!ruleIdentifier) {
|
||||
return <div>Rule not found</div>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder text="Loading the rule" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert title="Cannot load modify export" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!alertRule && !loading && !loadingBuildInfo) {
|
||||
// alert rule does not exist
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||
<Alert
|
||||
title="Cannot load the rule. The rule does not exist"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (alertRule && !isGrafanaRulerRule(alertRule.rule)) {
|
||||
// alert rule exists but is not a grafana-managed rule
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||
<Alert
|
||||
title="This rule is not a Grafana-managed alert rule"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||
{alertRule && <ModifyExportRuleForm ruleForm={alertRule ? formValuesFromExistingRule(alertRule) : undefined} />}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Field, Input, Text } from '@grafana/ui';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
|
||||
const recordingRuleNameValidationPattern = {
|
||||
message:
|
||||
'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
|
||||
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
|
||||
};
|
||||
|
||||
export const AlertRuleNameInput = () => {
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const ruleFormType = watch('type');
|
||||
const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
|
||||
|
||||
return (
|
||||
<RuleEditorSection
|
||||
stepNo={1}
|
||||
title={`Enter ${entityName} name`}
|
||||
description={
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{/* sigh language rules – we should use translations ideally but for now we deal with "a" and "an" */}
|
||||
Enter {entityName === 'alert rule' ? 'an' : 'a'} {entityName} name to identify your alert.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Field label="Name" error={errors?.name?.message} invalid={!!errors.name?.message}>
|
||||
<Input
|
||||
id="name"
|
||||
width={35}
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Must enter a name' },
|
||||
pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined,
|
||||
})}
|
||||
aria-label="name"
|
||||
placeholder={`Give your ${entityName} a name`}
|
||||
/>
|
||||
</Field>
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
@ -1,23 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { omit } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
|
||||
import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react-hook-form';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { config, logInfo } from '@grafana/runtime';
|
||||
import {
|
||||
Button,
|
||||
ConfirmModal,
|
||||
CustomScrollbar,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
Input,
|
||||
Spinner,
|
||||
Text,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
@ -27,73 +17,29 @@ import { useDispatch } from 'app/types';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { LogMessages, trackNewAlerRuleFormError } from '../../Analytics';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { deleteRuleAction, saveRuleFormAction } from '../../state/actions';
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { LogMessages, trackNewAlerRuleFormError } from '../../../Analytics';
|
||||
import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector';
|
||||
import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions';
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { initialAsyncRequestState } from '../../../utils/redux';
|
||||
import {
|
||||
getDefaultFormValues,
|
||||
getDefaultQueries,
|
||||
MINUTE,
|
||||
normalizeDefaultAnnotations,
|
||||
rulerRuleToFormValues,
|
||||
} from '../../utils/rule-form';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { GrafanaRuleExporter } from '../export/GrafanaRuleExporter';
|
||||
|
||||
import AnnotationsStep from './AnnotationsStep';
|
||||
import { CloudEvaluationBehavior } from './CloudEvaluationBehavior';
|
||||
import { GrafanaEvaluationBehavior } from './GrafanaEvaluationBehavior';
|
||||
import { NotificationsStep } from './NotificationsStep';
|
||||
import { RecordingRulesNameSpaceAndGroupStep } from './RecordingRulesNameSpaceAndGroupStep';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { RuleInspector } from './RuleInspector';
|
||||
import { QueryAndExpressionsStep } from './query-and-alert-condition/QueryAndExpressionsStep';
|
||||
import { translateRouteParamToRuleType } from './util';
|
||||
|
||||
const recordingRuleNameValidationPattern = {
|
||||
message:
|
||||
'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
|
||||
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
|
||||
};
|
||||
|
||||
const AlertRuleNameInput = () => {
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const ruleFormType = watch('type');
|
||||
const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
|
||||
|
||||
return (
|
||||
<RuleEditorSection
|
||||
stepNo={1}
|
||||
title={`Enter ${entityName} name`}
|
||||
description={
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{/* sigh language rules – we should use translations ideally but for now we deal with "a" and "an" */}
|
||||
Enter {entityName === 'alert rule' ? 'an' : 'a'} {entityName} name to identify your alert.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Field label="Name" error={errors?.name?.message} invalid={!!errors.name?.message}>
|
||||
<Input
|
||||
id="name"
|
||||
width={35}
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Must enter a name' },
|
||||
pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined,
|
||||
})}
|
||||
aria-label="name"
|
||||
placeholder={`Give your ${entityName} a name`}
|
||||
/>
|
||||
</Field>
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
} from '../../../utils/rule-form';
|
||||
import * as ruleId from '../../../utils/rule-id';
|
||||
import { GrafanaRuleExporter } from '../../export/GrafanaRuleExporter';
|
||||
import { AlertRuleNameInput } from '../AlertRuleNameInput';
|
||||
import AnnotationsStep from '../AnnotationsStep';
|
||||
import { CloudEvaluationBehavior } from '../CloudEvaluationBehavior';
|
||||
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
|
||||
import { NotificationsStep } from '../NotificationsStep';
|
||||
import { RecordingRulesNameSpaceAndGroupStep } from '../RecordingRulesNameSpaceAndGroupStep';
|
||||
import { RuleInspector } from '../RuleInspector';
|
||||
import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
|
||||
import { translateRouteParamToRuleType } from '../util';
|
||||
|
||||
type Props = {
|
||||
existing?: RuleWithLocation;
|
||||
@ -374,27 +320,24 @@ function formValuesFromPrefill(rule: Partial<RuleFormValues>): RuleFormValues {
|
||||
function formValuesFromExistingRule(rule: RuleWithLocation<RulerRuleDTO>) {
|
||||
return ignoreHiddenQueries(rulerRuleToFormValues(rule));
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
buttonSpinner: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
form: css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
contentOuter: css`
|
||||
background: ${theme.colors.background.primary};
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
};
|
||||
};
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
buttonSpinner: css({
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
form: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
contentOuter: css({
|
||||
background: theme.colors.background.primary,
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
}),
|
||||
flexRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
}),
|
||||
});
|
@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, CustomScrollbar, LinkButton } from '@grafana/ui';
|
||||
|
||||
import { AppChromeUpdate } from '../../../../../../core/components/AppChrome/AppChromeUpdate';
|
||||
import { RuleFormValues } from '../../../types/rule-form';
|
||||
import { MINUTE } from '../../../utils/rule-form';
|
||||
import { GrafanaExportDrawer } from '../../export/GrafanaExportDrawer';
|
||||
import { allGrafanaExportProviders, ExportFormats } from '../../export/providers';
|
||||
import { AlertRuleNameInput } from '../AlertRuleNameInput';
|
||||
import AnnotationsStep from '../AnnotationsStep';
|
||||
import { GrafanaEvaluationBehavior } from '../GrafanaEvaluationBehavior';
|
||||
import { NotificationsStep } from '../NotificationsStep';
|
||||
import { QueryAndExpressionsStep } from '../query-and-alert-condition/QueryAndExpressionsStep';
|
||||
|
||||
interface ModifyExportRuleFormProps {
|
||||
alertUid?: string;
|
||||
ruleForm?: RuleFormValues;
|
||||
}
|
||||
|
||||
type ModifyExportMode = 'rule' | 'group';
|
||||
|
||||
export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFormProps) {
|
||||
const formAPI = useForm<RuleFormValues>({
|
||||
mode: 'onSubmit',
|
||||
defaultValues: ruleForm,
|
||||
shouldFocusError: true,
|
||||
});
|
||||
|
||||
const existing = Boolean(ruleForm);
|
||||
const returnTo = `/alerting/list`;
|
||||
|
||||
const [showExporter, setShowExporter] = useState<ModifyExportMode | undefined>(undefined);
|
||||
|
||||
const [conditionErrorMsg, setConditionErrorMsg] = useState('');
|
||||
console.log('conditionErrorMsg', conditionErrorMsg);
|
||||
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? MINUTE);
|
||||
|
||||
const checkAlertCondition = (msg = '') => {
|
||||
setConditionErrorMsg(msg);
|
||||
};
|
||||
|
||||
const actionButtons = [
|
||||
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary">
|
||||
Cancel
|
||||
</LinkButton>,
|
||||
<Button key="export-rule" size="sm" onClick={() => setShowExporter('rule')}>
|
||||
Export Rule
|
||||
</Button>,
|
||||
<Button key="export-group" size="sm" onClick={() => setShowExporter('group')}>
|
||||
Export Group
|
||||
</Button>,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...formAPI}>
|
||||
<AppChromeUpdate actions={actionButtons} />
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||
<Stack direction="column" gap={3}>
|
||||
{/* Step 1 */}
|
||||
<AlertRuleNameInput />
|
||||
{/* Step 2 */}
|
||||
<QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} />
|
||||
{/* Step 3-4-5 */}
|
||||
|
||||
<GrafanaEvaluationBehavior
|
||||
evaluateEvery={evaluateEvery}
|
||||
setEvaluateEvery={setEvaluateEvery}
|
||||
existing={Boolean(existing)}
|
||||
/>
|
||||
|
||||
{/* Step 4 & 5 */}
|
||||
{/* Annotations only for cloud and Grafana */}
|
||||
<AnnotationsStep />
|
||||
{/* Notifications step*/}
|
||||
<NotificationsStep alertUid={alertUid} />
|
||||
</Stack>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
{showExporter && (
|
||||
<GrafanaRuleDesignExporter exportMode={showExporter} onClose={() => setShowExporter(undefined)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface GrafanaRuleDesignExporterProps {
|
||||
onClose: () => void;
|
||||
exportMode: ModifyExportMode;
|
||||
}
|
||||
|
||||
export const GrafanaRuleDesignExporter = ({ onClose, exportMode }: GrafanaRuleDesignExporterProps) => {
|
||||
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||
const title = exportMode === 'rule' ? 'Export Rule' : 'Export Group';
|
||||
|
||||
return (
|
||||
<GrafanaExportDrawer
|
||||
title={title}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onClose={onClose}
|
||||
formatProviders={Object.values(allGrafanaExportProviders)}
|
||||
>
|
||||
TODO
|
||||
</GrafanaExportDrawer>
|
||||
);
|
||||
};
|
@ -6,6 +6,7 @@ import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
Button,
|
||||
ClipboardButton,
|
||||
@ -144,6 +145,20 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
|
||||
|
||||
if (isGrafanaRulerRule(rulerRule) && canReadProvisioning) {
|
||||
moreActions.push(<Menu.Item label="Export" icon="download-alt" onClick={toggleShowExportDrawer} />);
|
||||
|
||||
if (config.featureToggles.alertingModifiedExport) {
|
||||
moreActions.push(
|
||||
<Menu.Item
|
||||
label="Modify export"
|
||||
icon="edit"
|
||||
onClick={() =>
|
||||
locationService.push(
|
||||
`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
moreActions.push(
|
||||
@ -162,20 +177,22 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
|
||||
{buttons.map((button, index) => (
|
||||
<React.Fragment key={index}>{button}</React.Fragment>
|
||||
))}
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{moreActions.map((action) => (
|
||||
<React.Fragment key={uniqueId('action_')}>{action}</React.Fragment>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" size="sm">
|
||||
More
|
||||
<Icon name="angle-down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{moreActions.length > 0 && (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{moreActions.map((action) => (
|
||||
<React.Fragment key={uniqueId('action_')}>{action}</React.Fragment>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" size="sm">
|
||||
More
|
||||
<Icon name="angle-down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Stack>
|
||||
{!!ruleToDelete && (
|
||||
<ConfirmModal
|
||||
|
@ -56,11 +56,9 @@ describe('RulesTable RBAC', () => {
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', async () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderRulesTable(grafanaRule);
|
||||
await user.click(ui.actionButtons.more.get());
|
||||
expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
|
||||
expect(ui.actionButtons.more.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', () => {
|
||||
@ -91,11 +89,9 @@ describe('RulesTable RBAC', () => {
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', async () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderRulesTable(cloudRule);
|
||||
await user.click(ui.actionButtons.more.get());
|
||||
expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
|
||||
expect(ui.actionButtons.more.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user