github.com/grafana/pyroscope@v1.18.0/public/app/pages/ContinuousSingleView.tsx (about)

     1  import React, { useEffect } from 'react';
     2  import 'react-dom';
     3  
     4  import { useAppDispatch, useAppSelector } from '@pyroscope/redux/hooks';
     5  import {
     6    fetchSingleView,
     7    setQuery,
     8    selectQueries,
     9    setDateRange,
    10    actions,
    11    fetchTagValues,
    12  } from '@pyroscope/redux/reducers/continuous';
    13  import useColorMode from '@pyroscope/hooks/colorMode.hook';
    14  import TimelineChartWrapper from '@pyroscope/components/TimelineChart/TimelineChartWrapper';
    15  import Toolbar from '@pyroscope/components/Toolbar';
    16  import ChartTitle from '@pyroscope/components/ChartTitle';
    17  import TagsBar from '@pyroscope/components/TagsBar';
    18  import useTimeZone from '@pyroscope/hooks/timeZone.hook';
    19  import PageTitle from '@pyroscope/components/PageTitle';
    20  import { getFormatter } from '@pyroscope/legacy/flamegraph/format/format';
    21  import { TooltipCallbackProps } from '@pyroscope/components/TimelineChart/Tooltip.plugin';
    22  import { Profile } from '@pyroscope/legacy/models';
    23  import useTags from '@pyroscope/hooks/tags.hook';
    24  import {
    25    TimelineTooltip,
    26    TimelineTooltipProps,
    27  } from '@pyroscope/components/TimelineTooltip';
    28  import { formatTitle } from './formatTitle';
    29  import { isLoadingOrReloading } from './loading';
    30  import { Panel } from '@pyroscope/components/Panel';
    31  import { PageContentWrapper } from '@pyroscope/pages/PageContentWrapper';
    32  import { FlameGraphWrapper } from '@pyroscope/components/FlameGraphWrapper';
    33  import styles from './ContinuousSingleView.module.css';
    34  
    35  type ContinuousSingleViewProps = {
    36    extraButton?: React.ReactNode;
    37    extraPanel?: React.ReactNode;
    38  };
    39  
    40  function ContinuousSingleView({
    41    extraButton,
    42    extraPanel,
    43  }: ContinuousSingleViewProps) {
    44    const dispatch = useAppDispatch();
    45    const { offset } = useTimeZone();
    46    const { colorMode } = useColorMode();
    47  
    48    const { query } = useAppSelector(selectQueries);
    49    const tags = useTags().regularTags;
    50    const { from, until, refreshToken, maxNodes } = useAppSelector(
    51      (state) => state.continuous
    52    );
    53  
    54    const { singleView } = useAppSelector((state) => state.continuous);
    55  
    56    useEffect(() => {
    57      if (from && until && query && maxNodes) {
    58        const fetchData = dispatch(fetchSingleView(null));
    59        return () => fetchData.abort('cancel');
    60      }
    61      return undefined;
    62    }, [from, until, query, refreshToken, maxNodes, dispatch]);
    63  
    64    const flamegraphRenderer = (() => {
    65      switch (singleView.type) {
    66        case 'loaded':
    67        case 'reloading': {
    68          return <FlameGraphWrapper profile={singleView.profile} />;
    69        }
    70  
    71        default: {
    72          return 'Loading';
    73        }
    74      }
    75    })();
    76  
    77    const getTimeline = () => {
    78      switch (singleView.type) {
    79        case 'loaded':
    80        case 'reloading': {
    81          return {
    82            data: singleView.timeline,
    83            color: colorMode === 'light' ? '#3b78e7' : undefined,
    84          };
    85        }
    86  
    87        default: {
    88          return {
    89            data: undefined,
    90          };
    91        }
    92      }
    93    };
    94  
    95    return (
    96      <div>
    97        <PageTitle title={formatTitle('Single', query)} />
    98        <PageContentWrapper>
    99          <Toolbar
   100            onSelectedApp={(query) => {
   101              dispatch(setQuery(query));
   102            }}
   103          />
   104          <TagsBar
   105            query={query}
   106            tags={tags}
   107            onRefresh={() => dispatch(actions.refresh())}
   108            onSetQuery={(q) => dispatch(actions.setQuery(q))}
   109            onSelectedLabel={(label, query) => {
   110              dispatch(fetchTagValues({ query, label }));
   111            }}
   112          />
   113  
   114          <Panel
   115            isLoading={isLoadingOrReloading([singleView.type])}
   116            title={
   117              <ChartTitle
   118                className="singleView-timeline-title"
   119                titleKey={singleView?.profile?.metadata.name as any}
   120              />
   121            }
   122          >
   123            <TimelineChartWrapper
   124              timezone={offset === 0 ? 'utc' : 'browser'}
   125              data-testid="timeline-single"
   126              id="timeline-chart-single"
   127              timelineA={getTimeline()}
   128              onSelect={(from, until) => dispatch(setDateRange({ from, until }))}
   129              height="125px"
   130              selectionType="single"
   131              onHoverDisplayTooltip={(data) =>
   132                createTooltip(query, data, singleView.profile)
   133              }
   134            />
   135          </Panel>
   136          <Panel
   137            isLoading={isLoadingOrReloading([singleView.type])}
   138            headerActions={extraButton}
   139          >
   140            {extraPanel ? (
   141              <div className={styles.flamegraphContainer}>
   142                <div className={styles.flamegraphComponent}>
   143                  {flamegraphRenderer}
   144                </div>
   145                <div className={styles.extraPanel}>{extraPanel}</div>
   146              </div>
   147            ) : (
   148              flamegraphRenderer
   149            )}
   150          </Panel>
   151        </PageContentWrapper>
   152      </div>
   153    );
   154  }
   155  
   156  function createTooltip(
   157    query: string,
   158    data: TooltipCallbackProps,
   159    profile?: Profile
   160  ) {
   161    if (!profile) {
   162      return null;
   163    }
   164  
   165    const values = prepareTimelineTooltipContent(profile, query, data);
   166  
   167    if (values.length <= 0) {
   168      return null;
   169    }
   170  
   171    return <TimelineTooltip timeLabel={data.timeLabel} items={values} />;
   172  }
   173  
   174  // Converts data from TimelineChartWrapper into TimelineTooltip
   175  function prepareTimelineTooltipContent(
   176    profile: Profile,
   177    query: string,
   178    data: TooltipCallbackProps
   179  ): TimelineTooltipProps['items'] {
   180    const formatter = getFormatter(
   181      profile.flamebearer.numTicks,
   182      profile.metadata.sampleRate,
   183      profile.metadata.units
   184    );
   185  
   186    // Filter non empty values
   187    return (
   188      data.values
   189        .map((a) => {
   190          return {
   191            label: query,
   192            // TODO: horrible API
   193            value: a?.closest?.[1],
   194          };
   195        })
   196        // Sometimes closest is null
   197        .filter((a) => {
   198          return a.value;
   199        })
   200        .map((a) => {
   201          return {
   202            ...a,
   203            value: formatter.format(a.value, profile.metadata.sampleRate, true),
   204          };
   205        })
   206    );
   207  }
   208  
   209  export default ContinuousSingleView;