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}> 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 194 <span className={styles.periodLabel}>{period.label}</span> 195 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 }