diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 78fa54a6d32..0ffd28ddf1c 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -65,9 +65,10 @@ const ui = { ruleGroup: byTestId('rule-group'), cloudRulesSourceErrors: byTestId('cloud-rulessource-errors'), groupCollapseToggle: byTestId('group-collapse-toggle'), - ruleCollapseToggle: byTestId('rule-collapse-toggle'), - alertCollapseToggle: byTestId('alert-collapse-toggle'), + ruleCollapseToggle: byTestId('collapse-toggle'), rulesTable: byTestId('rules-table'), + ruleRow: byTestId('row'), + expandedContent: byTestId('expanded-content'), }; describe('RuleList', () => { @@ -241,7 +242,7 @@ describe('RuleList', () => { const table = await ui.rulesTable.find(groups[1]); // check that rule rows are rendered properly - let ruleRows = table.querySelectorAll(':scope > tbody > tr'); + let ruleRows = ui.ruleRow.getAll(table); expect(ruleRows).toHaveLength(4); expect(ruleRows[0]).toHaveTextContent('Recording rule'); @@ -261,10 +262,7 @@ describe('RuleList', () => { // expand alert details userEvent.click(ui.ruleCollapseToggle.get(ruleRows[1])); - ruleRows = table.querySelectorAll(':scope > tbody > tr'); - expect(ruleRows).toHaveLength(5); - - const ruleDetails = ruleRows[2]; + const ruleDetails = ui.expandedContent.get(ruleRows[1]); expect(ruleDetails).toHaveTextContent('Labelsseverity=warningfoo=bar'); expect(ruleDetails).toHaveTextContent('Expressiontopk ( 5 , foo ) [ 5m ]'); @@ -272,28 +270,25 @@ describe('RuleList', () => { expect(ruleDetails).toHaveTextContent('Matching instances'); // finally, check instances table - const instancesTable = ruleDetails.querySelector('table'); + const instancesTable = byTestId('dynamic-table').get(ruleDetails); expect(instancesTable).toBeInTheDocument(); - let instanceRows = instancesTable?.querySelectorAll(':scope > tbody > tr'); + const instanceRows = byTestId('row').getAll(instancesTable); expect(instanceRows).toHaveLength(2); expect(instanceRows![0]).toHaveTextContent('Firingfoo=barseverity=warning2021-03-18 13:47:05'); expect(instanceRows![1]).toHaveTextContent('Firingfoo=bazseverity=error2021-03-18 13:47:05'); // expand details of an instance - userEvent.click(ui.alertCollapseToggle.get(instanceRows![0])); - instanceRows = instancesTable?.querySelectorAll(':scope > tbody > tr')!; - expect(instanceRows).toHaveLength(3); + userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0])); - const alertDetails = instanceRows[1]; + const alertDetails = byTestId('expanded-content').get(instanceRows[0]); expect(alertDetails).toHaveTextContent('Value2e+10'); expect(alertDetails).toHaveTextContent('messagefirst alert message'); // collapse everything again - userEvent.click(ui.alertCollapseToggle.get(instanceRows![0])); - expect(instancesTable?.querySelectorAll(':scope > tbody > tr')).toHaveLength(2); - userEvent.click(ui.ruleCollapseToggle.get(ruleRows[1])); - expect(table.querySelectorAll(':scope > tbody > tr')).toHaveLength(4); + userEvent.click(ui.ruleCollapseToggle.get(instanceRows![0])); + expect(byTestId('expanded-content').query(instanceRows[0])).not.toBeInTheDocument(); + userEvent.click(ui.ruleCollapseToggle.getAll(ruleRows[1])[0]); userEvent.click(ui.groupCollapseToggle.get(groups[1])); expect(ui.rulesTable.query()).not.toBeInTheDocument(); }); diff --git a/public/app/features/alerting/unified/components/DetailsField.tsx b/public/app/features/alerting/unified/components/DetailsField.tsx index 52f59f02603..50c06c29724 100644 --- a/public/app/features/alerting/unified/components/DetailsField.tsx +++ b/public/app/features/alerting/unified/components/DetailsField.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import { css, cx } from '@emotion/css'; -import { GrafanaTheme } from '@grafana/data'; -import { useStyles } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; interface Props { label: React.ReactNode; @@ -10,7 +10,7 @@ interface Props { } export const DetailsField: FC = ({ className, label, horizontal, children }) => { - const styles = useStyles(getStyles); + const styles = useStyles2(getStyles); return (
@@ -20,27 +20,30 @@ export const DetailsField: FC = ({ className, label, horizontal, children ); }; -const getStyles = (theme: GrafanaTheme) => ({ +const getStyles = (theme: GrafanaTheme2) => ({ fieldHorizontal: css` flex-direction: row; + ${theme.breakpoints.down('md')} { + flex-direction: column; + } `, fieldVertical: css` flex-direction: column; `, field: css` display: flex; - margin: ${theme.spacing.md} 0; + margin: ${theme.spacing(2)} 0; & > div:first-child { width: 110px; - padding-right: ${theme.spacing.sm}; + padding-right: ${theme.spacing(1)}; font-size: ${theme.typography.size.sm}; - font-weight: ${theme.typography.weight.semibold}; + font-weight: ${theme.typography.fontWeightBold}; line-height: 1.8; } & > div:nth-child(2) { flex: 1; - color: ${theme.colors.textSemiWeak}; + color: ${theme.colors.text.secondary}; } `, }); diff --git a/public/app/features/alerting/unified/components/DynamicTable.tsx b/public/app/features/alerting/unified/components/DynamicTable.tsx index 13dc4310869..fb36cb3523d 100644 --- a/public/app/features/alerting/unified/components/DynamicTable.tsx +++ b/public/app/features/alerting/unified/components/DynamicTable.tsx @@ -1,23 +1,20 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useState } from 'react'; import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { IconButton, useStyles2, useTheme2 } from '@grafana/ui'; -import { useMedia } from 'react-use'; +import { IconButton, useStyles2 } from '@grafana/ui'; export interface DynamicTableColumnProps { id: string | number; label: string; - renderCell?: (item: DynamicTableItemProps, index: number) => ReactNode; + renderCell: (item: DynamicTableItemProps, index: number) => ReactNode; size?: number | string; } export interface DynamicTableItemProps { id: string | number; data: T; - renderExpandedContent?: () => ReactNode; - isExpanded?: boolean; } export interface DynamicTableProps { @@ -25,10 +22,16 @@ export interface DynamicTableProps { items: Array>; isExpandable?: boolean; - onCollapse?: (id: DynamicTableItemProps) => void; - onExpand?: (id: DynamicTableItemProps) => void; + + // provide these to manually control expanded status + onCollapse?: (item: DynamicTableItemProps) => void; + onExpand?: (item: DynamicTableItemProps) => void; + isExpanded?: (item: DynamicTableItemProps) => boolean; + renderExpandedContent?: (item: DynamicTableItemProps, index: number) => ReactNode; - testIdGenerator?: (item: DynamicTableItemProps) => string; + testIdGenerator?: (item: DynamicTableItemProps, index: number) => string; + renderPrefixHeader?: () => ReactNode; + renderPrefixCell?: (item: DynamicTableItemProps, index: number) => ReactNode; } export const DynamicTable = ({ @@ -37,16 +40,38 @@ export const DynamicTable = ({ isExpandable = false, onCollapse, onExpand, + isExpanded, renderExpandedContent, testIdGenerator, -}: DynamicTableProps) => { - const styles = useStyles2(getStyles(cols, isExpandable)); - const theme = useTheme2(); - const isMobile = useMedia(`(${theme.breakpoints.down('sm')})`); + // render a cell BEFORE expand icon for header/ each row. + // currently use by RuleList to render guidelines + renderPrefixCell, + renderPrefixHeader, +}: DynamicTableProps) => { + if ((onCollapse || onExpand || isExpanded) && !(onCollapse && onExpand && isExpanded)) { + throw new Error('either all of onCollapse, onExpand, isExpanded must be provided, or none'); + } + if ((isExpandable || renderExpandedContent) && !(isExpandable && renderExpandedContent)) { + throw new Error('either both isExpanded and renderExpandedContent must be provided, or neither'); + } + const styles = useStyles2(getStyles(cols, isExpandable, !!renderPrefixHeader)); + + const [expandedIds, setExpandedIds] = useState>([]); + + const toggleExpanded = (item: DynamicTableItemProps) => { + if (isExpanded && onCollapse && onExpand) { + isExpanded(item) ? onCollapse(item) : onExpand(item); + } else { + setExpandedIds( + expandedIds.includes(item.id) ? expandedIds.filter((itemId) => itemId !== item.id) : [...expandedIds, item.id] + ); + } + }; return ( -
-
+
+
+ {renderPrefixHeader && renderPrefixHeader()} {isExpandable &&
} {cols.map((col) => (
@@ -55,36 +80,45 @@ export const DynamicTable = ({ ))}
- {items.map((item, index) => ( -
- {isExpandable && ( -
- (item.isExpanded ? onCollapse?.(item) : onExpand?.(item))} - type="button" - /> -
- )} - {cols.map((col) => ( -
- {col.renderCell?.(item, index)} -
- ))} - {item.isExpanded && ( -
- {item.renderExpandedContent ? item.renderExpandedContent() : renderExpandedContent?.(item, index)} -
- )} -
- ))} + {items.map((item, index) => { + const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id); + return ( +
+ {renderPrefixCell && renderPrefixCell(item, index)} + {isExpandable && ( +
+ toggleExpanded(item)} + type="button" + /> +
+ )} + {cols.map((col) => ( +
+ {col.renderCell(item, index)} +
+ ))} + {isItemExpanded && renderExpandedContent && ( +
+ {renderExpandedContent(item, index)} +
+ )} +
+ ); + })}
); }; -const getStyles = (cols: Array>, isExpandable: boolean) => { +const getStyles = ( + cols: Array>, + isExpandable: boolean, + hasPrefixCell: boolean +) => { const sizes = cols.map((col) => { if (!col.size) { return 'auto'; @@ -101,6 +135,10 @@ const getStyles = (cols: Array>, i sizes.unshift('calc(1em + 16px)'); } + if (hasPrefixCell) { + sizes.unshift('0'); + } + return (theme: GrafanaTheme2) => ({ container: css` border: 1px solid ${theme.colors.border.strong}; @@ -128,11 +166,18 @@ const getStyles = (cols: Array>, i &:first-child { display: none; } + + ${hasPrefixCell + ? ` + & > *:first-child { + display: none; + } + ` + : ''} } `, cell: css` align-items: center; - display: grid; padding: ${theme.spacing(1)}; ${theme.breakpoints.down('sm')} { @@ -141,12 +186,16 @@ const getStyles = (cols: Array>, i } `, bodyCell: css` + overflow: hidden; + word-break: break-all; ${theme.breakpoints.down('sm')} { grid-column-end: right; grid-column-start: right; &::before { content: attr(data-column); + display: block; + color: ${theme.colors.text.primary}; } } `, @@ -160,11 +209,13 @@ const getStyles = (cols: Array>, i `, expandedContentRow: css` grid-column-end: ${sizes.length + 1}; - grid-column-start: 2; + grid-column-start: ${hasPrefixCell ? 3 : 2}; grid-row: 2; padding: 0 ${theme.spacing(3)} 0 ${theme.spacing(1)}; + position: relative; ${theme.breakpoints.down('sm')} { + grid-column-start: 2; border-top: 1px solid ${theme.colors.border.strong}; grid-row: auto; padding: ${theme.spacing(1)} 0 0 0; diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx index 811f512add1..cf1aa95df1a 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx @@ -1,13 +1,7 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Button, HorizontalGroup, IconButton } from '@grafana/ui'; import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes'; -import { - addCustomExpandedContent, - collapseItem, - expandItem, - prepareItems, - removeCustomExpandedContent, -} from '../../utils/dynamicTable'; +import { prepareItems } from '../../utils/dynamicTable'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { AmRoutesExpandedForm } from './AmRoutesExpandedForm'; import { AmRoutesExpandedRead } from './AmRoutesExpandedRead'; @@ -25,42 +19,13 @@ type RouteTableColumnProps = DynamicTableColumnProps; type RouteTableItemProps = DynamicTableItemProps; export const AmRoutesTable: FC = ({ isAddMode, onCancelAdd, onChange, receivers, routes }) => { - const [items, setItems] = useState([]); + const [editMode, setEditMode] = useState(false); - const getRenderEditExpandedContent = useCallback( - // eslint-disable-next-line react/display-name - (item: RouteTableItemProps, index: number) => () => ( - { - setItems((items) => { - let newItems = collapseItem(items, item.id); - newItems = removeCustomExpandedContent(newItems, item.id); + const [expandedId, setExpandedId] = useState(); - return newItems; - }); + const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []); - if (isAddMode) { - onCancelAdd(); - } - }} - onSave={(data) => { - const newRoutes = [...routes]; - - newRoutes[index] = { - ...newRoutes[index], - ...data, - }; - - setItems((items) => collapseItem(items, item.id)); - - onChange(newRoutes); - }} - receivers={receivers} - routes={item.data} - /> - ), - [isAddMode, onCancelAdd, onChange, receivers, routes] - ); + const collapseItem = useCallback(() => setExpandedId(undefined), []); const cols: RouteTableColumnProps[] = [ { @@ -91,13 +56,10 @@ export const AmRoutesTable: FC = ({ isAddMode, onCancelAdd, return null; } - const expandWithCustomContent = () => - setItems((items) => { - let newItems = expandItem(items, item.id); - newItems = addCustomExpandedContent(newItems, item.id, getRenderEditExpandedContent(item, index)); - - return newItems; - }); + const expandWithCustomContent = () => { + expandItem(item); + setEditMode(true); + }; return ( @@ -122,50 +84,63 @@ export const AmRoutesTable: FC = ({ isAddMode, onCancelAdd, }, ]; + const items = useMemo(() => prepareItems(routes), [routes]); + + // expand the last item when adding useEffect(() => { - const items = prepareItems(routes).map((item, index, arr) => { - if (isAddMode && index === arr.length - 1) { - return { - ...item, - isExpanded: true, - renderExpandedContent: getRenderEditExpandedContent(item, index), - }; - } - - return { - ...item, - isExpanded: false, - renderExpandedContent: undefined, - }; - }); - - setItems(items); - }, [routes, getRenderEditExpandedContent, isAddMode]); + if (isAddMode && items.length) { + setExpandedId(items[items.length - 1].id); + } + }, [isAddMode, items]); return ( setItems((items) => collapseItem(items, item.id))} - onExpand={(item: RouteTableItemProps) => setItems((items) => expandItem(items, item.id))} testIdGenerator={() => 'am-routes-row'} - renderExpandedContent={(item: RouteTableItemProps, index) => ( - { - const newRoutes = [...routes]; + onCollapse={collapseItem} + onExpand={expandItem} + isExpanded={(item) => expandedId === item.id} + renderExpandedContent={(item: RouteTableItemProps, index) => + isAddMode || editMode ? ( + { + if (isAddMode) { + onCancelAdd(); + } + setEditMode(false); + }} + onSave={(data) => { + const newRoutes = [...routes]; - newRoutes[index] = { - ...item.data, - ...data, - }; + newRoutes[index] = { + ...newRoutes[index], + ...data, + }; + setEditMode(false); + onChange(newRoutes); + }} + receivers={receivers} + routes={item.data} + /> + ) : ( + { + const newRoutes = [...routes]; - onChange(newRoutes); - }} - receivers={receivers} - routes={item.data} - /> - )} + newRoutes[index] = { + ...item.data, + ...data, + }; + + onChange(newRoutes); + }} + receivers={receivers} + routes={item.data} + /> + ) + } /> ); }; diff --git a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx index c04f0aa7ee5..4a2a34ed4e4 100644 --- a/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/AlertInstancesTable.tsx @@ -1,101 +1,40 @@ import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; import { Alert } from 'app/types/unified-alerting'; -import { css, cx } from '@emotion/css'; -import React, { FC, Fragment, useMemo, useState } from 'react'; -import { getAlertTableStyles } from '../../styles/table'; +import { css } from '@emotion/css'; +import React, { FC, useMemo } from 'react'; import { alertInstanceKey } from '../../utils/rules'; import { AlertLabels } from '../AlertLabels'; -import { CollapseToggle } from '../CollapseToggle'; import { AlertInstanceDetails } from './AlertInstanceDetails'; import { AlertStateTag } from './AlertStateTag'; - -type AlertWithKey = Alert & { key: string }; +import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; interface Props { instances: Alert[]; } +type AlertTableColumnProps = DynamicTableColumnProps; +type AlertTableItemProps = DynamicTableItemProps; + export const AlertInstancesTable: FC = ({ instances }) => { - const styles = useStyles2(getStyles); - const tableStyles = useStyles2(getAlertTableStyles); - - const [expandedKeys, setExpandedKeys] = useState([]); - // add key & sort instance. API returns instances in random order, different every time. - const sortedInstances = useMemo( - (): AlertWithKey[] => + const items = useMemo( + (): AlertTableItemProps[] => instances .map((instance) => ({ - ...instance, - key: alertInstanceKey(instance), + data: instance, + id: alertInstanceKey(instance), })) - .sort((a, b) => a.key.localeCompare(b.key)), + .sort((a, b) => a.id.localeCompare(b.id)), [instances] ); - const toggleExpandedState = (ruleKey: string) => - setExpandedKeys( - expandedKeys.includes(ruleKey) ? expandedKeys.filter((key) => key !== ruleKey) : [...expandedKeys, ruleKey] - ); - return ( - - - - - - - - - - - - - - - - - {sortedInstances.map(({ key, ...instance }, idx) => { - const isExpanded = expandedKeys.includes(key); - - // don't allow expanding if there's nothing to show - const isExpandable = instance.value || !!Object.keys(instance.annotations ?? {}).length; - return ( - - - - - - - - {isExpanded && isExpandable && ( - - - - - )} - - ); - })} - -
StateLabelsCreated
- {isExpandable && ( - toggleExpandedState(key)} - data-testid="alert-collapse-toggle" - /> - )} - - - - - - {instance.activeAt.startsWith('0001') ? '-' : instance.activeAt.substr(0, 19).replace('T', ' ')} -
- -
+ } + /> ); }; @@ -121,3 +60,28 @@ export const getStyles = (theme: GrafanaTheme2) => ({ } `, }); + +const columns: AlertTableColumnProps[] = [ + { + id: 'state', + label: 'State', + // eslint-disable-next-line react/display-name + renderCell: ({ data: { state } }) => , + size: '80px', + }, + { + id: 'labels', + label: 'Labels', + // eslint-disable-next-line react/display-name + renderCell: ({ data: { labels } }) => , + }, + { + id: 'created', + label: 'Created', + // eslint-disable-next-line react/display-name + renderCell: ({ data: { activeAt } }) => ( + <>{activeAt.startsWith('0001') ? '-' : activeAt.substr(0, 19).replace('T', ' ')} + ), + size: '150px', + }, +]; diff --git a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx index 1af71cb9453..dd5e9248d91 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetails.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetails.tsx @@ -1,8 +1,8 @@ -import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; +import { CombinedRule } from 'app/types/unified-alerting'; import React, { FC, useMemo } from 'react'; -import { useStyles } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { css, cx } from '@emotion/css'; -import { GrafanaTheme } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules'; import { isCloudRulesSource } from '../../utils/datasource'; import { AnnotationDetailsField } from '../AnnotationDetailsField'; @@ -16,13 +16,15 @@ import { RuleDetailsActionButtons } from './RuleDetailsActionButtons'; interface Props { rule: CombinedRule; - rulesSource: RulesSource; } -export const RuleDetails: FC = ({ rule, rulesSource }) => { - const styles = useStyles(getStyles); +export const RuleDetails: FC = ({ rule }) => { + const styles = useStyles2(getStyles); - const { promRule } = rule; + const { + promRule, + namespace: { rulesSource }, + } = rule; const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim()); @@ -98,23 +100,28 @@ export const RuleDetails: FC = ({ rule, rulesSource }) => { ); }; -export const getStyles = (theme: GrafanaTheme) => ({ +export const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` display: flex; flex-direction: row; + ${theme.breakpoints.down('md')} { + flex-direction: column; + } `, leftSide: css` flex: 1; `, rightSide: css` - padding-left: 90px; - width: 300px; + ${theme.breakpoints.up('md')} { + padding-left: 90px; + width: 300px; + } `, exprRow: css` margin-bottom: 46px; `, dataSourceIcon: css` - width: ${theme.spacing.md}; - height: ${theme.spacing.md}; + width: ${theme.spacing(2)}; + height: ${theme.spacing(2)}; `, }); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 99bb0021880..5408c2ec715 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -172,10 +172,12 @@ export const getStyles = (theme: GrafanaTheme2) => ({ display: flex; flex-direction: row; justify-content: space-between; + flex-wrap: wrap; border-bottom: solid 1px ${theme.colors.border.medium}; `, button: css` height: 24px; + margin-top: ${theme.spacing(1)}; font-size: ${theme.typography.size.sm}; `, }); diff --git a/public/app/features/alerting/unified/components/rules/RuleState.tsx b/public/app/features/alerting/unified/components/rules/RuleState.tsx index fd95b26ab61..04b8da8c167 100644 --- a/public/app/features/alerting/unified/components/rules/RuleState.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleState.tsx @@ -49,14 +49,14 @@ export const RuleState: FC = ({ rule, isDeleting, isCreating }) => { if (isDeleting) { return ( - + deleting ); } else if (isCreating) { return ( - + {' '} creating @@ -64,7 +64,7 @@ export const RuleState: FC = ({ rule, isDeleting, isCreating }) => { ); } else if (promRule && isAlertingRule(promRule)) { return ( - + {forTime} diff --git a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx index a6f42777df2..b9466765a87 100644 --- a/public/app/features/alerting/unified/components/rules/RulesFilter.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesFilter.tsx @@ -135,14 +135,15 @@ const getStyles = (theme: GrafanaTheme) => { display: flex; flex-direction: row; align-items: flex-end; + width: 100%; + flex-wrap: wrap; `, spaceBetween: css` justify-content: space-between; `, rowChild: css` - & + & { - margin-left: ${theme.spacing.sm}; - } + margin-right: ${theme.spacing.sm}; + margin-top: ${theme.spacing.sm}; `, clearButton: css` align-self: flex-end; diff --git a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx index 321f80b9180..d77eb2fb7dd 100644 --- a/public/app/features/alerting/unified/components/rules/RulesGroup.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesGroup.tsx @@ -106,11 +106,18 @@ export const getStyles = (theme: GrafanaTheme2) => ({ align-items: center; padding: ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0; background-color: ${theme.colors.background.secondary}; + flex-wrap: wrap; `, headerStats: css` span { vertical-align: middle; } + + ${theme.breakpoints.down('sm')} { + order: 2; + width: 100%; + padding-left: ${theme.spacing(1)}; + } `, heading: css` margin-left: ${theme.spacing(1)}; diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 3b3edfe3bc4..aa8df099d37 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -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; +type RuleTableItemProps = DynamicTableItemProps; interface Props { rules: CombinedRule[]; @@ -29,123 +31,76 @@ export const RulesTable: FC = ({ showGroupColumn = false, showSummaryColumn = false, }) => { - const hasRuler = useHasRuler(); - const styles = useStyles2(getStyles); - const tableStyles = useStyles2(getAlertTableStyles); - - const [expandedKeys, setExpandedKeys] = useState([]); - - const toggleExpandedState = (ruleKey: string) => - setExpandedKeys( - expandedKeys.includes(ruleKey) ? expandedKeys.filter((key) => key !== ruleKey) : [...expandedKeys, ruleKey] - ); const wrapperClass = cx(styles.wrapper, className, { [styles.wrapperMargin]: showGuidelines }); + 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 columns = useColumns(showSummaryColumn, showGroupColumn, showGuidelines, items.length); + if (!rules.length) { return
{emptyMessage}
; } return ( -
- - - - - - - {showSummaryColumn && } - {showGroupColumn && } - - - - - - - - {showSummaryColumn && } - {showGroupColumn && } - - - - {(() => { - 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 ( - - - - - - - {showSummaryColumn && } - {showGroupColumn && ( - - )} - - {isExpanded && ( - - - - +
+ ( + <> + {!(index === rules.length - 1) && showGuidelines ? ( +
+ ) : null} + + + )} + renderPrefixHeader={ + showGuidelines + ? () => ( +
+
+
+ ) + : undefined + } + renderPrefixCell={ + showGuidelines + ? (_, index) => ( +
+
+ {!(index === rules.length - 1) && ( +
)} - - ); - }); - })()} -
-
- {showGuidelines &&
} -
StateNameHealthSummaryGroup
- {showGuidelines && ( - <> -
- {!(ruleIdx === rules.length - 1) && ( -
- )} - - )} - toggleExpandedState(key)} - data-testid="rule-collapse-toggle" - /> -
- - {rule.name}{promRule && }{rule.annotations[Annotation.summary] ?? ''}{isCloudRulesSource(rulesSource) ? `${namespace.name} > ${group.name}` : namespace.name}
- {!(ruleIdx === rules.length - 1) && showGuidelines && ( -
- )} -
- -
+
+ ) + : undefined + } + />
); }; 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 && ( + <> +
+ {!(ruleIdx === totalRules - 1) && ( +
+ )} + + )} + + + ); + }, + 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 ? : 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]); +} diff --git a/public/app/features/alerting/unified/utils/dynamicTable.ts b/public/app/features/alerting/unified/utils/dynamicTable.ts index 5bb56f32c63..25b227809e6 100644 --- a/public/app/features/alerting/unified/utils/dynamicTable.ts +++ b/public/app/features/alerting/unified/utils/dynamicTable.ts @@ -8,64 +8,3 @@ export const prepareItems = ( id: idCreator?.(item) ?? index, data: item, })); - -export const collapseItem = ( - items: Array>, - itemId: DynamicTableItemProps['id'] -): Array> => - items.map((currentItem) => { - if (currentItem.id !== itemId) { - return currentItem; - } - - return { - ...currentItem, - isExpanded: false, - }; - }); - -export const expandItem = ( - items: Array>, - itemId: DynamicTableItemProps['id'] -): Array> => - items.map((currentItem) => { - if (currentItem.id !== itemId) { - return currentItem; - } - - return { - ...currentItem, - isExpanded: true, - }; - }); - -export const addCustomExpandedContent = ( - items: Array>, - itemId: DynamicTableItemProps['id'], - renderExpandedContent: DynamicTableItemProps['renderExpandedContent'] -): Array> => - items.map((currentItem) => { - if (currentItem.id !== itemId) { - return currentItem; - } - - return { - ...currentItem, - renderExpandedContent, - }; - }); - -export const removeCustomExpandedContent = ( - items: Array>, - itemId: DynamicTableItemProps['id'] -): Array> => - items.map((currentItem) => { - if (currentItem.id !== itemId) { - return currentItem; - } - - return { - ...currentItem, - renderExpandedContent: undefined, - }; - });