|
11 | 11 | // See the License for the specific language governing permissions and |
12 | 12 | // limitations under the License. |
13 | 13 |
|
14 | | -import React, {useEffect, useRef, useState} from 'react'; |
| 14 | +import React from 'react'; |
15 | 15 |
|
16 | | -import {Transition} from '@headlessui/react'; |
17 | | -import {Icon} from '@iconify/react'; |
18 | | -import Select from 'react-select'; |
19 | | - |
20 | | -import {Button} from '@parca/components'; |
21 | | - |
22 | | -import { |
23 | | - FIELD_FUNCTION_FILE_NAME, |
24 | | - FIELD_FUNCTION_NAME, |
25 | | - FIELD_LABELS, |
26 | | - FIELD_LOCATION_ADDRESS, |
27 | | - FIELD_MAPPING_FILE, |
28 | | -} from '../../../ProfileIcicleGraph/IcicleGraphArrow'; |
29 | | - |
30 | | -interface LabelSelectorProps { |
31 | | - labels: string[]; |
32 | | - groupBy: string[]; |
33 | | - setGroupByLabels: (labels: string[]) => void; |
34 | | - isOpen: boolean; |
35 | | - labelsButtonRef: React.RefObject<HTMLDivElement>; |
36 | | - setIsLabelSelectorOpen: (isOpen: boolean) => void; |
37 | | -} |
38 | | - |
39 | | -interface LabelSelectorProps { |
40 | | - labels: string[]; |
41 | | - groupBy: string[]; |
42 | | - setGroupByLabels: (labels: string[]) => void; |
43 | | -} |
44 | | - |
45 | | -interface LabelOption { |
46 | | - label: string; |
47 | | - value: string; |
48 | | -} |
49 | | - |
50 | | -interface GroupByDropdownProps { |
51 | | - groupBy: string[]; |
52 | | - toggleGroupBy: (key: string) => void; |
53 | | - onLabelClick: () => void; |
54 | | - labelsButtonRef: React.RefObject<HTMLDivElement>; |
55 | | -} |
56 | | - |
57 | | -const groupByOptions = [ |
58 | | - { |
59 | | - value: FIELD_FUNCTION_NAME, |
60 | | - label: 'Function Name', |
61 | | - description: 'Stacktraces are grouped by function names.', |
62 | | - disabled: true, |
63 | | - }, |
64 | | - { |
65 | | - value: FIELD_FUNCTION_FILE_NAME, |
66 | | - label: 'Filename', |
67 | | - description: 'Stacktraces are grouped by filenames.', |
68 | | - disabled: false, |
69 | | - }, |
70 | | - { |
71 | | - value: FIELD_LOCATION_ADDRESS, |
72 | | - label: 'Address', |
73 | | - description: 'Stacktraces are grouped by addresses.', |
74 | | - disabled: false, |
75 | | - }, |
76 | | - { |
77 | | - value: FIELD_MAPPING_FILE, |
78 | | - label: 'Binary', |
79 | | - description: 'Stacktraces are grouped by binaries.', |
80 | | - disabled: false, |
81 | | - }, |
82 | | -]; |
83 | | - |
84 | | -const LabelSelector: React.FC<LabelSelectorProps> = ({ |
85 | | - labels, |
86 | | - groupBy, |
87 | | - setGroupByLabels, |
88 | | - isOpen, |
89 | | - labelsButtonRef, |
90 | | - setIsLabelSelectorOpen, |
91 | | -}) => { |
92 | | - const [position, setPosition] = useState({top: 0, left: 0}); |
93 | | - |
94 | | - useEffect(() => { |
95 | | - if (isOpen && labelsButtonRef.current !== null) { |
96 | | - const rect = labelsButtonRef.current.getBoundingClientRect(); |
97 | | - const parentRect = labelsButtonRef.current.offsetParent?.getBoundingClientRect() ?? { |
98 | | - top: 0, |
99 | | - left: 0, |
100 | | - }; |
101 | | - |
102 | | - setPosition({ |
103 | | - top: rect.bottom - parentRect.top, |
104 | | - left: rect.right - parentRect.left + 4, |
105 | | - }); |
106 | | - } |
107 | | - }, [isOpen, labelsButtonRef]); |
108 | | - |
109 | | - if (!isOpen) return null; |
110 | | - |
111 | | - return ( |
112 | | - <div |
113 | | - className="absolute w-64 ml-4 z-20" |
114 | | - style={{ |
115 | | - top: `${position.top}px`, |
116 | | - left: `${position.left}px`, |
117 | | - }} |
118 | | - > |
119 | | - <Select<LabelOption, true> |
120 | | - isMulti |
121 | | - name="labels" |
122 | | - options={labels.map(label => ({label, value: `${FIELD_LABELS}.${label}`}))} |
123 | | - className="parca-select-container text-sm w-full border-gray-300 border rounded-md" |
124 | | - classNamePrefix="parca-select" |
125 | | - value={groupBy |
126 | | - .filter(l => l.startsWith(FIELD_LABELS)) |
127 | | - .map(l => ({value: l, label: l.slice(FIELD_LABELS.length + 1)}))} |
128 | | - onChange={newValue => { |
129 | | - setGroupByLabels(newValue.map(option => option.value)); |
130 | | - setIsLabelSelectorOpen(false); |
131 | | - }} |
132 | | - placeholder="Select labels..." |
133 | | - styles={{ |
134 | | - menu: provided => ({ |
135 | | - ...provided, |
136 | | - position: 'relative', |
137 | | - marginBottom: 0, |
138 | | - boxShadow: 'none', |
139 | | - marginTop: 0, |
140 | | - }), |
141 | | - control: provided => ({ |
142 | | - ...provided, |
143 | | - boxShadow: 'none', |
144 | | - borderBottom: '1px solid #e2e8f0', |
145 | | - borderRight: 0, |
146 | | - borderLeft: 0, |
147 | | - borderTop: 0, |
148 | | - borderBottomLeftRadius: 0, |
149 | | - borderBottomRightRadius: 0, |
150 | | - ':hover': { |
151 | | - borderColor: '#e2e8f0', |
152 | | - borderBottomLeftRadius: 0, |
153 | | - borderBottomRightRadius: 0, |
154 | | - }, |
155 | | - }), |
156 | | - }} |
157 | | - menuIsOpen={true} |
158 | | - /> |
159 | | - </div> |
160 | | - ); |
161 | | -}; |
162 | | - |
163 | | -const GroupByDropdown: React.FC<GroupByDropdownProps> = ({ |
164 | | - groupBy, |
165 | | - toggleGroupBy, |
166 | | - onLabelClick, |
167 | | - labelsButtonRef, |
168 | | -}) => { |
169 | | - const [isDropdownOpen, setIsDropdownOpen] = useState(false); |
170 | | - const dropdownRef = useRef<HTMLDivElement>(null); |
171 | | - |
172 | | - useEffect(() => { |
173 | | - const handleClickOutside = (event: MouseEvent): void => { |
174 | | - if ( |
175 | | - isDropdownOpen && |
176 | | - dropdownRef.current != null && |
177 | | - !dropdownRef.current.contains(event.target as Node) |
178 | | - ) { |
179 | | - setIsDropdownOpen(false); |
180 | | - } |
181 | | - }; |
182 | | - |
183 | | - document.addEventListener('mousedown', handleClickOutside); |
184 | | - return () => { |
185 | | - document.removeEventListener('mousedown', handleClickOutside); |
186 | | - }; |
187 | | - }, [isDropdownOpen]); |
188 | | - |
189 | | - const label = |
190 | | - groupBy.length === 0 |
191 | | - ? 'Nothing' |
192 | | - : groupBy.length === 1 |
193 | | - ? groupByOptions.find(option => option.value === groupBy[0])?.label |
194 | | - : 'Multiple'; |
195 | | - |
196 | | - const selectedLabels = groupBy |
197 | | - .filter(l => l.startsWith(FIELD_LABELS)) |
198 | | - .map(l => l.slice(FIELD_LABELS.length + 1)); |
199 | | - |
200 | | - return ( |
201 | | - <div className="relative" ref={dropdownRef}> |
202 | | - <label className="text-sm">Group by</label> |
203 | | - <div className="relative text-left" id="h-group-by-filter"> |
204 | | - <Button |
205 | | - variant="neutral" |
206 | | - onClick={() => setIsDropdownOpen(!isDropdownOpen)} |
207 | | - className="relative w-max cursor-default rounded-md border bg-white py-2 pl-3 pr-[1.7rem] text-left text-sm shadow-sm dark:border-gray-600 dark:bg-gray-900 sm:text-sm" |
208 | | - > |
209 | | - <span className="block overflow-x-hidden text-ellipsis">{label}</span> |
210 | | - <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400"> |
211 | | - <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" /> |
212 | | - </span> |
213 | | - </Button> |
214 | | - |
215 | | - <Transition |
216 | | - as="div" |
217 | | - leave="transition ease-in duration-100" |
218 | | - leaveFrom="opacity-100" |
219 | | - leaveTo="opacity-0" |
220 | | - show={isDropdownOpen} |
221 | | - > |
222 | | - <div className="absolute left-0 z-10 mt-1 min-w-[400px] overflow-auto rounded-md bg-gray-50 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:ring-white dark:ring-opacity-20 sm:text-sm"> |
223 | | - <div className="p-4"> |
224 | | - <fieldset> |
225 | | - <div className="space-y-5"> |
226 | | - {groupByOptions.map(({value, label, description, disabled}) => ( |
227 | | - <div key={value} className="relative flex items-start"> |
228 | | - <div className="flex h-6 items-center"> |
229 | | - <input |
230 | | - id={value} |
231 | | - name={value} |
232 | | - type="checkbox" |
233 | | - disabled={disabled} |
234 | | - className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" |
235 | | - checked={groupBy.includes(value)} |
236 | | - onChange={() => toggleGroupBy(value)} |
237 | | - /> |
238 | | - </div> |
239 | | - <div className="ml-3 text-sm leading-6"> |
240 | | - <label |
241 | | - htmlFor={value} |
242 | | - className="font-medium text-gray-900 dark:text-gray-200" |
243 | | - > |
244 | | - {label} |
245 | | - </label> |
246 | | - <p className="text-gray-500 dark:text-gray-400">{description}</p> |
247 | | - </div> |
248 | | - </div> |
249 | | - ))} |
250 | | - <div |
251 | | - className="ml-7 flex flex-col items-start text-sm leading-6 cursor-pointer" |
252 | | - onClick={onLabelClick} |
253 | | - ref={labelsButtonRef} |
254 | | - > |
255 | | - <div className="flex justify-between w-full items-center"> |
256 | | - <div> |
257 | | - <span className="font-medium text-gray-900 dark:text-gray-200">Labels</span> |
258 | | - <p className="text-gray-500 dark:text-gray-400"> |
259 | | - Stacktraces are grouped by labels. |
260 | | - </p> |
261 | | - </div> |
262 | | - |
263 | | - <Icon icon="flowbite:caret-right-solid" className="h-[14px] w-[14px]" /> |
264 | | - </div> |
265 | | - |
266 | | - {selectedLabels.length > 0 && ( |
267 | | - <div className="flex gap-2 flex-wrap"> |
268 | | - <span className="text-gray-500 dark:text-gray-200">Selected labels:</span> |
269 | | - |
270 | | - <div className="flex flex-wrap gap-3"> |
271 | | - {selectedLabels.map(label => ( |
272 | | - <span |
273 | | - key={label} |
274 | | - className="mr-2 px-3 py-1 text-xs text-gray-700 dark:text-gray-200 bg-gray-200 rounded-md dark:bg-gray-800" |
275 | | - > |
276 | | - {label} |
277 | | - </span> |
278 | | - ))} |
279 | | - </div> |
280 | | - </div> |
281 | | - )} |
282 | | - </div> |
283 | | - </div> |
284 | | - </fieldset> |
285 | | - </div> |
286 | | - </div> |
287 | | - </Transition> |
288 | | - </div> |
289 | | - </div> |
290 | | - ); |
291 | | -}; |
| 16 | +import GroupByLabelsDropdown from '../GroupByLabelsDropdown'; |
292 | 17 |
|
293 | 18 | interface GroupByControlsProps { |
294 | 19 | groupBy: string[]; |
295 | 20 | labels: string[]; |
296 | | - toggleGroupBy: (key: string) => void; |
297 | 21 | setGroupByLabels: (labels: string[]) => void; |
298 | 22 | } |
299 | 23 |
|
300 | | -const GroupByControls: React.FC<GroupByControlsProps> = ({ |
301 | | - groupBy, |
302 | | - labels, |
303 | | - toggleGroupBy, |
304 | | - setGroupByLabels, |
305 | | -}) => { |
306 | | - const [isLabelSelectorOpen, setIsLabelSelectorOpen] = useState(false); |
307 | | - |
308 | | - const labelsButton = useRef<HTMLDivElement>(null); |
309 | | - const labelSelectorRef = useRef<HTMLDivElement>(null); |
310 | | - |
311 | | - useEffect(() => { |
312 | | - const handleClickOutside = (event: MouseEvent): void => { |
313 | | - if ( |
314 | | - isLabelSelectorOpen && |
315 | | - labelSelectorRef.current !== null && |
316 | | - !labelSelectorRef.current.contains(event.target as Node) && |
317 | | - labelsButton.current !== null && |
318 | | - !labelsButton.current.contains(event.target as Node) |
319 | | - ) { |
320 | | - setIsLabelSelectorOpen(false); |
321 | | - } |
322 | | - }; |
323 | | - |
324 | | - document.addEventListener('mousedown', handleClickOutside); |
325 | | - return () => { |
326 | | - document.removeEventListener('mousedown', handleClickOutside); |
327 | | - }; |
328 | | - }, [isLabelSelectorOpen]); |
329 | | - |
| 24 | +const GroupByControls: React.FC<GroupByControlsProps> = ({groupBy, labels, setGroupByLabels}) => { |
330 | 25 | return ( |
331 | 26 | <div className="inline-flex items-start"> |
332 | | - <div className="relative flex items-start"> |
333 | | - <GroupByDropdown |
| 27 | + <div className="relative flex gap-3 items-start"> |
| 28 | + <GroupByLabelsDropdown |
| 29 | + labels={labels} |
334 | 30 | groupBy={groupBy} |
335 | | - toggleGroupBy={toggleGroupBy} |
336 | | - onLabelClick={() => setIsLabelSelectorOpen(!isLabelSelectorOpen)} |
337 | | - labelsButtonRef={labelsButton} |
| 31 | + setGroupByLabels={setGroupByLabels} |
338 | 32 | /> |
339 | | - <div ref={labelSelectorRef}> |
340 | | - <LabelSelector |
341 | | - labels={labels} |
342 | | - groupBy={groupBy} |
343 | | - setGroupByLabels={setGroupByLabels} |
344 | | - isOpen={isLabelSelectorOpen} |
345 | | - labelsButtonRef={labelsButton} |
346 | | - setIsLabelSelectorOpen={setIsLabelSelectorOpen} |
347 | | - /> |
348 | | - </div> |
349 | 33 | </div> |
350 | 34 | </div> |
351 | 35 | ); |
|
0 commit comments