mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix Alert rule form editing when associated panel has no id (#77209)
This commit is contained in:
parent
e01d096ce2
commit
3c915d0502
@ -21,6 +21,7 @@ import {
|
||||
|
||||
import { cloneRuleDefinition, CloneRuleEditor } from './CloneRuleEditor';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { mockSearchApi } from './mockApi';
|
||||
import {
|
||||
mockDataSource,
|
||||
MockDataSourceSrv,
|
||||
@ -30,7 +31,6 @@ import {
|
||||
mockStore,
|
||||
} from './mocks';
|
||||
import { mockAlertmanagerConfigResponse } from './mocks/alertmanagerApi';
|
||||
import { mockSearchApiResponse } from './mocks/grafanaApi';
|
||||
import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from './mocks/rulerApi';
|
||||
import { AlertingQueryRunner } from './state/AlertingQueryRunner';
|
||||
import { RuleFormValues } from './types/rule-form';
|
||||
@ -156,7 +156,7 @@ describe('CloneRuleEditor', function () {
|
||||
'folder-one': [{ name: 'group1', interval: '20s', rules: [originRule] }],
|
||||
});
|
||||
|
||||
mockSearchApiResponse(server, []);
|
||||
mockSearchApi(server).search([]);
|
||||
mockAlertmanagerConfigResponse(server, GRAFANA_RULES_SOURCE_NAME, amConfig);
|
||||
|
||||
render(<CloneRuleEditor sourceRuleId={{ uid: 'grafana-rule-1', ruleSourceName: 'grafana' }} />, {
|
||||
@ -209,7 +209,7 @@ describe('CloneRuleEditor', function () {
|
||||
rules: [originRule],
|
||||
});
|
||||
|
||||
mockSearchApiResponse(server, []);
|
||||
mockSearchApi(server).search([]);
|
||||
mockAlertmanagerConfigResponse(server, GRAFANA_RULES_SOURCE_NAME, amConfig);
|
||||
|
||||
render(
|
||||
|
@ -12,7 +12,7 @@ import { TestProvider } from '../../../../../../test/helpers/TestProvider';
|
||||
import { AlertmanagerChoice } from '../../../../../plugins/datasource/alertmanager/types';
|
||||
import { DashboardSearchItemType } from '../../../../search/types';
|
||||
import { mockAlertRuleApi, mockApi, mockExportApi, mockSearchApi, setupMswServer } from '../../mockApi';
|
||||
import { getGrafanaRule, mockDataSource } from '../../mocks';
|
||||
import { getGrafanaRule, mockDashboardSearchItem, mockDataSource } from '../../mocks';
|
||||
import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
@ -96,14 +96,11 @@ describe('GrafanaModifyExport', () => {
|
||||
it('Should render edit form for the specified rule', async () => {
|
||||
mockApi(server).eval({ results: { A: { frames: [] } } });
|
||||
mockSearchApi(server).search([
|
||||
{
|
||||
mockDashboardSearchItem({
|
||||
title: grafanaRule.namespace.name,
|
||||
uid: 'folder-test-uid',
|
||||
id: 1,
|
||||
url: '',
|
||||
tags: [],
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, {
|
||||
[grafanaRule.namespace.name]: [{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }],
|
||||
|
@ -1,20 +1,18 @@
|
||||
import { findByRole, findByText, findByTitle, getByTestId, queryByText, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import React from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import 'core-js/stable/structured-clone';
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { defaultDashboard } from '@grafana/schema';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { DashboardDTO } from '../../../../../types';
|
||||
import { DashboardSearchItem, DashboardSearchItemType } from '../../../../search/types';
|
||||
import { mockStore } from '../../mocks';
|
||||
import { mockSearchApiResponse } from '../../mocks/grafanaApi';
|
||||
import { DashboardSearchItemType } from '../../../../search/types';
|
||||
import { mockDashboardApi } from '../../mockApi';
|
||||
import { mockDashboardDto, mockDashboardSearchItem, mockStore } from '../../mocks';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { getDefaultFormValues } from '../../utils/rule-form';
|
||||
@ -36,6 +34,8 @@ const ui = {
|
||||
setDashboardButton: byRole('button', { name: 'Link dashboard and panel' }),
|
||||
annotationKeys: byTestId('annotation-key-', { exact: false }),
|
||||
annotationValues: byTestId('annotation-value-', { exact: false }),
|
||||
dashboardAnnotation: byTestId('dashboard-annotation'),
|
||||
panelAnnotation: byTestId('panel-annotation'),
|
||||
dashboardPicker: {
|
||||
dialog: byRole('dialog'),
|
||||
heading: byRole('heading', { name: 'Select dashboard and panel' }),
|
||||
@ -85,7 +85,7 @@ describe('AnnotationsField', function () {
|
||||
|
||||
describe('Dashboard and panel picker', function () {
|
||||
it('should display dashboard and panel selector when select button clicked', async function () {
|
||||
mockSearchApiResponse(server, []);
|
||||
mockDashboardApi(server).search([]);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
@ -98,11 +98,11 @@ describe('AnnotationsField', function () {
|
||||
});
|
||||
|
||||
it('should enable Confirm button only when dashboard and panel selected', async function () {
|
||||
mockSearchApiResponse(server, [
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
]);
|
||||
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
@ -128,11 +128,11 @@ describe('AnnotationsField', function () {
|
||||
});
|
||||
|
||||
it('should add selected dashboard and panel as annotations', async function () {
|
||||
mockSearchApiResponse(server, [
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
]);
|
||||
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
@ -164,11 +164,11 @@ describe('AnnotationsField', function () {
|
||||
});
|
||||
|
||||
it('should not show rows as panels', async function () {
|
||||
mockSearchApiResponse(server, [
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
]);
|
||||
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
@ -193,11 +193,11 @@ describe('AnnotationsField', function () {
|
||||
});
|
||||
|
||||
it('should show panels within collapsed rows', async function () {
|
||||
mockSearchApiResponse(server, [
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
]);
|
||||
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
@ -231,7 +231,7 @@ describe('AnnotationsField', function () {
|
||||
// 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.
|
||||
it.skip('should update existing dashboard and panel identifies', async function () {
|
||||
mockSearchApiResponse(server, [
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
mockDashboardSearchItem({
|
||||
title: 'My other dashboard',
|
||||
@ -240,7 +240,7 @@ describe('AnnotationsField', function () {
|
||||
}),
|
||||
]);
|
||||
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
@ -250,7 +250,7 @@ describe('AnnotationsField', function () {
|
||||
],
|
||||
})
|
||||
);
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My other dashboard',
|
||||
uid: 'dash-other-uid',
|
||||
@ -297,67 +297,64 @@ describe('AnnotationsField', function () {
|
||||
expect(annotationValueElements[1]).toHaveTextContent('3');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render warning icon for panels of type other than graph and timeseries', async function () {
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
]);
|
||||
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
panels: [
|
||||
{ id: 1, title: 'First panel', type: 'bar' },
|
||||
{ id: 2, title: 'Second panel', type: 'graph' },
|
||||
{ type: 'timeseries' }, // Panels might NOT have id and title fields
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('should render when panels do not contain certain fields', async () => {
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
]);
|
||||
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
panels: [{ type: 'row' }, { type: 'timeseries' }, { id: 4, type: 'graph' }, { title: 'Graph', type: 'graph' }],
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<FormWrapper
|
||||
formValues={{
|
||||
annotations: [
|
||||
{ key: Annotation.dashboardUID, value: 'dash-test-uid' },
|
||||
{ key: Annotation.panelID, value: '1' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await ui.dashboardAnnotation.find()).toBeInTheDocument();
|
||||
expect(ui.dashboardAnnotation.get()).toHaveTextContent('My dashboard');
|
||||
expect(ui.panelAnnotation.query()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
function mockGetDashboardResponse(dashboard: DashboardDTO) {
|
||||
server.use(
|
||||
rest.get(`/api/dashboards/uid/${dashboard.dashboard.uid}`, (req, res, ctx) =>
|
||||
res(ctx.json<DashboardDTO>(dashboard))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function mockDashboardSearchItem(searchItem: Partial<DashboardSearchItem>) {
|
||||
return {
|
||||
title: '',
|
||||
uid: '',
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
url: '',
|
||||
uri: '',
|
||||
items: [],
|
||||
tags: [],
|
||||
slug: '',
|
||||
isStarred: false,
|
||||
...searchItem,
|
||||
};
|
||||
}
|
||||
|
||||
function mockDashboardDto(dashboard: Partial<DashboardDTO['dashboard']>): DashboardDTO {
|
||||
return {
|
||||
dashboard: {
|
||||
...defaultDashboard,
|
||||
...dashboard,
|
||||
} as DashboardDTO['dashboard'],
|
||||
meta: {},
|
||||
};
|
||||
}
|
@ -7,17 +7,17 @@ import { useToggle } from 'react-use';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Field, Input, Text, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { dashboardApi } from '../../api/dashboardApi';
|
||||
import { DashboardModel } from '../../../../dashboard/state';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { Annotation, annotationLabels } from '../../utils/constants';
|
||||
|
||||
import AnnotationHeaderField from './AnnotationHeaderField';
|
||||
import DashboardAnnotationField from './DashboardAnnotationField';
|
||||
import { DashboardPicker, mergePanels, PanelDTO } from './DashboardPicker';
|
||||
import { DashboardPicker, getVisualPanels, PanelDTO } from './DashboardPicker';
|
||||
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { useDashboardQuery } from './useDashboardQuery';
|
||||
|
||||
const AnnotationsStep = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -35,31 +35,26 @@ const AnnotationsStep = () => {
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'annotations' });
|
||||
|
||||
const selectedDashboardUid = annotations.find((annotation) => annotation.key === Annotation.dashboardUID)?.value;
|
||||
const selectedPanelId = annotations.find((annotation) => annotation.key === Annotation.panelID)?.value;
|
||||
const selectedPanelId = Number(annotations.find((annotation) => annotation.key === Annotation.panelID)?.value);
|
||||
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<DashboardDataDTO | undefined>(undefined);
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<DashboardModel | undefined>(undefined);
|
||||
const [selectedPanel, setSelectedPanel] = useState<PanelDTO | undefined>(undefined);
|
||||
|
||||
const { useDashboardQuery } = dashboardApi;
|
||||
|
||||
const { currentData: dashboardResult, isFetching: isDashboardFetching } = useDashboardQuery(
|
||||
{ uid: selectedDashboardUid ?? '' },
|
||||
{ skip: !selectedDashboardUid }
|
||||
);
|
||||
const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDashboardFetching) {
|
||||
if (isDashboardFetching || !dashboardModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedDashboard(dashboardResult?.dashboard);
|
||||
setSelectedDashboard(dashboardModel);
|
||||
|
||||
const allPanels = mergePanels(dashboardResult);
|
||||
const currentPanel = allPanels.find((panel) => panel.id.toString() === selectedPanelId);
|
||||
const allPanels = getVisualPanels(dashboardModel);
|
||||
const currentPanel = allPanels.find((panel) => panel.id === selectedPanelId);
|
||||
setSelectedPanel(currentPanel);
|
||||
}, [selectedPanelId, dashboardResult, isDashboardFetching]);
|
||||
}, [selectedPanelId, dashboardModel, isDashboardFetching]);
|
||||
|
||||
const setSelectedDashboardAndPanelId = (dashboardUid: string, panelId: string) => {
|
||||
const setSelectedDashboardAndPanelId = (dashboardUid: string, panelId: number) => {
|
||||
const updatedAnnotations = produce(annotations, (draft) => {
|
||||
const dashboardAnnotation = draft.find((a) => a.key === Annotation.dashboardUID);
|
||||
const panelAnnotation = draft.find((a) => a.key === Annotation.panelID);
|
||||
@ -71,9 +66,9 @@ const AnnotationsStep = () => {
|
||||
}
|
||||
|
||||
if (panelAnnotation) {
|
||||
panelAnnotation.value = panelId;
|
||||
panelAnnotation.value = panelId.toString();
|
||||
} else {
|
||||
draft.push({ key: Annotation.panelID, value: panelId });
|
||||
draft.push({ key: Annotation.panelID, value: panelId.toString() });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { noop } from 'lodash';
|
||||
import React from 'react';
|
||||
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
|
||||
import 'core-js/stable/structured-clone';
|
||||
|
||||
import { TestProvider } from '../../../../../../test/helpers/TestProvider';
|
||||
import { DashboardSearchItemType } from '../../../../search/types';
|
||||
import { mockDashboardApi, setupMswServer } from '../../mockApi';
|
||||
import { mockDashboardDto, mockDashboardSearchItem } from '../../mocks';
|
||||
|
||||
import { DashboardPicker } from './DashboardPicker';
|
||||
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 });
|
||||
});
|
||||
|
||||
const server = setupMswServer();
|
||||
|
||||
mockDashboardApi(server).search([
|
||||
mockDashboardSearchItem({ uid: 'dash-1', type: DashboardSearchItemType.DashDB, title: 'Dashboard 1' }),
|
||||
mockDashboardSearchItem({ uid: 'dash-2', type: DashboardSearchItemType.DashDB, title: 'Dashboard 2' }),
|
||||
mockDashboardSearchItem({ uid: 'dash-3', type: DashboardSearchItemType.DashDB, title: 'Dashboard 3' }),
|
||||
]);
|
||||
|
||||
mockDashboardApi(server).dashboard(
|
||||
mockDashboardDto({
|
||||
uid: 'dash-2',
|
||||
title: 'Dashboard 2',
|
||||
panels: [{ type: 'graph' }, { type: 'timeseries' }],
|
||||
})
|
||||
);
|
||||
|
||||
const ui = {
|
||||
dashboardButton: (name: RegExp) => byRole('button', { name }),
|
||||
};
|
||||
|
||||
describe('DashboardPicker', () => {
|
||||
it('Renders panels without ids', async () => {
|
||||
render(<DashboardPicker isOpen={true} onChange={noop} onDismiss={noop} dashboardUid="dash-2" panelId={2} />, {
|
||||
wrapper: TestProvider,
|
||||
});
|
||||
|
||||
expect(await ui.dashboardButton(/Dashboard 1/).find()).toBeInTheDocument();
|
||||
expect(await ui.dashboardButton(/Dashboard 2/).find()).toBeInTheDocument();
|
||||
expect(await ui.dashboardButton(/Dashboard 3/).find()).toBeInTheDocument();
|
||||
|
||||
expect(await ui.dashboardButton(/<No title>/).findAll()).toHaveLength(2);
|
||||
});
|
||||
});
|
@ -17,14 +17,17 @@ import {
|
||||
Tooltip,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
import { DashboardModel } from '../../../../dashboard/state';
|
||||
import { dashboardApi } from '../../api/dashboardApi';
|
||||
|
||||
import { useDashboardQuery } from './useDashboardQuery';
|
||||
|
||||
export interface PanelDTO {
|
||||
id?: number;
|
||||
title?: string;
|
||||
type: string;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
function panelSort(a: PanelDTO, b: PanelDTO) {
|
||||
@ -42,24 +45,12 @@ function panelSort(a: PanelDTO, b: PanelDTO) {
|
||||
|
||||
interface DashboardPickerProps {
|
||||
isOpen: boolean;
|
||||
dashboardUid?: string | undefined;
|
||||
panelId?: string | undefined;
|
||||
onChange: (dashboardUid: string, panelId: string) => void;
|
||||
dashboardUid?: string;
|
||||
panelId?: number;
|
||||
onChange: (dashboardUid: string, panelId: number) => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export function mergePanels(dashboardResult: DashboardDTO | undefined) {
|
||||
const panels = dashboardResult?.dashboard?.panels?.filter((panel) => panel.type !== 'row') || [];
|
||||
const nestedPanels =
|
||||
dashboardResult?.dashboard?.panels
|
||||
?.filter((row: { collapsed: boolean }) => row.collapsed)
|
||||
.map((collapsedRow: { panels: PanelDTO[] }) => collapsedRow.panels) || [];
|
||||
|
||||
const allDashboardPanels = [...panels, ...nestedPanels.flat()];
|
||||
|
||||
return allDashboardPanels;
|
||||
}
|
||||
|
||||
export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDismiss }: DashboardPickerProps) => {
|
||||
const styles = useStyles2(getPickerStyles);
|
||||
|
||||
@ -70,26 +61,23 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
const [debouncedDashboardFilter, setDebouncedDashboardFilter] = useState('');
|
||||
|
||||
const [panelFilter, setPanelFilter] = useState('');
|
||||
const { useSearchQuery, useDashboardQuery } = dashboardApi;
|
||||
const { useSearchQuery } = dashboardApi;
|
||||
|
||||
const { currentData: filteredDashboards = [], isFetching: isDashSearchFetching } = useSearchQuery({
|
||||
query: debouncedDashboardFilter,
|
||||
});
|
||||
const { currentData: dashboardResult, isFetching: isDashboardFetching } = useDashboardQuery(
|
||||
{ uid: selectedDashboardUid ?? '' },
|
||||
{ skip: !selectedDashboardUid }
|
||||
);
|
||||
const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
||||
|
||||
const handleDashboardChange = useCallback((dashboardUid: string) => {
|
||||
setSelectedDashboardUid(dashboardUid);
|
||||
setSelectedPanelId(undefined);
|
||||
}, []);
|
||||
|
||||
const allDashboardPanels = mergePanels(dashboardResult);
|
||||
const allDashboardPanels = getVisualPanels(dashboardModel);
|
||||
|
||||
const filteredPanels =
|
||||
allDashboardPanels
|
||||
?.filter((panel) => panel.title?.toLowerCase().includes(panelFilter.toLowerCase()))
|
||||
.filter((panel) => panel.title?.toLowerCase().includes(panelFilter.toLowerCase()))
|
||||
.sort(panelSort) ?? [];
|
||||
|
||||
const currentPanel: PanelDTO | undefined = allDashboardPanels.find(
|
||||
@ -145,7 +133,7 @@ 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 = panel.id && selectedPanelId === panel.id?.toString();
|
||||
const isSelected = Boolean(panel.id) && selectedPanelId === panel.id;
|
||||
const isAlertingCompatible = panel.type === 'graph' || panel.type === 'timeseries';
|
||||
const disabled = !isValidPanelIdentifier(panel);
|
||||
|
||||
@ -158,7 +146,7 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
[styles.rowOdd]: index % 2 === 1,
|
||||
[styles.rowSelected]: isSelected,
|
||||
})}
|
||||
onClick={() => (disabled ? noop : setSelectedPanelId(panel.id?.toString()))}
|
||||
onClick={() => (disabled ? noop : setSelectedPanelId(panel.id))}
|
||||
>
|
||||
<div className={styles.rowButtonTitle} title={panelTitle}>
|
||||
{panelTitle}
|
||||
@ -187,11 +175,11 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
contentClassName={styles.modalContent}
|
||||
>
|
||||
{/* This alert shows if the selected dashboard is not found in the first page of dashboards */}
|
||||
{!selectedDashboardIsInPageResult && dashboardUid && (
|
||||
{!selectedDashboardIsInPageResult && dashboardUid && dashboardModel && (
|
||||
<Alert title="Current selection" severity="info" topSpacing={0} bottomSpacing={1} className={styles.modalAlert}>
|
||||
<div>
|
||||
Dashboard: {dashboardResult?.dashboard.title} ({dashboardResult?.dashboard.uid}) in folder{' '}
|
||||
{dashboardResult?.meta.folderTitle ?? 'General'}
|
||||
Dashboard: {dashboardModel.title} ({dashboardModel.uid}) in folder{' '}
|
||||
{dashboardModel.meta?.folderTitle ?? 'General'}
|
||||
</div>
|
||||
{currentPanel && (
|
||||
<div>
|
||||
@ -274,6 +262,20 @@ export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDis
|
||||
);
|
||||
};
|
||||
|
||||
export function getVisualPanels(dashboardModel: DashboardModel | undefined) {
|
||||
if (!dashboardModel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const panelsWithoutRows = dashboardModel.panels.filter((panel) => panel.type !== 'row');
|
||||
const panelsNestedInRows = dashboardModel.panels
|
||||
.filter((rowPanel) => rowPanel.collapsed)
|
||||
.flatMap((collapsedRow) => collapsedRow.panels ?? []);
|
||||
|
||||
const allDashboardPanels = [...panelsWithoutRows, ...panelsNestedInRows];
|
||||
return allDashboardPanels;
|
||||
}
|
||||
|
||||
const isValidPanelIdentifier = (panel: PanelDTO): boolean => {
|
||||
return typeof panel.id === 'number' && typeof panel.type === 'string';
|
||||
};
|
||||
|
@ -0,0 +1,27 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
import { DashboardDTO } from '../../../../../types';
|
||||
import { DashboardModel } from '../../../../dashboard/state';
|
||||
import { dashboardApi } from '../../api/dashboardApi';
|
||||
|
||||
const convertToDashboardModel = memoizeOne((dashboardDTO: DashboardDTO) => {
|
||||
// RTKQuery freezes all returned objects. DashboardModel constructor runs migrations which might change the internal object
|
||||
// Hence we need to add structuredClone to make a deep copy of the API response object
|
||||
const { dashboard, meta } = structuredClone(dashboardDTO);
|
||||
return new DashboardModel(dashboard, meta);
|
||||
});
|
||||
|
||||
export function useDashboardQuery(dashboardUid?: string) {
|
||||
const queryData = dashboardApi.endpoints.dashboard.useQuery(
|
||||
{ uid: dashboardUid ?? '' },
|
||||
{
|
||||
skip: !dashboardUid,
|
||||
selectFromResult: ({ currentData, data, ...rest }) => ({
|
||||
dashboardModel: currentData ? convertToDashboardModel(currentData) : undefined,
|
||||
...rest,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return queryData;
|
||||
}
|
@ -22,8 +22,8 @@ import {
|
||||
MatcherOperator,
|
||||
Route,
|
||||
} from '../../../plugins/datasource/alertmanager/types';
|
||||
import { FolderDTO, NotifierDTO } from '../../../types';
|
||||
import { DashboardSearchHit } from '../../search/types';
|
||||
import { DashboardDTO, FolderDTO, NotifierDTO } from '../../../types';
|
||||
import { DashboardSearchItem } from '../../search/types';
|
||||
|
||||
import { CreateIntegrationDTO, NewOnCallIntegrationDTO, OnCallIntegrationDTO } from './api/onCallApi';
|
||||
import { AlertingQueryResponse } from './state/AlertingQueryRunner';
|
||||
@ -397,12 +397,27 @@ export function mockFolderApi(server: SetupServer) {
|
||||
|
||||
export function mockSearchApi(server: SetupServer) {
|
||||
return {
|
||||
search: (results: DashboardSearchHit[]) => {
|
||||
search: (results: DashboardSearchItem[]) => {
|
||||
server.use(rest.get(`/api/search`, (_, res, ctx) => res(ctx.status(200), ctx.json(results))));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function mockDashboardApi(server: SetupServer) {
|
||||
return {
|
||||
search: (results: DashboardSearchItem[]) => {
|
||||
server.use(rest.get(`/api/search`, (_, res, ctx) => res(ctx.status(200), ctx.json(results))));
|
||||
},
|
||||
dashboard: (response: DashboardDTO) => {
|
||||
server.use(
|
||||
rest.get(`/api/dashboards/uid/${response.dashboard.uid}`, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(response))
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Creates a MSW server and sets up beforeAll, afterAll and beforeEach handlers for it
|
||||
export function setupMswServer() {
|
||||
const server = setupServer();
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
TestDataSourceResponse,
|
||||
} from '@grafana/data';
|
||||
import { config, DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime';
|
||||
import { defaultDashboard } from '@grafana/schema';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import {
|
||||
@ -30,7 +31,7 @@ import {
|
||||
SilenceState,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction, FolderDTO, NotifiersState, ReceiversState, StoreState } from 'app/types';
|
||||
import { AccessControlAction, DashboardDTO, FolderDTO, NotifiersState, ReceiversState, StoreState } from 'app/types';
|
||||
import {
|
||||
Alert,
|
||||
AlertingRule,
|
||||
@ -55,6 +56,8 @@ import {
|
||||
RulerRulesConfigDTO,
|
||||
} from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
|
||||
|
||||
let nextDataSourceId = 1;
|
||||
|
||||
export function mockDataSource<T extends DataSourceJsonData = DataSourceJsonData>(
|
||||
@ -684,6 +687,36 @@ export function mockAlertWithState(state: GrafanaAlertState, labels?: {}): Alert
|
||||
return { activeAt: '', annotations: {}, labels: labels || {}, state: state, value: '' };
|
||||
}
|
||||
|
||||
export function mockDashboardSearchItem(searchItem: Partial<DashboardSearchItem>) {
|
||||
return {
|
||||
title: '',
|
||||
uid: '',
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
url: '',
|
||||
uri: '',
|
||||
items: [],
|
||||
tags: [],
|
||||
slug: '',
|
||||
isStarred: false,
|
||||
...searchItem,
|
||||
};
|
||||
}
|
||||
|
||||
export function mockDashboardDto(
|
||||
dashboard: Partial<DashboardDTO['dashboard']>,
|
||||
meta?: Partial<DashboardDTO['meta']>
|
||||
): DashboardDTO {
|
||||
return {
|
||||
dashboard: {
|
||||
uid: 'dashboard-test',
|
||||
title: 'Dashboard test',
|
||||
schemaVersion: defaultDashboard.schemaVersion,
|
||||
...dashboard,
|
||||
},
|
||||
meta: { ...meta },
|
||||
};
|
||||
}
|
||||
|
||||
export const onCallPluginMetaMock: PluginMeta = {
|
||||
name: 'Grafana OnCall',
|
||||
id: 'grafana-oncall-app',
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { rest } from 'msw';
|
||||
import { SetupServer } from 'msw/node';
|
||||
|
||||
import { DashboardSearchItem } from '../../../search/types';
|
||||
|
||||
export function mockSearchApiResponse(server: SetupServer, searchResult: DashboardSearchItem[]) {
|
||||
server.use(rest.get('/api/search', (req, res, ctx) => res(ctx.json<DashboardSearchItem[]>(searchResult))));
|
||||
}
|
Loading…
Reference in New Issue
Block a user