import { css } from '@emotion/css'; import React, { useCallback, useRef, useState } from 'react'; import SVG from 'react-inlinesvg'; import { usePopper } from 'react-popper'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Icon, Portal, TagList, useTheme2 } from '@grafana/ui'; import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSectionItem, OnToggleChecked } from '../types'; import { SearchCardExpanded } from './SearchCardExpanded'; import { SearchCheckbox } from './SearchCheckbox'; const DELAY_BEFORE_EXPANDING = 500; export interface Props { editable?: boolean; item: DashboardSectionItem; onTagSelected?: (name: string) => any; onToggleChecked?: OnToggleChecked; } export function getThumbnailURL(uid: string, isLight?: boolean) { return `/api/dashboards/uid/${uid}/img/thumb/${isLight ? 'light' : 'dark'}`; } export function SearchCard({ editable, item, onTagSelected, onToggleChecked }: Props) { const [hasImage, setHasImage] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [showExpandedView, setShowExpandedView] = useState(false); const timeout = useRef(null); // Popper specific logic const offsetCallback = useCallback(({ placement, reference, popper }) => { let result: [number, number] = [0, 0]; if (placement === 'bottom' || placement === 'top') { result = [0, -(reference.height + popper.height) / 2]; } else if (placement === 'left' || placement === 'right') { result = [-(reference.width + popper.width) / 2, 0]; } return result; }, []); const [markerElement, setMarkerElement] = React.useState(null); const [popperElement, setPopperElement] = React.useState(null); const { styles: popperStyles, attributes } = usePopper(markerElement, popperElement, { modifiers: [ { name: 'offset', options: { offset: offsetCallback, }, }, ], }); const theme = useTheme2(); const imageSrc = getThumbnailURL(item.uid!, theme.isLight); const styles = getStyles( theme, markerElement?.getBoundingClientRect().width, popperElement?.getBoundingClientRect().width ); const onShowExpandedView = async () => { setShowExpandedView(true); if (item.uid && !lastUpdated) { const dashboard = await backendSrv.getDashboardByUid(item.uid); const { updated } = dashboard.meta; if (updated) { setLastUpdated(new Date(updated).toLocaleString()); } else { setLastUpdated(null); } } }; const onMouseEnter = () => { timeout.current = window.setTimeout(onShowExpandedView, DELAY_BEFORE_EXPANDING); }; const onMouseMove = () => { if (timeout.current) { window.clearTimeout(timeout.current); } timeout.current = window.setTimeout(onShowExpandedView, DELAY_BEFORE_EXPANDING); }; const onMouseLeave = () => { if (timeout.current) { window.clearTimeout(timeout.current); } setShowExpandedView(false); }; const onCheckboxClick = (ev: React.MouseEvent) => { ev.stopPropagation(); ev.preventDefault(); onToggleChecked?.(item); }; const onTagClick = (tag: string, ev: React.MouseEvent) => { ev.stopPropagation(); ev.preventDefault(); onTagSelected?.(tag); }; return ( setMarkerElement(ref as unknown as HTMLDivElement)} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onMouseMove={onMouseMove} >
{hasImage ? ( setHasImage(false)} /> ) : (
{item.icon ? ( ) : ( )}
)}
{item.title}
{showExpandedView && (
)}
); } const getStyles = (theme: GrafanaTheme2, markerWidth = 0, popperWidth = 0) => { const IMAGE_HORIZONTAL_MARGIN = theme.spacing(4); return { card: css` background-color: ${theme.colors.background.secondary}; border: 1px solid ${theme.colors.border.medium}; border-radius: 4px; display: flex; flex-direction: column; &:hover { background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)}; } `, checkbox: css` left: 0; margin: ${theme.spacing(1)}; position: absolute; top: 0; `, expandedView: css` @keyframes expand { 0% { transform: scale(${markerWidth / popperWidth}); } 100% { transform: scale(1); } } animation: expand ${theme.transitions.duration.shortest}ms ease-in-out 0s 1 normal; background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)}; `, image: css` aspect-ratio: 4 / 3; box-shadow: ${theme.shadows.z1}; margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0; width: calc(100% - (2 * ${IMAGE_HORIZONTAL_MARGIN})); `, imageContainer: css` flex: 1; position: relative; &:after { background: linear-gradient(180deg, rgba(196, 196, 196, 0) 0%, rgba(127, 127, 127, 0.25) 100%); bottom: 0; content: ''; left: 0; margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0; position: absolute; right: 0; top: 0; } `, imagePlaceholder: css` align-items: center; aspect-ratio: 4 / 3; color: ${theme.colors.text.secondary}; display: flex; justify-content: center; margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0; width: calc(100% - (2 * ${IMAGE_HORIZONTAL_MARGIN})); `, info: css` align-items: center; background-color: ${theme.colors.background.canvas}; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; display: flex; height: ${theme.spacing(7)}; gap: ${theme.spacing(1)}; padding: 0 ${theme.spacing(2)}; z-index: 1; `, portal: css` pointer-events: none; `, title: css` display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `, }; };