The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/alerting/unified/components/rules/RulesGroup.tsx

402 lines
12 KiB

import { css } from '@emotion/css';
import pluralize from 'pluralize';
import React, { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Badge, ConfirmModal, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { useDispatch } from 'app/types';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import { LogMessages, logInfo } from '../../Analytics';
import { useFolder } from '../../hooks/useFolder';
import { useHasRuler } from '../../hooks/useHasRuler';
import { deleteRulesGroupAction } from '../../state/actions';
import { useRulesAccess } from '../../utils/accessControlHooks';
import { GRAFANA_RULES_SOURCE_NAME, isCloudRulesSource } from '../../utils/datasource';
import { makeFolderLink, makeFolderSettingsLink } from '../../utils/misc';
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
import { CollapseToggle } from '../CollapseToggle';
import { RuleLocation } from '../RuleLocation';
import { GrafanaRuleFolderExporter } from '../export/GrafanaRuleFolderExporter';
import { GrafanaRuleGroupExporter } from '../export/GrafanaRuleGroupExporter';
import { decodeGrafanaNamespace } from '../expressions/util';
import { ActionIcon } from './ActionIcon';
import { EditCloudGroupModal } from './EditRuleGroupModal';
import { ReorderCloudGroupModal } from './ReorderRuleGroupModal';
Alerting: Add limits and move state and label matching filters to the BE (#66267) * WIP * Add instance totals to combined rule. Use totals to display instances stats in the UI * WIP * add global summaries, fix TS errors * fix useCombined test * fix test * use activeAt from rule when available * Fix NaN in global stats * Add no data total to global summary * Add totals recalculation for filtered rules * Fix instances totals, remove instances filtering from alert list view * Update tests * Fetch alerts considering filtering label matchers * WIP - Fetch alerts appending state filter to endpoint * Fix multiple values for state in request being applyied * fix test * Calculate hidden by for grafana managed alerts * Use INSTANCES_DISPLAY_LIMIT constant for limiting alert instances instead of 1 * Rename matchers parameter according to API changes * Fix calculating total number of grafana instances * Rename matcher prop after previous change * Display button to remove max instances limit * Change matcher query param to be an array of strings * Add test for paramsWithMatcherAndState method * Refactor matcher to be an string array to be consistent with state * Use matcher query string as matcher object type (encoded JSON) * Avoind encoding matcher parameters twice * fix tests * Enable toggle for the limit/show all button and restore limit and filters when we come back from custom view * Move getMatcherListFromString method to utils/alertmanager.ts * Fix limit toggle button being shown when it's not necessary * Use filteredTotals from be response to calculate hidden by count * Fix variables not being replaced correctly * Fix total shown to be all the instances filtered without limits * Adress some PR review comments * Move paramsWithMatcherAndState inside prometheusUrlBuilder method --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com> Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com> Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>
3 years ago
import { RuleGroupStats } from './RuleStats';
import { RulesTable } from './RulesTable';
type ViewMode = 'grouped' | 'list';
interface Props {
namespace: CombinedRuleNamespace;
group: CombinedRuleGroup;
expandAll: boolean;
viewMode: ViewMode;
}
export const RulesGroup = React.memo(({ group, namespace, expandAll, viewMode }: Props) => {
const { rulesSource } = namespace;
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const [isEditingGroup, setIsEditingGroup] = useState(false);
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
const [isReorderingGroup, setIsReorderingGroup] = useState(false);
const [isExporting, setIsExporting] = useState<'group' | 'folder' | undefined>(undefined);
const [isCollapsed, setIsCollapsed] = useState(!expandAll);
const { canEditRules } = useRulesAccess();
useEffect(() => {
setIsCollapsed(!expandAll);
}, [expandAll]);
const { hasRuler, rulerRulesLoaded } = useHasRuler();
const rulerRule = group.rules[0]?.rulerRule;
const folderUID = (rulerRule && isGrafanaRulerRule(rulerRule) && rulerRule.grafana_alert.namespace_uid) || undefined;
const { folder } = useFolder(folderUID);
// group "is deleting" if rules source has ruler, but this group has no rules that are in ruler
const isDeleting =
hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && !group.rules.find((rule) => !!rule.rulerRule);
const isFederated = isFederatedRuleGroup(group);
// check if group has provisioned items
const isProvisioned = group.rules.some((rule) => {
return isGrafanaRulerRule(rule.rulerRule) && rule.rulerRule.grafana_alert.provenance;
});
// check what view mode we are in
const isListView = viewMode === 'list';
const isGroupView = viewMode === 'grouped';
const deleteGroup = () => {
dispatch(deleteRulesGroupAction(namespace, group));
setIsDeletingGroup(false);
};
const actionIcons: React.ReactNode[] = [];
// for grafana, link to folder views
if (isDeleting) {
actionIcons.push(
<Stack key="is-deleting">
<Spinner />
deleting
</Stack>
);
} else if (rulesSource === GRAFANA_RULES_SOURCE_NAME) {
if (folderUID) {
const baseUrl = makeFolderLink(folderUID);
if (folder?.canSave) {
if (isGroupView && !isProvisioned) {
actionIcons.push(
<ActionIcon
aria-label="edit rule group"
data-testid="edit-group"
key="edit"
icon="pen"
tooltip="edit rule group"
onClick={() => setIsEditingGroup(true)}
/>
);
actionIcons.push(
<ActionIcon
aria-label="re-order rules"
data-testid="reorder-group"
key="reorder"
icon="exchange-alt"
tooltip="reorder rules"
className={styles.rotate90}
onClick={() => setIsReorderingGroup(true)}
/>
);
}
if (isListView) {
actionIcons.push(
<ActionIcon
aria-label="go to folder"
key="goto"
icon="folder-open"
tooltip="go to folder"
to={baseUrl}
target="__blank"
/>
);
if (folder?.canAdmin) {
actionIcons.push(
<ActionIcon
aria-label="manage permissions"
key="manage-perms"
icon="lock"
tooltip="manage permissions"
to={baseUrl + '/permissions'}
target="__blank"
/>
);
}
}
}
if (folder) {
if (isListView) {
actionIcons.push(
<ActionIcon
aria-label="export rule folder"
data-testid="export-folder"
key="export-folder"
icon="download-alt"
tooltip="Export rules folder"
onClick={() => setIsExporting('folder')}
/>
);
} else if (isGroupView) {
actionIcons.push(
<ActionIcon
aria-label="export rule group"
data-testid="export-group"
key="export-group"
icon="download-alt"
tooltip="Export rule group"
onClick={() => setIsExporting('group')}
/>
);
}
}
}
} else if (canEditRules(rulesSource.name) && hasRuler(rulesSource)) {
if (!isFederated) {
actionIcons.push(
<ActionIcon
aria-label="edit rule group"
data-testid="edit-group"
key="edit"
icon="pen"
tooltip="edit rule group"
onClick={() => setIsEditingGroup(true)}
/>
);
actionIcons.push(
<ActionIcon
aria-label="re-order rules"
data-testid="reorder-group"
key="reorder"
icon="exchange-alt"
tooltip="re-order rules"
className={styles.rotate90}
onClick={() => setIsReorderingGroup(true)}
/>
);
}
actionIcons.push(
<ActionIcon
aria-label="delete rule group"
data-testid="delete-group"
key="delete-group"
icon="trash-alt"
tooltip="delete rule group"
onClick={() => setIsDeletingGroup(true)}
/>
);
}
// ungrouped rules are rules that are in the "default" group name
const groupName = isListView ? (
<RuleLocation namespace={decodeGrafanaNamespace(namespace).name} />
) : (
<RuleLocation namespace={decodeGrafanaNamespace(namespace).name} group={group.name} />
);
const closeEditModal = (saved = false) => {
if (!saved) {
logInfo(LogMessages.leavingRuleGroupEdit);
}
setIsEditingGroup(false);
};
return (
<div className={styles.wrapper} data-testid="rule-group">
<div className={styles.header} data-testid="rule-group-header">
<CollapseToggle
size="sm"
className={styles.collapseToggle}
isCollapsed={isCollapsed}
onToggle={setIsCollapsed}
data-testid={selectors.components.AlertRules.groupToggle}
/>
<Icon name={isCollapsed ? 'folder' : 'folder-open'} />
{isCloudRulesSource(rulesSource) && (
<Tooltip content={rulesSource.name} placement="top">
<img
alt={rulesSource.meta.name}
className={styles.dataSourceIcon}
src={rulesSource.meta.info.logos.small}
/>
</Tooltip>
)}
{
// eslint-disable-next-line
<div className={styles.groupName} onClick={() => setIsCollapsed(!isCollapsed)}>
{isFederated && <Badge color="purple" text="Federated" />} {groupName}
</div>
}
<div className={styles.spacer} />
<div className={styles.headerStats}>
Alerting: Add limits and move state and label matching filters to the BE (#66267) * WIP * Add instance totals to combined rule. Use totals to display instances stats in the UI * WIP * add global summaries, fix TS errors * fix useCombined test * fix test * use activeAt from rule when available * Fix NaN in global stats * Add no data total to global summary * Add totals recalculation for filtered rules * Fix instances totals, remove instances filtering from alert list view * Update tests * Fetch alerts considering filtering label matchers * WIP - Fetch alerts appending state filter to endpoint * Fix multiple values for state in request being applyied * fix test * Calculate hidden by for grafana managed alerts * Use INSTANCES_DISPLAY_LIMIT constant for limiting alert instances instead of 1 * Rename matchers parameter according to API changes * Fix calculating total number of grafana instances * Rename matcher prop after previous change * Display button to remove max instances limit * Change matcher query param to be an array of strings * Add test for paramsWithMatcherAndState method * Refactor matcher to be an string array to be consistent with state * Use matcher query string as matcher object type (encoded JSON) * Avoind encoding matcher parameters twice * fix tests * Enable toggle for the limit/show all button and restore limit and filters when we come back from custom view * Move getMatcherListFromString method to utils/alertmanager.ts * Fix limit toggle button being shown when it's not necessary * Use filteredTotals from be response to calculate hidden by count * Fix variables not being replaced correctly * Fix total shown to be all the instances filtered without limits * Adress some PR review comments * Move paramsWithMatcherAndState inside prometheusUrlBuilder method --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com> Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com> Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>
3 years ago
<RuleGroupStats group={group} />
</div>
{isProvisioned && (
<>
<div className={styles.actionsSeparator}>|</div>
<div className={styles.actionIcons}>
<Badge color="purple" text="Provisioned" />
</div>
</>
)}
{!!actionIcons.length && (
<>
<div className={styles.actionsSeparator}>|</div>
<div className={styles.actionIcons}>
<Stack gap={0.5}>{actionIcons}</Stack>
</div>
</>
)}
</div>
{!isCollapsed && (
<RulesTable
showSummaryColumn={true}
className={styles.rulesTable}
showGuidelines={true}
showNextEvaluationColumn={Boolean(group.interval)}
rules={group.rules}
/>
)}
Alerting: Improve UI for making more clear that evaluation interval belongs to the group (#56397) * In GrafanaEvaluationBehaviour component : Split evaluation interval from for duration and add button to edit to allow editing it and warning * Move folder and group fields to the evaluation section in the alert form * Include 'Group behaviour' info in a card and fix 'Edit group behaviour' button onClick. * Create hook for getting groups for a particular folder * Use dropdown in group instead of input and fill it with groups that belong to the selected folder * Add evaluation interval for each group in dropdown , and show warning in case user wants to update it * Avoid saving evaluation interval when some rules in the same group would have invalid For with this change * Clear group value when reseting the drop down * Remove evaluationEvery from form values, show this as a label and add a button to edit the group * Open EditRuleGroupModal for editing evaluation interval form the alert rule form * Fix aligment in group behaviour card * compact space in evaluation behaviour card and change group drop down label * In EditgroupModal, in case of grafana managed group, show folder instead of namespcace label and disable the folder name input * Add edge case in rulesInSameGroupHaveInvalidFor method when For value is zero * Vertically align annotations input to the evaluation section in alert rule form * Fix width when editing new group * Add placeholder for group input * Make folder and group in modal readonly from alert form and disable edit group button when new group * Update texts * Don't show evaluation behaviour section until folder and group are selected * Update texts * Fix merge conflits * Fix wrong margin in evaluation label * Remove non-used isRulerGrafanaRuleDTO method * Remove negative margin to avoid overlaping on Firefox
3 years ago
{isEditingGroup && (
<EditCloudGroupModal
namespace={namespace}
group={group}
Alerting: Improve UI for making more clear that evaluation interval belongs to the group (#56397) * In GrafanaEvaluationBehaviour component : Split evaluation interval from for duration and add button to edit to allow editing it and warning * Move folder and group fields to the evaluation section in the alert form * Include 'Group behaviour' info in a card and fix 'Edit group behaviour' button onClick. * Create hook for getting groups for a particular folder * Use dropdown in group instead of input and fill it with groups that belong to the selected folder * Add evaluation interval for each group in dropdown , and show warning in case user wants to update it * Avoid saving evaluation interval when some rules in the same group would have invalid For with this change * Clear group value when reseting the drop down * Remove evaluationEvery from form values, show this as a label and add a button to edit the group * Open EditRuleGroupModal for editing evaluation interval form the alert rule form * Fix aligment in group behaviour card * compact space in evaluation behaviour card and change group drop down label * In EditgroupModal, in case of grafana managed group, show folder instead of namespcace label and disable the folder name input * Add edge case in rulesInSameGroupHaveInvalidFor method when For value is zero * Vertically align annotations input to the evaluation section in alert rule form * Fix width when editing new group * Add placeholder for group input * Make folder and group in modal readonly from alert form and disable edit group button when new group * Update texts * Don't show evaluation behaviour section until folder and group are selected * Update texts * Fix merge conflits * Fix wrong margin in evaluation label * Remove non-used isRulerGrafanaRuleDTO method * Remove negative margin to avoid overlaping on Firefox
3 years ago
onClose={() => closeEditModal()}
folderUrl={folder?.canEdit ? makeFolderSettingsLink(folder.uid) : undefined}
folderUid={folderUID}
Alerting: Improve UI for making more clear that evaluation interval belongs to the group (#56397) * In GrafanaEvaluationBehaviour component : Split evaluation interval from for duration and add button to edit to allow editing it and warning * Move folder and group fields to the evaluation section in the alert form * Include 'Group behaviour' info in a card and fix 'Edit group behaviour' button onClick. * Create hook for getting groups for a particular folder * Use dropdown in group instead of input and fill it with groups that belong to the selected folder * Add evaluation interval for each group in dropdown , and show warning in case user wants to update it * Avoid saving evaluation interval when some rules in the same group would have invalid For with this change * Clear group value when reseting the drop down * Remove evaluationEvery from form values, show this as a label and add a button to edit the group * Open EditRuleGroupModal for editing evaluation interval form the alert rule form * Fix aligment in group behaviour card * compact space in evaluation behaviour card and change group drop down label * In EditgroupModal, in case of grafana managed group, show folder instead of namespcace label and disable the folder name input * Add edge case in rulesInSameGroupHaveInvalidFor method when For value is zero * Vertically align annotations input to the evaluation section in alert rule form * Fix width when editing new group * Add placeholder for group input * Make folder and group in modal readonly from alert form and disable edit group button when new group * Update texts * Don't show evaluation behaviour section until folder and group are selected * Update texts * Fix merge conflits * Fix wrong margin in evaluation label * Remove non-used isRulerGrafanaRuleDTO method * Remove negative margin to avoid overlaping on Firefox
3 years ago
/>
)}
{isReorderingGroup && (
<ReorderCloudGroupModal
group={group}
folderUid={folderUID}
namespace={namespace}
onClose={() => setIsReorderingGroup(false)}
/>
)}
<ConfirmModal
isOpen={isDeletingGroup}
title="Delete group"
body={
<div>
<p>
Deleting &quot;<strong>{group.name}</strong>&quot; will permanently remove the group and{' '}
{group.rules.length} alert {pluralize('rule', group.rules.length)} belonging to it.
</p>
<p>Are you sure you want to delete this group?</p>
</div>
}
onConfirm={deleteGroup}
onDismiss={() => setIsDeletingGroup(false)}
confirmText="Delete"
/>
{folder && isExporting === 'folder' && (
<GrafanaRuleFolderExporter folder={folder} onClose={() => setIsExporting(undefined)} />
)}
{folder && isExporting === 'group' && (
<GrafanaRuleGroupExporter
folderUid={folder.uid}
groupName={group.name}
onClose={() => setIsExporting(undefined)}
/>
)}
</div>
);
});
RulesGroup.displayName = 'RulesGroup';
export const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css``,
header: css`
display: flex;
flex-direction: row;
align-items: center;
padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0;
flex-wrap: nowrap;
border-bottom: 1px solid ${theme.colors.border.weak};
&:hover {
background-color: ${theme.components.table.rowHoverBackground};
}
`,
headerStats: css`
flex-shrink: 0;
span {
vertical-align: middle;
}
${theme.breakpoints.down('sm')} {
order: 2;
width: 100%;
padding-left: ${theme.spacing(1)};
}
`,
groupName: css`
margin-left: ${theme.spacing(1)};
margin-bottom: 0;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`,
spacer: css`
flex: 1;
`,
collapseToggle: css`
background: none;
border: none;
margin-top: -${theme.spacing(1)};
margin-bottom: -${theme.spacing(1)};
svg {
margin-bottom: 0;
}
`,
dataSourceIcon: css`
width: ${theme.spacing(2)};
height: ${theme.spacing(2)};
margin-left: ${theme.spacing(2)};
`,
dataSourceOrigin: css`
margin-right: 1em;
color: ${theme.colors.text.disabled};
`,
actionsSeparator: css`
margin: 0 ${theme.spacing(2)};
`,
actionIcons: css`
width: 80px;
align-items: center;
flex-shrink: 0;
`,
rulesTable: css`
margin: ${theme.spacing(2, 0)};
`,
rotate90: css`
transform: rotate(90deg);
`,
};
};