mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Alert creation UI changes (#73835)
This commit is contained in:
parent
d50ccd6741
commit
a2c93bb8bc
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -453,6 +453,7 @@ lerna.json @grafana/frontend-ops
|
||||
/public/robots.txt @grafana/frontend-ops
|
||||
/public/sass/ @grafana/grafana-frontend-platform
|
||||
/public/test/ @grafana/grafana-frontend-platform
|
||||
/public/test/helpers/alertingRuleEditor.tsx @grafana/alerting-frontend
|
||||
/public/views/ @grafana/grafana-frontend-platform
|
||||
|
||||
/public/app/features/explore/Logs/ @grafana/observability-logs
|
||||
|
@ -71,7 +71,7 @@ afterAll(() => {
|
||||
|
||||
const ui = {
|
||||
inputs: {
|
||||
name: byRole('textbox', { name: /rule name name for the alert rule\./i }),
|
||||
name: byRole('textbox', { name: 'name' }),
|
||||
expr: byTestId('expr'),
|
||||
folderContainer: byTestId(selectors.components.FolderPicker.containerV2),
|
||||
namespace: byTestId('namespace-picker'),
|
||||
|
@ -5,6 +5,7 @@ import { NavModelItem } from '@grafana/data';
|
||||
import { withErrorBoundary } from '@grafana/ui';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertWarning } from './AlertWarning';
|
||||
import { CloneRuleEditor } from './CloneRuleEditor';
|
||||
@ -16,27 +17,37 @@ import { fetchRulesSourceBuildInfoAction } from './state/actions';
|
||||
import { useRulesAccess } from './utils/accessControlHooks';
|
||||
import * as ruleId from './utils/rule-id';
|
||||
|
||||
type RuleEditorProps = GrafanaRouteComponentProps<{ id?: string }>;
|
||||
type RuleEditorProps = GrafanaRouteComponentProps<{ id?: string; type?: 'recording' | 'alerting' }>;
|
||||
|
||||
const defaultPageNav: Partial<NavModelItem> = {
|
||||
icon: 'bell',
|
||||
id: 'alert-rule-view',
|
||||
};
|
||||
|
||||
const getPageNav = (state: 'edit' | 'add') => {
|
||||
if (state === 'edit') {
|
||||
return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit rule' };
|
||||
} else if (state === 'add') {
|
||||
return { ...defaultPageNav, id: 'alert-rule-add', text: 'Add rule' };
|
||||
// sadly we only get the "type" when a new rule is being created, when editing an existing recording rule we can't actually know it from the URL
|
||||
const getPageNav = (identifier?: RuleIdentifier, type?: 'recording' | 'alerting') => {
|
||||
if (type === 'recording') {
|
||||
if (identifier) {
|
||||
// this branch should never trigger actually, the type param isn't used when editing rules
|
||||
return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit recording rule' };
|
||||
} else {
|
||||
return { ...defaultPageNav, id: 'alert-rule-add', text: 'New recording rule' };
|
||||
}
|
||||
}
|
||||
|
||||
if (identifier) {
|
||||
// keep this one ambiguous, don't mentiond a specific alert type here
|
||||
return { ...defaultPageNav, id: 'alert-rule-edit', text: 'Edit rule' };
|
||||
} else {
|
||||
return { ...defaultPageNav, id: 'alert-rule-add', text: 'New alert rule' };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const RuleEditor = ({ match }: RuleEditorProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [searchParams] = useURLSearchParams();
|
||||
|
||||
const { id } = match.params;
|
||||
const { id, type } = match.params;
|
||||
const identifier = ruleId.tryParse(id, true);
|
||||
|
||||
const copyFromId = searchParams.get('copyFrom') ?? undefined;
|
||||
@ -75,7 +86,7 @@ const RuleEditor = ({ match }: RuleEditorProps) => {
|
||||
}, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier, loading]);
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={getPageNav(identifier ? 'edit' : 'add')}>
|
||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={getPageNav(identifier, type)}>
|
||||
{getContent()}
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
|
@ -186,7 +186,7 @@ describe('RuleEditor cloud: checking editable data sources', () => {
|
||||
|
||||
await ui.inputs.name.find();
|
||||
|
||||
const switchToCloudButton = screen.getByText('Switch to data source-managed alert rule');
|
||||
const switchToCloudButton = screen.getByText('Data source-managed');
|
||||
expect(switchToCloudButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(switchToCloudButton);
|
||||
|
@ -13,6 +13,12 @@ import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRul
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { mockApi, mockFeatureDiscoveryApi, setupMswServer } from './mockApi';
|
||||
import { disableRBAC, mockDataSource } from './mocks';
|
||||
import {
|
||||
defaultAlertmanagerChoiceResponse,
|
||||
emptyExternalAlertmanagersResponse,
|
||||
mockAlertmanagerChoiceResponse,
|
||||
mockAlertmanagersResponse,
|
||||
} from './mocks/alertmanagerApi';
|
||||
import { fetchRulerRulesIfNotFetchedYet } from './state/actions';
|
||||
import { setupDataSources } from './testSetup/datasources';
|
||||
import { buildInfoResponse } from './testSetup/featureDiscovery';
|
||||
@ -48,6 +54,8 @@ setupDataSources(dataSources.default);
|
||||
const server = setupMswServer();
|
||||
|
||||
mockFeatureDiscoveryApi(server).discoverDsFeatures(dataSources.default, buildInfoResponse.mimir);
|
||||
mockAlertmanagerChoiceResponse(server, defaultAlertmanagerChoiceResponse);
|
||||
mockAlertmanagersResponse(server, emptyExternalAlertmanagersResponse);
|
||||
mockApi(server).eval({ results: {} });
|
||||
|
||||
// these tests are rather slow because we have to wait for various API calls and mocks to be called
|
||||
@ -110,10 +118,11 @@ describe('RuleEditor cloud', () => {
|
||||
expect(removeExpressionsButtons).toHaveLength(2);
|
||||
|
||||
// Needs to wait for featrue discovery API call to finish - Check if ruler enabled
|
||||
await waitFor(() => expect(screen.getByText('Switch to data source-managed alert rule')).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByText('Data source-managed')).toBeInTheDocument());
|
||||
|
||||
const switchToCloudButton = screen.getByText('Switch to data source-managed alert rule');
|
||||
const switchToCloudButton = screen.getByText('Data source-managed');
|
||||
expect(switchToCloudButton).toBeInTheDocument();
|
||||
expect(switchToCloudButton).not.toBeDisabled();
|
||||
|
||||
await user.click(switchToCloudButton);
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconSize, useStyles2, Button } from '@grafana/ui';
|
||||
import { IconSize, Button } from '@grafana/ui';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLButtonElement> {
|
||||
isCollapsed: boolean;
|
||||
@ -23,8 +21,6 @@ export const CollapseToggle = ({
|
||||
size = 'xl',
|
||||
...restOfProps
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
@ -32,7 +28,7 @@ export const CollapseToggle = ({
|
||||
variant="secondary"
|
||||
aria-expanded={!isCollapsed}
|
||||
aria-controls={idControlled}
|
||||
className={cx(styles.expandButton, className)}
|
||||
className={className}
|
||||
icon={isCollapsed ? 'angle-right' : 'angle-down'}
|
||||
onClick={() => onToggle(!isCollapsed)}
|
||||
{...restOfProps}
|
||||
@ -41,9 +37,3 @@ export const CollapseToggle = ({
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
expandButton: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
|
@ -5,8 +5,19 @@ import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatc
|
||||
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, useStyles2 } from '@grafana/ui';
|
||||
import {
|
||||
Button,
|
||||
ConfirmModal,
|
||||
CustomScrollbar,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
Input,
|
||||
Spinner,
|
||||
Text,
|
||||
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';
|
||||
@ -48,7 +59,6 @@ const recordingRuleNameValidationPattern = {
|
||||
};
|
||||
|
||||
const AlertRuleNameInput = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
@ -56,22 +66,29 @@ const AlertRuleNameInput = () => {
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const ruleFormType = watch('type');
|
||||
const entityName = ruleFormType === RuleFormType.cloudRecording ? 'recording rule' : 'alert rule';
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={1} title="Set alert rule name.">
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Rule name"
|
||||
description="Name for the alert rule."
|
||||
error={errors?.name?.message}
|
||||
invalid={!!errors.name?.message}
|
||||
<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 an alert name' },
|
||||
required: { value: true, message: 'Must enter a name' },
|
||||
pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined,
|
||||
})}
|
||||
placeholder="Give your alert rule a name."
|
||||
aria-label="name"
|
||||
placeholder={`Give your ${entityName} a name`}
|
||||
/>
|
||||
</Field>
|
||||
</RuleEditorSection>
|
||||
@ -254,7 +271,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
||||
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
|
||||
<div className={styles.contentOuter}>
|
||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||
<div className={styles.contentInner}>
|
||||
<Stack direction="column" gap={3}>
|
||||
{/* Step 1 */}
|
||||
<AlertRuleNameInput />
|
||||
{/* Step 2 */}
|
||||
@ -282,7 +299,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
||||
<NotificationsStep alertUid={uidFromParams} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</form>
|
||||
@ -369,29 +386,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
contentInner: css`
|
||||
flex: 1;
|
||||
padding: ${theme.spacing(2)};
|
||||
`,
|
||||
contentOuter: css`
|
||||
background: ${theme.colors.background.primary};
|
||||
border: 1px solid ${theme.colors.border.weak};
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
margin-top: ${theme.spacing(1)};
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
formInput: css`
|
||||
width: 275px;
|
||||
|
||||
& + & {
|
||||
margin-left: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { FieldArrayWithId, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { InputControl, Text } from '@grafana/ui';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { Annotation, annotationDescriptions, annotationLabels } from '../../utils/constants';
|
||||
@ -21,58 +20,49 @@ const AnnotationHeaderField = ({
|
||||
annotation: Annotation;
|
||||
index: number;
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { control } = useFormContext<RuleFormValues>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className={styles.annotationContainer}>
|
||||
<Stack direction="column" gap={0}>
|
||||
<label>
|
||||
{
|
||||
<InputControl
|
||||
name={`annotations.${index}.key`}
|
||||
defaultValue={annotationField.key}
|
||||
render={({ field: { ref, ...field } }) => {
|
||||
if (!annotationLabels[annotation]) {
|
||||
return <CustomAnnotationHeaderField field={field} />;
|
||||
}
|
||||
|
||||
let label;
|
||||
|
||||
switch (annotationField.key) {
|
||||
case Annotation.dashboardUID:
|
||||
return <div>Dashboard and panel</div>;
|
||||
label = 'Dashboard and panel';
|
||||
case Annotation.panelID:
|
||||
return <span></span>;
|
||||
label = '';
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
{annotationLabels[annotation] && (
|
||||
<span className={styles.annotationTitle} data-testid={`annotation-key-${index}`}>
|
||||
{annotationLabels[annotation]}
|
||||
{' (optional)'}
|
||||
</span>
|
||||
)}
|
||||
{!annotationLabels[annotation] && <CustomAnnotationHeaderField field={field} />}
|
||||
</div>
|
||||
);
|
||||
label = annotationLabels[annotation] && annotationLabels[annotation] + ' (optional)';
|
||||
}
|
||||
|
||||
return (
|
||||
<span data-testid={`annotation-key-${index}`}>
|
||||
<Text color="primary" variant="bodySmall">
|
||||
{label}
|
||||
</Text>
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
control={control}
|
||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||
/>
|
||||
}
|
||||
</label>
|
||||
<div className={styles.annotationDescription}>{annotationDescriptions[annotation]}</div>
|
||||
</div>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
{annotationDescriptions[annotation]}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
annotationTitle: css`
|
||||
color: ${theme.colors.text.primary};
|
||||
margin-bottom: 3px;
|
||||
`,
|
||||
|
||||
annotationContainer: css`
|
||||
margin-top: 5px;
|
||||
`,
|
||||
|
||||
annotationDescription: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
});
|
||||
|
||||
export default AnnotationHeaderField;
|
||||
|
@ -6,7 +6,7 @@ import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Field, Input, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Field, Input, Text, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { dashboardApi } from '../../api/dashboardApi';
|
||||
@ -97,8 +97,10 @@ const AnnotationsStep = () => {
|
||||
'https://grafana.com/docs/grafana/latest/alerting/fundamentals/annotation-label/variables-label-annotation';
|
||||
|
||||
return (
|
||||
<Stack gap={0.5}>
|
||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Add annotations to provide more context in your alert notifications.
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText={`Annotations add metadata to provide more information on the alert in your alert notifications.
|
||||
For example, add a Summary annotation to tell you which value caused the alert to fire or which server it happened on.
|
||||
@ -112,8 +114,8 @@ const AnnotationsStep = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={4} title="Add annotations" description={getAnnotationsSectionDescription()}>
|
||||
<div className={styles.flexColumn}>
|
||||
<RuleEditorSection stepNo={4} title="Add annotations" description={getAnnotationsSectionDescription()} fullWidth>
|
||||
<Stack direction="column" gap={1}>
|
||||
{fields.map((annotationField, index: number) => {
|
||||
const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url');
|
||||
const ValueInputComponent = isUrl ? Input : TextArea;
|
||||
@ -206,7 +208,7 @@ const AnnotationsStep = () => {
|
||||
onDismiss={() => setShowPanelSelector(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
@ -223,11 +225,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
gap: ${theme.spacing(1)};
|
||||
display: flex;
|
||||
`,
|
||||
flexColumn: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
field: css`
|
||||
margin-bottom: ${theme.spacing(0.5)};
|
||||
`,
|
||||
|
@ -25,7 +25,7 @@ export const CloudEvaluationBehavior = () => {
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Set alert evaluation behavior">
|
||||
<RuleEditorSection stepNo={3} title="Set evaluation behavior">
|
||||
<Field
|
||||
label="Pending period"
|
||||
description="Period in which an alert rule can be in breach of the condition until the alert rule fires."
|
||||
|
@ -4,7 +4,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { AsyncSelect, Button, Field, Input, InputControl, Label, Modal, useStyles2 } from '@grafana/ui';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { AsyncSelect, Button, Field, Input, InputControl, Label, Modal, Text, useStyles2 } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { createFolder } from 'app/features/manage-dashboards/state/actions';
|
||||
@ -122,7 +123,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.evaluationGroupsContainer}>
|
||||
<div>
|
||||
{
|
||||
<Field
|
||||
label={
|
||||
@ -135,9 +136,12 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
invalid={!!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"
|
||||
{...field}
|
||||
@ -147,6 +151,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
resetGroup();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
name="folder"
|
||||
rules={{
|
||||
@ -156,12 +161,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)) || <div>Creating new folder...</div>}
|
||||
</Field>
|
||||
}
|
||||
|
||||
<div className={styles.addButton}>
|
||||
<span>or</span>
|
||||
<Text color="secondary">or</Text>
|
||||
<Button
|
||||
onClick={onOpenFolderCreationModal}
|
||||
type="button"
|
||||
@ -172,13 +172,17 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
>
|
||||
New folder
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)) || <div>Creating new folder...</div>}
|
||||
</Stack>
|
||||
</Field>
|
||||
}
|
||||
{isCreatingFolder && (
|
||||
<FolderCreationModal onCreate={handleFolderCreation} onClose={() => setIsCreatingFolder(false)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.evaluationGroupsContainer}>
|
||||
<div>
|
||||
<Field
|
||||
label="Evaluation group"
|
||||
data-testid="group-picker"
|
||||
@ -187,8 +191,10 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
error={errors.group?.message}
|
||||
invalid={!!errors.group?.message}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field }, fieldState }) => (
|
||||
<div style={{ width: 420 }}>
|
||||
<AsyncSelect
|
||||
disabled={!folder || loading}
|
||||
inputId="group"
|
||||
@ -218,6 +224,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
)}
|
||||
placeholder={'Select an evaluation group...'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
name="group"
|
||||
control={control}
|
||||
@ -228,10 +235,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className={styles.addButton}>
|
||||
<span>or</span>
|
||||
<Text color="secondary">or</Text>
|
||||
<Button
|
||||
onClick={onOpenEvaluationGroupCreationModal}
|
||||
type="button"
|
||||
@ -242,7 +246,8 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf
|
||||
>
|
||||
New evaluation group
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Field>
|
||||
{isCreatingEvaluationGroup && (
|
||||
<EvaluationGroupCreationModal
|
||||
onCreate={handleEvalGroupCreation}
|
||||
@ -407,40 +412,18 @@ function EvaluationGroupCreationModal({
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
max-width: ${theme.breakpoints.values.lg}px;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
evaluationGroupsContainer: css`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${theme.spacing(2)};
|
||||
`,
|
||||
|
||||
addButton: css`
|
||||
display: flex;
|
||||
direction: row;
|
||||
gap: ${theme.spacing(2)};
|
||||
line-height: 2;
|
||||
margin-top: 35px;
|
||||
`,
|
||||
formInput: css`
|
||||
max-width: ${theme.breakpoints.values.sm}px;
|
||||
flex-grow: 1;
|
||||
|
||||
label {
|
||||
width: ${theme.breakpoints.values.sm}px;
|
||||
}
|
||||
`,
|
||||
|
||||
modal: css`
|
||||
width: ${theme.breakpoints.values.sm}px;
|
||||
`,
|
||||
|
||||
modalTitle: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
|
@ -4,7 +4,7 @@ import { RegisterOptions, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Field, Icon, IconButton, Input, InputControl, Label, Switch, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Field, Icon, IconButton, Input, InputControl, Label, Switch, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
||||
import { logInfo, LogMessages } from '../../Analytics';
|
||||
@ -185,11 +185,13 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
||||
}
|
||||
|
||||
function getDescription() {
|
||||
const textToRender = 'Define how the alert rule is evaluated.';
|
||||
const docsLink = 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/';
|
||||
|
||||
return (
|
||||
<Stack gap={0.5}>
|
||||
{`${textToRender}`}
|
||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Define how the alert rule is evaluated.
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText="Evaluation groups are containers for evaluating alert and recording rules. An evaluation group defines an evaluation interval - how often a rule is checked. Alert rules within the same evaluation group are evaluated sequentially"
|
||||
externalLink={docsLink}
|
||||
@ -218,7 +220,7 @@ export function GrafanaEvaluationBehavior({
|
||||
|
||||
return (
|
||||
// TODO remove "and alert condition" for recording rules
|
||||
<RuleEditorSection stepNo={3} title="Set alert evaluation behavior" description={getDescription()}>
|
||||
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription()}>
|
||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||
<FolderGroupAndEvaluationInterval setEvaluateEvery={setEvaluateEvery} evaluateEvery={evaluateEvery} />
|
||||
<ForInput evaluateEvery={evaluateEvery} />
|
||||
@ -252,7 +254,6 @@ export function GrafanaEvaluationBehavior({
|
||||
isCollapsed={!showErrorHandling}
|
||||
onToggle={(collapsed) => setShowErrorHandling(!collapsed)}
|
||||
text="Configure no data and error handling"
|
||||
className={styles.collapseToggle}
|
||||
/>
|
||||
{showErrorHandling && (
|
||||
<>
|
||||
@ -296,9 +297,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
inlineField: css`
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
collapseToggle: css`
|
||||
margin: ${theme.spacing(2, 0, 2, -1)};
|
||||
`,
|
||||
evaluateLabel: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
|
@ -5,7 +5,18 @@ import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook
|
||||
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Field, InlineLabel, Label, useStyles2, Tooltip, Icon, Input, LoadingPlaceholder } from '@grafana/ui';
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
InlineLabel,
|
||||
Label,
|
||||
useStyles2,
|
||||
Text,
|
||||
Tooltip,
|
||||
Icon,
|
||||
Input,
|
||||
LoadingPlaceholder,
|
||||
} from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
@ -129,7 +140,7 @@ const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName
|
||||
<>
|
||||
{loading && <LoadingPlaceholder text="Loading" />}
|
||||
{!loading && (
|
||||
<>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
@ -182,7 +193,7 @@ const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName
|
||||
);
|
||||
})}
|
||||
<AddButton className={styles.addLabelButton} append={append} />
|
||||
</>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -245,14 +256,16 @@ const LabelsWithoutSuggestions: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const LabelsField: FC<Props> = ({ className, dataSourceName }) => {
|
||||
const LabelsField: FC<Props> = ({ dataSourceName }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx(className, styles.wrapper)}>
|
||||
<Label>
|
||||
<Stack gap={0.5}>
|
||||
<span>Custom Labels</span>
|
||||
<div>
|
||||
<Label description="A set of default labels is automatically added. Add additional labels as required.">
|
||||
<Stack gap={0.5} alignItems="center">
|
||||
<Text variant="bodySmall" color="primary">
|
||||
Labels
|
||||
</Text>
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
@ -265,15 +278,7 @@ const LabelsField: FC<Props> = ({ className, dataSourceName }) => {
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Label>
|
||||
<>
|
||||
<div className={styles.flexRow}>
|
||||
<InlineLabel width={18}>Labels</InlineLabel>
|
||||
<div className={styles.flexColumn}>
|
||||
{dataSourceName && <LabelsWithSuggestions dataSourceName={dataSourceName} />}
|
||||
{!dataSourceName && <LabelsWithoutSuggestions />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
{dataSourceName ? <LabelsWithSuggestions dataSourceName={dataSourceName} /> : <LabelsWithoutSuggestions />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -283,9 +288,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
icon: css`
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
wrapper: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
flexColumn: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -318,7 +320,8 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
`,
|
||||
labelInput: css`
|
||||
width: 175px;
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
margin-bottom: -${theme.spacing(1)};
|
||||
|
||||
& + & {
|
||||
margin-left: ${theme.spacing(1)};
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Icon, Toggletip, useStyles2 } from '@grafana/ui';
|
||||
import { Icon, Text, Toggletip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface NeedHelpInfoProps {
|
||||
contentText: string | JSX.Element;
|
||||
@ -25,9 +25,11 @@ export function NeedHelpInfo({ contentText, externalLink, linkText, title }: Nee
|
||||
footer={
|
||||
externalLink ? (
|
||||
<a href={externalLink} target="_blank" rel="noreferrer">
|
||||
<div className={styles.infoLink}>
|
||||
{linkText} <Icon name="external-link-alt" />
|
||||
</div>
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Text color="link">
|
||||
{linkText} <Icon size="sm" name="external-link-alt" />
|
||||
</Text>
|
||||
</Stack>
|
||||
</a>
|
||||
) : undefined
|
||||
}
|
||||
@ -35,8 +37,12 @@ export function NeedHelpInfo({ contentText, externalLink, linkText, title }: Nee
|
||||
placement="bottom-start"
|
||||
>
|
||||
<div className={styles.helpInfo}>
|
||||
<Icon name="question-circle" />
|
||||
<div className={styles.helpInfoText}>Need help?</div>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<Icon name="question-circle" size="sm" />
|
||||
<Text variant="bodySmall" color="primary">
|
||||
Need help?
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</Toggletip>
|
||||
);
|
||||
@ -48,21 +54,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
helpInfo: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
margin-left: ${theme.spacing(1)};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
cursor: pointer;
|
||||
color: ${theme.colors.text.primary};
|
||||
`,
|
||||
helpInfoText: css`
|
||||
margin-left: ${theme.spacing(0.5)};
|
||||
text-decoration: underline;
|
||||
`,
|
||||
infoLink: css`
|
||||
color: ${theme.colors.text.link};
|
||||
`,
|
||||
});
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Card, Icon, Link, useStyles2 } from '@grafana/ui';
|
||||
import { Icon, Text } from '@grafana/ui';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
@ -18,8 +16,7 @@ type NotificationsStepProps = {
|
||||
alertUid?: string;
|
||||
};
|
||||
export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { watch, getValues } = useFormContext<RuleFormValues & { location?: string }>();
|
||||
const { watch } = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const [type, labels, queries, condition, folder, alertName] = watch([
|
||||
'type',
|
||||
@ -31,15 +28,15 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
]);
|
||||
|
||||
const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME;
|
||||
const hasLabelsDefined = getNonEmptyLabels(getValues('labels')).length > 0;
|
||||
|
||||
const shouldRenderPreview = Boolean(condition) && Boolean(folder) && type === RuleFormType.grafana;
|
||||
const shouldRenderPreview = type === RuleFormType.grafana;
|
||||
|
||||
const NotificationsStepDescription = () => {
|
||||
return (
|
||||
<div className={styles.stepDescription}>
|
||||
<div>Add custom labels to change the way your notifications are routed.</div>
|
||||
|
||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Add custom labels to change the way your notifications are routed.
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText={
|
||||
<Stack gap={1}>
|
||||
@ -55,9 +52,9 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className={styles.infoLink}>
|
||||
<Text color="link">
|
||||
Read about notification routing. <Icon name="external-link-alt" />
|
||||
</div>
|
||||
</Text>
|
||||
</a>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={0}>
|
||||
@ -70,16 +67,16 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className={styles.infoLink}>
|
||||
<Text color="link">
|
||||
Read about Labels and annotations. <Icon name="external-link-alt" />
|
||||
</div>
|
||||
</Text>
|
||||
</a>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
title="Notification routing"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@ -88,31 +85,20 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
stepNo={type === RuleFormType.cloudRecording ? 4 : 5}
|
||||
title={type === RuleFormType.cloudRecording ? 'Add labels' : 'Configure notifications'}
|
||||
description={
|
||||
type === RuleFormType.cloudRecording ? (
|
||||
'Add labels to help you better manage your recording rules'
|
||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||
{type === RuleFormType.cloudRecording ? (
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Add labels to help you better manage your recording rules
|
||||
</Text>
|
||||
) : (
|
||||
<NotificationsStepDescription />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={styles.contentWrapper}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{!hasLabelsDefined && type !== RuleFormType.cloudRecording && (
|
||||
<Card className={styles.card}>
|
||||
<Card.Heading>Default policy</Card.Heading>
|
||||
<Card.Description>
|
||||
All alert instances are handled by the default policy if no other matching policies are found. To view
|
||||
and edit the default policy, go to <Link href="/alerting/routes">Notification Policies</Link>
|
||||
or contact your Admin if you are using provisioning.
|
||||
</Card.Description>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<LabelsField dataSourceName={dataSourceName} />
|
||||
</div>
|
||||
</div>
|
||||
{shouldRenderPreview &&
|
||||
condition &&
|
||||
folder && ( // need to check for condition and folder again because of typescript
|
||||
{shouldRenderPreview && (
|
||||
<NotificationPreview
|
||||
alertQueries={queries}
|
||||
customLabels={labels}
|
||||
@ -125,43 +111,3 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
|
||||
interface Label {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function getNonEmptyLabels(labels: Label[]) {
|
||||
return labels.filter((label) => label.key && label.value);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
contentWrapper: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
hideButton: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
cursor: pointer;
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
card: css`
|
||||
max-width: 500px;
|
||||
`,
|
||||
flowChart: css`
|
||||
margin-right: ${theme.spacing(3)};
|
||||
`,
|
||||
title: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
stepDescription: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
display: flex;
|
||||
gap: ${theme.spacing(1)};
|
||||
)};
|
||||
`,
|
||||
infoLink: css`
|
||||
color: ${theme.colors.text.link};
|
||||
`,
|
||||
});
|
||||
|
@ -1,70 +1,58 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { FieldSet, useStyles2 } from '@grafana/ui';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { FieldSet, Text, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface RuleEditorSectionProps {
|
||||
title: string;
|
||||
stepNo: number;
|
||||
description?: string | ReactElement;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const RuleEditorSection = ({
|
||||
title,
|
||||
stepNo,
|
||||
children,
|
||||
fullWidth = false,
|
||||
description,
|
||||
}: React.PropsWithChildren<RuleEditorSectionProps>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.parent}>
|
||||
<div>
|
||||
<span className={styles.stepNo}>{stepNo}</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<FieldSet label={title} className={styles.fieldset}>
|
||||
<FieldSet
|
||||
className={cx(fullWidth && styles.fullWidth)}
|
||||
label={
|
||||
<Text variant="h3">
|
||||
{stepNo}. {title}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Stack direction="column">
|
||||
{description && <div className={styles.description}>{description}</div>}
|
||||
{children}
|
||||
</Stack>
|
||||
</FieldSet>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
fieldset: css`
|
||||
legend {
|
||||
font-size: 16px;
|
||||
padding-top: ${theme.spacing(0.5)};
|
||||
}
|
||||
`,
|
||||
parent: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: ${theme.breakpoints.values.xl};
|
||||
& + & {
|
||||
margin-top: ${theme.spacing(4)};
|
||||
}
|
||||
max-width: ${theme.breakpoints.values.xl}px;
|
||||
border: solid 1px ${theme.colors.border.weak};
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
padding: ${theme.spacing(2)} ${theme.spacing(3)};
|
||||
`,
|
||||
description: css`
|
||||
margin-top: -${theme.spacing(2)};
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
stepNo: css`
|
||||
display: inline-block;
|
||||
width: ${theme.spacing(4)};
|
||||
height: ${theme.spacing(4)};
|
||||
line-height: ${theme.spacing(4)};
|
||||
border-radius: ${theme.shape.radius.circle};
|
||||
text-align: center;
|
||||
color: ${theme.colors.text.maxContrast};
|
||||
background-color: ${theme.colors.background.canvas};
|
||||
font-size: ${theme.typography.size.lg};
|
||||
margin-right: ${theme.spacing(2)};
|
||||
`,
|
||||
content: css`
|
||||
flex: 1;
|
||||
fullWidth: css`
|
||||
width: 100%;
|
||||
`,
|
||||
});
|
||||
|
@ -139,7 +139,7 @@ describe('NotificationPreview', () => {
|
||||
mockOneAlertManager();
|
||||
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
|
||||
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="" folder={folder} />, {
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
|
||||
@ -147,20 +147,25 @@ describe('NotificationPreview', () => {
|
||||
await waitFor(() => {
|
||||
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// we expect the alert manager label to be missing as there is only one alert manager configured to receive alerts
|
||||
await waitFor(() => {
|
||||
expect(ui.grafanaAlertManagerLabel.query()).not.toBeInTheDocument();
|
||||
expect(ui.otherAlertManagerLabel.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const matchingPoliciesElements = ui.route.queryAll();
|
||||
expect(matchingPoliciesElements).toHaveLength(1);
|
||||
expect(matchingPoliciesElements[0]).toHaveTextContent(/tomato = red/);
|
||||
});
|
||||
});
|
||||
it('should render notification preview with alert manager sections, when having more than one alert manager configured to receive alerts', async () => {
|
||||
// two alert managers configured to receive alerts
|
||||
mockTwoAlertManagers();
|
||||
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
|
||||
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="" folder={folder} />, {
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
await waitFor(() => {
|
||||
@ -171,14 +176,12 @@ describe('NotificationPreview', () => {
|
||||
await waitFor(() => {
|
||||
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(ui.loadingIndicator.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// we expect the alert manager label to be present as there is more than one alert manager configured to receive alerts
|
||||
await waitFor(() => {
|
||||
expect(ui.grafanaAlertManagerLabel.query()).toBeInTheDocument();
|
||||
|
||||
expect(ui.otherAlertManagerLabel.query()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const matchingPoliciesElements = ui.route.queryAll();
|
||||
expect(matchingPoliciesElements).toHaveLength(2);
|
||||
@ -191,7 +194,7 @@ describe('NotificationPreview', () => {
|
||||
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
|
||||
mockHasEditPermission(true);
|
||||
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="" folder={folder} />, {
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
await waitFor(() => {
|
||||
@ -222,7 +225,7 @@ describe('NotificationPreview', () => {
|
||||
mockPreviewApiResponse(server, [{ labels: [{ tomato: 'red', avocate: 'green' }] }]);
|
||||
mockHasEditPermission(false);
|
||||
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="" folder={folder} />, {
|
||||
render(<NotificationPreview alertQueries={[alertQuery]} customLabels={[]} condition="A" folder={folder} />, {
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
await waitFor(() => {
|
||||
|
@ -20,12 +20,14 @@ interface NotificationPreviewProps {
|
||||
value: string;
|
||||
}>;
|
||||
alertQueries: AlertQuery[];
|
||||
condition: string;
|
||||
folder: Folder;
|
||||
condition: string | null;
|
||||
folder: Folder | null;
|
||||
alertName?: string;
|
||||
alertUid?: string;
|
||||
}
|
||||
|
||||
// TODO the scroll position keeps resetting when we preview
|
||||
// this is to be expected because the list of routes dissapears as we start the request but is very annoying
|
||||
export const NotificationPreview = ({
|
||||
alertQueries,
|
||||
customLabels,
|
||||
@ -35,16 +37,21 @@ export const NotificationPreview = ({
|
||||
alertUid,
|
||||
}: NotificationPreviewProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const disabled = !condition || !folder;
|
||||
|
||||
const { usePreviewMutation } = alertRuleApi;
|
||||
const previewEndpoint = alertRuleApi.endpoints.preview;
|
||||
|
||||
const [trigger, { data = [], isLoading, isUninitialized: previewUninitialized }] = usePreviewMutation();
|
||||
const [trigger, { data = [], isLoading, isUninitialized: previewUninitialized }] = previewEndpoint.useMutation();
|
||||
|
||||
// potential instances are the instances that are going to be routed to the notification policies
|
||||
// convert data to list of labels: are the representation of the potential instances
|
||||
const potentialInstances = compact(data.flatMap((label) => label?.labels));
|
||||
|
||||
const onPreview = () => {
|
||||
if (!folder || !condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the potential labels given the alert queries, the condition and the custom labels (autogenerated labels are calculated on the BE side)
|
||||
trigger({
|
||||
alertQueries: alertQueries,
|
||||
@ -60,32 +67,35 @@ export const NotificationPreview = ({
|
||||
const alertManagerSourceNamesAndImage = useGetAlertManagersSourceNamesAndImage();
|
||||
|
||||
const onlyOneAM = alertManagerSourceNamesAndImage.length === 1;
|
||||
const renderHowToPreview = !Boolean(data?.length) && !isLoading;
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Stack direction="column">
|
||||
<div className={styles.routePreviewHeaderRow}>
|
||||
<div className={styles.previewHeader}>
|
||||
<Text element="h4">Alert instance routing preview</Text>
|
||||
{isLoading && previewUninitialized && (
|
||||
<Text color="secondary" variant="bodySmall">
|
||||
Loading...
|
||||
</Text>
|
||||
)}
|
||||
{previewUninitialized ? (
|
||||
<Text color="secondary" variant="bodySmall">
|
||||
When you have your folder selected and your query and labels are configured, click "Preview
|
||||
routing" to see the results here.
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="secondary" variant="bodySmall">
|
||||
Based on the labels added, alert instances are routed to the following notification policies. Expand each
|
||||
notification policy below to view more details.
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.button}>
|
||||
<Button icon="sync" variant="secondary" type="button" onClick={onPreview}>
|
||||
<Button icon="sync" variant="secondary" type="button" onClick={onPreview} disabled={disabled}>
|
||||
Preview routing
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!renderHowToPreview && (
|
||||
<div className={styles.textMuted}>
|
||||
Based on the labels added, alert instances are routed to the following notification policies. Expand each
|
||||
notification policy below to view more details.
|
||||
</div>
|
||||
)}
|
||||
{isLoading && <div className={styles.textMuted}>Loading...</div>}
|
||||
{renderHowToPreview && (
|
||||
<div className={styles.previewHowToText}>
|
||||
{`When your query and labels are configured, click "Preview routing" to see the results here.`}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !previewUninitialized && potentialInstances.length > 0 && (
|
||||
<Suspense fallback={<LoadingPlaceholder text="Loading preview..." />}>
|
||||
{alertManagerSourceNamesAndImage.map((alertManagerSource) => (
|
||||
@ -107,15 +117,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
width: auto;
|
||||
border: 0;
|
||||
`,
|
||||
textMuted: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
previewHowToText: css`
|
||||
display: flex;
|
||||
color: ${theme.colors.text.secondary};
|
||||
justify-content: center;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
previewHeader: css`
|
||||
margin: 0;
|
||||
`,
|
||||
@ -123,14 +124,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
`,
|
||||
collapseLabel: css`
|
||||
flex: 1;
|
||||
`,
|
||||
button: css`
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
`,
|
||||
tagsInDetails: css`
|
||||
display: flex;
|
||||
|
@ -88,7 +88,6 @@ export default withErrorBoundary(NotificationPreviewByAlertManager);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
alertManagerRow: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
|
@ -342,6 +342,22 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
||||
<RuleEditorSection
|
||||
stepNo={2}
|
||||
title={type !== RuleFormType.cloudRecording ? 'Define query and alert condition' : 'Define query'}
|
||||
description={
|
||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Define queries and/or expressions and then choose one of them as the alert rule condition. This is the
|
||||
threshold that an alert rule must meet or exceed in order to fire.
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText={`An alert rule consists of one or more queries and expressions that select the data you want to measure.
|
||||
Define queries and/or expressions and then choose one of them as the alert rule condition. This is the threshold that an alert rule must meet or exceed in order to fire.
|
||||
For more information on queries and expressions, see Query and transform data.`}
|
||||
externalLink={`https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/`}
|
||||
linkText={`Read about query and condition`}
|
||||
title="Define query and alert condition"
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
{/* This is the cloud data source selector */}
|
||||
{(type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) && (
|
||||
@ -396,22 +412,6 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
||||
{isGrafanaManagedType && (
|
||||
<Stack direction="column">
|
||||
{/* Data Queries */}
|
||||
<Stack direction="row" gap={1} alignItems="baseline">
|
||||
<div className={styles.mutedText}>
|
||||
Define queries and/or expressions and then choose one of them as the alert rule condition. This is the
|
||||
threshold that an alert rule must meet or exceed in order to fire.
|
||||
</div>
|
||||
|
||||
<NeedHelpInfo
|
||||
contentText={`An alert rule consists of one or more queries and expressions that select the data you want to measure.
|
||||
Define queries and/or expressions and then choose one of them as the alert rule condition. This is the threshold that an alert rule must meet or exceed in order to fire.
|
||||
For more information on queries and expressions, see Query and transform data.`}
|
||||
externalLink={`https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/`}
|
||||
linkText={`Read about query and condition`}
|
||||
title="Define query and alert condition"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<QueryEditor
|
||||
queries={dataQueries}
|
||||
expressions={expressionQueries}
|
||||
@ -443,8 +443,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
|
||||
onClickSwitch={onClickSwitch}
|
||||
/>
|
||||
{/* Expression Queries */}
|
||||
<Stack direction="column" gap={0}>
|
||||
<Text element="h5">Expressions</Text>
|
||||
<div className={styles.mutedText}>Manipulate data returned from queries with math and other operations.</div>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Manipulate data returned from queries with math and other operations.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<ExpressionsEditor
|
||||
queries={queries}
|
||||
panelData={queryPreviewData}
|
||||
@ -515,11 +520,6 @@ function TypeSelectorButton({ onClickType }: { onClickType: (type: ExpressionQue
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
mutedText: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
margin-top: ${theme.spacing(-1)};
|
||||
`,
|
||||
addQueryButton: css`
|
||||
width: fit-content;
|
||||
`,
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { DataSourceJsonData } from '@grafana/schema';
|
||||
import { Alert, useStyles2 } from '@grafana/ui';
|
||||
import { RadioButtonGroup, Text } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -39,13 +38,11 @@ const onlyOneDSInQueries = (queries: AlertQuery[]) => {
|
||||
const getCanSwitch = ({
|
||||
queries,
|
||||
ruleFormType,
|
||||
editingExistingRule,
|
||||
rulesSourcesWithRuler,
|
||||
}: {
|
||||
rulesSourcesWithRuler: Array<DataSourceInstanceSettings<DataSourceJsonData>>;
|
||||
queries: AlertQuery[];
|
||||
ruleFormType: RuleFormType | undefined;
|
||||
editingExistingRule: boolean;
|
||||
}) => {
|
||||
// get available rule types
|
||||
const availableRuleTypes = getAvailableRuleTypes();
|
||||
@ -57,12 +54,11 @@ const getCanSwitch = ({
|
||||
|
||||
//let's check if we switch to cloud type
|
||||
const canSwitchToCloudRule =
|
||||
!editingExistingRule &&
|
||||
!isRecordingRuleType &&
|
||||
onlyOneDS &&
|
||||
rulesSourcesWithRuler.some((dsJsonData) => dsJsonData.uid === dataSourceIdFromQueries);
|
||||
|
||||
const canSwitchToGrafanaRule = !editingExistingRule && !isRecordingRuleType;
|
||||
const canSwitchToGrafanaRule = !isRecordingRuleType;
|
||||
// check for enabled types
|
||||
const grafanaTypeEnabled = availableRuleTypes.enabledRuleTypes.includes(RuleFormType.grafana);
|
||||
const cloudTypeEnabled = availableRuleTypes.enabledRuleTypes.includes(RuleFormType.cloudAlerting);
|
||||
@ -83,43 +79,6 @@ export interface SmartAlertTypeDetectorProps {
|
||||
onClickSwitch: () => void;
|
||||
}
|
||||
|
||||
const getContentText = (ruleFormType: RuleFormType, isEditing: boolean, dataSourceName: string, canSwitch: boolean) => {
|
||||
if (isEditing) {
|
||||
if (ruleFormType === RuleFormType.grafana) {
|
||||
return {
|
||||
contentText: `Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported data sources, including having multiple data sources in the same rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported. `,
|
||||
title: `This alert rule is managed by Grafana.`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
contentText: `Data source-managed alert rules can be used for Grafana Mimir or Grafana Loki data sources which have been configured to support rule creation. The use of expressions or multiple queries is not supported.`,
|
||||
title: `This alert rule is managed by the data source ${dataSourceName}.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (canSwitch) {
|
||||
if (ruleFormType === RuleFormType.cloudAlerting) {
|
||||
return {
|
||||
contentText:
|
||||
'Data source-managed alert rules can be used for Grafana Mimir or Grafana Loki data sources which have been configured to support rule creation. The use of expressions or multiple queries is not supported.',
|
||||
title: `This alert rule is managed by the data source ${dataSourceName}. If you want to use expressions or have multiple queries, switch to a Grafana-managed alert rule.`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
contentText:
|
||||
'Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported data sources, including having multiple data sources in the same rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported.',
|
||||
title: `This alert rule will be managed by Grafana. The selected data source is configured to support rule creation.`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// it can be only grafana rule
|
||||
return {
|
||||
contentText: `Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported data sources, including having multiple data sources in the same rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported.`,
|
||||
title: `Based on the data sources selected, this alert rule is managed by Grafana.`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function SmartAlertTypeDetector({
|
||||
editingExistingRule,
|
||||
rulesSourcesWithRuler,
|
||||
@ -127,54 +86,77 @@ export function SmartAlertTypeDetector({
|
||||
onClickSwitch,
|
||||
}: SmartAlertTypeDetectorProps) {
|
||||
const { getValues } = useFormContext<RuleFormValues>();
|
||||
const [ruleFormType] = getValues(['type']);
|
||||
const canSwitch = getCanSwitch({ queries, ruleFormType, rulesSourcesWithRuler });
|
||||
|
||||
const [ruleFormType, dataSourceName] = getValues(['type', 'dataSourceName']);
|
||||
const styles = useStyles2(getStyles);
|
||||
const options = [
|
||||
{ label: 'Grafana-managed', value: RuleFormType.grafana },
|
||||
{ label: 'Data source-managed', value: RuleFormType.cloudAlerting },
|
||||
];
|
||||
|
||||
const canSwitch = getCanSwitch({ queries, ruleFormType, editingExistingRule, rulesSourcesWithRuler });
|
||||
|
||||
const typeTitle =
|
||||
ruleFormType === RuleFormType.cloudAlerting ? 'Data source-managed alert rule' : 'Grafana-managed alert rule';
|
||||
const switchToLabel = ruleFormType !== RuleFormType.cloudAlerting ? 'data source-managed' : 'Grafana-managed';
|
||||
|
||||
const content = ruleFormType
|
||||
? getContentText(ruleFormType, editingExistingRule, dataSourceName ?? '', canSwitch)
|
||||
: undefined;
|
||||
// if we can't switch to data-source managed, disable it
|
||||
// TODO figure out how to show a popover to the user to indicate _why_ it's disabled
|
||||
const disabledOptions = canSwitch ? [] : [RuleFormType.cloudAlerting];
|
||||
|
||||
return (
|
||||
<div className={styles.alert}>
|
||||
<Alert
|
||||
severity="info"
|
||||
title={typeTitle}
|
||||
onRemove={canSwitch ? onClickSwitch : undefined}
|
||||
buttonContent={`Switch to ${switchToLabel} alert rule`}
|
||||
>
|
||||
<Stack gap={0.5} direction="row" alignItems={'baseline'}>
|
||||
<div className={styles.alertText}>{content?.title}</div>
|
||||
<div className={styles.needInfo}>
|
||||
<Stack direction="column" gap={1} alignItems="flex-start">
|
||||
<Stack direction="column" gap={0}>
|
||||
<Text variant="h5">Rule type</Text>
|
||||
<Stack direction="row" gap={0.5} alignItems="baseline">
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Select where the alert rule will be managed.
|
||||
</Text>
|
||||
<NeedHelpInfo
|
||||
contentText={content?.contentText ?? ''}
|
||||
externalLink={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/alert-rule-types/`}
|
||||
linkText={`Read about alert rule types`}
|
||||
contentText={
|
||||
<>
|
||||
<Text color="primary" variant="h6">
|
||||
Grafana-managed alert rules
|
||||
</Text>
|
||||
<p>
|
||||
Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported
|
||||
data sources, including having multiple data sources in the same rule. You can also add expressions to
|
||||
transform your data and set alert conditions. Using images in alert notifications is also supported.
|
||||
</p>
|
||||
<Text color="primary" variant="h6">
|
||||
Data source-managed alert rules
|
||||
</Text>
|
||||
<p>
|
||||
Data source-managed alert rules can be used for Grafana Mimir or Grafana Loki data sources which have
|
||||
been configured to support rule creation. The use of expressions or multiple queries is not supported.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
externalLink="https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/alert-rule-types/"
|
||||
linkText="Read about alert rule types"
|
||||
title="Alert rule types"
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Alert>
|
||||
</div>
|
||||
</Stack>
|
||||
<RadioButtonGroup
|
||||
options={options}
|
||||
disabled={editingExistingRule}
|
||||
disabledOptions={disabledOptions}
|
||||
value={ruleFormType}
|
||||
onChange={onClickSwitch}
|
||||
/>
|
||||
{/* editing an existing rule, we just show "cannot be changed" */}
|
||||
{editingExistingRule && (
|
||||
<Text color="secondary">The alert rule type cannot be changed for an existing rule.</Text>
|
||||
)}
|
||||
{/* in regular alert creation we tell the user what options they have when using a cloud data source */}
|
||||
{!editingExistingRule && (
|
||||
<>
|
||||
{canSwitch ? (
|
||||
<Text color="secondary">
|
||||
{ruleFormType === RuleFormType.grafana
|
||||
? 'The data source selected in your query supports alert rule management. Switch to data source-managed if you want the alert rule to be managed by the data source instead of Grafana.'
|
||||
: 'Switch to Grafana-managed to use expressions, multiple queries, images in notifications and various other features.'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="secondary">Based on the selected data sources this alert rule will be Grafana-managed.</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
alertText: css`
|
||||
max-width: fit-content;
|
||||
flex: 1;
|
||||
`,
|
||||
alert: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
needInfo: css`
|
||||
flex: 1;
|
||||
max-width: fit-content;
|
||||
`,
|
||||
});
|
||||
|
@ -2,16 +2,27 @@ import { rest } from 'msw';
|
||||
import { SetupServer } from 'msw/node';
|
||||
|
||||
import {
|
||||
AlertmanagerChoice,
|
||||
AlertManagerCortexConfig,
|
||||
ExternalAlertmanagersResponse,
|
||||
} from '../../../../plugins/datasource/alertmanager/types';
|
||||
import { AlertmanagersChoiceResponse } from '../api/alertmanagerApi';
|
||||
import { getDatasourceAPIUid } from '../utils/datasource';
|
||||
|
||||
export const defaultAlertmanagerChoiceResponse: AlertmanagersChoiceResponse = {
|
||||
alertmanagersChoice: AlertmanagerChoice.Internal,
|
||||
numExternalAlertmanagers: 0,
|
||||
};
|
||||
export function mockAlertmanagerChoiceResponse(server: SetupServer, response: AlertmanagersChoiceResponse) {
|
||||
server.use(rest.get('/api/v1/ngalert', (req, res, ctx) => res(ctx.status(200), ctx.json(response))));
|
||||
}
|
||||
|
||||
export const emptyExternalAlertmanagersResponse: ExternalAlertmanagersResponse = {
|
||||
data: {
|
||||
droppedAlertManagers: [],
|
||||
activeAlertManagers: [],
|
||||
},
|
||||
};
|
||||
export function mockAlertmanagersResponse(server: SetupServer, response: ExternalAlertmanagersResponse) {
|
||||
server.use(rest.get('/api/v1/ngalert/alertmanagers', (req, res, ctx) => res(ctx.status(200), ctx.json(response))));
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { TestProvider } from './TestProvider';
|
||||
|
||||
export const ui = {
|
||||
inputs: {
|
||||
name: byRole('textbox', { name: /rule name name for the alert rule\./i }),
|
||||
name: byRole('textbox', { name: 'name' }),
|
||||
alertType: byTestId('alert-type-picker'),
|
||||
dataSource: byTestId('datasource-picker'),
|
||||
folder: byTestId('folder-picker'),
|
||||
|
Loading…
Reference in New Issue
Block a user