[v9.4.x] Alerting: Disable alerting incompatible panels in the dashboard picke… (#65780)

* Alerting: Disable alerting incompatible panels in the dashboard picker (#65341)

* Hide dashboard hint when dashboard already selected

* Disable panels of types other than graph and timeseries

* Add a test checking disabled panels

* Make all panels selectable

* Fix tests

(cherry picked from commit bde77e4f79)

* Skip flakey test
This commit is contained in:
Konrad Lalik
2023-04-04 09:54:01 +02:00
committed by GitHub
parent 285d5809c5
commit cd4cb967a5
2 changed files with 170 additions and 78 deletions
@@ -1,4 +1,4 @@
import { findByText, findByTitle, render } from '@testing-library/react';
import { findByRole, findByText, findByTitle, getByTestId, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
@@ -107,8 +107,8 @@ describe('AnnotationsField', function () {
title: 'My dashboard',
uid: 'dash-test-uid',
panels: [
{ id: 1, title: 'First panel' },
{ id: 2, title: 'Second panel' },
{ id: 1, title: 'First panel', type: 'timeseries' },
{ id: 2, title: 'Second panel', type: 'timeseries' },
],
})
);
@@ -137,8 +137,8 @@ describe('AnnotationsField', function () {
title: 'My dashboard',
uid: 'dash-test-uid',
panels: [
{ id: 1, title: 'First panel' },
{ id: 2, title: 'Second panel' },
{ id: 1, title: 'First panel', type: 'graph' },
{ id: 2, title: 'Second panel', type: 'graph' },
],
})
);
@@ -186,8 +186,8 @@ describe('AnnotationsField', function () {
title: 'My dashboard',
uid: 'dash-test-uid',
panels: [
{ id: 1, title: 'First panel' },
{ id: 2, title: 'Second panel' },
{ id: 1, title: 'First panel', type: 'timeseries' },
{ id: 2, title: 'Second panel', type: 'timeseries' },
],
})
);
@@ -195,7 +195,7 @@ describe('AnnotationsField', function () {
mockDashboardDto({
title: 'My other dashboard',
uid: 'dash-other-uid',
panels: [{ id: 3, title: 'Third panel' }],
panels: [{ id: 3, title: 'Third panel', type: 'timeseries' }],
})
);
@@ -216,10 +216,12 @@ describe('AnnotationsField', function () {
expect(annotationValueElements[0]).toHaveTextContent('dash-test-uid');
expect(annotationValueElements[1]).toHaveTextContent('1');
const { confirmButton, dialog } = ui.dashboardPicker;
await user.click(ui.setDashboardButton.get());
await user.click(await findByTitle(ui.dashboardPicker.dialog.get(), 'My other dashboard'));
await user.click(await findByText(ui.dashboardPicker.dialog.get(), 'Third panel'));
await user.click(ui.dashboardPicker.confirmButton.get());
await user.click(await findByRole(dialog.get(), 'button', { name: /My other dashboard/ }));
await user.click(await findByRole(dialog.get(), 'button', { name: /Third panel/ }));
await user.click(confirmButton.get());
expect(ui.dashboardPicker.dialog.query()).not.toBeInTheDocument();
@@ -235,6 +237,36 @@ describe('AnnotationsField', function () {
expect(annotationKeyElements[1]).toHaveTextContent('Panel ID');
expect(annotationValueElements[1]).toHaveTextContent('3');
});
it('should render warning icon for panels of type other than graph and timeseries', async function () {
mockSearchApiResponse(server, [
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
]);
mockGetDashboardResponse(
mockDashboardDto({
title: 'My dashboard',
uid: 'dash-test-uid',
panels: [
{ id: 1, title: 'First panel', type: 'bar' },
{ id: 2, title: 'Second panel', type: 'graph' },
],
})
);
const user = userEvent.setup();
render(<FormWrapper formValues={{ annotations: [] }} />);
const { dialog } = ui.dashboardPicker;
await user.click(ui.setDashboardButton.get());
await user.click(await findByTitle(dialog.get(), 'My dashboard'));
const warnedPanel = await findByRole(dialog.get(), 'button', { name: /First panel/ });
expect(getByTestId(warnedPanel, 'warning-icon')).toBeInTheDocument();
});
});
});
@@ -5,13 +5,24 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import { GrafanaTheme2 } from '@grafana/data/src';
import { FilterInput, LoadingPlaceholder, useStyles2, Icon, Modal, Button, Alert } from '@grafana/ui';
import {
FilterInput,
LoadingPlaceholder,
useStyles2,
Icon,
Modal,
Button,
Alert,
clearButtonStyles,
Tooltip,
} from '@grafana/ui';
import { dashboardApi } from '../../api/dashboardApi';
export interface PanelDTO {
id: number;
title?: string;
type: string;
}
function panelSort(a: PanelDTO, b: PanelDTO) {
@@ -62,7 +73,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
const filteredPanels =
dashboardResult?.dashboard?.panels
?.filter((panel): panel is PanelDTO => typeof panel.id === 'number')
?.filter((panel): panel is PanelDTO => typeof panel.id === 'number' && typeof panel.type === 'string')
?.filter((panel) => panel.title?.toLowerCase().includes(panelFilter.toLowerCase()))
.sort(panelSort) ?? [];
@@ -102,10 +113,10 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
<div
title={dashboard.title}
style={style}
className={cx(styles.row, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
className={cx(styles.rowButton, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
onClick={() => handleDashboardChange(dashboard.uid)}
>
<div className={styles.dashboardTitle}>{dashboard.title}</div>
<div className={cx(styles.dashboardTitle, styles.rowButtonTitle)}>{dashboard.title}</div>
<div className={styles.dashboardFolder}>
<Icon name="folder" /> {dashboard.folderTitle ?? 'General'}
</div>
@@ -115,16 +126,28 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => {
const panel = filteredPanels[index];
const panelTitle = panel.title || '<No title>';
const isSelected = selectedPanelId === panel.id.toString();
const isAlertingCompatible = panel.type === 'graph' || panel.type === 'timeseries';
return (
<div
<button
style={style}
className={cx(styles.row, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
className={cx(styles.rowButton, styles.panelButton, {
[styles.rowOdd]: index % 2 === 1,
[styles.rowSelected]: isSelected,
})}
onClick={() => setSelectedPanelId(panel.id.toString())}
>
{panel.title || '<No title>'}
</div>
<div className={styles.rowButtonTitle} title={panelTitle}>
{panelTitle}
</div>
{!isAlertingCompatible && (
<Tooltip content="Alert tab will be disabled for this panel. It is only supported on graph and timeseries panels">
<Icon name="exclamation-triangle" className={styles.warnIcon} data-testid="warning-icon" />
</Tooltip>
)}
</button>
);
};
@@ -184,12 +207,16 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
</div>
<div className={styles.column}>
{!dashboardUid && !isDashboardFetching && <div>Select a dashboard to get a list of available panels</div>}
{!selectedDashboardUid && !isDashboardFetching && (
<div className={styles.selectDashboardPlaceholder}>
<div>Select a dashboard to get a list of available panels</div>
</div>
)}
{isDashboardFetching && (
<LoadingPlaceholder text="Loading dashboard..." className={styles.loadingPlaceholder} />
)}
{!isDashboardFetching && (
{selectedDashboardUid && !isDashboardFetching && (
<AutoSizer>
{({ width, height }) => (
<FixedSizeList itemSize={32} height={height} width={width} itemCount={filteredPanels.length}>
@@ -221,60 +248,93 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
);
};
const getPickerStyles = (theme: GrafanaTheme2) => ({
container: css`
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: min-content auto;
gap: ${theme.spacing(2)};
flex: 1;
`,
column: css`
flex: 1 1 auto;
`,
dashboardTitle: css`
height: 22px;
font-weight: ${theme.typography.fontWeightBold};
`,
dashboardFolder: css`
height: 20px;
font-size: ${theme.typography.bodySmall.fontSize};
color: ${theme.colors.text.secondary};
display: flex;
flex-direction: row;
justify-content: flex-start;
column-gap: ${theme.spacing(1)};
align-items: center;
`,
row: css`
padding: ${theme.spacing(0.5)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
border: 2px solid transparent;
`,
rowSelected: css`
border-color: ${theme.colors.primary.border};
`,
rowOdd: css`
background-color: ${theme.colors.background.secondary};
`,
loadingPlaceholder: css`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`,
modal: css`
height: 100%;
`,
modalContent: css`
flex: 1;
display: flex;
flex-direction: column;
`,
modalAlert: css`
flex-grow: 0;
`,
});
const getPickerStyles = (theme: GrafanaTheme2) => {
const clearButton = clearButtonStyles(theme);
return {
container: css`
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: min-content auto;
gap: ${theme.spacing(2)};
flex: 1;
`,
column: css`
flex: 1 1 auto;
`,
dashboardTitle: css`
height: 22px;
font-weight: ${theme.typography.fontWeightBold};
`,
dashboardFolder: css`
height: 20px;
font-size: ${theme.typography.bodySmall.fontSize};
color: ${theme.colors.text.secondary};
display: flex;
flex-direction: row;
justify-content: flex-start;
column-gap: ${theme.spacing(1)};
align-items: center;
`,
rowButton: css`
${clearButton};
padding: ${theme.spacing(0.5)};
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
white-space: nowrap;
cursor: pointer;
border: 2px solid transparent;
&:disabled {
cursor: not-allowed;
color: ${theme.colors.text.disabled};
}
`,
rowButtonTitle: css`
text-overflow: ellipsis;
overflow: hidden;
`,
rowSelected: css`
border-color: ${theme.colors.primary.border};
`,
rowOdd: css`
background-color: ${theme.colors.background.secondary};
`,
panelButton: css`
display: flex;
gap: ${theme.spacing(1)};
justify-content: space-between;
align-items: center;
`,
loadingPlaceholder: css`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`,
selectDashboardPlaceholder: css`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
font-weight: ${theme.typography.fontWeightBold};
`,
modal: css`
height: 100%;
`,
modalContent: css`
flex: 1;
display: flex;
flex-direction: column;
`,
modalAlert: css`
flex-grow: 0;
`,
warnIcon: css`
fill: ${theme.colors.warning.main};
`,
};
};