mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
bf4906ed34
commit
bde77e4f79
@ -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 userEvent from '@testing-library/user-event';
|
||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
import { setupServer } from 'msw/node';
|
import { setupServer } from 'msw/node';
|
||||||
@ -107,8 +107,8 @@ describe('AnnotationsField', function () {
|
|||||||
title: 'My dashboard',
|
title: 'My dashboard',
|
||||||
uid: 'dash-test-uid',
|
uid: 'dash-test-uid',
|
||||||
panels: [
|
panels: [
|
||||||
{ id: 1, title: 'First panel' },
|
{ id: 1, title: 'First panel', type: 'timeseries' },
|
||||||
{ id: 2, title: 'Second panel' },
|
{ id: 2, title: 'Second panel', type: 'timeseries' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -137,8 +137,8 @@ describe('AnnotationsField', function () {
|
|||||||
title: 'My dashboard',
|
title: 'My dashboard',
|
||||||
uid: 'dash-test-uid',
|
uid: 'dash-test-uid',
|
||||||
panels: [
|
panels: [
|
||||||
{ id: 1, title: 'First panel' },
|
{ id: 1, title: 'First panel', type: 'graph' },
|
||||||
{ id: 2, title: 'Second panel' },
|
{ id: 2, title: 'Second panel', type: 'graph' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -171,7 +171,7 @@ describe('AnnotationsField', function () {
|
|||||||
|
|
||||||
// this test _should_ work in theory but something is stopping the 'onClick' function on the dashboard item
|
// this test _should_ work in theory but something is stopping the 'onClick' function on the dashboard item
|
||||||
// to trigger "handleDashboardChange" – skipping it for now but has been manually tested.
|
// to trigger "handleDashboardChange" – skipping it for now but has been manually tested.
|
||||||
it.skip('should update existing dashboard and panel identifies', async function () {
|
it('should update existing dashboard and panel identifies', async function () {
|
||||||
mockSearchApiResponse(server, [
|
mockSearchApiResponse(server, [
|
||||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||||
mockDashboardSearchItem({
|
mockDashboardSearchItem({
|
||||||
@ -186,8 +186,8 @@ describe('AnnotationsField', function () {
|
|||||||
title: 'My dashboard',
|
title: 'My dashboard',
|
||||||
uid: 'dash-test-uid',
|
uid: 'dash-test-uid',
|
||||||
panels: [
|
panels: [
|
||||||
{ id: 1, title: 'First panel' },
|
{ id: 1, title: 'First panel', type: 'timeseries' },
|
||||||
{ id: 2, title: 'Second panel' },
|
{ id: 2, title: 'Second panel', type: 'timeseries' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -195,7 +195,7 @@ describe('AnnotationsField', function () {
|
|||||||
mockDashboardDto({
|
mockDashboardDto({
|
||||||
title: 'My other dashboard',
|
title: 'My other dashboard',
|
||||||
uid: 'dash-other-uid',
|
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[0]).toHaveTextContent('dash-test-uid');
|
||||||
expect(annotationValueElements[1]).toHaveTextContent('1');
|
expect(annotationValueElements[1]).toHaveTextContent('1');
|
||||||
|
|
||||||
|
const { confirmButton, dialog } = ui.dashboardPicker;
|
||||||
|
|
||||||
await user.click(ui.setDashboardButton.get());
|
await user.click(ui.setDashboardButton.get());
|
||||||
await user.click(await findByTitle(ui.dashboardPicker.dialog.get(), 'My other dashboard'));
|
await user.click(await findByRole(dialog.get(), 'button', { name: /My other dashboard/ }));
|
||||||
await user.click(await findByText(ui.dashboardPicker.dialog.get(), 'Third panel'));
|
await user.click(await findByRole(dialog.get(), 'button', { name: /Third panel/ }));
|
||||||
await user.click(ui.dashboardPicker.confirmButton.get());
|
await user.click(confirmButton.get());
|
||||||
|
|
||||||
expect(ui.dashboardPicker.dialog.query()).not.toBeInTheDocument();
|
expect(ui.dashboardPicker.dialog.query()).not.toBeInTheDocument();
|
||||||
|
|
||||||
@ -235,6 +237,36 @@ describe('AnnotationsField', function () {
|
|||||||
expect(annotationKeyElements[1]).toHaveTextContent('Panel ID');
|
expect(annotationKeyElements[1]).toHaveTextContent('Panel ID');
|
||||||
expect(annotationValueElements[1]).toHaveTextContent('3');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Alert,
|
Alert,
|
||||||
clearButtonStyles,
|
clearButtonStyles,
|
||||||
|
Tooltip,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { dashboardApi } from '../../api/dashboardApi';
|
import { dashboardApi } from '../../api/dashboardApi';
|
||||||
@ -21,6 +22,7 @@ import { dashboardApi } from '../../api/dashboardApi';
|
|||||||
export interface PanelDTO {
|
export interface PanelDTO {
|
||||||
id: number;
|
id: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelSort(a: PanelDTO, b: PanelDTO) {
|
function panelSort(a: PanelDTO, b: PanelDTO) {
|
||||||
@ -71,7 +73,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
|
|
||||||
const filteredPanels =
|
const filteredPanels =
|
||||||
dashboardResult?.dashboard?.panels
|
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()))
|
?.filter((panel) => panel.title?.toLowerCase().includes(panelFilter.toLowerCase()))
|
||||||
.sort(panelSort) ?? [];
|
.sort(panelSort) ?? [];
|
||||||
|
|
||||||
@ -115,7 +117,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
className={cx(styles.rowButton, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
|
className={cx(styles.rowButton, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
|
||||||
onClick={() => handleDashboardChange(dashboard.uid)}
|
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}>
|
<div className={styles.dashboardFolder}>
|
||||||
<Icon name="folder" /> {dashboard.folderTitle ?? 'General'}
|
<Icon name="folder" /> {dashboard.folderTitle ?? 'General'}
|
||||||
</div>
|
</div>
|
||||||
@ -125,16 +127,28 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
|
|
||||||
const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => {
|
const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => {
|
||||||
const panel = filteredPanels[index];
|
const panel = filteredPanels[index];
|
||||||
|
const panelTitle = panel.title || '<No title>';
|
||||||
const isSelected = selectedPanelId === panel.id.toString();
|
const isSelected = selectedPanelId === panel.id.toString();
|
||||||
|
const isAlertingCompatible = panel.type === 'graph' || panel.type === 'timeseries';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={style}
|
style={style}
|
||||||
className={cx(styles.rowButton, { [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())}
|
onClick={() => setSelectedPanelId(panel.id.toString())}
|
||||||
>
|
>
|
||||||
{panel.title || '<No title>'}
|
<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>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -195,12 +209,16 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.column}>
|
<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 && (
|
{isDashboardFetching && (
|
||||||
<LoadingPlaceholder text="Loading dashboard..." className={styles.loadingPlaceholder} />
|
<LoadingPlaceholder text="Loading dashboard..." className={styles.loadingPlaceholder} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isDashboardFetching && (
|
{selectedDashboardUid && !isDashboardFetching && (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
<FixedSizeList itemSize={32} height={height} width={width} itemCount={filteredPanels.length}>
|
<FixedSizeList itemSize={32} height={height} width={width} itemCount={filteredPanels.length}>
|
||||||
@ -269,6 +287,15 @@ const getPickerStyles = (theme: GrafanaTheme2) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: ${theme.colors.text.disabled};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
rowButtonTitle: css`
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
`,
|
`,
|
||||||
rowSelected: css`
|
rowSelected: css`
|
||||||
border-color: ${theme.colors.primary.border};
|
border-color: ${theme.colors.primary.border};
|
||||||
@ -276,12 +303,27 @@ const getPickerStyles = (theme: GrafanaTheme2) => {
|
|||||||
rowOdd: css`
|
rowOdd: css`
|
||||||
background-color: ${theme.colors.background.secondary};
|
background-color: ${theme.colors.background.secondary};
|
||||||
`,
|
`,
|
||||||
|
panelButton: css`
|
||||||
|
display: flex;
|
||||||
|
gap: ${theme.spacing(1)};
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
loadingPlaceholder: css`
|
loadingPlaceholder: css`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: 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`
|
modal: css`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
@ -293,5 +335,8 @@ const getPickerStyles = (theme: GrafanaTheme2) => {
|
|||||||
modalAlert: css`
|
modalAlert: css`
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
`,
|
`,
|
||||||
|
warnIcon: css`
|
||||||
|
fill: ${theme.colors.warning.main};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user