github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/EnhancedAppSelector/EnhancedAppSelector.tsx (about) 1 import React, { useState, useMemo } from 'react'; 2 import { App } from '@webapp/models/app'; 3 import ModalWithToggle from '@webapp/ui/Modals/ModalWithToggle'; 4 import Input from '@webapp/ui/Input'; 5 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 import { faSlidersH } from '@fortawesome/free-solid-svg-icons/faSlidersH'; 7 import { faUndo } from '@fortawesome/free-solid-svg-icons/faUndo'; 8 import { Tooltip } from '@webapp/ui/Tooltip'; 9 import { SpyNameFirstClassType } from '@pyroscope/models/src'; 10 import cl from 'classnames'; 11 import SelectButton from '../AppSelector/SelectButton'; 12 import useFilters from './useFilters'; 13 import { SPY_NAMES_TOOLTIPS, SPY_NAMES_ICONS } from './SpyNameIcons'; 14 import styles from './EnhancedAppSelector.module.scss'; 15 16 export interface EnhancedAppSelector { 17 /** Triggered when an app is selected */ 18 onSelected: (name: string) => void; 19 20 apps: App[]; 21 22 selectedAppName: string; 23 } 24 25 // TODO: this file has a lot of repetition with AppSelector 26 // We should remove the old implementation (AppSelector) 27 // When this one actually gets used 28 function EnhancedAppSelector({ 29 onSelected, 30 selectedAppName, 31 apps, 32 }: EnhancedAppSelector) { 33 return ( 34 <div className={styles.container}> 35 Application: 36 <SelectorModalWithToggler 37 selectAppName={onSelected} 38 apps={apps} 39 appName={selectedAppName} 40 /> 41 </div> 42 ); 43 } 44 45 const DELIMITER = '.'; 46 const isGroupMember = (groupName: string, name: string) => 47 name.indexOf(groupName) === 0 && 48 (name[groupName.length] === DELIMITER || name.length === groupName.length); 49 50 const getGroupMembers = (names: string[], name: string) => 51 names.filter((a) => isGroupMember(name, a)); 52 53 const getGroupNameFromAppName = (groups: string[], fullName: string) => 54 groups.filter((g) => isGroupMember(g, fullName))[0] || ''; 55 56 const getGroups = (filteredAppNames: string[]) => { 57 const allGroups = filteredAppNames.map((i) => { 58 const arr = i.split(DELIMITER); 59 const cutProfileType = arr.length > 1 ? arr.slice(0, -1) : arr; 60 return cutProfileType.join(DELIMITER); 61 }); 62 63 const uniqGroups = Array.from(new Set(allGroups)); 64 65 const dedupedUniqGroups = uniqGroups.filter((x) => { 66 return !uniqGroups.find((y) => x !== y && isGroupMember(y, x)); 67 }); 68 69 const groupOrApp = dedupedUniqGroups.map((u) => { 70 const appNamesEntries = getGroupMembers(filteredAppNames, u); 71 72 return appNamesEntries.length > 1 ? u : appNamesEntries?.[0]; 73 }); 74 75 return groupOrApp; 76 }; 77 78 const getSelectedApp = ( 79 appName: string, 80 groups: string[], 81 selected: string[] 82 ) => { 83 const isFirstLevel = !!(groups.indexOf(appName) !== -1); 84 85 if (selected.length !== 0) { 86 return selected; 87 } 88 89 if (isFirstLevel) { 90 return [appName]; 91 } 92 return [getGroupNameFromAppName(groups, appName), appName]; 93 }; 94 95 interface SelectorModalWithTogglerProps { 96 appName: string; 97 apps: App[]; 98 selectAppName: (name: string) => void; 99 } 100 101 const SelectorModalWithToggler = ({ 102 appName, 103 apps, 104 selectAppName, 105 }: SelectorModalWithTogglerProps) => { 106 const [isModalOpen, setModalOpenStatus] = useState(false); 107 const { 108 filters, 109 filteredAppNames, 110 spyNameValues, 111 profileTypeValues, 112 handleFilterChange, 113 resetClickableFilters, 114 } = useFilters(apps); 115 116 // selected is an array of strings 117 // 0 corresponds to string of group / app name selected in the left pane 118 // 1 corresponds to string of app name selected in the right pane 119 const [selected, setSelected] = useState<string[]>([]); 120 121 const groups = useMemo(() => getGroups(filteredAppNames), [filteredAppNames]); 122 const selectedApp = getSelectedApp(appName, groups, selected); 123 124 const profilesNames = useMemo(() => { 125 if (!selectedApp?.[0]) { 126 return []; 127 } 128 129 const filtered = getGroupMembers(filteredAppNames, selectedApp?.[0]); 130 131 if (filtered.length > 1) { 132 return filtered; 133 } 134 135 return []; 136 }, [selectedApp, groups, filteredAppNames]); 137 138 const onSelect = ({ index, name }: { index: number; name: string }) => { 139 const filtered = getGroupMembers(filteredAppNames, name); 140 141 if (filtered.length === 1 || index === 1) { 142 selectAppName(filtered?.[0]); 143 setModalOpenStatus(false); 144 } 145 146 const arr = Array.from(selectedApp); 147 148 if (index === 0 && arr?.length > 1) { 149 arr.pop(); 150 } 151 152 arr[index] = name; 153 154 setSelected(arr); 155 }; 156 157 const listHeight = useMemo(() => { 158 const height = (window?.innerHeight || 0) - 160; 159 160 const listRequiredHeight = 161 // 35 is list item height 162 Math.max(groups?.length || 0, profilesNames?.length || 0) * 35; 163 164 if (height && listRequiredHeight) { 165 return height >= listRequiredHeight ? 'auto' : `${height}px`; 166 } 167 168 return 'auto'; 169 }, [groups, profilesNames]); 170 171 return ( 172 <ModalWithToggle 173 isModalOpen={isModalOpen} 174 setModalOpenStatus={setModalOpenStatus} 175 modalClassName={styles.appSelectorModal} 176 modalHeight={listHeight} 177 noDataEl={ 178 !filteredAppNames?.length ? ( 179 <div data-testid="app-selector-no-data" className={styles.noData}> 180 No Data 181 </div> 182 ) : null 183 } 184 toggleText={appName || 'Select application'} 185 headerEl={ 186 <div className={styles.header}> 187 <div> 188 <div className={styles.headerTitle}>SELECT APPLICATION</div> 189 <Input 190 name="application seach" 191 type="text" 192 placeholder="Type an app" 193 value={filters.search.unwrapOr('')} 194 onChange={(e) => handleFilterChange('search', e.target.value)} 195 className={styles.searchInput} 196 testId="application-search" 197 /> 198 </div> 199 <div> 200 <div className={styles.headerTitle}> 201 <FontAwesomeIcon icon={faSlidersH} /> FILTERS 202 <button 203 className={styles.resetFilters} 204 disabled={ 205 filters.profileTypes.isNothing && filters.spyNames.isNothing 206 } 207 onClick={resetClickableFilters} 208 > 209 <FontAwesomeIcon icon={faUndo} /> 210 </button> 211 </div> 212 <div> 213 <div className={styles.filter}> 214 <div className={styles.filterName}>Language</div> 215 <div className={styles.iconsContainer}> 216 {spyNameValues.map((v) => ( 217 <Tooltip placement="top" title={SPY_NAMES_TOOLTIPS[v]}> 218 <button 219 type="button" 220 key={v} 221 data-testid={v} 222 className={cl(styles.icon, { 223 [styles.active]: 224 filters.spyNames 225 .unwrapOr( 226 [] as (SpyNameFirstClassType | 'unknown')[] 227 ) 228 .indexOf(v) !== -1, 229 })} 230 onClick={() => handleFilterChange('spyNames', v)} 231 > 232 {SPY_NAMES_ICONS[v]} 233 </button> 234 </Tooltip> 235 ))} 236 </div> 237 </div> 238 <div className={styles.filter}> 239 <div className={styles.filterName}>Profile type</div> 240 <div className={styles.profileTypesContainer}> 241 {profileTypeValues.map((v) => ( 242 <button 243 type="button" 244 key={v} 245 className={cl(styles.profileType, { 246 [styles.active]: 247 filters.profileTypes 248 .unwrapOr([] as string[]) 249 .indexOf(v) !== -1, 250 })} 251 onClick={() => handleFilterChange('profileTypes', v)} 252 > 253 {v} 254 </button> 255 ))} 256 </div> 257 </div> 258 </div> 259 </div> 260 </div> 261 } 262 leftSideEl={groups.map((name) => ( 263 <SelectButton 264 name={name} 265 onClick={() => onSelect({ index: 0, name })} 266 fullList={filteredAppNames} 267 isSelected={selectedApp?.[0] === name} 268 key={name} 269 /> 270 ))} 271 rightSideEl={profilesNames.map((name) => ( 272 <SelectButton 273 name={name} 274 onClick={() => onSelect({ index: 1, name })} 275 fullList={filteredAppNames} 276 isSelected={selectedApp?.[1] === name} 277 key={name} 278 /> 279 ))} 280 /> 281 ); 282 }; 283 284 export default EnhancedAppSelector;