mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add dashboard and panel picker to the rule form (#58304)
Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
parent
245a59548c
commit
cae5d89d0f
22
public/app/features/alerting/unified/api/dashboardApi.ts
Normal file
22
public/app/features/alerting/unified/api/dashboardApi.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { DashboardDTO } from '../../../../types';
|
||||
import { DashboardSearchItem } from '../../../search/types';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
export const dashboardApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
search: build.query<DashboardSearchItem[], { query?: string }>({
|
||||
query: ({ query }) => {
|
||||
const params = new URLSearchParams({ type: 'dash-db', limit: '1000', page: '1', sort: 'name_sort' });
|
||||
if (query) {
|
||||
params.set('query', query);
|
||||
}
|
||||
|
||||
return { url: `/api/search?${params.toString()}` };
|
||||
},
|
||||
}),
|
||||
dashboard: build.query<DashboardDTO, { uid: string }>({
|
||||
query: ({ uid }) => ({ url: `/api/dashboards/uid/${uid}` }),
|
||||
}),
|
||||
}),
|
||||
});
|
@ -0,0 +1,276 @@
|
||||
import { findByText, findByTitle, 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 { Provider } from 'react-redux';
|
||||
import { byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { DashboardDTO } from '../../../../../types';
|
||||
import { DashboardSearchItem, DashboardSearchItemType } from '../../../../search/types';
|
||||
import { mockStore } from '../../mocks';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { getDefaultFormValues } from '../../utils/rule-form';
|
||||
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import AnnotationsField from './AnnotationsField';
|
||||
|
||||
// To get anything displayed inside the Autosize component we need to mock it
|
||||
// Ref https://github.com/bvaughn/react-window/issues/454#issuecomment-646031139
|
||||
jest.mock(
|
||||
'react-virtualized-auto-sizer',
|
||||
() =>
|
||||
({ children }: { children: ({ height, width }: { height: number; width: number }) => JSX.Element }) =>
|
||||
children({ height: 500, width: 330 })
|
||||
);
|
||||
|
||||
const ui = {
|
||||
setDashboardButton: byRole('button', { name: 'Set dashboard and panel' }),
|
||||
annotationKeys: byTestId('annotation-key-', { exact: false }),
|
||||
annotationValues: byTestId('annotation-value-', { exact: false }),
|
||||
dashboardPicker: {
|
||||
dialog: byRole('dialog'),
|
||||
heading: byRole('heading', { name: 'Select dashboard and panel' }),
|
||||
confirmButton: byRole('button', { name: 'Confirm' }),
|
||||
},
|
||||
} as const;
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
function FormWrapper({ formValues }: { formValues?: Partial<RuleFormValues> }) {
|
||||
const store = mockStore(() => null);
|
||||
const formApi = useForm<RuleFormValues>({ defaultValues: { ...getDefaultFormValues(), ...formValues } });
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<FormProvider {...formApi}>
|
||||
<AnnotationsField />
|
||||
</FormProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('AnnotationsField', function () {
|
||||
it('should display default list of annotations', function () {
|
||||
render(<FormWrapper />);
|
||||
|
||||
const annotationElements = ui.annotationKeys.getAll();
|
||||
|
||||
expect(annotationElements).toHaveLength(3);
|
||||
expect(annotationElements[0]).toHaveTextContent('Summary');
|
||||
expect(annotationElements[1]).toHaveTextContent('Description');
|
||||
expect(annotationElements[2]).toHaveTextContent('Runbook URL');
|
||||
});
|
||||
|
||||
describe('Dashboard and panel picker', function () {
|
||||
it('should display dashboard and panel selector when select button clicked', async function () {
|
||||
mockSearchResponse([]);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<FormWrapper />);
|
||||
|
||||
await user.click(ui.setDashboardButton.get());
|
||||
|
||||
expect(ui.dashboardPicker.dialog.get()).toBeInTheDocument();
|
||||
expect(ui.dashboardPicker.heading.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should enable Confirm button only when dashboard and panel selected', async function () {
|
||||
mockSearchResponse([
|
||||
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' },
|
||||
{ id: 2, title: 'Second panel' },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<FormWrapper />);
|
||||
|
||||
await user.click(ui.setDashboardButton.get());
|
||||
expect(ui.dashboardPicker.confirmButton.get()).toBeDisabled();
|
||||
|
||||
await user.click(await findByTitle(ui.dashboardPicker.dialog.get(), 'My dashboard'));
|
||||
expect(ui.dashboardPicker.confirmButton.get()).toBeDisabled();
|
||||
|
||||
await user.click(await findByText(ui.dashboardPicker.dialog.get(), 'First panel'));
|
||||
expect(ui.dashboardPicker.confirmButton.get()).toBeEnabled();
|
||||
});
|
||||
|
||||
it('should add selected dashboard and panel as annotations', async function () {
|
||||
mockSearchResponse([
|
||||
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' },
|
||||
{ id: 2, title: 'Second panel' },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<FormWrapper formValues={{ annotations: [] }} />);
|
||||
|
||||
await user.click(ui.setDashboardButton.get());
|
||||
await user.click(await findByTitle(ui.dashboardPicker.dialog.get(), 'My dashboard'));
|
||||
|
||||
await user.click(await findByText(ui.dashboardPicker.dialog.get(), 'Second panel'));
|
||||
|
||||
await user.click(ui.dashboardPicker.confirmButton.get());
|
||||
|
||||
const annotationKeyElements = ui.annotationKeys.getAll();
|
||||
const annotationValueElements = ui.annotationValues.getAll();
|
||||
|
||||
expect(ui.dashboardPicker.dialog.query()).not.toBeInTheDocument();
|
||||
|
||||
expect(annotationKeyElements).toHaveLength(2);
|
||||
expect(annotationValueElements).toHaveLength(2);
|
||||
|
||||
expect(annotationKeyElements[0]).toHaveTextContent('Dashboard UID');
|
||||
expect(annotationValueElements[0]).toHaveTextContent('dash-test-uid');
|
||||
|
||||
expect(annotationKeyElements[1]).toHaveTextContent('Panel ID');
|
||||
expect(annotationValueElements[1]).toHaveTextContent('2');
|
||||
});
|
||||
|
||||
// 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 () {
|
||||
mockSearchResponse([
|
||||
mockDashboardSearchItem({ title: 'My dashboard', uid: 'dash-test-uid', type: DashboardSearchItemType.DashDB }),
|
||||
mockDashboardSearchItem({
|
||||
title: 'My other dashboard',
|
||||
uid: 'dash-other-uid',
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
}),
|
||||
]);
|
||||
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardDto({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-test-uid',
|
||||
panels: [
|
||||
{ id: 1, title: 'First panel' },
|
||||
{ id: 2, title: 'Second panel' },
|
||||
],
|
||||
})
|
||||
);
|
||||
mockGetDashboardResponse(
|
||||
mockDashboardDto({
|
||||
title: 'My other dashboard',
|
||||
uid: 'dash-other-uid',
|
||||
panels: [{ id: 3, title: 'Third panel' }],
|
||||
})
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<FormWrapper
|
||||
formValues={{
|
||||
annotations: [
|
||||
{ key: Annotation.dashboardUID, value: 'dash-test-uid' },
|
||||
{ key: Annotation.panelID, value: '1' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
let annotationValueElements = ui.annotationValues.getAll();
|
||||
expect(annotationValueElements[0]).toHaveTextContent('dash-test-uid');
|
||||
expect(annotationValueElements[1]).toHaveTextContent('1');
|
||||
|
||||
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());
|
||||
|
||||
expect(ui.dashboardPicker.dialog.query()).not.toBeInTheDocument();
|
||||
|
||||
const annotationKeyElements = ui.annotationKeys.getAll();
|
||||
annotationValueElements = ui.annotationValues.getAll();
|
||||
|
||||
expect(annotationKeyElements).toHaveLength(2);
|
||||
expect(annotationValueElements).toHaveLength(2);
|
||||
|
||||
expect(annotationKeyElements[0]).toHaveTextContent('Dashboard UID');
|
||||
expect(annotationValueElements[0]).toHaveTextContent('dash-other-uid');
|
||||
|
||||
expect(annotationKeyElements[1]).toHaveTextContent('Panel ID');
|
||||
expect(annotationValueElements[1]).toHaveTextContent('3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockSearchResponse(searchResult: DashboardSearchItem[]) {
|
||||
server.use(rest.get('/api/search', (req, res, ctx) => res(ctx.json<DashboardSearchItem[]>(searchResult))));
|
||||
}
|
||||
|
||||
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: [],
|
||||
isStarred: false,
|
||||
...searchItem,
|
||||
};
|
||||
}
|
||||
|
||||
function mockDashboardDto(dashboard: Partial<DashboardDTO['dashboard']>) {
|
||||
return {
|
||||
dashboard: {
|
||||
title: '',
|
||||
uid: '',
|
||||
templating: { list: [] },
|
||||
panels: [],
|
||||
...dashboard,
|
||||
},
|
||||
meta: {},
|
||||
};
|
||||
}
|
@ -1,21 +1,29 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import produce from 'immer';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Field, Input, InputControl, Label, TextArea, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
|
||||
import { AnnotationKeyInput } from './AnnotationKeyInput';
|
||||
import { DashboardPicker } from './DashboardPicker';
|
||||
|
||||
const AnnotationsField = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [showPanelSelector, setShowPanelSelector] = useToggle(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
watch,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useFormContext<RuleFormValues>();
|
||||
const annotations = watch('annotations');
|
||||
|
||||
@ -26,6 +34,31 @@ const AnnotationsField = () => {
|
||||
|
||||
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 setSelectedDashboardAndPanelId = (dashboardUid: string, panelId: string) => {
|
||||
const updatedAnnotations = produce(annotations, (draft) => {
|
||||
const dashboardAnnotation = draft.find((a) => a.key === Annotation.dashboardUID);
|
||||
const panelAnnotation = draft.find((a) => a.key === Annotation.panelID);
|
||||
|
||||
if (dashboardAnnotation) {
|
||||
dashboardAnnotation.value = dashboardUid;
|
||||
} else {
|
||||
draft.push({ key: Annotation.dashboardUID, value: dashboardUid });
|
||||
}
|
||||
|
||||
if (panelAnnotation) {
|
||||
panelAnnotation.value = panelId;
|
||||
} else {
|
||||
draft.push({ key: Annotation.panelID, value: panelId });
|
||||
}
|
||||
});
|
||||
|
||||
setValue('annotations', updatedAnnotations);
|
||||
setShowPanelSelector(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>Summary and annotations</Label>
|
||||
@ -81,17 +114,31 @@ const AnnotationsField = () => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className={styles.addAnnotationsButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
>
|
||||
Add info
|
||||
</Button>
|
||||
<Stack direction="row" gap={1}>
|
||||
<Button
|
||||
className={styles.addAnnotationsButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
>
|
||||
Add annotation
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" icon="dashboard" onClick={() => setShowPanelSelector(true)}>
|
||||
Set dashboard and panel
|
||||
</Button>
|
||||
</Stack>
|
||||
{showPanelSelector && (
|
||||
<DashboardPicker
|
||||
isOpen={true}
|
||||
dashboardUid={selectedDashboardUid}
|
||||
panelId={selectedPanelId}
|
||||
onChange={setSelectedDashboardAndPanelId}
|
||||
onDismiss={() => setShowPanelSelector(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,280 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { CSSProperties, useCallback, useMemo, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
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 { dashboardApi } from '../../api/dashboardApi';
|
||||
|
||||
export interface PanelDTO {
|
||||
id: number;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
function panelSort(a: PanelDTO, b: PanelDTO) {
|
||||
if (a.title && b.title) {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
if (a.title && !b.title) {
|
||||
return 1;
|
||||
} else if (!a.title && b.title) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface DashboardPickerProps {
|
||||
isOpen: boolean;
|
||||
dashboardUid?: string | undefined;
|
||||
panelId?: string | undefined;
|
||||
onChange: (dashboardUid: string, panelId: string) => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const DashboardPicker = ({ dashboardUid, panelId, isOpen, onChange, onDismiss }: DashboardPickerProps) => {
|
||||
const styles = useStyles2(getPickerStyles);
|
||||
|
||||
const [selectedDashboardUid, setSelectedDashboardUid] = useState(dashboardUid);
|
||||
const [selectedPanelId, setSelectedPanelId] = useState(panelId);
|
||||
|
||||
const [dashboardFilter, setDashboardFilter] = useState('');
|
||||
const [debouncedDashboardFilter, setDebouncedDashboardFilter] = useState('');
|
||||
|
||||
const [panelFilter, setPanelFilter] = useState('');
|
||||
const { useSearchQuery, useDashboardQuery } = dashboardApi;
|
||||
|
||||
const { currentData: filteredDashboards = [], isFetching: isDashSearchFetching } = useSearchQuery({
|
||||
query: debouncedDashboardFilter,
|
||||
});
|
||||
const { currentData: dashboardResult, isFetching: isDashboardFetching } = useDashboardQuery(
|
||||
{ uid: selectedDashboardUid ?? '' },
|
||||
{ skip: !selectedDashboardUid }
|
||||
);
|
||||
|
||||
const handleDashboardChange = useCallback((dashboardUid: string) => {
|
||||
setSelectedDashboardUid(dashboardUid);
|
||||
setSelectedPanelId(undefined);
|
||||
}, []);
|
||||
|
||||
const filteredPanels =
|
||||
dashboardResult?.dashboard?.panels
|
||||
?.filter((panel): panel is PanelDTO => typeof panel.id === 'number')
|
||||
?.filter((panel) => panel.title?.toLowerCase().includes(panelFilter.toLowerCase()))
|
||||
.sort(panelSort) ?? [];
|
||||
|
||||
const currentPanel = dashboardResult?.dashboard?.panels?.find((panel) => panel.id.toString() === selectedPanelId);
|
||||
|
||||
const selectedDashboardIndex = useMemo(() => {
|
||||
return filteredDashboards.map((dashboard) => dashboard.uid).indexOf(selectedDashboardUid ?? '');
|
||||
}, [filteredDashboards, selectedDashboardUid]);
|
||||
|
||||
const isDefaultSelection = dashboardUid && dashboardUid === selectedDashboardUid;
|
||||
const selectedDashboardIsInPageResult = selectedDashboardIndex >= 0;
|
||||
|
||||
const scrollToItem = useCallback(
|
||||
(node) => {
|
||||
const canScroll = selectedDashboardIndex >= 0;
|
||||
|
||||
if (isDefaultSelection && canScroll) {
|
||||
node?.scrollToItem(selectedDashboardIndex, 'smart');
|
||||
}
|
||||
},
|
||||
[isDefaultSelection, selectedDashboardIndex]
|
||||
);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
setDebouncedDashboardFilter(dashboardFilter);
|
||||
},
|
||||
500,
|
||||
[dashboardFilter]
|
||||
);
|
||||
|
||||
const DashboardRow = ({ index, style }: { index: number; style?: CSSProperties }) => {
|
||||
const dashboard = filteredDashboards[index];
|
||||
const isSelected = selectedDashboardUid === dashboard.uid;
|
||||
|
||||
return (
|
||||
<div
|
||||
title={dashboard.title}
|
||||
style={style}
|
||||
className={cx(styles.row, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
|
||||
onClick={() => handleDashboardChange(dashboard.uid)}
|
||||
>
|
||||
<div className={styles.dashboardTitle}>{dashboard.title}</div>
|
||||
<div className={styles.dashboardFolder}>
|
||||
<Icon name="folder" /> {dashboard.folderTitle ?? 'General'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => {
|
||||
const panel = filteredPanels[index];
|
||||
const isSelected = selectedPanelId === panel.id.toString();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={cx(styles.row, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
|
||||
onClick={() => setSelectedPanelId(panel.id.toString())}
|
||||
>
|
||||
{panel.title || '<No title>'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Select dashboard and panel"
|
||||
closeOnEscape
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
className={styles.modal}
|
||||
contentClassName={styles.modalContent}
|
||||
>
|
||||
{/* This alert shows if the selected dashboard is not found in the first page of dashboards */}
|
||||
{!selectedDashboardIsInPageResult && dashboardUid && (
|
||||
<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'}
|
||||
</div>
|
||||
{Boolean(currentPanel) && (
|
||||
<div>
|
||||
Panel: {currentPanel.title} ({currentPanel.id})
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
<div className={styles.container}>
|
||||
<FilterInput
|
||||
value={dashboardFilter}
|
||||
onChange={setDashboardFilter}
|
||||
title="Search dashboard"
|
||||
placeholder="Search dashboard"
|
||||
autoFocus
|
||||
/>
|
||||
<FilterInput value={panelFilter} onChange={setPanelFilter} title="Search panel" placeholder="Search panel" />
|
||||
|
||||
<div className={styles.column}>
|
||||
{isDashSearchFetching && (
|
||||
<LoadingPlaceholder text="Loading dashboards..." className={styles.loadingPlaceholder} />
|
||||
)}
|
||||
|
||||
{!isDashSearchFetching && (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
ref={scrollToItem}
|
||||
itemSize={50}
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={filteredDashboards.length}
|
||||
>
|
||||
{DashboardRow}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.column}>
|
||||
{!dashboardUid && !isDashboardFetching && <div>Select a dashboard to get a list of available panels</div>}
|
||||
{isDashboardFetching && (
|
||||
<LoadingPlaceholder text="Loading dashboard..." className={styles.loadingPlaceholder} />
|
||||
)}
|
||||
|
||||
{!isDashboardFetching && (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<FixedSizeList itemSize={32} height={height} width={width} itemCount={filteredPanels.length}>
|
||||
{PanelRow}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Modal.ButtonRow>
|
||||
<Button type="button" variant="secondary" onClick={onDismiss}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={!(selectedDashboardUid && selectedPanelId)}
|
||||
onClick={() => {
|
||||
if (selectedDashboardUid && selectedPanelId) {
|
||||
onChange(selectedDashboardUid, selectedPanelId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
`,
|
||||
});
|
@ -18,7 +18,7 @@ export function DetailsStep() {
|
||||
<RuleEditorSection
|
||||
stepNo={type === RuleFormType.cloudRecording ? 3 : 4}
|
||||
title={
|
||||
type === RuleFormType.cloudRecording ? 'Add details for your recording rule' : 'Add details for your alert'
|
||||
type === RuleFormType.cloudRecording ? 'Add details for your recording rule' : 'Add details for your alert rule'
|
||||
}
|
||||
description={
|
||||
type === RuleFormType.cloudRecording
|
||||
|
@ -6,7 +6,7 @@ import { Provider } from 'react-redux';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import { DataSourceJsonData, DataSourceSettings } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
@ -17,12 +17,8 @@ import { useExternalDataSourceAlertmanagers } from './useExternalAmSelector';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user