github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/pages/ContinuousComparisonView.tsx (about)

     1  import React, { useEffect } from 'react';
     2  import 'react-dom';
     3  
     4  import Box from '@webapp/ui/Box';
     5  import { FlamegraphRenderer } from '@pyroscope/flamegraph/src/FlamegraphRenderer';
     6  import { useAppDispatch, useAppSelector } from '@webapp/redux/hooks';
     7  import {
     8    selectContinuousState,
     9    actions,
    10    selectComparisonState,
    11    fetchComparisonSide,
    12    fetchTagValues,
    13    selectQueries,
    14    selectTimelineSides,
    15    selectAnnotationsOrDefault,
    16  } from '@webapp/redux/reducers/continuous';
    17  import SideTimelineComparator from '@webapp/components/SideTimelineComparator';
    18  import TimelineChartWrapper from '@webapp/components/TimelineChart/TimelineChartWrapper';
    19  import SyncTimelines from '@webapp/components/TimelineChart/SyncTimelines';
    20  import Toolbar from '@webapp/components/Toolbar';
    21  import ExportData from '@webapp/components/ExportData';
    22  import useExportToFlamegraphDotCom from '@webapp/components/exportToFlamegraphDotCom.hook';
    23  import TagsBar from '@webapp/components/TagsBar';
    24  import ChartTitle from '@webapp/components/ChartTitle';
    25  import useTimeZone from '@webapp/hooks/timeZone.hook';
    26  import useColorMode from '@webapp/hooks/colorMode.hook';
    27  import { isExportToFlamegraphDotComEnabled } from '@webapp/util/features';
    28  import { LoadingOverlay } from '@webapp/ui/LoadingOverlay';
    29  import PageTitle from '@webapp/components/PageTitle';
    30  import { Query } from '@webapp/models/query';
    31  import styles from './ContinuousComparison.module.css';
    32  import useTags from '../hooks/tags.hook';
    33  import useTimelines, {
    34    leftColor,
    35    rightColor,
    36    selectionColor,
    37  } from '../hooks/timeline.hook';
    38  import usePopulateLeftRightQuery from '../hooks/populateLeftRightQuery.hook';
    39  import useFlamegraphSharedQuery from '../hooks/flamegraphSharedQuery.hook';
    40  import { formatTitle } from './formatTitle';
    41  import { isLoadingOrReloading } from './loading';
    42  
    43  function ComparisonApp() {
    44    const dispatch = useAppDispatch();
    45    const {
    46      leftFrom,
    47      rightFrom,
    48      leftUntil,
    49      rightUntil,
    50      refreshToken,
    51      from,
    52      until,
    53    } = useAppSelector(selectContinuousState);
    54    const { leftQuery, rightQuery } = useAppSelector(selectQueries);
    55    const { offset } = useTimeZone();
    56    const { colorMode } = useColorMode();
    57    usePopulateLeftRightQuery();
    58    const {
    59      left: comparisonLeft,
    60      right: comparisonRight,
    61      comparisonMode,
    62    } = useAppSelector(selectComparisonState);
    63    const { leftTags, rightTags } = useTags();
    64    const { leftTimeline, rightTimeline } = useTimelines();
    65    const sharedQuery = useFlamegraphSharedQuery();
    66    const annotations = useAppSelector(
    67      selectAnnotationsOrDefault('comparisonView')
    68    );
    69  
    70    const timelines = useAppSelector(selectTimelineSides);
    71    const isLoading = isLoadingOrReloading([
    72      comparisonLeft.type,
    73      comparisonRight.type,
    74      timelines.left.type,
    75      timelines.right.type,
    76    ]);
    77  
    78    useEffect(() => {
    79      if (leftQuery) {
    80        const fetchLeftQueryData = dispatch(
    81          fetchComparisonSide({ side: 'left', query: leftQuery })
    82        );
    83        return fetchLeftQueryData.abort;
    84      }
    85      return undefined;
    86    }, [leftFrom, leftUntil, leftQuery, refreshToken]);
    87  
    88    useEffect(() => {
    89      if (rightQuery) {
    90        const fetchRightQueryData = dispatch(
    91          fetchComparisonSide({ side: 'right', query: rightQuery })
    92        );
    93  
    94        return fetchRightQueryData.abort;
    95      }
    96      return undefined;
    97    }, [rightFrom, rightUntil, rightQuery, refreshToken]);
    98  
    99    const leftSide = comparisonLeft.profile;
   100    const rightSide = comparisonRight.profile;
   101    const exportToFlamegraphDotComLeftFn = useExportToFlamegraphDotCom(leftSide);
   102    const exportToFlamegraphDotComRightFn =
   103      useExportToFlamegraphDotCom(rightSide);
   104    const timezone = offset === 0 ? 'utc' : 'browser';
   105    const isSidesHasSameUnits =
   106      leftSide &&
   107      rightSide &&
   108      leftSide.metadata.units === rightSide.metadata.units;
   109  
   110    const handleCompare = ({
   111      from,
   112      until,
   113      leftFrom,
   114      leftTo,
   115      rightFrom,
   116      rightTo,
   117    }: {
   118      from: string;
   119      until: string;
   120      leftFrom: string;
   121      leftTo: string;
   122      rightFrom: string;
   123      rightTo: string;
   124    }) => {
   125      dispatch(
   126        actions.setFromAndUntil({
   127          from,
   128          until,
   129        })
   130      );
   131      dispatch(actions.setRight({ from: rightFrom, until: rightTo }));
   132      dispatch(actions.setLeft({ from: leftFrom, until: leftTo }));
   133    };
   134  
   135    const setComparisonMode = (mode: {
   136      active: boolean;
   137      period: {
   138        label: string;
   139        ms: number;
   140      };
   141    }) => {
   142      dispatch(actions.setComparisonMode(mode));
   143    };
   144  
   145    const handleSelectMain = (from: string, until: string) => {
   146      setComparisonMode({
   147        ...comparisonMode,
   148        active: false,
   149      });
   150      dispatch(actions.setFromAndUntil({ from, until }));
   151    };
   152  
   153    const handleSelectLeft = (from: string, until: string) => {
   154      setComparisonMode({
   155        ...comparisonMode,
   156        active: false,
   157      });
   158      dispatch(actions.setLeft({ from, until }));
   159    };
   160  
   161    const handleSelectRight = (from: string, until: string) => {
   162      setComparisonMode({
   163        ...comparisonMode,
   164        active: false,
   165      });
   166      dispatch(actions.setRight({ from, until }));
   167    };
   168  
   169    const handleSelectedApp = (query: Query) => {
   170      setComparisonMode({
   171        ...comparisonMode,
   172        active: false,
   173      });
   174      dispatch(actions.setQuery(query));
   175    };
   176  
   177    return (
   178      <div>
   179        <PageTitle title={formatTitle('Comparison', leftQuery, rightQuery)} />
   180        <div className="main-wrapper">
   181          <Toolbar onSelectedApp={handleSelectedApp} />
   182          <Box>
   183            <LoadingOverlay active={isLoading}>
   184              <TimelineChartWrapper
   185                data-testid="timeline-main"
   186                id="timeline-chart-double"
   187                format="lines"
   188                height="125px"
   189                annotations={annotations}
   190                timelineA={leftTimeline}
   191                timelineB={rightTimeline}
   192                onSelect={handleSelectMain}
   193                syncCrosshairsWith={[
   194                  'timeline-chart-left',
   195                  'timeline-chart-right',
   196                ]}
   197                selection={{
   198                  left: {
   199                    from: leftFrom,
   200                    to: leftUntil,
   201                    color: leftColor,
   202                    overlayColor: leftColor.alpha(0.3),
   203                  },
   204                  right: {
   205                    from: rightFrom,
   206                    to: rightUntil,
   207                    color: rightColor,
   208                    overlayColor: rightColor.alpha(0.3),
   209                  },
   210                }}
   211                timezone={timezone}
   212                title={
   213                  <ChartTitle
   214                    titleKey={
   215                      isSidesHasSameUnits ? leftSide.metadata.units : undefined
   216                    }
   217                  />
   218                }
   219                selectionType="double"
   220              />
   221              <SyncTimelines
   222                isDataLoading={isLoading}
   223                comparisonModeActive={comparisonMode.active}
   224                timeline={leftTimeline}
   225                leftSelection={{ from: leftFrom, to: leftUntil }}
   226                rightSelection={{ from: rightFrom, to: rightUntil }}
   227                onSync={(from, until) => {
   228                  dispatch(actions.setFromAndUntil({ from, until }));
   229                }}
   230              />
   231            </LoadingOverlay>
   232          </Box>
   233          <div
   234            className="comparison-container"
   235            data-testid="comparison-container"
   236          >
   237            <Box className={styles.comparisonPane}>
   238              <LoadingOverlay active={isLoading} spinnerPosition="baseline">
   239                <div className={styles.timelineTitleWrapper}>
   240                  <ChartTitle titleKey="baseline" color={leftColor} />
   241                  <SideTimelineComparator
   242                    setComparisonMode={setComparisonMode}
   243                    comparisonMode={comparisonMode}
   244                    onCompare={handleCompare}
   245                    selection={{
   246                      from,
   247                      until,
   248                      left: {
   249                        from: leftFrom,
   250                        to: leftUntil,
   251                        color: leftColor,
   252                        overlayColor: leftColor.alpha(0.3),
   253                      },
   254                      right: {
   255                        from: rightFrom,
   256                        to: rightUntil,
   257                        color: rightColor,
   258                        overlayColor: rightColor.alpha(0.3),
   259                      },
   260                    }}
   261                  />
   262                </div>
   263  
   264                <TagsBar
   265                  query={leftQuery}
   266                  tags={leftTags}
   267                  onRefresh={() => dispatch(actions.refresh())}
   268                  onSetQuery={(q) => dispatch(actions.setLeftQuery(q))}
   269                  onSelectedLabel={(label, query) => {
   270                    dispatch(fetchTagValues({ query, label }));
   271                  }}
   272                />
   273                <FlamegraphRenderer
   274                  showCredit={false}
   275                  panesOrientation="vertical"
   276                  profile={leftSide}
   277                  data-testid="flamegraph-renderer-left"
   278                  colorMode={colorMode}
   279                  sharedQuery={{ ...sharedQuery, id: 'left' }}
   280                  ExportData={
   281                    // Don't export PNG since the exportPng code is broken
   282                    leftSide && (
   283                      <ExportData
   284                        flamebearer={leftSide}
   285                        exportJSON
   286                        exportHTML
   287                        exportPprof
   288                        exportFlamegraphDotCom={isExportToFlamegraphDotComEnabled}
   289                        exportFlamegraphDotComFn={exportToFlamegraphDotComLeftFn}
   290                      />
   291                    )
   292                  }
   293                >
   294                  <TimelineChartWrapper
   295                    key="timeline-chart-left"
   296                    id="timeline-chart-left"
   297                    data-testid="timeline-left"
   298                    selectionWithHandler
   299                    syncCrosshairsWith={[
   300                      'timeline-chart-double',
   301                      'timeline-chart-right',
   302                    ]}
   303                    timelineA={leftTimeline}
   304                    selection={{
   305                      left: {
   306                        from: leftFrom,
   307                        to: leftUntil,
   308                        color: selectionColor,
   309                        overlayColor: selectionColor.alpha(0.3),
   310                      },
   311                    }}
   312                    selectionType="single"
   313                    onSelect={handleSelectLeft}
   314                    timezone={timezone}
   315                  />
   316                </FlamegraphRenderer>
   317              </LoadingOverlay>
   318            </Box>
   319  
   320            <Box className={styles.comparisonPane}>
   321              <LoadingOverlay spinnerPosition="baseline" active={isLoading}>
   322                <div className={styles.timelineTitleWrapper}>
   323                  <ChartTitle titleKey="comparison" color={rightColor} />
   324                </div>
   325                <TagsBar
   326                  query={rightQuery}
   327                  tags={rightTags}
   328                  onRefresh={() => dispatch(actions.refresh())}
   329                  onSetQuery={(q) => dispatch(actions.setRightQuery(q))}
   330                  onSelectedLabel={(label, query) => {
   331                    dispatch(fetchTagValues({ query, label }));
   332                  }}
   333                />
   334                <FlamegraphRenderer
   335                  showCredit={false}
   336                  profile={rightSide}
   337                  data-testid="flamegraph-renderer-right"
   338                  panesOrientation="vertical"
   339                  colorMode={colorMode}
   340                  sharedQuery={{ ...sharedQuery, id: 'right' }}
   341                  ExportData={
   342                    // Don't export PNG since the exportPng code is broken
   343                    rightSide && (
   344                      <ExportData
   345                        flamebearer={rightSide}
   346                        exportJSON
   347                        exportHTML
   348                        exportPprof
   349                        exportFlamegraphDotCom={isExportToFlamegraphDotComEnabled}
   350                        exportFlamegraphDotComFn={exportToFlamegraphDotComRightFn}
   351                      />
   352                    )
   353                  }
   354                >
   355                  <TimelineChartWrapper
   356                    key="timeline-chart-right"
   357                    id="timeline-chart-right"
   358                    data-testid="timeline-right"
   359                    timelineA={rightTimeline}
   360                    syncCrosshairsWith={[
   361                      'timeline-chart-double',
   362                      'timeline-chart-left',
   363                    ]}
   364                    selectionWithHandler
   365                    selection={{
   366                      right: {
   367                        from: rightFrom,
   368                        to: rightUntil,
   369                        color: selectionColor,
   370                        overlayColor: selectionColor.alpha(0.3),
   371                      },
   372                    }}
   373                    selectionType="single"
   374                    onSelect={handleSelectRight}
   375                    timezone={timezone}
   376                  />
   377                </FlamegraphRenderer>
   378              </LoadingOverlay>
   379            </Box>
   380          </div>
   381        </div>
   382      </div>
   383    );
   384  }
   385  
   386  export default ComparisonApp;