Alerting: Fix Alert rule form editing when associated panel has no id (#77209)

This commit is contained in:
Konrad Lalik 2023-10-31 08:38:25 +01:00 committed by GitHub
parent e01d096ce2
commit 3c915d0502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 259 additions and 149 deletions

View File

@ -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(

View File

@ -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!] }],

View File

@ -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: {},
};
}

View File

@ -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() });
}
});

View File

@ -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);
});
});

View File

@ -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';
};

View File

@ -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;
}

View File

@ -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();

View File

@ -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',

View File

@ -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))));
}