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  }