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;