import React, { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom' import FlipMove from 'react-flip-move'; import { displayMetricValue, metricLabelFor } from './metrics'; import FadeIn from '../../fade-in' import MoreLink from '../more-link' import Bar from '../bar' import LazyLoader from '../../components/lazy-loader' import classNames from 'classnames' const MAX_ITEMS = 9 const MIN_HEIGHT = 380 const ROW_HEIGHT = 32 const ROW_GAP_HEIGHT = 4 const DATA_CONTAINER_HEIGHT = (ROW_HEIGHT + ROW_GAP_HEIGHT) * (MAX_ITEMS - 1) + ROW_HEIGHT const COL_MIN_WIDTH = 70 function FilterLink({filterQuery, onClick, children}) { const className = classNames('max-w-max w-full flex md:overflow-hidden', { 'hover:underline': !!filterQuery }) if (filterQuery) { return ( { children } ) } else { return { children } } } function ExternalLink({item, externalLinkDest}) { const dest = externalLinkDest && externalLinkDest(item) if (dest) { return ( ) } return null } // The main function component for rendering list reports and making them react to what // is happening on the dashboard. // A `fetchData` function must be passed through props. This function defines the format // of the data, which is expected to be a list of objects. Think of these objects as rows // with keys being columns. The number of columns is dynamic and should be configured // via the `metrics` input list. For example: // | keyLabel | METRIC_1.label | METRIC_2.label | ... // |--------------------|---------------------------|---------------------------|----- // | LISTITEM_1.name | LISTITEM_1[METRIC_1.name] | LISTITEM_1[METRIC_2.name] | ... // | LISTITEM_2.name | LISTITEM_2[METRIC_1.name] | LISTITEM_2[METRIC_2.name] | ... // Further configuration of the report is possible through optional props. // REQUIRED PROPS: // * `keyLabel` - What each entry in the list represents (for UI only). // * `query` - The query object representing the current state of the dashboard. // * `fetchData` - a function that returns an `api.get` promise that will resolve to the // list of data. // * `metrics` - a list of `metric` objects. Each `metric` object is required to have at // least the `name` and the `label` keys. If the metric should have a different label // in realtime or goal-filtered views, we'll use `realtimeLabel` and `GoalFilterLabel`. // * `getFilterFor` - a function that takes a list item and returns the query link (with // the filter) to navigate to when this list item is clicked. If a list item is not // supposed to be clickable, this function should return `null` for that list item. // OPTIONAL PROPS: // * `onClick` - function with additional action to be taken when a list entry is clicked. // * `detailsLink` - the pathname to the detailed view of this report. E.g.: // `/dummy.site/pages` // * `externalLinkDest` - a function that takes a list item and returns an external url // to navigate to. If this prop is given, an additional icon is rendered upon hovering // the entry. // * `renderIcon` - a function that takes a list item and returns the // HTML of an icon (such as a flag, favicon, or a screen size icon) for a listItem. // * `color` - color of the comparison bars in light-mode export default function ListReport(props) { const [state, setState] = useState({loading: true, list: null}) const [visible, setVisible] = useState(false) const metrics = props.metrics const colMinWidth = props.colMinWidth || COL_MIN_WIDTH const isRealtime = props.query.period === 'realtime' const goalFilterApplied = !!props.query.filters.goal const fetchData = useCallback(() => { if (!isRealtime) { setState({loading: true, list: null}) } props.fetchData() .then((res) => setState({loading: false, list: res})) }, [props.keyLabel, props.query]) const onVisible = () => { setVisible(true) } useEffect(() => { if (isRealtime) { // When a goal filter is applied or removed, we always want the component to go into a // loading state, even in realtime mode, because the metrics list will change. We can // only read the new metrics once the new list is loaded. setState({loading: true, list: null}) } }, [goalFilterApplied]); useEffect(() => { if (visible) { if (isRealtime) { document.addEventListener('tick', fetchData) } fetchData() } return () => { document.removeEventListener('tick', fetchData) } }, [props.keyLabel, props.query, visible]); function renderReport() { if (state.list && state.list.length > 0) { return (