go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/timeline/side_panel.tsx (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { Box, styled, useTheme } from '@mui/material'; 16 import { axisLeft, select } from 'd3'; 17 import { ReactNode, forwardRef, useEffect, useRef } from 'react'; 18 import { Virtuoso } from 'react-virtuoso'; 19 20 import { useTimelineConfig } from './context'; 21 22 interface OptionalChildrenProps { 23 readonly children?: ReactNode; 24 } 25 26 function SidePanelItem({ ...props }: OptionalChildrenProps) { 27 return <g {...props} />; 28 } 29 30 const SidePanelSvg = forwardRef<HTMLDivElement, OptionalChildrenProps>( 31 function SidePanelSvg({ children, ...props }, _ref) { 32 const config = useTimelineConfig(); 33 34 const gridLineElement = useRef<SVGGElement | null>(null); 35 useEffect(() => { 36 const horizontalGridLines = axisLeft(config.yScale) 37 .ticks(config.itemCount) 38 .tickFormat(() => '') 39 .tickSize(-config.sidePanelWidth) 40 .tickFormat(() => ''); 41 horizontalGridLines(select(gridLineElement.current!)); 42 }, [config.yScale, config.itemCount, config.sidePanelWidth]); 43 const height = config.itemHeight * config.itemCount; 44 45 return ( 46 <svg {...props} height={height} width={config.sidePanelWidth}> 47 <g 48 ref={gridLineElement} 49 transform={`translate(0.5, 0.5)`} 50 css={{ '& line': { stroke: 'var(--divider-color)' } }} 51 /> 52 {children} 53 <path 54 d={`m0.5,-1v${height + 2}m${config.sidePanelWidth - 1},0v${ 55 -height - 2 56 }`} 57 stroke="currentcolor" 58 /> 59 </svg> 60 ); 61 }, 62 ); 63 64 const Container = styled(Box)` 65 grid-area: side-panel; 66 position: sticky; 67 left: 0; 68 z-index: 1; 69 `; 70 71 export interface SidePanelProps { 72 readonly content: (index: number) => ReactNode; 73 } 74 75 export function SidePanel({ content }: SidePanelProps) { 76 const theme = useTheme(); 77 const config = useTimelineConfig(); 78 79 return ( 80 <Container 81 sx={{ 82 backgroundColor: theme.palette.background.default, 83 width: config.sidePanelWidth, 84 }} 85 > 86 <Virtuoso 87 useWindowScroll 88 components={{ Item: SidePanelItem, List: SidePanelSvg }} 89 totalCount={config.itemCount} 90 fixedItemHeight={config.itemHeight} 91 itemContent={(index) => { 92 return ( 93 <g transform={`translate(0, ${(index + 0.5) * config.itemHeight})`}> 94 {content(index)} 95 </g> 96 ); 97 }} 98 /> 99 </Container> 100 ); 101 }