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
This commit is contained in:
@@ -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 */}
|
||||
|
||||
+28
-26
@@ -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"
|
||||
|
||||
+174
-178
@@ -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 />}
|
||||
<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)} />
|
||||
) : (
|
||||
<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
|
||||
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>
|
||||
</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>
|
||||
<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}
|
||||
<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'}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
{isImmutablePolicy ? (
|
||||
isAutogeneratedPolicyRoot ? (
|
||||
<AutogeneratedRootIndicator />
|
||||
) : (
|
||||
<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
|
||||
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>
|
||||
</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>
|
||||
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
|
||||
</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,
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-3
@@ -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>
|
||||
|
||||
+23
-25
@@ -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>
|
||||
);
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+22
-24
@@ -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 />
|
||||
<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>
|
||||
</>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+5
-7
@@ -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}>
|
||||
|
||||
+30
-32
@@ -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))}
|
||||
id="namespaceName"
|
||||
readOnly={intervalEditOnly || isGrafanaManagedGroup}
|
||||
value={folderTitle}
|
||||
{...register('namespaceName', {
|
||||
required: 'Namespace name is required.',
|
||||
})}
|
||||
/>
|
||||
<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
|
||||
</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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+11
-13
@@ -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 {
|
||||
|
||||
+8
-10
@@ -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>
|
||||
),
|
||||
},
|
||||
}),
|
||||
|
||||
+13
-15
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user