Skip to content

Commit 8e7fc5e

Browse files
authored
ui: Redesign the visualisation toolbar (#5742)
* area/ui: Add levels of profiling and group by labels dropdown * fix lint errors * Move "Levels of profiling" to now renamed "Preferences" dropdown * Add more settings to the preferences dropdown * Add an invert call stack button * Add a SwitchMenuItem component * Customize toolbar for Table view * Fix issues with view selector inner button * Single selection for levels of profiling * Add ts-expect error back * add min-width to labels dropdown
1 parent af6aba0 commit 8e7fc5e

File tree

13 files changed

+392
-387
lines changed

13 files changed

+392
-387
lines changed

ui/packages/app/web/src/components/ui/Navbar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const links: {[path: string]: {label: string; href: string; external: boolean}}
3232
'/': {label: 'Explorer', href: `/`, external: false},
3333
'/compare': {label: 'Compare', href: 'compare', external: false},
3434
'/targets': {label: 'Targets', href: `/targets`, external: false},
35-
'/settings': {label: 'Settings', href: `/settings`, external: false},
3635
'/help': {label: 'Help', href: 'https://parca.dev/docs/overview', external: true},
3736
};
3837

ui/packages/app/web/src/style/react-select.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
.dark .parca-select__option:hover,
1616
.dark .parca-select__option--is-focused {
17-
background: #005dee !important;
17+
background: #4f46e5 !important;
1818
}
1919

2020
.dark .parca-select__input-container {

ui/packages/shared/profile/src/ProfileView/components/ActionButtons/GroupByDropdown.tsx

Lines changed: 7 additions & 323 deletions
Original file line numberDiff line numberDiff line change
@@ -11,341 +11,25 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
import React, {useEffect, useRef, useState} from 'react';
14+
import React from 'react';
1515

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';
29217

29318
interface GroupByControlsProps {
29419
groupBy: string[];
29520
labels: string[];
296-
toggleGroupBy: (key: string) => void;
29721
setGroupByLabels: (labels: string[]) => void;
29822
}
29923

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}) => {
33025
return (
33126
<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}
33430
groupBy={groupBy}
335-
toggleGroupBy={toggleGroupBy}
336-
onLabelClick={() => setIsLabelSelectorOpen(!isLabelSelectorOpen)}
337-
labelsButtonRef={labelsButton}
31+
setGroupByLabels={setGroupByLabels}
33832
/>
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>
34933
</div>
35034
</div>
35135
);

0 commit comments

Comments
 (0)