|
|
|
@ -1,16 +1,18 @@ |
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data'; |
|
|
|
|
import { useStyles2 } from '@grafana/ui'; |
|
|
|
|
import React, { FC, Fragment, useState } from 'react'; |
|
|
|
|
import { CollapseToggle } from '../CollapseToggle'; |
|
|
|
|
import React, { FC, useMemo } from 'react'; |
|
|
|
|
import { css, cx } from '@emotion/css'; |
|
|
|
|
import { RuleDetails } from './RuleDetails'; |
|
|
|
|
import { getAlertTableStyles } from '../../styles/table'; |
|
|
|
|
import { isCloudRulesSource } from '../../utils/datasource'; |
|
|
|
|
import { useHasRuler } from '../../hooks/useHasRuler'; |
|
|
|
|
import { CombinedRule } from 'app/types/unified-alerting'; |
|
|
|
|
import { Annotation } from '../../utils/constants'; |
|
|
|
|
import { RuleState } from './RuleState'; |
|
|
|
|
import { RuleHealth } from './RuleHealth'; |
|
|
|
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; |
|
|
|
|
|
|
|
|
|
type RuleTableColumnProps = DynamicTableColumnProps<CombinedRule>; |
|
|
|
|
type RuleTableItemProps = DynamicTableItemProps<CombinedRule>; |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
rules: CombinedRule[]; |
|
|
|
@ -29,123 +31,76 @@ export const RulesTable: FC<Props> = ({ |
|
|
|
|
showGroupColumn = false, |
|
|
|
|
showSummaryColumn = false, |
|
|
|
|
}) => { |
|
|
|
|
const hasRuler = useHasRuler(); |
|
|
|
|
|
|
|
|
|
const styles = useStyles2(getStyles); |
|
|
|
|
const tableStyles = useStyles2(getAlertTableStyles); |
|
|
|
|
|
|
|
|
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); |
|
|
|
|
const wrapperClass = cx(styles.wrapper, className, { [styles.wrapperMargin]: showGuidelines }); |
|
|
|
|
|
|
|
|
|
const toggleExpandedState = (ruleKey: string) => |
|
|
|
|
setExpandedKeys( |
|
|
|
|
expandedKeys.includes(ruleKey) ? expandedKeys.filter((key) => key !== ruleKey) : [...expandedKeys, ruleKey] |
|
|
|
|
); |
|
|
|
|
const items = useMemo((): RuleTableItemProps[] => { |
|
|
|
|
const seenKeys: string[] = []; |
|
|
|
|
return rules.map((rule, ruleIdx) => { |
|
|
|
|
let key = JSON.stringify([rule.promRule?.type, rule.labels, rule.query, rule.name, rule.annotations]); |
|
|
|
|
if (seenKeys.includes(key)) { |
|
|
|
|
key += `-${ruleIdx}`; |
|
|
|
|
} |
|
|
|
|
seenKeys.push(key); |
|
|
|
|
return { |
|
|
|
|
id: key, |
|
|
|
|
data: rule, |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
}, [rules]); |
|
|
|
|
|
|
|
|
|
const wrapperClass = cx(styles.wrapper, className, { [styles.wrapperMargin]: showGuidelines }); |
|
|
|
|
const columns = useColumns(showSummaryColumn, showGroupColumn, showGuidelines, items.length); |
|
|
|
|
|
|
|
|
|
if (!rules.length) { |
|
|
|
|
return <div className={cx(wrapperClass, styles.emptyMessage)}>{emptyMessage}</div>; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div className={wrapperClass}> |
|
|
|
|
<table className={tableStyles.table} data-testid="rules-table"> |
|
|
|
|
<colgroup> |
|
|
|
|
<col className={tableStyles.colExpand} /> |
|
|
|
|
<col className={styles.state} /> |
|
|
|
|
<col /> |
|
|
|
|
<col /> |
|
|
|
|
{showSummaryColumn && <col />} |
|
|
|
|
{showGroupColumn && <col />} |
|
|
|
|
</colgroup> |
|
|
|
|
<thead> |
|
|
|
|
<tr> |
|
|
|
|
<th className={styles.relative}> |
|
|
|
|
{showGuidelines && <div className={cx(styles.headerGuideline, styles.guideline)} />} |
|
|
|
|
</th> |
|
|
|
|
<th>State</th> |
|
|
|
|
<th>Name</th> |
|
|
|
|
<th>Health</th> |
|
|
|
|
{showSummaryColumn && <th>Summary</th>} |
|
|
|
|
{showGroupColumn && <th>Group</th>} |
|
|
|
|
</tr> |
|
|
|
|
</thead> |
|
|
|
|
<tbody> |
|
|
|
|
{(() => { |
|
|
|
|
const seenKeys: string[] = []; |
|
|
|
|
return rules.map((rule, ruleIdx) => { |
|
|
|
|
const { namespace, group } = rule; |
|
|
|
|
const { rulesSource } = namespace; |
|
|
|
|
let key = JSON.stringify([rule.promRule?.type, rule.labels, rule.query, rule.name, rule.annotations]); |
|
|
|
|
if (seenKeys.includes(key)) { |
|
|
|
|
key += `-${ruleIdx}`; |
|
|
|
|
} |
|
|
|
|
seenKeys.push(key); |
|
|
|
|
const isExpanded = expandedKeys.includes(key); |
|
|
|
|
const { promRule, rulerRule } = rule; |
|
|
|
|
const isDeleting = !!(hasRuler(rulesSource) && promRule && !rulerRule); |
|
|
|
|
const isCreating = !!(hasRuler(rulesSource) && rulerRule && !promRule); |
|
|
|
|
|
|
|
|
|
let detailsColspan = 3; |
|
|
|
|
if (showGroupColumn) { |
|
|
|
|
detailsColspan += 1; |
|
|
|
|
} |
|
|
|
|
if (showSummaryColumn) { |
|
|
|
|
detailsColspan += 1; |
|
|
|
|
} |
|
|
|
|
return ( |
|
|
|
|
<Fragment key={key}> |
|
|
|
|
<tr className={ruleIdx % 2 === 0 ? tableStyles.evenRow : undefined}> |
|
|
|
|
<td className={styles.relative}> |
|
|
|
|
{showGuidelines && ( |
|
|
|
|
<> |
|
|
|
|
<div className={cx(styles.ruleTopGuideline, styles.guideline)} /> |
|
|
|
|
{!(ruleIdx === rules.length - 1) && ( |
|
|
|
|
<div className={cx(styles.ruleBottomGuideline, styles.guideline)} /> |
|
|
|
|
)} |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
<CollapseToggle |
|
|
|
|
isCollapsed={!isExpanded} |
|
|
|
|
onToggle={() => toggleExpandedState(key)} |
|
|
|
|
data-testid="rule-collapse-toggle" |
|
|
|
|
/> |
|
|
|
|
</td> |
|
|
|
|
<td> |
|
|
|
|
<RuleState rule={rule} isDeleting={isDeleting} isCreating={isCreating} /> |
|
|
|
|
</td> |
|
|
|
|
<td>{rule.name}</td> |
|
|
|
|
<td>{promRule && <RuleHealth rule={promRule} />}</td> |
|
|
|
|
{showSummaryColumn && <td>{rule.annotations[Annotation.summary] ?? ''}</td>} |
|
|
|
|
{showGroupColumn && ( |
|
|
|
|
<td>{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}</td> |
|
|
|
|
)} |
|
|
|
|
</tr> |
|
|
|
|
{isExpanded && ( |
|
|
|
|
<tr className={ruleIdx % 2 === 0 ? tableStyles.evenRow : undefined}> |
|
|
|
|
<td className={styles.relative}> |
|
|
|
|
{!(ruleIdx === rules.length - 1) && showGuidelines && ( |
|
|
|
|
<div className={cx(styles.ruleContentGuideline, styles.guideline)} /> |
|
|
|
|
)} |
|
|
|
|
</td> |
|
|
|
|
<td colSpan={detailsColspan}> |
|
|
|
|
<RuleDetails rulesSource={rulesSource} rule={rule} /> |
|
|
|
|
</td> |
|
|
|
|
</tr> |
|
|
|
|
<div className={wrapperClass} data-testid="rules-table"> |
|
|
|
|
<DynamicTable |
|
|
|
|
cols={columns} |
|
|
|
|
isExpandable={true} |
|
|
|
|
items={items} |
|
|
|
|
renderExpandedContent={({ data: rule }, index) => ( |
|
|
|
|
<> |
|
|
|
|
{!(index === rules.length - 1) && showGuidelines ? ( |
|
|
|
|
<div className={cx(styles.ruleContentGuideline, styles.guideline)} /> |
|
|
|
|
) : null} |
|
|
|
|
<RuleDetails rule={rule} /> |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
renderPrefixHeader={ |
|
|
|
|
showGuidelines |
|
|
|
|
? () => ( |
|
|
|
|
<div className={styles.relative}> |
|
|
|
|
<div className={cx(styles.headerGuideline, styles.guideline)} /> |
|
|
|
|
</div> |
|
|
|
|
) |
|
|
|
|
: undefined |
|
|
|
|
} |
|
|
|
|
renderPrefixCell={ |
|
|
|
|
showGuidelines |
|
|
|
|
? (_, index) => ( |
|
|
|
|
<div className={styles.relative}> |
|
|
|
|
<div className={cx(styles.ruleTopGuideline, styles.guideline)} /> |
|
|
|
|
{!(index === rules.length - 1) && ( |
|
|
|
|
<div className={cx(styles.ruleBottomGuideline, styles.guideline)} /> |
|
|
|
|
)} |
|
|
|
|
</Fragment> |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
})()} |
|
|
|
|
</tbody> |
|
|
|
|
</table> |
|
|
|
|
</div> |
|
|
|
|
) |
|
|
|
|
: undefined |
|
|
|
|
} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
wrapperMargin: css` |
|
|
|
|
margin-left: 36px; |
|
|
|
|
${theme.breakpoints.up('md')} { |
|
|
|
|
margin-left: 36px; |
|
|
|
|
} |
|
|
|
|
`,
|
|
|
|
|
emptyMessage: css` |
|
|
|
|
padding: ${theme.spacing(1)}; |
|
|
|
@ -178,11 +133,16 @@ export const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
`,
|
|
|
|
|
relative: css` |
|
|
|
|
position: relative; |
|
|
|
|
height: 100%; |
|
|
|
|
`,
|
|
|
|
|
guideline: css` |
|
|
|
|
left: -19px; |
|
|
|
|
border-left: 1px solid ${theme.colors.border.medium}; |
|
|
|
|
position: absolute; |
|
|
|
|
|
|
|
|
|
${theme.breakpoints.down('md')} { |
|
|
|
|
display: none; |
|
|
|
|
} |
|
|
|
|
`,
|
|
|
|
|
ruleTopGuideline: css` |
|
|
|
|
width: 18px; |
|
|
|
@ -197,6 +157,7 @@ export const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
ruleContentGuideline: css` |
|
|
|
|
top: 0; |
|
|
|
|
bottom: 0; |
|
|
|
|
left: -49px !important; |
|
|
|
|
`,
|
|
|
|
|
headerGuideline: css` |
|
|
|
|
top: -24px; |
|
|
|
@ -206,3 +167,76 @@ export const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
width: 110px; |
|
|
|
|
`,
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showGuidelines: boolean, totalRules: number) { |
|
|
|
|
const hasRuler = useHasRuler(); |
|
|
|
|
const styles = useStyles2(getStyles); |
|
|
|
|
|
|
|
|
|
return useMemo((): RuleTableColumnProps[] => { |
|
|
|
|
const columns: RuleTableColumnProps[] = [ |
|
|
|
|
{ |
|
|
|
|
id: 'state', |
|
|
|
|
label: 'State', |
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
|
|
|
renderCell: ({ data: rule }, ruleIdx) => { |
|
|
|
|
const { namespace } = rule; |
|
|
|
|
const { rulesSource } = namespace; |
|
|
|
|
const { promRule, rulerRule } = rule; |
|
|
|
|
const isDeleting = !!(hasRuler(rulesSource) && promRule && !rulerRule); |
|
|
|
|
const isCreating = !!(hasRuler(rulesSource) && rulerRule && !promRule); |
|
|
|
|
return ( |
|
|
|
|
<> |
|
|
|
|
{showGuidelines && ( |
|
|
|
|
<> |
|
|
|
|
<div className={cx(styles.ruleTopGuideline, styles.guideline)} /> |
|
|
|
|
{!(ruleIdx === totalRules - 1) && ( |
|
|
|
|
<div className={cx(styles.ruleBottomGuideline, styles.guideline)} /> |
|
|
|
|
)} |
|
|
|
|
</> |
|
|
|
|
)} |
|
|
|
|
<RuleState rule={rule} isDeleting={isDeleting} isCreating={isCreating} /> |
|
|
|
|
</> |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
size: '165px', |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: 'name', |
|
|
|
|
label: 'Name', |
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
|
|
|
renderCell: ({ data: rule }) => rule.name, |
|
|
|
|
size: 5, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: 'health', |
|
|
|
|
label: 'Health', |
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
|
|
|
renderCell: ({ data: { promRule } }) => (promRule ? <RuleHealth rule={promRule} /> : null), |
|
|
|
|
size: '75px', |
|
|
|
|
}, |
|
|
|
|
]; |
|
|
|
|
if (showSummaryColumn) { |
|
|
|
|
columns.push({ |
|
|
|
|
id: 'summary', |
|
|
|
|
label: 'Summary', |
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
|
|
|
renderCell: ({ data: rule }) => rule.annotations[Annotation.summary] ?? '', |
|
|
|
|
size: 5, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
if (showGroupColumn) { |
|
|
|
|
columns.push({ |
|
|
|
|
id: 'group', |
|
|
|
|
label: 'Group', |
|
|
|
|
// eslint-disable-next-line react/display-name
|
|
|
|
|
renderCell: ({ data: rule }) => { |
|
|
|
|
const { namespace, group } = rule; |
|
|
|
|
const { rulesSource } = namespace; |
|
|
|
|
return isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name; |
|
|
|
|
}, |
|
|
|
|
size: 5, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
return columns; |
|
|
|
|
}, [hasRuler, showSummaryColumn, showGroupColumn, showGuidelines, totalRules, styles]); |
|
|
|
|
} |
|
|
|
|