github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/AppSelector/index.tsx (about) 1 import React, { useState, useEffect, useMemo } from 'react'; 2 import ModalWithToggle from '@webapp/ui/Modals/ModalWithToggle'; 3 import Input from '@webapp/ui/Input'; 4 import { App } from '@webapp/models/app'; 5 import SelectButton from '@webapp/components/AppSelector/SelectButton'; 6 import { Label, LabelString } from '@webapp/components/AppSelector/Label'; 7 import styles from './AppSelector.module.scss'; 8 9 interface AppSelectorProps { 10 /** Triggered when an app is selected */ 11 onSelected: (name: string) => void; 12 13 /** List of all applications */ 14 apps: Pick<App, 'name'>[]; 15 16 selectedAppName: string; 17 } 18 19 const AppSelector = ({ 20 onSelected, 21 selectedAppName, 22 apps, 23 }: AppSelectorProps) => { 24 const selectAppName = (name: string) => { 25 onSelected(name); 26 }; 27 28 const appNames = apps.map((a) => a.name); 29 30 return ( 31 <div className={styles.container}> 32 <Label /> 33 <SelectorModalWithToggler 34 selectAppName={selectAppName} 35 appNames={appNames} 36 appName={selectedAppName} 37 /> 38 </div> 39 ); 40 }; 41 42 export default AppSelector; 43 44 const DELIMITER = '.'; 45 const isGroupMember = (groupName: string, name: string) => 46 name.indexOf(groupName) === 0 && 47 (name[groupName.length] === DELIMITER || name.length === groupName.length); 48 49 const getGroupMembers = (names: string[], name: string) => 50 names.filter((a) => isGroupMember(name, a)); 51 52 const getGroupNameFromAppName = (groups: string[], fullName: string) => 53 groups.filter((g) => isGroupMember(g, fullName))[0] || ''; 54 55 const getGroups = (filteredAppNames: string[]) => { 56 const allGroups = filteredAppNames.map((i) => { 57 const arr = i.split(DELIMITER); 58 const cutProfileType = arr.length > 1 ? arr.slice(0, -1) : arr; 59 return cutProfileType.join(DELIMITER); 60 }); 61 62 const uniqGroups = Array.from(new Set(allGroups)); 63 64 const dedupedUniqGroups = uniqGroups.filter((x) => { 65 return !uniqGroups.find((y) => x !== y && isGroupMember(y, x)); 66 }); 67 68 const groupOrApp = dedupedUniqGroups.map((u) => { 69 const appNamesEntries = getGroupMembers(filteredAppNames, u); 70 71 return appNamesEntries.length > 1 ? u : appNamesEntries?.[0]; 72 }); 73 74 return groupOrApp; 75 }; 76 77 interface SelectorModalWithTogglerProps { 78 appNames: string[]; 79 selectAppName: (name: string) => void; 80 appName: string; 81 } 82 83 export const SelectorModalWithToggler = ({ 84 appNames, 85 selectAppName, 86 appName, 87 }: SelectorModalWithTogglerProps) => { 88 const [filter, setFilter] = useState(''); 89 const [isModalOpen, setModalOpenStatus] = useState(false); 90 91 // selected is an array of strings 92 // 0 corresponds to string of group / app name selected in the left pane 93 // 1 corresponds to string of app name selected in the right pane 94 const [selected, setSelected] = useState<string[]>([]); 95 const filteredAppNames = useMemo( 96 // filtered names by search input 97 () => 98 appNames.filter((n: string) => 99 n.toLowerCase().includes(filter.trim().toLowerCase()) 100 ), 101 [filter, appNames] 102 ); 103 104 const groups = useMemo(() => getGroups(filteredAppNames), [filteredAppNames]); 105 106 const profileTypes = useMemo(() => { 107 if (!selected?.[0]) { 108 return []; 109 } 110 111 const filtered = getGroupMembers(filteredAppNames, selected?.[0]); 112 113 if (filtered.length > 1) { 114 return filtered; 115 } 116 117 return []; 118 }, [selected, groups, filteredAppNames]); 119 120 const onSelect = ({ index, name }: { index: number; name: string }) => { 121 const filtered = getGroupMembers(filteredAppNames, name); 122 123 if (filtered.length === 1 || index === 1) { 124 selectAppName(filtered?.[0]); 125 setModalOpenStatus(false); 126 } 127 128 const arr = Array.from(selected); 129 130 if (index === 0 && arr?.length > 1) { 131 arr.pop(); 132 } 133 134 arr[index] = name; 135 136 setSelected(arr); 137 }; 138 139 useEffect(() => { 140 if (appName && !selected.length && groups.length) { 141 if (groups.indexOf(appName) !== -1) { 142 setSelected([appName]); 143 setModalOpenStatus(false); 144 } else { 145 setSelected([getGroupNameFromAppName(groups, appName), appName]); 146 } 147 } 148 }, [appName, selected, groups]); 149 150 const listHeight = useMemo(() => { 151 const height = (window?.innerHeight || 0) - 160; 152 153 const listRequiredHeight = 154 // 35 is list item height 155 Math.max(groups?.length || 0, profileTypes?.length || 0) * 35; 156 157 if (height && listRequiredHeight) { 158 return height >= listRequiredHeight ? 'auto' : `${height}px`; 159 } 160 161 return 'auto'; 162 }, [groups, profileTypes]); 163 164 return ( 165 <ModalWithToggle 166 isModalOpen={isModalOpen} 167 setModalOpenStatus={setModalOpenStatus} 168 modalClassName={styles.appSelectorModal} 169 modalHeight={listHeight} 170 noDataEl={ 171 !filteredAppNames?.length ? ( 172 <div data-testid="app-selector-no-data" className={styles.noData}> 173 No Data 174 </div> 175 ) : null 176 } 177 toggleText={appName || LabelString} 178 headerEl={ 179 <> 180 <div className={styles.headerTitle}>{LabelString}</div> 181 <Input 182 name="application seach" 183 type="text" 184 placeholder="Type an app" 185 value={filter} 186 onChange={(e) => setFilter(e.target.value)} 187 className={styles.search} 188 testId="application-search" 189 /> 190 </> 191 } 192 leftSideEl={groups.map((name) => ( 193 <SelectButton 194 name={name} 195 onClick={() => onSelect({ index: 0, name })} 196 fullList={appNames} 197 isSelected={selected?.[0] === name} 198 key={name} 199 /> 200 ))} 201 rightSideEl={profileTypes.map((name) => ( 202 <SelectButton 203 name={name} 204 onClick={() => onSelect({ index: 1, name })} 205 fullList={appNames} 206 isSelected={selected?.[1] === name} 207 key={name} 208 /> 209 ))} 210 /> 211 ); 212 };