mirror of https://github.com/grafana/grafana
Alerting: Add dashboard and panel picker to the rule form (#58304)
Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>pull/59145/head
parent
245a59548c
commit
cae5d89d0f
@ -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: {}, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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; |
||||||
|
`,
|
||||||
|
}); |
||||||
Loading…
Reference in new issue