mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: make rule list page work on mobile (#35991)
This commit is contained in:
parent
8660d0692e
commit
e5f6ab7fd2
@ -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<HTMLTableRowElement>(':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<HTMLTableRowElement>(':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<HTMLTableRowElement>(':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<HTMLTableRowElement>(':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<HTMLTableRowElement>(':scope > tbody > tr')).toHaveLength(2);
|
||||
userEvent.click(ui.ruleCollapseToggle.get(ruleRows[1]));
|
||||
expect(table.querySelectorAll<HTMLTableRowElement>(':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();
|
||||
});
|
||||
|
@ -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<Props> = ({ className, label, horizontal, children }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx(className, styles.field, horizontal ? styles.fieldHorizontal : styles.fieldVertical)}>
|
||||
@ -20,27 +20,30 @@ export const DetailsField: FC<Props> = ({ 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};
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
@ -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<T = unknown> {
|
||||
id: string | number;
|
||||
label: string;
|
||||
|
||||
renderCell?: (item: DynamicTableItemProps<T>, index: number) => ReactNode;
|
||||
renderCell: (item: DynamicTableItemProps<T>, index: number) => ReactNode;
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export interface DynamicTableItemProps<T = unknown> {
|
||||
id: string | number;
|
||||
data: T;
|
||||
|
||||
renderExpandedContent?: () => ReactNode;
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export interface DynamicTableProps<T = unknown> {
|
||||
@ -25,10 +22,16 @@ export interface DynamicTableProps<T = unknown> {
|
||||
items: Array<DynamicTableItemProps<T>>;
|
||||
|
||||
isExpandable?: boolean;
|
||||
onCollapse?: (id: DynamicTableItemProps<T>) => void;
|
||||
onExpand?: (id: DynamicTableItemProps<T>) => void;
|
||||
|
||||
// provide these to manually control expanded status
|
||||
onCollapse?: (item: DynamicTableItemProps<T>) => void;
|
||||
onExpand?: (item: DynamicTableItemProps<T>) => void;
|
||||
isExpanded?: (item: DynamicTableItemProps<T>) => boolean;
|
||||
|
||||
renderExpandedContent?: (item: DynamicTableItemProps<T>, index: number) => ReactNode;
|
||||
testIdGenerator?: (item: DynamicTableItemProps<T>) => string;
|
||||
testIdGenerator?: (item: DynamicTableItemProps<T>, index: number) => string;
|
||||
renderPrefixHeader?: () => ReactNode;
|
||||
renderPrefixCell?: (item: DynamicTableItemProps<T>, index: number) => ReactNode;
|
||||
}
|
||||
|
||||
export const DynamicTable = <T extends object>({
|
||||
@ -37,16 +40,38 @@ export const DynamicTable = <T extends object>({
|
||||
isExpandable = false,
|
||||
onCollapse,
|
||||
onExpand,
|
||||
isExpanded,
|
||||
renderExpandedContent,
|
||||
testIdGenerator,
|
||||
}: DynamicTableProps<T>) => {
|
||||
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<T>) => {
|
||||
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<Array<DynamicTableItemProps['id']>>([]);
|
||||
|
||||
const toggleExpanded = (item: DynamicTableItemProps<T>) => {
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.container} data-testid="dynamic-table">
|
||||
<div className={styles.row} data-testid="header">
|
||||
{renderPrefixHeader && renderPrefixHeader()}
|
||||
{isExpandable && <div className={styles.cell} />}
|
||||
{cols.map((col) => (
|
||||
<div className={styles.cell} key={col.id}>
|
||||
@ -55,36 +80,45 @@ export const DynamicTable = <T extends object>({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{items.map((item, index) => (
|
||||
<div className={styles.row} key={item.id} data-testid={testIdGenerator?.(item)}>
|
||||
{items.map((item, index) => {
|
||||
const isItemExpanded = isExpanded ? isExpanded(item) : expandedIds.includes(item.id);
|
||||
return (
|
||||
<div className={styles.row} key={item.id} data-testid={testIdGenerator?.(item, index) ?? 'row'}>
|
||||
{renderPrefixCell && renderPrefixCell(item, index)}
|
||||
{isExpandable && (
|
||||
<div className={cx(styles.cell, styles.expandCell)}>
|
||||
<IconButton
|
||||
size={isMobile ? 'xl' : 'md'}
|
||||
size="xl"
|
||||
data-testid="collapse-toggle"
|
||||
className={styles.expandButton}
|
||||
name={item.isExpanded ? 'angle-down' : 'angle-right'}
|
||||
onClick={() => (item.isExpanded ? onCollapse?.(item) : onExpand?.(item))}
|
||||
name={isItemExpanded ? 'angle-down' : 'angle-right'}
|
||||
onClick={() => toggleExpanded(item)}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{cols.map((col) => (
|
||||
<div className={cx(styles.cell, styles.bodyCell)} data-column={col.label} key={`${item.id}-${col.id}`}>
|
||||
{col.renderCell?.(item, index)}
|
||||
{col.renderCell(item, index)}
|
||||
</div>
|
||||
))}
|
||||
{item.isExpanded && (
|
||||
<div className={styles.expandedContentRow}>
|
||||
{item.renderExpandedContent ? item.renderExpandedContent() : renderExpandedContent?.(item, index)}
|
||||
{isItemExpanded && renderExpandedContent && (
|
||||
<div className={styles.expandedContentRow} data-testid="expanded-content">
|
||||
{renderExpandedContent(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = <T extends unknown>(cols: Array<DynamicTableColumnProps<T>>, isExpandable: boolean) => {
|
||||
const getStyles = <T extends unknown>(
|
||||
cols: Array<DynamicTableColumnProps<T>>,
|
||||
isExpandable: boolean,
|
||||
hasPrefixCell: boolean
|
||||
) => {
|
||||
const sizes = cols.map((col) => {
|
||||
if (!col.size) {
|
||||
return 'auto';
|
||||
@ -101,6 +135,10 @@ const getStyles = <T extends unknown>(cols: Array<DynamicTableColumnProps<T>>, 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 = <T extends unknown>(cols: Array<DynamicTableColumnProps<T>>, 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 = <T extends unknown>(cols: Array<DynamicTableColumnProps<T>>, 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 = <T extends unknown>(cols: Array<DynamicTableColumnProps<T>>, 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;
|
||||
|
@ -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<FormAmRoute>;
|
||||
type RouteTableItemProps = DynamicTableItemProps<FormAmRoute>;
|
||||
|
||||
export const AmRoutesTable: FC<AmRoutesTableProps> = ({ isAddMode, onCancelAdd, onChange, receivers, routes }) => {
|
||||
const [items, setItems] = useState<RouteTableItemProps[]>([]);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
const getRenderEditExpandedContent = useCallback(
|
||||
// eslint-disable-next-line react/display-name
|
||||
(item: RouteTableItemProps, index: number) => () => (
|
||||
<AmRoutesExpandedForm
|
||||
onCancel={() => {
|
||||
setItems((items) => {
|
||||
let newItems = collapseItem(items, item.id);
|
||||
newItems = removeCustomExpandedContent(newItems, item.id);
|
||||
const [expandedId, setExpandedId] = useState<string | number>();
|
||||
|
||||
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<AmRoutesTableProps> = ({ 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 (
|
||||
<HorizontalGroup>
|
||||
@ -122,35 +84,47 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({ 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),
|
||||
};
|
||||
if (isAddMode && items.length) {
|
||||
setExpandedId(items[items.length - 1].id);
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
isExpanded: false,
|
||||
renderExpandedContent: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
setItems(items);
|
||||
}, [routes, getRenderEditExpandedContent, isAddMode]);
|
||||
}, [isAddMode, items]);
|
||||
|
||||
return (
|
||||
<DynamicTable
|
||||
cols={cols}
|
||||
isExpandable={true}
|
||||
items={items}
|
||||
onCollapse={(item: RouteTableItemProps) => setItems((items) => collapseItem(items, item.id))}
|
||||
onExpand={(item: RouteTableItemProps) => setItems((items) => expandItem(items, item.id))}
|
||||
testIdGenerator={() => 'am-routes-row'}
|
||||
renderExpandedContent={(item: RouteTableItemProps, index) => (
|
||||
onCollapse={collapseItem}
|
||||
onExpand={expandItem}
|
||||
isExpanded={(item) => expandedId === item.id}
|
||||
renderExpandedContent={(item: RouteTableItemProps, index) =>
|
||||
isAddMode || editMode ? (
|
||||
<AmRoutesExpandedForm
|
||||
onCancel={() => {
|
||||
if (isAddMode) {
|
||||
onCancelAdd();
|
||||
}
|
||||
setEditMode(false);
|
||||
}}
|
||||
onSave={(data) => {
|
||||
const newRoutes = [...routes];
|
||||
|
||||
newRoutes[index] = {
|
||||
...newRoutes[index],
|
||||
...data,
|
||||
};
|
||||
setEditMode(false);
|
||||
onChange(newRoutes);
|
||||
}}
|
||||
receivers={receivers}
|
||||
routes={item.data}
|
||||
/>
|
||||
) : (
|
||||
<AmRoutesExpandedRead
|
||||
onChange={(data) => {
|
||||
const newRoutes = [...routes];
|
||||
@ -165,7 +139,8 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({ isAddMode, onCancelAdd,
|
||||
receivers={receivers}
|
||||
routes={item.data}
|
||||
/>
|
||||
)}
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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<Alert>;
|
||||
type AlertTableItemProps = DynamicTableItemProps<Alert>;
|
||||
|
||||
export const AlertInstancesTable: FC<Props> = ({ instances }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
// 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 (
|
||||
<table className={cx(tableStyles.table, styles.table)}>
|
||||
<colgroup>
|
||||
<col className={styles.colExpand} />
|
||||
<col className={styles.colState} />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>State</th>
|
||||
<th>Labels</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<Fragment key={key}>
|
||||
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td>
|
||||
{isExpandable && (
|
||||
<CollapseToggle
|
||||
isCollapsed={!isExpanded}
|
||||
onToggle={() => toggleExpandedState(key)}
|
||||
data-testid="alert-collapse-toggle"
|
||||
<DynamicTable
|
||||
cols={columns}
|
||||
isExpandable={true}
|
||||
items={items}
|
||||
renderExpandedContent={({ data }) => <AlertInstanceDetails instance={data} />}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<AlertStateTag state={instance.state} />
|
||||
</td>
|
||||
<td className={styles.labelsCell}>
|
||||
<AlertLabels labels={instance.labels} />
|
||||
</td>
|
||||
<td className={styles.createdCell}>
|
||||
{instance.activeAt.startsWith('0001') ? '-' : instance.activeAt.substr(0, 19).replace('T', ' ')}
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && isExpandable && (
|
||||
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td></td>
|
||||
<td colSpan={3}>
|
||||
<AlertInstanceDetails instance={instance} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 } }) => <AlertStateTag state={state} />,
|
||||
size: '80px',
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderCell: ({ data: { labels } }) => <AlertLabels labels={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',
|
||||
},
|
||||
];
|
||||
|
@ -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<Props> = ({ rule, rulesSource }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
export const RuleDetails: FC<Props> = ({ 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<Props> = ({ 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`
|
||||
${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)};
|
||||
`,
|
||||
});
|
||||
|
@ -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};
|
||||
`,
|
||||
});
|
||||
|
@ -49,14 +49,14 @@ export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
|
||||
|
||||
if (isDeleting) {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Spinner />
|
||||
deleting
|
||||
</HorizontalGroup>
|
||||
);
|
||||
} else if (isCreating) {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
{' '}
|
||||
<Spinner />
|
||||
creating
|
||||
@ -64,7 +64,7 @@ export const RuleState: FC<Props> = ({ rule, isDeleting, isCreating }) => {
|
||||
);
|
||||
} else if (promRule && isAlertingRule(promRule)) {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<AlertStateTag state={promRule.state} />
|
||||
{forTime}
|
||||
</HorizontalGroup>
|
||||
|
@ -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;
|
||||
|
@ -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)};
|
||||
|
@ -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 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 <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={wrapperClass} data-testid="rules-table">
|
||||
<DynamicTable
|
||||
cols={columns}
|
||||
isExpandable={true}
|
||||
items={items}
|
||||
renderExpandedContent={({ data: rule }, index) => (
|
||||
<>
|
||||
<div className={cx(styles.ruleTopGuideline, styles.guideline)} />
|
||||
{!(ruleIdx === rules.length - 1) && (
|
||||
<div className={cx(styles.ruleBottomGuideline, styles.guideline)} />
|
||||
)}
|
||||
{!(index === rules.length - 1) && showGuidelines ? (
|
||||
<div className={cx(styles.ruleContentGuideline, styles.guideline)} />
|
||||
) : null}
|
||||
<RuleDetails rule={rule} />
|
||||
</>
|
||||
)}
|
||||
<CollapseToggle
|
||||
isCollapsed={!isExpanded}
|
||||
onToggle={() => toggleExpandedState(key)}
|
||||
data-testid="rule-collapse-toggle"
|
||||
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)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapperMargin: css`
|
||||
${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]);
|
||||
}
|
||||
|
@ -8,64 +8,3 @@ export const prepareItems = <T = unknown>(
|
||||
id: idCreator?.(item) ?? index,
|
||||
data: item,
|
||||
}));
|
||||
|
||||
export const collapseItem = <T = unknown>(
|
||||
items: Array<DynamicTableItemProps<T>>,
|
||||
itemId: DynamicTableItemProps<T>['id']
|
||||
): Array<DynamicTableItemProps<T>> =>
|
||||
items.map((currentItem) => {
|
||||
if (currentItem.id !== itemId) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentItem,
|
||||
isExpanded: false,
|
||||
};
|
||||
});
|
||||
|
||||
export const expandItem = <T = unknown>(
|
||||
items: Array<DynamicTableItemProps<T>>,
|
||||
itemId: DynamicTableItemProps<T>['id']
|
||||
): Array<DynamicTableItemProps<T>> =>
|
||||
items.map((currentItem) => {
|
||||
if (currentItem.id !== itemId) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentItem,
|
||||
isExpanded: true,
|
||||
};
|
||||
});
|
||||
|
||||
export const addCustomExpandedContent = <T = unknown>(
|
||||
items: Array<DynamicTableItemProps<T>>,
|
||||
itemId: DynamicTableItemProps<T>['id'],
|
||||
renderExpandedContent: DynamicTableItemProps['renderExpandedContent']
|
||||
): Array<DynamicTableItemProps<T>> =>
|
||||
items.map((currentItem) => {
|
||||
if (currentItem.id !== itemId) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentItem,
|
||||
renderExpandedContent,
|
||||
};
|
||||
});
|
||||
|
||||
export const removeCustomExpandedContent = <T = unknown>(
|
||||
items: Array<DynamicTableItemProps<T>>,
|
||||
itemId: DynamicTableItemProps<T>['id']
|
||||
): Array<DynamicTableItemProps<T>> =>
|
||||
items.map((currentItem) => {
|
||||
if (currentItem.id !== itemId) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentItem,
|
||||
renderExpandedContent: undefined,
|
||||
};
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user