import React from 'react'; import { VList, VListHandle } from 'virtua'; import cn from 'classnames'; import { Duration } from 'luxon'; import { NoContent, Button } from 'UI'; import { percentOf } from 'App/utils'; import BarRow from './BarRow'; import stl from './timeTable.module.css'; import autoscrollStl from '../autoscroll.module.css'; import JumpButton from '../JumpButton'; import { observer } from 'mobx-react-lite'; type Timed = { time: number; }; type Durationed = { duration: number; }; type CanBeRed = { //+isRed: boolean, isRed: boolean; }; interface Row extends Timed, Durationed, CanBeRed { [key: string]: any; key: string; } type Line = { color: string; // Maybe use typescript? hint?: string; onClick?: any; } & Timed; type Column = { label: string; width: number; dataKey?: string; render?: (row: any) => void; referenceLines?: Array; style?: React.CSSProperties; onClick?: void; } & RenderOrKey; type RenderOrKey = | { render?: (row: Row) => React.ReactNode; key?: string; } | { dataKey: string; }; type Props = { className?: string; rows: Array; children: Array; tableHeight?: number; activeIndex?: number; renderPopup?: boolean; navigation?: boolean; referenceLines?: any[]; additionalHeight?: number; hoverable?: boolean; onRowClick?: (row: any, index: number) => void; onJump?: (obj: { time: number }) => void; }; type TimeLineInfo = { timestart: number; timewidth: number; }; let _additionalHeight = 0; const ROW_HEIGHT = 24; const TIME_SECTIONS_COUNT = 8; const ZERO_TIMEWIDTH = 1000; function formatTime(ms: number) { if (ms < 0) return ''; if (ms < 1000) return Duration.fromMillis(ms).toFormat('0.SSS'); return Duration.fromMillis(ms).toFormat('mm:ss'); } function computeTimeLine( rows: Array, firstVisibleRowIndex: number, visibleCount: number ): TimeLineInfo { const visibleRows = rows.slice( firstVisibleRowIndex, firstVisibleRowIndex + visibleCount + _additionalHeight ); let timestart = visibleRows.length > 0 ? Math.min(...visibleRows.map((r) => r.time)) : 0; // TODO: GraphQL requests do not have a duration, so their timeline is borked. Assume a duration of 0.2s for every GraphQL request const timeend = visibleRows.length > 0 ? Math.max(...visibleRows.map((r) => r.time + (r.duration ?? 200))) : 0; let timewidth = timeend - timestart; const offset = timewidth / 70; if (timestart >= offset) { timestart -= offset; } timewidth *= 1.5; // += offset; if (timewidth === 0) { timewidth = ZERO_TIMEWIDTH; } return { timestart, timewidth, }; } function TimeTable(props: Props) { const tableHeight = props.tableHeight || 195; const visibleCount = Math.ceil(tableHeight / ROW_HEIGHT); const [timerange, setTimerange] = React.useState({ timestart: 0, timewidth: 0, }); const [firstVisibleRowIndex, setFirstVisibleRowIndex] = React.useState(0); const scroller = React.createRef(); const { timestart, timewidth } = timerange; React.useEffect(() => { const { timestart, timewidth } = computeTimeLine( props.rows, firstVisibleRowIndex, visibleCount ); setTimerange({ timestart, timewidth }); }, [ props.rows.length, visibleCount, _additionalHeight, firstVisibleRowIndex, ]); React.useEffect(() => { if (props.activeIndex && props.activeIndex >= 0 && scroller.current) { scroller.current.scrollToIndex(props.activeIndex); setFirstVisibleRowIndex(props.activeIndex ?? 0); } }, [props.activeIndex]); const onJump = (index: any) => { if (props.onJump) { props.onJump(props.rows[index]); } }; const onPrevClick = () => { let prevRedIndex = -1; for (let i = firstVisibleRowIndex - 1; i >= 0; i--) { if (props.rows[i].isRed) { prevRedIndex = i; break; } } if (scroller.current != null) { scroller.current.scrollToIndex(prevRedIndex); } }; const onNextClick = () => { let prevRedIndex = -1; for (let i = firstVisibleRowIndex + 1; i < props.rows.length; i++) { if (props.rows[i].isRed) { prevRedIndex = i; break; } } if (scroller.current != null) { scroller.current.scrollToIndex(prevRedIndex); } }; const { className, rows, navigation = false, referenceLines = [], additionalHeight = 0, renderPopup, hoverable, onRowClick, activeIndex, } = props; const columns = props.children.filter((i: any) => !i.hidden); _additionalHeight = additionalHeight; const sectionDuration = Math.round(timewidth / TIME_SECTIONS_COUNT); const timeColumns: number[] = []; if (timewidth > 0) { for (let i = 0; i < TIME_SECTIONS_COUNT; i++) { timeColumns.push(timestart + i * sectionDuration); } } const visibleRefLines = referenceLines.filter( ({ time }) => time > timestart && time < timestart + timewidth ); const columnsSumWidth = columns.reduce((sum, { width }) => sum + width, 0); return (
{navigation && (
)}
{columns.map(({ label, width, dataKey, onClick = null }) => (
onColumnClick(dataKey, onClick)} > {label}
))}
{timeColumns.map((time, i) => (
{formatTime(time)}
))}
{timeColumns.map((_, index) => (
))} {visibleRefLines.map(({ time, color, onClick }) => (
))}
{ const firstVisibleRowIndex = Math.floor( offset / ROW_HEIGHT + 0.33 ); setFirstVisibleRowIndex(firstVisibleRowIndex); }} > {(index) => ( )}
); } function RowRenderer({ index, row, columns, timestart, timewidth, renderPopup, hoverable, onRowClick, activeIndex, onJump, }: any) { return (
activeIndex, } )} onClick={ typeof onRowClick === 'function' ? () => onRowClick(row, index) : undefined } id="table-row" >
onJump(index)} />
); } const RowColumns = React.memo(({ columns, row }: any) => { return columns.map(({ dataKey, render, width, label }: any) => (
{render ? render(row) : row[dataKey || ''] || ( {'empty'} )}
)) }) export default observer(TimeTable);