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;