import { css, cx } from '@emotion/css'; import { noop } from 'lodash'; import { 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 { Alert, Button, clearButtonStyles, FilterInput, Icon, LoadingPlaceholder, Modal, Tooltip, useStyles2, } from '@grafana/ui'; 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) { 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; panelId?: number; onChange: (dashboardUid: string, panelId: number) => 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 } = dashboardApi; const { currentData: filteredDashboards = [], isFetching: isDashSearchFetching } = useSearchQuery({ query: debouncedDashboardFilter, }); const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid); const handleDashboardChange = useCallback((dashboardUid: string) => { setSelectedDashboardUid(dashboardUid); setSelectedPanelId(undefined); }, []); const allDashboardPanels = getVisualPanels(dashboardModel); const filteredPanels = allDashboardPanels .filter((panel) => panel.title?.toLowerCase().includes(panelFilter.toLowerCase())) .sort(panelSort) ?? []; const currentPanel: PanelDTO | undefined = allDashboardPanels.find( (panel: PanelDTO) => isValidPanel(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: FixedSizeList) => { 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 ( ); }; const PanelRow = ({ index, style }: { index: number; style: CSSProperties }) => { const panel = filteredPanels[index]; const panelTitle = panel.title || ''; const isSelected = Boolean(panel.id) && selectedPanelId === panel.id; const isAlertingCompatible = panel.type === 'graph' || panel.type === 'timeseries'; const disabled = !isValidPanel(panel); return ( ); }; return ( {/* This alert shows if the selected dashboard is not found in the first page of dashboards */} {!selectedDashboardIsInPageResult && dashboardUid && dashboardModel && (
Dashboard: {dashboardModel.title} ({dashboardModel.uid}) in folder{' '} {dashboardModel.meta?.folderTitle ?? 'Dashboards'}
{currentPanel && (
Panel: {currentPanel.title} ({currentPanel.id})
)}
)}
{isDashSearchFetching && ( )} {!isDashSearchFetching && ( {({ height, width }) => ( {DashboardRow} )} )}
{!selectedDashboardUid && !isDashboardFetching && (
Select a dashboard to get a list of available panels
)} {isDashboardFetching && ( )} {selectedDashboardUid && !isDashboardFetching && ( {({ width, height }) => ( {PanelRow} )} )}
); }; 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 isValidPanel = (panel: PanelDTO): boolean => { const hasValidID = typeof panel.id === 'number'; const isValidPanelType = typeof panel.type === 'string'; const isLibraryPanel = 'libraryPanel' in panel; return hasValidID && (isValidPanelType || isLibraryPanel); }; const getPickerStyles = (theme: GrafanaTheme2) => { const clearButton = clearButtonStyles(theme); return { container: css({ display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: 'min-content auto', gap: theme.spacing(2), flex: 1, }), column: css({ flex: '1 1 auto', }), dashboardTitle: css({ height: '22px', fontWeight: theme.typography.fontWeightBold, }), dashboardFolder: css({ height: '20px', fontSize: theme.typography.bodySmall.fontSize, color: theme.colors.text.secondary, display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', columnGap: theme.spacing(1), alignItems: 'center', }), rowButton: css(clearButton, { padding: theme.spacing(0.5), overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'left', whiteSpace: 'nowrap', cursor: 'pointer', border: '2px solid transparent', '&:disabled': { cursor: 'not-allowed', color: theme.colors.text.disabled, }, }), rowButtonTitle: css({ textOverflow: 'ellipsis', overflow: 'hidden', }), rowSelected: css({ borderColor: theme.colors.primary.border, }), rowOdd: css({ backgroundColor: theme.colors.background.secondary, }), panelButton: css({ display: 'flex', gap: theme.spacing(1), justifyContent: 'space-between', alignItems: 'center', }), loadingPlaceholder: css({ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', }), selectDashboardPlaceholder: css({ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', textAlign: 'center', fontWeight: theme.typography.fontWeightBold, }), modal: css({ height: '100%', }), modalContent: css({ flex: 1, display: 'flex', flexDirection: 'column', }), modalAlert: css({ flexGrow: 0, }), warnIcon: css({ fill: theme.colors.warning.main, }), }; };