github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/Sidebar.tsx (about) 1 import React, { useMemo } from 'react'; 2 import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons'; 3 import { faChartBar } from '@fortawesome/free-solid-svg-icons/faChartBar'; 4 import { faColumns } from '@fortawesome/free-solid-svg-icons/faColumns'; 5 import { faFileAlt } from '@fortawesome/free-solid-svg-icons/faFileAlt'; 6 import { faCog } from '@fortawesome/free-solid-svg-icons/faCog'; 7 import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'; 8 import { faSlack } from '@fortawesome/free-brands-svg-icons/faSlack'; 9 import { faGithub } from '@fortawesome/free-brands-svg-icons/faGithub'; 10 import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft'; 11 import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt'; 12 import { faSync } from '@fortawesome/free-solid-svg-icons/faSync'; 13 import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch'; 14 15 import Sidebar, { 16 MenuItem, 17 SidebarHeader, 18 SidebarFooter, 19 SidebarContent, 20 SubMenu, 21 Menu, 22 } from '@webapp/ui/Sidebar'; 23 import { useAppSelector, useAppDispatch } from '@webapp/redux/hooks'; 24 import { 25 selectSidebarCollapsed, 26 collapseSidebar, 27 uncollapseSidebar, 28 recalculateSidebar, 29 } from '@webapp/redux/reducers/ui'; 30 // import useColorMode from '@webapp/hooks/colorMode.hook'; 31 import { useLocation, NavLink } from 'react-router-dom'; 32 import { 33 isAdhocUIEnabled, 34 isAuthRequired, 35 isExemplarsPageEnabled, 36 } from '@webapp/util/features'; 37 import Icon from '@webapp/ui/Icon'; 38 import clsx from 'clsx'; 39 import { useWindowWidth } from '@react-hook/window-size'; 40 import { 41 AdhocIcon, 42 ExemplarsIcon, 43 MergeExemplarsIcon, 44 } from './SidebarCustomIcons'; 45 import styles from './Sidebar.module.css'; 46 import { PAGES } from '../pages/constants'; 47 import { mountURL } from '../services/base'; 48 49 function signOut() { 50 // By visiting /logout we're clearing jwtCookie 51 window.location.href = mountURL('/logout'); 52 } 53 54 export function SidebarComponent() { 55 const collapsed = useAppSelector(selectSidebarCollapsed); 56 // const { changeColorMode, colorMode } = useColorMode(); 57 const dispatch = useAppDispatch(); 58 59 const { search, pathname } = useLocation(); 60 const windowWidth = useWindowWidth(); 61 const authEnabled = isAuthRequired; 62 63 // the component doesn't seem to support setting up an active item 64 // so we must set it up manually 65 // https://github.com/azouaoui-med/react-pro-sidebar/issues/84 66 const isRouteActive = (route: string) => { 67 if ( 68 route === PAGES.CONTINOUS_SINGLE_VIEW || 69 route === PAGES.COMPARISON_VIEW || 70 route === PAGES.ADHOC_COMPARISON || 71 route === PAGES.TRACING_EXEMPLARS_SINGLE || 72 route === PAGES.TRACING_EXEMPLARS_MERGE 73 ) { 74 return pathname === route; 75 } 76 77 return pathname.startsWith(route); 78 }; 79 80 const isSidebarVisible = useMemo( 81 () => 82 ( 83 [ 84 PAGES.CONTINOUS_SINGLE_VIEW, 85 PAGES.COMPARISON_VIEW, 86 PAGES.ADHOC_COMPARISON, 87 PAGES.COMPARISON_DIFF_VIEW, 88 PAGES.SETTINGS, 89 PAGES.SERVICE_DISCOVERY, 90 PAGES.ADHOC_SINGLE, 91 PAGES.ADHOC_COMPARISON, 92 PAGES.ADHOC_COMPARISON_DIFF, 93 PAGES.TAG_EXPLORER, 94 PAGES.TRACING_EXEMPLARS_MERGE, 95 PAGES.TRACING_EXEMPLARS_SINGLE, 96 ] as string[] 97 ).includes(pathname) || pathname.startsWith(PAGES.SETTINGS), 98 [pathname] 99 ); 100 101 React.useLayoutEffect(() => { 102 dispatch(recalculateSidebar()); 103 }, [windowWidth]); 104 105 // TODO 106 // simplify this 107 const isContinuousActive = 108 isRouteActive(PAGES.CONTINOUS_SINGLE_VIEW) || 109 isRouteActive(PAGES.COMPARISON_VIEW) || 110 isRouteActive(PAGES.COMPARISON_DIFF_VIEW) || 111 isRouteActive(PAGES.TAG_EXPLORER); 112 const isAdhocActive = 113 isRouteActive(PAGES.ADHOC_SINGLE) || 114 isRouteActive(PAGES.ADHOC_COMPARISON) || 115 isRouteActive(PAGES.ADHOC_COMPARISON_DIFF); 116 const isTracingActive = 117 isRouteActive(PAGES.TRACING_EXEMPLARS_MERGE) || 118 isRouteActive(PAGES.TRACING_EXEMPLARS_SINGLE); 119 const isSettingsActive = isRouteActive(PAGES.SETTINGS); 120 121 const adhoc = ( 122 <SubMenu 123 title="Adhoc Profiling" 124 icon={<AdhocIcon />} 125 active={isAdhocActive} 126 defaultOpen={isAdhocActive} 127 data-testid="sidebar-adhoc" 128 > 129 {collapsed && ( 130 <SidebarHeader className={styles.collapsedHeader}> 131 Adhoc Profiling 132 </SidebarHeader> 133 )} 134 <MenuItem 135 data-testid="sidebar-adhoc-single" 136 active={isRouteActive(PAGES.ADHOC_SINGLE)} 137 icon={<Icon icon={faWindowMaximize} />} 138 > 139 Single View 140 <NavLink to={{ pathname: PAGES.ADHOC_SINGLE, search }} exact /> 141 </MenuItem> 142 <MenuItem 143 data-testid="sidebar-adhoc-comparison" 144 active={isRouteActive(PAGES.ADHOC_COMPARISON)} 145 icon={<Icon icon={faColumns} />} 146 > 147 Comparison View 148 <NavLink to={{ pathname: PAGES.ADHOC_COMPARISON, search }} exact /> 149 </MenuItem> 150 <MenuItem 151 data-testid="sidebar-adhoc-comparison-diff" 152 active={isRouteActive(PAGES.ADHOC_COMPARISON_DIFF)} 153 icon={<Icon icon={faChartBar} />} 154 > 155 Diff View 156 <NavLink to={{ pathname: PAGES.ADHOC_COMPARISON_DIFF, search }} exact /> 157 </MenuItem> 158 </SubMenu> 159 ); 160 161 const toggleCollapse = () => { 162 const action = collapsed ? uncollapseSidebar : collapseSidebar; 163 dispatch(action()); 164 }; 165 166 return isSidebarVisible ? ( 167 <Sidebar collapsed={collapsed}> 168 <SidebarHeader> 169 <div className={styles.logo}> 170 <div className="logo-main" /> 171 <span 172 className={clsx(styles.logoText, { 173 [styles.logoTextCollapsed]: collapsed, 174 })} 175 > 176 Pyroscope 177 </span> 178 </div> 179 </SidebarHeader> 180 <SidebarContent> 181 <Menu iconShape="square" popperArrow> 182 <SubMenu 183 title="Continuous Profiling" 184 icon={<Icon icon={faSync} />} 185 active={isContinuousActive} 186 defaultOpen={isContinuousActive} 187 data-testid="sidebar-continuous" 188 > 189 {collapsed && ( 190 <SidebarHeader className={styles.collapsedHeader}> 191 Continuous Profiling 192 </SidebarHeader> 193 )} 194 <MenuItem 195 data-testid="sidebar-explore-page" 196 active={isRouteActive(PAGES.TAG_EXPLORER)} 197 icon={<Icon icon={faSearch} />} 198 > 199 Tag explorer 200 <NavLink to={{ pathname: PAGES.TAG_EXPLORER, search }} exact /> 201 </MenuItem> 202 <MenuItem 203 data-testid="sidebar-continuous-single" 204 active={isRouteActive(PAGES.CONTINOUS_SINGLE_VIEW)} 205 icon={<Icon icon={faWindowMaximize} />} 206 > 207 Single View 208 <NavLink 209 activeClassName="active-route" 210 data-testid="sidebar-root" 211 to={{ pathname: PAGES.CONTINOUS_SINGLE_VIEW, search }} 212 exact 213 /> 214 </MenuItem> 215 <MenuItem 216 data-testid="sidebar-continuous-comparison" 217 active={isRouteActive(PAGES.COMPARISON_VIEW)} 218 icon={<Icon icon={faColumns} />} 219 > 220 Comparison View 221 <NavLink to={{ pathname: PAGES.COMPARISON_VIEW, search }} exact /> 222 </MenuItem> 223 <MenuItem 224 data-testid="sidebar-continuous-diff" 225 active={isRouteActive(PAGES.COMPARISON_DIFF_VIEW)} 226 icon={<Icon icon={faChartBar} />} 227 > 228 Diff View 229 <NavLink 230 to={{ pathname: PAGES.COMPARISON_DIFF_VIEW, search }} 231 exact 232 /> 233 </MenuItem> 234 </SubMenu> 235 {isAdhocUIEnabled && adhoc} 236 {isExemplarsPageEnabled && ( 237 <SubMenu 238 title="Tracing Exemplars" 239 icon={<ExemplarsIcon />} 240 active={isTracingActive} 241 defaultOpen={isTracingActive} 242 > 243 {collapsed && ( 244 <SidebarHeader className={styles.collapsedHeader}> 245 Tracing Exemplars 246 </SidebarHeader> 247 )} 248 <MenuItem 249 active={isRouteActive(PAGES.TRACING_EXEMPLARS_SINGLE)} 250 icon={<ExemplarsIcon />} 251 > 252 Exemplars 253 <NavLink 254 activeClassName="active-route" 255 to={{ pathname: PAGES.TRACING_EXEMPLARS_SINGLE, search }} 256 exact 257 /> 258 </MenuItem> 259 <MenuItem 260 active={isRouteActive(PAGES.TRACING_EXEMPLARS_MERGE)} 261 icon={<MergeExemplarsIcon />} 262 > 263 Merge Exemplars 264 <NavLink 265 activeClassName="active-route" 266 to={{ pathname: PAGES.TRACING_EXEMPLARS_MERGE, search }} 267 exact 268 /> 269 </MenuItem> 270 </SubMenu> 271 )} 272 </Menu> 273 </SidebarContent> 274 <SidebarFooter> 275 <Menu iconShape="square"> 276 {authEnabled && ( 277 <MenuItem 278 data-testid="sidebar-settings" 279 active={isSettingsActive} 280 icon={<Icon icon={faCog} />} 281 > 282 Settings 283 <NavLink to={{ pathname: PAGES.SETTINGS, search }} exact /> 284 </MenuItem> 285 )} 286 <MenuItem icon={<Icon icon={faInfoCircle} />}> 287 Scrape Targets 288 <NavLink to={{ pathname: PAGES.SERVICE_DISCOVERY, search }} exact /> 289 </MenuItem> 290 <MenuItem icon={<Icon icon={faFileAlt} />}> 291 <a 292 rel="noreferrer" 293 target="_blank" 294 href="https://pyroscope.io/docs" 295 > 296 Documentation 297 </a> 298 </MenuItem> 299 <MenuItem icon={<Icon icon={faSlack} />}> 300 <a 301 rel="noreferrer" 302 target="_blank" 303 href="https://pyroscope.io/slack" 304 > 305 Slack 306 </a> 307 </MenuItem> 308 <MenuItem icon={<Icon icon={faGithub} />}> 309 <a 310 rel="noreferrer" 311 target="_blank" 312 href="https://github.com/pyroscope-io/pyroscope" 313 > 314 Github 315 </a> 316 </MenuItem> 317 {isAuthRequired && ( 318 <MenuItem 319 onClick={() => signOut()} 320 icon={<Icon icon={faSignOutAlt} />} 321 > 322 Sign out 323 </MenuItem> 324 )} 325 <MenuItem 326 data-testid="collapse-sidebar" 327 className={`${styles.collapseIcon} ${ 328 collapsed ? styles.collapsedIconCollapsed : '' 329 }`} 330 onClick={toggleCollapse} 331 icon={<Icon icon={faChevronLeft} />} 332 > 333 Collapse Sidebar 334 </MenuItem> 335 </Menu> 336 </SidebarFooter> 337 {/* <select 338 value={colorMode} 339 onChange={(e: ChangeEvent<HTMLSelectElement>) => 340 changeColorMode(e.target?.value as 'light' | 'dark') 341 } 342 > 343 <option value="dark">dark</option> 344 <option value="light">light</option> 345 </select> */} 346 </Sidebar> 347 ) : null; 348 } 349 350 export default SidebarComponent;