github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/SideTimelineComparator/index.tsx (about)

     1  import React, { useRef, useState } from 'react';
     2  import classNames from 'classnames/bind';
     3  import Button from '@webapp/ui/Button';
     4  import { Popover, PopoverBody } from '@webapp/ui/Popover';
     5  import { Portal } from '@webapp/ui/Portal';
     6  import { faChevronDown } from '@fortawesome/free-solid-svg-icons/faChevronDown';
     7  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
     8  import { Selection } from '@webapp/components/TimelineChart/markings';
     9  import { getSelectionBoundaries } from '@webapp/components/TimelineChart/SyncTimelines/getSelectionBoundaries';
    10  import { comparisonPeriods } from './periods';
    11  import styles from './styles.module.scss';
    12  
    13  const cx = classNames.bind(styles);
    14  
    15  type Boudaries = {
    16    from: string;
    17    until: string;
    18    leftFrom: string;
    19    leftTo: string;
    20    rightFrom: string;
    21    rightTo: string;
    22  };
    23  
    24  interface SideTimelineComparatorProps {
    25    onCompare: (params: Boudaries) => void;
    26    selection: {
    27      left: Selection;
    28      right: Selection;
    29      from: string;
    30      until: string;
    31    };
    32    comparisonMode: {
    33      active: boolean;
    34      period: {
    35        label: string;
    36        ms: number;
    37      };
    38    };
    39    setComparisonMode: (
    40      params: SideTimelineComparatorProps['comparisonMode']
    41    ) => void;
    42  }
    43  
    44  const getNewBoundaries = ({
    45    selection,
    46    period,
    47  }: {
    48    selection: SideTimelineComparatorProps['selection'];
    49    period: SideTimelineComparatorProps['comparisonMode']['period'];
    50  }) => {
    51    const { from: comparisonSelectionFrom, to: comparisonSelectionTo } =
    52      getSelectionBoundaries(selection.right);
    53  
    54    const diff = comparisonSelectionTo - comparisonSelectionFrom;
    55  
    56    return {
    57      from: String(comparisonSelectionTo - period.ms - diff * 2),
    58      until: String(comparisonSelectionTo),
    59      leftFrom: String(comparisonSelectionTo - period.ms - diff),
    60      leftTo: String(comparisonSelectionTo - period.ms),
    61      rightFrom: String(comparisonSelectionFrom),
    62      rightTo: String(comparisonSelectionTo),
    63    };
    64  };
    65  
    66  export default function SideTimelineComparator({
    67    onCompare,
    68    selection,
    69    setComparisonMode,
    70    comparisonMode,
    71  }: SideTimelineComparatorProps) {
    72    const [previousSelection, setPreviousSelection] = useState<Boudaries | null>(
    73      null
    74    );
    75    const refContainer = useRef(null);
    76    const [menuVisible, setMenuVisible] = useState(false);
    77  
    78    const { active, period } = comparisonMode;
    79  
    80    const { from: comparisonSelectionFrom, to: comparisonSelectionTo } =
    81      getSelectionBoundaries(selection.right);
    82  
    83    const diff = comparisonSelectionTo - comparisonSelectionFrom;
    84  
    85    const fullLength =
    86      comparisonSelectionTo - (comparisonSelectionTo - period.ms - diff * 2);
    87  
    88    const percent = fullLength ? (diff / fullLength) * 100 : null;
    89  
    90    const handleSelectPeriod = (period: { label: string; ms: number }) => {
    91      setComparisonMode({
    92        ...comparisonMode,
    93        period,
    94      });
    95  
    96      if (comparisonMode.active) {
    97        const newBoundaries = getNewBoundaries({ period, selection });
    98  
    99        onCompare(newBoundaries);
   100      }
   101    };
   102  
   103    const hanleToggleComparison = (e: React.ChangeEvent<HTMLInputElement>) => {
   104      const active = e.target.checked;
   105  
   106      if (active) {
   107        setPreviousSelection({
   108          from: selection.from,
   109          until: selection.until,
   110          leftFrom: selection.left.from,
   111          leftTo: selection.left.to,
   112          rightFrom: selection.right.from,
   113          rightTo: selection.right.to,
   114        });
   115  
   116        const newBoundaries = getNewBoundaries({ period, selection });
   117  
   118        onCompare(newBoundaries);
   119      } else if (previousSelection) {
   120        onCompare(previousSelection);
   121      }
   122  
   123      setComparisonMode({
   124        ...comparisonMode,
   125        active,
   126      });
   127    };
   128  
   129    const preview = percent ? (
   130      <div className={styles.preview}>
   131        <div className={styles.timeline}>
   132          <div className={styles.timelineBox}>
   133            <div
   134              className={styles.selection}
   135              style={{
   136                width: `${percent}%`,
   137                backgroundColor: selection.left.overlayColor.toString(),
   138                left: `${percent}%`,
   139              }}
   140            />
   141            <div
   142              style={{
   143                width: `${percent}%`,
   144                backgroundColor: selection.right.overlayColor.toString(),
   145                right: 0,
   146              }}
   147              className={styles.selection}
   148            />
   149          </div>
   150        </div>
   151        <div
   152          style={{ left: `${percent}%`, right: `${percent}%` }}
   153          className={styles.legend}
   154        >
   155          <div className={styles.legendLine} />
   156          <div className={styles.legendCaption}>{period.label}</div>
   157        </div>
   158      </div>
   159    ) : (
   160      <div>Please set the period</div>
   161    );
   162  
   163    return (
   164      <div className={styles.wrapper} ref={refContainer}>
   165        <input
   166          onChange={hanleToggleComparison}
   167          checked={active}
   168          type="checkbox"
   169          className={styles.toggleCompare}
   170        />
   171        <Button
   172          data-testid="open-comparator-button"
   173          onClick={() => setMenuVisible(!menuVisible)}
   174        >
   175          {period.label}
   176          <FontAwesomeIcon
   177            className={styles.openButtonIcon}
   178            icon={faChevronDown}
   179          />
   180        </Button>
   181        <span className={styles.caption}>&nbsp;&nbsp;to comparison</span>
   182        <Portal container={refContainer.current}>
   183          <Popover
   184            anchorPoint={{ x: 'calc(100% - 350px)', y: 42 }}
   185            isModalOpen
   186            setModalOpenStatus={() => setMenuVisible(false)}
   187            className={cx({ [styles.menu]: true, [styles.hidden]: !menuVisible })}
   188          >
   189            {menuVisible ? (
   190              <>
   191                <PopoverBody className={styles.body}>
   192                  <div className={styles.subtitle}>
   193                    Set baseline&nbsp;
   194                    <span className={styles.periodLabel}>{period.label}</span>
   195                    &nbsp;to comparison
   196                  </div>
   197                  <div className={styles.buttons}>
   198                    {comparisonPeriods.map((arr, i) => {
   199                      return (
   200                        <div
   201                          key={`preset-${i + 1}`}
   202                          className={styles.buttonsCol}
   203                        >
   204                          {arr.map((b) => {
   205                            return (
   206                              <Button
   207                                kind={
   208                                  period.label === b.label
   209                                    ? 'secondary'
   210                                    : 'default'
   211                                }
   212                                disabled={diff > b.ms}
   213                                key={b.label}
   214                                data-testid={b.label}
   215                                onClick={() => {
   216                                  handleSelectPeriod(b);
   217                                }}
   218                                className={styles.priorButton}
   219                              >
   220                                {b.label}
   221                              </Button>
   222                            );
   223                          })}
   224                        </div>
   225                      );
   226                    })}
   227                  </div>
   228                  <div className={styles.subtitle}>Preview</div>
   229                  {preview}
   230                </PopoverBody>
   231              </>
   232            ) : null}
   233          </Popover>
   234        </Portal>
   235      </div>
   236    );
   237  }