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' import { trimURL } from '../../util/url' 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`. If this is given as input to the ListReport, the Details button // will always be rendered. // * `maybeHideDetails` - set this to `true` if the details button should be hidden on // the condition that there are less than MAX_ITEMS entries in the list. (i.e . nothing // more to show) // * `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]); // returns a filtered `metrics` list. Since currently, the backend can return different // metrics based on filters and existing data, this function validates that the metrics // we want to display are actually there in the API response. function getAvailableMetrics() { return metrics.filter((metric) => { return state.list.some((listItem) => listItem[metric.name] != null) }) } function hiddenOnMobileClass(metric) { if (metric.hiddenOnMobile) { return 'hidden md:block' } else { return '' } } function renderReport() { if (state.list && state.list.length > 0) { return (