Alerting: Enable `jsx-no-useless-fragment` rule (#101884)

* Add no-useless-fragment rule for alerting code

* Auto-fix most no-useless-fragment cases

* Manually fix remaining no-useless-fragment cases

* Fix `invalid` passing to Field component

* Allow AlertingPageWrapper to have optional children
pull/101963/head^2
Tom Ratcliffe 2 months ago committed by GitHub
parent 700f1225df
commit 9870718c3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      eslint.config.js
  2. 10
      public/app/features/alerting/unified/RuleViewer.tsx
  3. 7
      public/app/features/alerting/unified/components/AlertingPageWrapper.tsx
  4. 16
      public/app/features/alerting/unified/components/contact-points/ContactPoint.tsx
  5. 54
      public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx
  6. 338
      public/app/features/alerting/unified/components/notification-policies/Policy.tsx
  7. 4
      public/app/features/alerting/unified/components/receivers/AlertInstanceModalSelector.tsx
  8. 22
      public/app/features/alerting/unified/components/receivers/TemplateForm.tsx
  9. 48
      public/app/features/alerting/unified/components/receivers/form/GenerateAlertDataModal.tsx
  10. 58
      public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx
  11. 2
      public/app/features/alerting/unified/components/receivers/form/fields/DeletedSubform.tsx
  12. 12
      public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx
  13. 48
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
  14. 12
      public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx
  15. 62
      public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx
  16. 219
      public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx
  17. 94
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx
  18. 20
      public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
  19. 2
      public/app/features/alerting/unified/components/rules/RulesFilter.test.tsx
  20. 24
      public/app/features/alerting/unified/components/rules/central-state-history/EventDetails.tsx
  21. 18
      public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx
  22. 34
      public/app/features/alerting/unified/home/Insights.tsx
  23. 28
      public/app/features/alerting/unified/rule-list/components/RuleGroupActionsMenu.tsx

@ -262,6 +262,7 @@ module.exports = [
'prefer-const': 'error',
'react/no-unused-prop-types': 'error',
'react/self-closing-comp': 'error',
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'unicorn/no-unused-properties': 'error',
},
},

@ -15,7 +15,7 @@ import { stringifyErrorLike } from './utils/misc';
import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id';
import { withPageErrorBoundary } from './withPageErrorBoundary';
const RuleViewer = (): JSX.Element => {
const RuleViewer = () => {
const params = useParams();
const id = getRuleIdFromPathname(params);
@ -48,11 +48,7 @@ const RuleViewer = (): JSX.Element => {
}
if (loading) {
return (
<AlertingPageWrapper pageNav={defaultPageNav} navId="alert-list" isLoading={true}>
<></>
</AlertingPageWrapper>
);
return <AlertingPageWrapper pageNav={defaultPageNav} navId="alert-list" isLoading={true} />;
}
if (rule) {
@ -73,7 +69,7 @@ const RuleViewer = (): JSX.Element => {
}
// we should never get to this state
return <></>;
return null;
};
export const defaultPageNav: NavModelItem = {

@ -1,4 +1,4 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, ReactNode } from 'react';
import { useLocation } from 'react-use';
import { Page } from 'app/core/components/Page/Page';
@ -12,9 +12,10 @@ import { NoAlertManagerWarning } from './NoAlertManagerWarning';
/**
* This is the main alerting page wrapper, used by the alertmanager page wrapper and the alert rules list view
*/
interface AlertingPageWrapperProps extends PageProps {
type AlertingPageWrapperProps = Omit<PageProps, 'children'> & {
isLoading?: boolean;
}
children?: ReactNode;
};
export const AlertingPageWrapper = ({ children, isLoading, ...rest }: AlertingPageWrapperProps) => (
<Page {...rest}>

@ -223,15 +223,13 @@ const ContactPointReceiverMetadataRow = ({ diagnostics, sendingResolved }: Conta
<Stack direction="row" gap={1}>
{/* this is shown when the last delivery failed – we don't show any additional metadata */}
{failedToSend ? (
<>
<MetaText color="error" icon="exclamation-circle">
<Tooltip content={diagnostics.lastNotifyAttemptError!}>
<span>
<Trans i18nKey="alerting.contact-points.last-delivery-failed">Last delivery attempt failed</Trans>
</span>
</Tooltip>
</MetaText>
</>
<MetaText color="error" icon="exclamation-circle">
<Tooltip content={diagnostics.lastNotifyAttemptError!}>
<span>
<Trans i18nKey="alerting.contact-points.last-delivery-failed">Last delivery attempt failed</Trans>
</span>
</Tooltip>
</MetaText>
) : (
<>
{/* this is shown when we have a last delivery attempt */}

@ -51,32 +51,34 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Field label="Default contact point" invalid={!!errors.receiver} error={errors.receiver?.message}>
<>
<div className={styles.container} data-testid="am-receiver-select">
<Controller
render={({ field: { onChange, ref, value, ...field } }) => (
<ContactPointSelector
selectProps={{
...field,
onChange: (changeValue) => handleContactPointSelect(changeValue, onChange),
}}
selectedContactPointName={value}
/>
)}
control={control}
name="receiver"
rules={{ required: { value: true, message: 'Required.' } }}
/>
<span>or</span>
<Link
className={styles.linkText}
href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}
>
Create a contact point
</Link>
</div>
</>
<Field
label="Default contact point"
invalid={Boolean(errors.receiver) ? true : undefined}
error={errors.receiver?.message}
>
<div className={styles.container} data-testid="am-receiver-select">
<Controller
render={({ field: { onChange, ref, value, ...field } }) => (
<ContactPointSelector
selectProps={{
...field,
onChange: (changeValue) => handleContactPointSelect(changeValue, onChange),
}}
selectedContactPointName={value}
/>
)}
control={control}
name="receiver"
rules={{ required: { value: true, message: 'Required.' } }}
/>
<span>or</span>
<Link
className={styles.linkText}
href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}
>
Create a contact point
</Link>
</div>
</Field>
<Field
label="Group by"

@ -212,179 +212,177 @@ const Policy = (props: PolicyComponentProps) => {
const showMore = moreCount > 0;
return (
<>
<Stack direction="column" gap={1.5}>
<div
className={styles.policyWrapper(hasFocus)}
data-testid={isDefaultPolicy ? 'am-root-route-container' : 'am-route-container'}
>
{/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */}
{continueMatching && <ContinueMatchingIndicator />}
{showMatchesAllLabelsWarning && <AllMatchesIndicator />}
<div className={styles.policyItemWrapper}>
<Stack direction="column" gap={1}>
{/* Matchers and actions */}
<div>
<Stack direction="row" alignItems="center" gap={1}>
{hasChildPolicies ? (
<IconButton
name={showPolicyChildren ? 'angle-down' : 'angle-right'}
onClick={togglePolicyChildren}
aria-label={showPolicyChildren ? 'Collapse' : 'Expand'}
/>
) : null}
{isImmutablePolicy ? (
isAutogeneratedPolicyRoot ? (
<AutogeneratedRootIndicator />
) : (
<DefaultPolicyIndicator />
)
) : hasMatchers ? (
<Matchers matchers={matchers ?? []} formatter={getAmMatcherFormatter(alertManagerSourceName)} />
<Stack direction="column" gap={1.5}>
<div
className={styles.policyWrapper(hasFocus)}
data-testid={isDefaultPolicy ? 'am-root-route-container' : 'am-route-container'}
>
{/* continueMatching and showMatchesAllLabelsWarning are mutually exclusive so the icons can't overlap */}
{continueMatching && <ContinueMatchingIndicator />}
{showMatchesAllLabelsWarning && <AllMatchesIndicator />}
<div className={styles.policyItemWrapper}>
<Stack direction="column" gap={1}>
{/* Matchers and actions */}
<div>
<Stack direction="row" alignItems="center" gap={1}>
{hasChildPolicies ? (
<IconButton
name={showPolicyChildren ? 'angle-down' : 'angle-right'}
onClick={togglePolicyChildren}
aria-label={showPolicyChildren ? 'Collapse' : 'Expand'}
/>
) : null}
{isImmutablePolicy ? (
isAutogeneratedPolicyRoot ? (
<AutogeneratedRootIndicator />
) : (
<span className={styles.metadata}>
<Trans i18nKey="alerting.policies.no-matchers">No matchers</Trans>
</span>
)}
<Spacer />
{/* TODO maybe we should move errors to the gutter instead? */}
{errors.length > 0 && <Errors errors={errors} />}
{provisioned && <ProvisioningBadge />}
<Stack direction="row" gap={0.5}>
{!isAutoGenerated && !readOnly && (
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
{isDefaultPolicy ? (
<DefaultPolicyIndicator />
)
) : hasMatchers ? (
<Matchers matchers={matchers ?? []} formatter={getAmMatcherFormatter(alertManagerSourceName)} />
) : (
<span className={styles.metadata}>
<Trans i18nKey="alerting.policies.no-matchers">No matchers</Trans>
</span>
)}
<Spacer />
{/* TODO maybe we should move errors to the gutter instead? */}
{errors.length > 0 && <Errors errors={errors} />}
{provisioned && <ProvisioningBadge />}
<Stack direction="row" gap={0.5}>
{!isAutoGenerated && !readOnly && (
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
<ConditionalWrap shouldWrap={provisioned} wrap={ProvisionedTooltip}>
{isDefaultPolicy ? (
<Button
variant="secondary"
icon="plus"
size="sm"
disabled={provisioned}
type="button"
onClick={() => onAddPolicy(currentRoute, 'child')}
>
<Trans i18nKey="alerting.policies.new-child">New child policy</Trans>
</Button>
) : (
<Dropdown
overlay={
<Menu>
<Menu.Item
label="New sibling above"
icon="arrow-up"
onClick={() => onAddPolicy(currentRoute, 'above')}
/>
<Menu.Item
label="New sibling below"
icon="arrow-down"
onClick={() => onAddPolicy(currentRoute, 'below')}
/>
<Menu.Divider />
<Menu.Item
label="New child policy"
icon="plus"
onClick={() => onAddPolicy(currentRoute, 'child')}
/>
</Menu>
}
>
<Button
variant="secondary"
icon="plus"
size="sm"
variant="secondary"
disabled={provisioned}
icon="angle-down"
type="button"
onClick={() => onAddPolicy(currentRoute, 'child')}
>
<Trans i18nKey="alerting.policies.new-child">New child policy</Trans>
<Trans i18nKey="alerting.policies.new-policy">Add new policy</Trans>
</Button>
) : (
<Dropdown
overlay={
<Menu>
<Menu.Item
label="New sibling above"
icon="arrow-up"
onClick={() => onAddPolicy(currentRoute, 'above')}
/>
<Menu.Item
label="New sibling below"
icon="arrow-down"
onClick={() => onAddPolicy(currentRoute, 'below')}
/>
<Menu.Divider />
<Menu.Item
label="New child policy"
icon="plus"
onClick={() => onAddPolicy(currentRoute, 'child')}
/>
</Menu>
}
>
<Button
size="sm"
variant="secondary"
disabled={provisioned}
icon="angle-down"
type="button"
>
<Trans i18nKey="alerting.policies.new-policy">Add new policy</Trans>
</Button>
</Dropdown>
)}
</ConditionalWrap>
</Authorize>
)}
{dropdownMenuActions.length > 0 && (
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
<MoreButton
aria-label={isDefaultPolicy ? 'more actions for default policy' : 'more actions for policy'}
data-testid="more-actions"
/>
</Dropdown>
)}
</Stack>
</Dropdown>
)}
</ConditionalWrap>
</Authorize>
)}
{dropdownMenuActions.length > 0 && (
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
<MoreButton
aria-label={isDefaultPolicy ? 'more actions for default policy' : 'more actions for policy'}
data-testid="more-actions"
/>
</Dropdown>
)}
</Stack>
</div>
{/* Metadata row */}
<MetadataRow
matchingInstancesPreview={matchingInstancesPreview}
numberOfAlertInstances={numberOfAlertInstances}
contactPoint={contactPoint ?? undefined}
groupBy={groupBy}
muteTimings={muteTimings}
activeTimings={activeTimings}
timingOptions={timingOptions}
inheritedProperties={inheritedProperties}
alertManagerSourceName={alertManagerSourceName}
receivers={receivers}
matchingAlertGroups={matchingAlertGroups}
matchers={matchers}
isDefaultPolicy={isDefaultPolicy}
onShowAlertInstances={onShowAlertInstances}
/>
</Stack>
</div>
</Stack>
</div>
{/* Metadata row */}
<MetadataRow
matchingInstancesPreview={matchingInstancesPreview}
numberOfAlertInstances={numberOfAlertInstances}
contactPoint={contactPoint ?? undefined}
groupBy={groupBy}
muteTimings={muteTimings}
activeTimings={activeTimings}
timingOptions={timingOptions}
inheritedProperties={inheritedProperties}
alertManagerSourceName={alertManagerSourceName}
receivers={receivers}
matchingAlertGroups={matchingAlertGroups}
matchers={matchers}
isDefaultPolicy={isDefaultPolicy}
onShowAlertInstances={onShowAlertInstances}
/>
</Stack>
</div>
<div className={styles.childPolicies}>
{showPolicyChildren && (
<>
{pageOfChildren.map((child) => {
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
// This child is autogenerated if it's the autogenerated root or if it's a child of an autogenerated policy.
const isThisChildAutoGenerated = isAutoGeneratedRootAndSimplifiedEnabled(child) || isAutoGenerated;
/* pass the "readOnly" prop from the parent, because for any child policy , if its parent it's not editable,
</div>
<div className={styles.childPolicies}>
{showPolicyChildren && (
<>
{pageOfChildren.map((child) => {
const childInheritedProperties = getInheritedProperties(currentRoute, child, inheritedProperties);
// This child is autogenerated if it's the autogenerated root or if it's a child of an autogenerated policy.
const isThisChildAutoGenerated = isAutoGeneratedRootAndSimplifiedEnabled(child) || isAutoGenerated;
/* pass the "readOnly" prop from the parent, because for any child policy , if its parent it's not editable,
then the child policy should not be editable either */
const isThisChildReadOnly = readOnly || provisioned || isAutoGenerated;
return (
<Policy
key={child.id}
currentRoute={child}
receivers={receivers}
contactPointsState={contactPointsState}
readOnly={isThisChildReadOnly}
inheritedProperties={childInheritedProperties}
onAddPolicy={onAddPolicy}
onEditPolicy={onEditPolicy}
onDeletePolicy={onDeletePolicy}
onShowAlertInstances={onShowAlertInstances}
alertManagerSourceName={alertManagerSourceName}
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={matchingInstancesPreview}
isAutoGenerated={isThisChildAutoGenerated}
provisioned={provisioned}
/>
);
})}
{showMore && (
<Button
size="sm"
icon="angle-down"
variant="secondary"
className={styles.moreButtons}
onClick={() => setVisibleChildPolicies(visibleChildPolicies + POLICIES_PER_PAGE)}
>
<Trans i18nKey="alerting.policies.n-more-policies" count={moreCount}>
{{ count: moreCount }} additional policies
</Trans>
</Button>
)}
</>
)}
</div>
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
</Stack>
</>
const isThisChildReadOnly = readOnly || provisioned || isAutoGenerated;
return (
<Policy
key={child.id}
currentRoute={child}
receivers={receivers}
contactPointsState={contactPointsState}
readOnly={isThisChildReadOnly}
inheritedProperties={childInheritedProperties}
onAddPolicy={onAddPolicy}
onEditPolicy={onEditPolicy}
onDeletePolicy={onDeletePolicy}
onShowAlertInstances={onShowAlertInstances}
alertManagerSourceName={alertManagerSourceName}
routesMatchingFilters={routesMatchingFilters}
matchingInstancesPreview={matchingInstancesPreview}
isAutoGenerated={isThisChildAutoGenerated}
provisioned={provisioned}
/>
);
})}
{showMore && (
<Button
size="sm"
icon="angle-down"
variant="secondary"
className={styles.moreButtons}
onClick={() => setVisibleChildPolicies(visibleChildPolicies + POLICIES_PER_PAGE)}
>
<Trans i18nKey="alerting.policies.n-more-policies" count={moreCount}>
{{ count: moreCount }} additional policies
</Trans>
</Button>
)}
</>
)}
</div>
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
</Stack>
);
};
@ -513,14 +511,12 @@ function MetadataRow({
)}
{timingOptions && <TimingOptionsMeta timingOptions={timingOptions} />}
{hasInheritedProperties && (
<>
<MetaText icon="corner-down-right-alt" data-testid="inherited-properties">
<span>
<Trans i18nKey="alerting.policies.metadata.inherited">Inherited</Trans>
</span>
<InheritedProperties properties={inheritedProperties} />
</MetaText>
</>
<MetaText icon="corner-down-right-alt" data-testid="inherited-properties">
<span>
<Trans i18nKey="alerting.policies.metadata.inherited">Inherited</Trans>
</span>
<InheritedProperties properties={inheritedProperties} />
</MetaText>
)}
</Stack>
</div>

@ -113,9 +113,7 @@ export function AlertInstanceModalSelector({
>
<div className={cx(styles.ruleTitle, styles.rowButtonTitle)}>{ruleName}</div>
<div className={styles.alertFolder}>
<>
<Icon name="folder" /> {filteredRules[ruleName][0].labels.grafana_folder ?? ''}
</>
<Icon name="folder" /> {filteredRules[ruleName][0].labels.grafana_folder ?? ''}
</div>
</button>
);

@ -317,18 +317,16 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
</div>
{/* preview column – full height and half-width */}
{isGrafanaAlertManager && (
<>
<div {...rowSplitter.secondaryProps}>
<div {...rowSplitter.splitterProps} />
<TemplatePreview
payload={payload}
templateName={watch('title')}
setPayloadFormatError={setPayloadFormatError}
payloadFormatError={payloadFormatError}
className={cx(styles.templatePreview, styles.minEditorSize)}
/>
</div>
</>
<div {...rowSplitter.secondaryProps}>
<div {...rowSplitter.splitterProps} />
<TemplatePreview
payload={payload}
templateName={watch('title')}
setPayloadFormatError={setPayloadFormatError}
payloadFormatError={payloadFormatError}
className={cx(styles.templatePreview, styles.minEditorSize)}
/>
</div>
)}
</div>
</Stack>

@ -97,31 +97,29 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
setStatus('firing');
}}
>
<>
<Card>
<Stack direction="column" gap={1}>
<div className={styles.section}>
<AnnotationsStep />
</div>
<div className={styles.section}>
<LabelsField />
</div>
<div className={styles.flexWrapper}>
<RadioButtonGroup value={status} options={alertOptions} onChange={(value) => setStatus(value)} />
<Button
onClick={onAdd}
className={styles.onAddButton}
icon="plus-circle"
type="button"
variant="secondary"
disabled={!labelsOrAnnotationsAdded()}
>
Add alert data
</Button>
</div>
</Stack>
</Card>
</>
<Card>
<Stack direction="column" gap={1}>
<div className={styles.section}>
<AnnotationsStep />
</div>
<div className={styles.section}>
<LabelsField />
</div>
<div className={styles.flexWrapper}>
<RadioButtonGroup value={status} options={alertOptions} onChange={(value) => setStatus(value)} />
<Button
onClick={onAdd}
className={styles.onAddButton}
icon="plus-circle"
type="button"
variant="secondary"
disabled={!labelsOrAnnotationsAdded()}
>
Add alert data
</Button>
</div>
</Stack>
</Card>
<div className={styles.onSubmitWrapper} />
{alerts.length > 0 && (
<Stack direction="column" gap={1}>

@ -197,38 +197,36 @@ export function ReceiverForm<R extends ChannelValues>({
/>
);
})}
<>
{isEditable && (
<Button
type="button"
icon="plus"
variant="secondary"
onClick={() => append({ ...defaultItem, __id: String(Math.random()) })}
>
Add contact point integration
</Button>
)}
<div className={styles.buttons}>
{isEditable && (
<Button
type="button"
icon="plus"
variant="secondary"
onClick={() => append({ ...defaultItem, __id: String(Math.random()) })}
>
Add contact point integration
</Button>
<>
{isSubmitting && (
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}
{!isSubmitting && <Button type="submit">Save contact point</Button>}
</>
)}
<div className={styles.buttons}>
{isEditable && (
<>
{isSubmitting && (
<Button disabled={true} icon="spinner" variant="primary">
Saving...
</Button>
)}
{!isSubmitting && <Button type="submit">Save contact point</Button>}
</>
)}
<LinkButton
disabled={isSubmitting}
variant="secondary"
data-testid="cancel-button"
href={makeAMLink('/alerting/notifications', alertManagerSourceName)}
>
<Trans i18nKey="alerting.common.cancel">Cancel</Trans>
</LinkButton>
</div>
</>
<LinkButton
disabled={isSubmitting}
variant="secondary"
data-testid="cancel-button"
href={makeAMLink('/alerting/notifications', alertManagerSourceName)}
>
<Trans i18nKey="alerting.common.cancel">Cancel</Trans>
</LinkButton>
</div>
</form>
</FormProvider>
);

@ -17,5 +17,5 @@ export function DeletedSubForm({ pathPrefix }: Props): JSX.Element {
register(`${pathPrefix}.__deleted`);
}, [register, pathPrefix]);
return <></>;
return <>{null}</>;
}

@ -240,16 +240,12 @@ function NeedHelpInfoForNotificationPolicy() {
contentText={
<Stack gap={1} direction="column">
<Stack direction="column" gap={0}>
<>
Firing alert instances are routed to notification policies based on matching labels. The default
notification policy matches all alert instances.
</>
Firing alert instances are routed to notification policies based on matching labels. The default
notification policy matches all alert instances.
</Stack>
<Stack direction="column" gap={0}>
<>
Custom labels change the way your notifications are routed. First, add labels to your alert rule and then
connect them to your notification policy by adding label matchers.
</>
Custom labels change the way your notifications are routed. First, add labels to your alert rule and then
connect them to your notification policy by adding label matchers.
<a
href={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/notification-policies/`}
target="_blank"

@ -92,31 +92,29 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
];
return (
<>
<FormProvider {...formAPI}>
<AppChromeUpdate actions={actionButtons} />
<form onSubmit={(e) => e.preventDefault()}>
<div>
<Stack direction="column" gap={3}>
{/* Step 1 */}
<AlertRuleNameAndMetric />
{/* Step 2 */}
<QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} />
{/* Step 3-4-5 */}
<GrafanaFolderAndLabelsStep />
{/* Step 4 & 5 */}
<GrafanaEvaluationBehaviorStep existing={Boolean(existing)} enableProvisionedGroups={true} />
{/* Notifications step*/}
<NotificationsStep alertUid={alertUid} />
{/* Annotations only for cloud and Grafana */}
<AnnotationsStep />
</Stack>
</div>
</form>
{exportData && <GrafanaRuleDesignExporter exportValues={exportData} onClose={onClose} uid={alertUid} />}
</FormProvider>
</>
<FormProvider {...formAPI}>
<AppChromeUpdate actions={actionButtons} />
<form onSubmit={(e) => e.preventDefault()}>
<div>
<Stack direction="column" gap={3}>
{/* Step 1 */}
<AlertRuleNameAndMetric />
{/* Step 2 */}
<QueryAndExpressionsStep editingExistingRule={existing} onDataChange={checkAlertCondition} />
{/* Step 3-4-5 */}
<GrafanaFolderAndLabelsStep />
{/* Step 4 & 5 */}
<GrafanaEvaluationBehaviorStep existing={Boolean(existing)} enableProvisionedGroups={true} />
{/* Notifications step*/}
<NotificationsStep alertUid={alertUid} />
{/* Annotations only for cloud and Grafana */}
<AnnotationsStep />
</Stack>
</div>
</form>
{exportData && <GrafanaRuleDesignExporter exportValues={exportData} onClose={onClose} uid={alertUid} />}
</FormProvider>
);
}

@ -81,13 +81,11 @@ export function NotificationRouteDetailsModal({
{isDefault && <div className={styles.textMuted}>Default policy</div>}
<div className={styles.separator(1)} />
{!isDefault && (
<>
<PolicyPath
route={route}
routesByIdMap={routesByIdMap}
matcherFormatter={getAmMatcherFormatter(alertManagerSourceName)}
/>
</>
<PolicyPath
route={route}
routesByIdMap={routesByIdMap}
matcherFormatter={getAmMatcherFormatter(alertManagerSourceName)}
/>
)}
<div className={styles.separator(4)} />
<div className={styles.contactPoint}>

@ -23,38 +23,36 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C
const ruleFormType = watch('type');
return (
<>
<div className={styles.flexRow}>
{(ruleFormType === RuleFormType.cloudAlerting || ruleFormType === RuleFormType.cloudRecording) && (
<Field
className={styles.formInput}
label={disabled ? 'Data source' : 'Select data source'}
error={errors.dataSourceName?.message}
invalid={!!errors.dataSourceName?.message}
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<CloudRulesSourcePicker
{...field}
disabled={disabled}
onChange={(ds: DataSourceInstanceSettings) => {
// reset expression as they don't need to persist after changing datasources
setValue('expression', '');
onChange(ds?.name ?? null);
onChangeCloudDatasource(ds?.uid ?? null);
}}
/>
)}
name="dataSourceName"
control={control}
rules={{
required: { value: true, message: 'Please select a data source' },
}}
/>
</Field>
)}
</div>
</>
<div className={styles.flexRow}>
{(ruleFormType === RuleFormType.cloudAlerting || ruleFormType === RuleFormType.cloudRecording) && (
<Field
className={styles.formInput}
label={disabled ? 'Data source' : 'Select data source'}
error={errors.dataSourceName?.message}
invalid={!!errors.dataSourceName?.message}
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<CloudRulesSourcePicker
{...field}
disabled={disabled}
onChange={(ds: DataSourceInstanceSettings) => {
// reset expression as they don't need to persist after changing datasources
setValue('expression', '');
onChange(ds?.name ?? null);
onChangeCloudDatasource(ds?.uid ?? null);
}}
/>
)}
name="dataSourceName"
control={control}
rules={{
required: { value: true, message: 'Please select a data source' },
}}
/>
</Field>
)}
</div>
);
};

@ -316,124 +316,113 @@ export function EditRuleGroupModalForm(props: ModalFormProps): React.ReactElemen
return (
<FormProvider {...formAPI}>
<form onSubmit={handleSubmit(onSubmit, onInvalid)} key={JSON.stringify(defaultValues)}>
<>
{!props.hideFolder && (
<Stack gap={1} alignItems={'center'}>
<Field
className={styles.formInput}
label={
<Label
htmlFor="namespaceName"
description={
!isGrafanaManagedGroup &&
'Change the current namespace name. Moving groups between namespaces is not supported'
}
>
{nameSpaceLabel}
</Label>
}
invalid={Boolean(errors.namespaceName) ? true : undefined}
error={errors.namespaceName?.message}
>
<Input
id="namespaceName"
readOnly={intervalEditOnly || isGrafanaManagedGroup}
value={folderTitle}
{...register('namespaceName', {
required: 'Namespace name is required.',
})}
/>
</Field>
{isGrafanaManagedGroup && props.folderUrl && (
<LinkButton
href={props.folderUrl}
title="Go to folder"
variant="secondary"
icon="folder-open"
target="_blank"
/>
)}
</Stack>
)}
<Field
label={
<Label
htmlFor="groupName"
description="A group evaluates all its rules over the same evaluation interval."
>
Evaluation group
</Label>
}
invalid={!!errors.groupName}
error={errors.groupName?.message}
>
<Input
autoFocus={true}
id="groupName"
readOnly={intervalEditOnly}
{...register('groupName', {
required: 'Evaluation group name is required.',
})}
/>
</Field>
<Field
label={
<Label
htmlFor="groupInterval"
description="How often is the rule evaluated. Applies to every rule within the group."
>
<Stack gap={0.5}>Evaluation interval</Stack>
</Label>
}
invalid={Boolean(errors.groupInterval) ? true : undefined}
error={errors.groupInterval?.message}
>
<Stack direction="column">
{!props.hideFolder && (
<Stack gap={1} alignItems={'center'}>
<Field
className={styles.formInput}
label={
<Label
htmlFor="namespaceName"
description={
!isGrafanaManagedGroup &&
'Change the current namespace name. Moving groups between namespaces is not supported'
}
>
{nameSpaceLabel}
</Label>
}
invalid={Boolean(errors.namespaceName) ? true : undefined}
error={errors.namespaceName?.message}
>
<Input
id="groupInterval"
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
{...register('groupInterval', evaluateEveryValidationOptions(rulesWithoutRecordingRules))}
/>
<EvaluationGroupQuickPick
currentInterval={getValues('groupInterval')}
onSelect={(value) => setValue('groupInterval', value, { shouldValidate: true, shouldDirty: true })}
id="namespaceName"
readOnly={intervalEditOnly || isGrafanaManagedGroup}
value={folderTitle}
{...register('namespaceName', {
required: 'Namespace name is required.',
})}
/>
</Stack>
</Field>
{/* if we're dealing with a Grafana-managed group, check if the evaluation interval is valid / permitted */}
{isGrafanaManagedGroup && checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && (
<EvaluationIntervalLimitExceeded />
)}
{!hasSomeNoRecordingRules && <div>This group does not contain alert rules.</div>}
{hasSomeNoRecordingRules && (
<>
<div>List of rules that belong to this group</div>
<div className={styles.evalRequiredLabel}>
#Eval column represents the number of evaluations needed before alert starts firing.
</div>
<RulesForGroupTable rulesWithoutRecordingRules={rulesWithoutRecordingRules} />
</>
)}
{error && <Alert title={'Failed to update rule group'}>{stringifyErrorLike(error)}</Alert>}
<div className={styles.modalButtons}>
<Modal.ButtonRow>
<Button
</Field>
{isGrafanaManagedGroup && props.folderUrl && (
<LinkButton
href={props.folderUrl}
title="Go to folder"
variant="secondary"
type="button"
disabled={loading}
onClick={() => onClose(false)}
fill="outline"
>
<Trans i18nKey="alerting.common.cancel">Cancel</Trans>
</Button>
<Button type="submit" disabled={!isDirty || !isValid || loading}>
{loading ? 'Saving...' : 'Save'}
</Button>
</Modal.ButtonRow>
</div>
</>
icon="folder-open"
target="_blank"
/>
)}
</Stack>
)}
<Field
label={
<Label htmlFor="groupName" description="A group evaluates all its rules over the same evaluation interval.">
Evaluation group
</Label>
}
invalid={!!errors.groupName}
error={errors.groupName?.message}
>
<Input
autoFocus={true}
id="groupName"
readOnly={intervalEditOnly}
{...register('groupName', {
required: 'Evaluation group name is required.',
})}
/>
</Field>
<Field
label={
<Label
htmlFor="groupInterval"
description="How often is the rule evaluated. Applies to every rule within the group."
>
<Stack gap={0.5}>Evaluation interval</Stack>
</Label>
}
invalid={Boolean(errors.groupInterval) ? true : undefined}
error={errors.groupInterval?.message}
>
<Stack direction="column">
<Input
id="groupInterval"
placeholder={DEFAULT_GROUP_EVALUATION_INTERVAL}
{...register('groupInterval', evaluateEveryValidationOptions(rulesWithoutRecordingRules))}
/>
<EvaluationGroupQuickPick
currentInterval={getValues('groupInterval')}
onSelect={(value) => setValue('groupInterval', value, { shouldValidate: true, shouldDirty: true })}
/>
</Stack>
</Field>
{/* if we're dealing with a Grafana-managed group, check if the evaluation interval is valid / permitted */}
{isGrafanaManagedGroup && checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && (
<EvaluationIntervalLimitExceeded />
)}
{!hasSomeNoRecordingRules && <div>This group does not contain alert rules.</div>}
{hasSomeNoRecordingRules && (
<>
<div>List of rules that belong to this group</div>
<div className={styles.evalRequiredLabel}>
#Eval column represents the number of evaluations needed before alert starts firing.
</div>
<RulesForGroupTable rulesWithoutRecordingRules={rulesWithoutRecordingRules} />
</>
)}
{error && <Alert title={'Failed to update rule group'}>{stringifyErrorLike(error)}</Alert>}
<div className={styles.modalButtons}>
<Modal.ButtonRow>
<Button variant="secondary" type="button" disabled={loading} onClick={() => onClose(false)} fill="outline">
<Trans i18nKey="alerting.common.cancel">Cancel</Trans>
</Button>
<Button type="submit" disabled={!isDirty || !isValid || loading}>
{loading ? 'Saving...' : 'Save'}
</Button>
</Modal.ButtonRow>
</div>
</form>
</FormProvider>
);

@ -158,54 +158,52 @@ const SavedSearches = () => {
const applySearch = useCallback((name: string) => {}, []);
return (
<>
<Stack direction="column" gap={2} alignItems="flex-end">
<Button variant="secondary" size="sm">
<Trans i18nKey="alerting.search.save-query">Save current search</Trans>
</Button>
<InteractiveTable<TableColumns>
columns={[
{
id: 'name',
header: 'Saved search name',
cell: ({ row }) => (
<Stack alignItems="center">
{row.original.name}
{row.original.default ? <Badge text="Default" color="blue" /> : null}
</Stack>
),
},
{
id: 'actions',
cell: ({ row }) => (
<Stack direction="row" alignItems="center">
<Button variant="secondary" fill="outline" size="sm" onClick={() => applySearch(row.original.name)}>
<Trans i18nKey="common.apply">Apply</Trans>
</Button>
<MoreButton size="sm" fill="outline" />
</Stack>
),
},
]}
data={[
{
name: 'My saved search',
default: true,
},
{
name: 'Another saved search',
},
{
name: 'This one has a really long name and some emojis too 🥒',
},
]}
getRowId={(row) => row.name}
/>
<Button variant="secondary">
<Trans i18nKey="common.close">Close</Trans>
</Button>
</Stack>
</>
<Stack direction="column" gap={2} alignItems="flex-end">
<Button variant="secondary" size="sm">
<Trans i18nKey="alerting.search.save-query">Save current search</Trans>
</Button>
<InteractiveTable<TableColumns>
columns={[
{
id: 'name',
header: 'Saved search name',
cell: ({ row }) => (
<Stack alignItems="center">
{row.original.name}
{row.original.default ? <Badge text="Default" color="blue" /> : null}
</Stack>
),
},
{
id: 'actions',
cell: ({ row }) => (
<Stack direction="row" alignItems="center">
<Button variant="secondary" fill="outline" size="sm" onClick={() => applySearch(row.original.name)}>
<Trans i18nKey="common.apply">Apply</Trans>
</Button>
<MoreButton size="sm" fill="outline" />
</Stack>
),
},
]}
data={[
{
name: 'My saved search',
default: true,
},
{
name: 'Another saved search',
},
{
name: 'This one has a really long name and some emojis too 🥒',
},
]}
getRowId={(row) => row.name}
/>
<Button variant="secondary">
<Trans i18nKey="common.close">Close</Trans>
</Button>
</Stack>
);
};

@ -82,17 +82,15 @@ export const NoRulesSplash = () => {
) : null
}
>
<>
<Trans i18nKey="alerting.list-view.empty.provisioning">
You can also define rules through file provisioning or Terraform.{' '}
<TextLink
href="https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/"
external
>
Learn more
</TextLink>
</Trans>
</>
<Trans i18nKey="alerting.list-view.empty.provisioning">
You can also define rules through file provisioning or Terraform.{' '}
<TextLink
href="https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/"
external
>
Learn more
</TextLink>
</Trans>
</EmptyState>
</div>
);

@ -16,7 +16,7 @@ jest.mock('./MultipleDataSourcePicker', () => {
const original = jest.requireActual('./MultipleDataSourcePicker');
return {
...original,
MultipleDataSourcePicker: () => <></>,
MultipleDataSourcePicker: () => null,
};
});

@ -220,19 +220,17 @@ const Annotations = ({ rule }: AnnotationsProps) => {
return null;
}
return (
<>
<div className={styles.metadataWrapper}>
{Object.entries(annotations).map(([name, value]) => {
const capitalizedName = capitalize(name);
return (
<MetaText direction="column" key={capitalizedName}>
{capitalizedName}
<AnnotationValue value={value} />
</MetaText>
);
})}
</div>
</>
<div className={styles.metadataWrapper}>
{Object.entries(annotations).map(([name, value]) => {
const capitalizedName = capitalize(name);
return (
<MetaText direction="column" key={capitalizedName}>
{capitalizedName}
<AnnotationValue value={value} />
</MetaText>
);
})}
</div>
);
};
interface ValueInTransitionProps {

@ -114,16 +114,14 @@ const LokiStateHistory = ({ ruleUID }: Props) => {
</Stack>
)}
{isEmpty(frameSubset) ? (
<>
<div className={styles.emptyState}>
{emptyStateMessage}
{totalRecordsCount > 0 && (
<Button variant="secondary" type="button" onClick={onFilterCleared}>
Clear filters
</Button>
)}
</div>
</>
<div className={styles.emptyState}>
{emptyStateMessage}
{totalRecordsCount > 0 && (
<Button variant="secondary" type="button" onClick={onFilterCleared}>
Clear filters
</Button>
)}
</div>
) : (
<>
<div className={styles.graphWrapper}>

@ -176,24 +176,22 @@ export function getInsightsScenes() {
component: SectionSubheader,
props: {
children: (
<>
<Text>
Monitor the status of your system{' '}
<Tooltip
content={
<div>
Alerting insights provides pre-built dashboards to monitor your alerting data.
<br />
<br />
You can identify patterns in why things go wrong and discover trends in alerting performance
within your organization.
</div>
}
>
<Icon name="info-circle" size="sm" />
</Tooltip>
</Text>
</>
<Text>
Monitor the status of your system{' '}
<Tooltip
content={
<div>
Alerting insights provides pre-built dashboards to monitor your alerting data.
<br />
<br />
You can identify patterns in why things go wrong and discover trends in alerting performance within
your organization.
</div>
}
>
<Icon name="info-circle" size="sm" />
</Tooltip>
</Text>
),
},
}),

@ -3,20 +3,18 @@ import { t } from 'app/core/internationalization';
export function RuleGroupActionsMenu() {
return (
<>
<Dropdown
overlay={
<Menu>
<Menu.Item label={t('alerting.group-actions.edit', 'Edit')} icon="pen" data-testid="edit-group-action" />
<Menu.Item label={t('alerting.group-actions.reorder', 'Re-order rules')} icon="flip" />
<Menu.Divider />
<Menu.Item label={t('alerting.group-actions.export', 'Export')} icon="download-alt" />
<Menu.Item label={t('alerting.group-actions.delete', 'Delete')} icon="trash-alt" destructive />
</Menu>
}
>
<IconButton name="ellipsis-h" aria-label={t('alerting.group-actions.actions-trigger', 'Rule group actions')} />
</Dropdown>
</>
<Dropdown
overlay={
<Menu>
<Menu.Item label={t('alerting.group-actions.edit', 'Edit')} icon="pen" data-testid="edit-group-action" />
<Menu.Item label={t('alerting.group-actions.reorder', 'Re-order rules')} icon="flip" />
<Menu.Divider />
<Menu.Item label={t('alerting.group-actions.export', 'Export')} icon="download-alt" />
<Menu.Item label={t('alerting.group-actions.delete', 'Delete')} icon="trash-alt" destructive />
</Menu>
}
>
<IconButton name="ellipsis-h" aria-label={t('alerting.group-actions.actions-trigger', 'Rule group actions')} />
</Dropdown>
);
}

Loading…
Cancel
Save