Skip to content

Commit 008a879

Browse files
authored
ui: Add refresh results to Group by and Sum by dropdowns (#5999)
* ui: Add reusable RefreshButton component and add to group by dropdown * Add refresh results button to sum by * Extract common Select components to be in SelectWithRefresh component * Add loading indicators for refresh results button
1 parent d6fc66f commit 008a879

File tree

19 files changed

+279
-115
lines changed

19 files changed

+279
-115
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2022 The Parca Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
import {Icon} from '@iconify/react';
15+
import cx from 'classnames';
16+
17+
interface RefreshButtonProps {
18+
onClick: () => void;
19+
disabled: boolean;
20+
title: string;
21+
testId: string;
22+
loading?: boolean;
23+
sticky?: boolean;
24+
}
25+
26+
const RefreshButton = ({
27+
onClick,
28+
disabled,
29+
title,
30+
testId,
31+
sticky = false,
32+
}: RefreshButtonProps): JSX.Element => {
33+
return (
34+
<div
35+
className={cx(
36+
'w-full flex items-center justify-center px-3 py-2 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700',
37+
sticky && 'sticky bottom-0 z-20 mt-auto'
38+
)}
39+
>
40+
<button
41+
onClick={e => {
42+
e.preventDefault();
43+
e.stopPropagation();
44+
onClick();
45+
}}
46+
disabled={disabled}
47+
className={cx(
48+
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
49+
disabled
50+
? 'cursor-wait opacity-50'
51+
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
52+
)}
53+
title={title}
54+
type="button"
55+
data-testid={testId}
56+
>
57+
<Icon
58+
icon="system-uicons:reset"
59+
className={cx('w-3 h-3 text-gray-500 dark:text-gray-400', disabled && 'animate-spin')}
60+
/>
61+
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
62+
</button>
63+
</div>
64+
);
65+
};
66+
67+
export default RefreshButton;

ui/packages/shared/components/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Modal from './Modal';
2626
import {NoDataPrompt} from './NoDataPrompt';
2727
import ParcaContext from './ParcaContext';
2828
import Pill, {PillVariant} from './Pill';
29+
import RefreshButton from './RefreshButton';
2930
import ResponsiveSvg from './ResponsiveSvg';
3031
import Select, {type SelectElement, type SelectItem} from './Select';
3132
import FlameGraphSkeleton, {FlameActionButtonPlaceholder} from './Skeletons/FlamegraphSkeleton';
@@ -69,6 +70,7 @@ export {
6970
ParcaContext,
7071
Pill,
7172
ResponsiveSvg,
73+
RefreshButton,
7274
Select,
7375
SourceSkeleton,
7476
Spinner,

ui/packages/shared/profile/src/MatchersInput/SuggestionsList.tsx

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
import {Fragment, useCallback, useEffect, useState} from 'react';
1515

1616
import {Transition} from '@headlessui/react';
17-
import {Icon} from '@iconify/react';
18-
import cx from 'classnames';
1917
import {usePopper} from 'react-popper';
2018

21-
import {useParcaContext} from '@parca/components';
19+
import {RefreshButton, useParcaContext} from '@parca/components';
2220
import {TEST_IDS, testId} from '@parca/test-utils';
2321

2422
import SuggestionItem from './SuggestionItem';
@@ -66,43 +64,6 @@ const LoadingSpinner = (): JSX.Element => {
6664
return <div className="pt-2 pb-4">{Spinner}</div>;
6765
};
6866

69-
interface RefreshButtonProps {
70-
onClick: () => void;
71-
disabled: boolean;
72-
title: string;
73-
testId: string;
74-
}
75-
76-
const RefreshButton = ({onClick, disabled, title, testId}: RefreshButtonProps): JSX.Element => {
77-
return (
78-
<div className="w-full flex items-center justify-center px-3 py-2 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
79-
<button
80-
onClick={e => {
81-
e.preventDefault();
82-
e.stopPropagation();
83-
onClick();
84-
}}
85-
disabled={disabled}
86-
className={cx(
87-
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
88-
disabled
89-
? 'cursor-wait opacity-50'
90-
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
91-
)}
92-
title={title}
93-
type="button"
94-
data-testid={testId}
95-
>
96-
<Icon
97-
icon="system-uicons:reset"
98-
className={cx('w-3 h-3 text-gray-500 dark:text-gray-400', disabled && 'animate-spin')}
99-
/>
100-
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
101-
</button>
102-
</div>
103-
);
104-
};
105-
10667
const transformLabelsForSuggestions = (labelNames: string, shouldTrimPrefix = false): string => {
10768
const trimmedLabel = shouldTrimPrefix ? labelNames.split('.').pop() ?? labelNames : labelNames;
10869
return trimmedLabel;

ui/packages/shared/profile/src/MatchersInput/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface ILabelNamesResult {
4646
interface UseLabelNames {
4747
result: ILabelNamesResult;
4848
loading: boolean;
49-
refetch: () => void;
49+
refetch: () => Promise<void>;
5050
}
5151

5252
export const useLabelNames = (
@@ -83,8 +83,8 @@ export const useLabelNames = (
8383
return {
8484
result: {response: data, error: error as Error},
8585
loading: isLoading,
86-
refetch: () => {
87-
void refetch();
86+
refetch: async () => {
87+
await refetch();
8888
},
8989
};
9090
};

ui/packages/shared/profile/src/ProfileSelector/QueryControls.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {useState} from 'react';
1515

1616
import {Switch} from '@headlessui/react';
1717
import {RpcError} from '@protobuf-ts/runtime-rpc';
18-
import Select, {type SelectInstance} from 'react-select';
18+
import {type SelectInstance} from 'react-select';
1919

2020
import {ProfileTypesResponse, QueryServiceClient} from '@parca/client';
2121
import {Button, DateTimeRange, DateTimeRangePicker, useParcaContext} from '@parca/components';
@@ -24,6 +24,7 @@ import {TEST_IDS, testId} from '@parca/test-utils';
2424

2525
import MatchersInput from '../MatchersInput';
2626
import ProfileTypeSelector from '../ProfileTypeSelector';
27+
import SelectWithRefresh from '../SelectWithRefresh';
2728
import SimpleMatchers from '../SimpleMatchers';
2829
import ViewMatchers from '../ViewMatchers';
2930

@@ -65,6 +66,7 @@ interface QueryControlsProps {
6566
setUserSumBySelection: (sumBy: string[]) => void;
6667
sumByRef: React.RefObject<SelectInstance>;
6768
profileType: ProfileType;
69+
refreshLabelNames: () => Promise<void>;
6870
}
6971

7072
export function QueryControls({
@@ -93,6 +95,7 @@ export function QueryControls({
9395
profileType,
9496
showSumBySelector,
9597
profileTypesError,
98+
refreshLabelNames,
9699
}: QueryControlsProps): JSX.Element {
97100
const {timezone} = useParcaContext();
98101
const [searchExecutedTimestamp, setSearchExecutedTimestamp] = useState<number>(0);
@@ -203,7 +206,7 @@ export function QueryControls({
203206
Sum by
204207
</label>
205208
</div>
206-
<Select<SelectOption, true>
209+
<SelectWithRefresh<SelectOption, true>
207210
id="h-sum-by-selector"
208211
data-testid={testId(TEST_IDS.SUM_BY_SELECT)['data-testid']}
209212
defaultValue={[]}
@@ -220,7 +223,16 @@ export function QueryControls({
220223
placeholder="Labels..."
221224
styles={{
222225
indicatorSeparator: () => ({display: 'none'}),
223-
menu: provided => ({...provided, width: 'max-content', zIndex: 50}), // Setting the same zIndex as drop down menus
226+
menu: provided => ({
227+
...provided,
228+
marginBottom: 0,
229+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
230+
marginTop: 10,
231+
zIndex: 50,
232+
minWidth: '320px',
233+
position: 'absolute',
234+
}),
235+
// menu: provided => ({...provided, width: 'max-content', zIndex: 50}), // Setting the same zIndex as drop down menus
224236
}}
225237
isLoading={sumBySelectionLoading}
226238
isDisabled={!profileType.delta}
@@ -245,6 +257,10 @@ export function QueryControls({
245257
currentRef.blur();
246258
}
247259
}}
260+
onRefresh={refreshLabelNames}
261+
refreshTitle="Refresh label names"
262+
refreshTestId="sum-by-refresh-button"
263+
menuTestId={TEST_IDS.SUM_BY_SELECT_FLYOUT}
248264
/>
249265
</div>
250266
)}

ui/packages/shared/profile/src/ProfileSelector/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,11 @@ const ProfileSelector = ({
174174
const from = timeRangeSelection.getFromMs();
175175
const to = timeRangeSelection.getToMs();
176176

177-
const {loading: labelNamesLoading, result} = useLabelNames(
178-
queryClient,
179-
profileType.toString(),
180-
from,
181-
to
182-
);
177+
const {
178+
loading: labelNamesLoading,
179+
result,
180+
refetch,
181+
} = useLabelNames(queryClient, profileType.toString(), from, to);
183182
const {loading: selectedLabelNamesLoading, result: selectedLabelNamesResult} = useLabelNames(
184183
queryClient,
185184
selectedProfileType.toString(),
@@ -329,6 +328,7 @@ const ProfileSelector = ({
329328
profileType={profileType}
330329
profileTypesError={error}
331330
viewComponent={viewComponent}
331+
refreshLabelNames={refetch}
332332
/>
333333
{comparing && (
334334
<div>

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,25 @@ interface GroupByControlsProps {
1919
groupBy: string[];
2020
labels: string[];
2121
setGroupByLabels: (labels: string[]) => void;
22+
metadataRefetch?: () => Promise<void>;
23+
metadataLoading: boolean;
2224
}
2325

24-
const GroupByControls: React.FC<GroupByControlsProps> = ({groupBy, labels, setGroupByLabels}) => {
26+
const GroupByControls: React.FC<GroupByControlsProps> = ({
27+
groupBy,
28+
labels,
29+
setGroupByLabels,
30+
metadataRefetch,
31+
metadataLoading,
32+
}) => {
2533
return (
2634
<div className="relative flex" id="h-group-by-controls">
2735
<GroupByLabelsDropdown
2836
labels={labels}
2937
groupBy={groupBy}
3038
setGroupByLabels={setGroupByLabels}
39+
metadataRefetch={metadataRefetch}
40+
metadataLoading={metadataLoading}
3141
/>
3242
</div>
3343
);

ui/packages/shared/profile/src/ProfileView/components/GroupByLabelsDropdown/index.tsx

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

14-
import Select from 'react-select';
15-
1614
import {TEST_IDS, testId} from '@parca/test-utils';
1715

1816
import {FIELD_LABELS} from '../../../ProfileFlameGraph/FlameGraphArrow';
17+
import {SelectWithRefresh} from '../../../SelectWithRefresh';
1918

2019
interface LabelOption {
2120
label: string;
@@ -26,9 +25,17 @@ interface Props {
2625
labels: string[];
2726
groupBy: string[];
2827
setGroupByLabels: (labels: string[]) => void;
28+
metadataRefetch?: () => Promise<void>;
29+
metadataLoading: boolean;
2930
}
3031

31-
const GroupByLabelsDropdown = ({labels, groupBy, setGroupByLabels}: Props): JSX.Element => {
32+
const GroupByLabelsDropdown = ({
33+
labels,
34+
groupBy,
35+
setGroupByLabels,
36+
metadataRefetch,
37+
metadataLoading,
38+
}: Props): JSX.Element => {
3239
return (
3340
<div className="flex flex-col relative" {...testId(TEST_IDS.GROUP_BY_CONTAINER)}>
3441
<div className="flex items-center justify-between">
@@ -37,35 +44,26 @@ const GroupByLabelsDropdown = ({labels, groupBy, setGroupByLabels}: Props): JSX.
3744
</label>
3845
</div>
3946

40-
<Select<LabelOption, true>
47+
<SelectWithRefresh<LabelOption, true>
4148
isMulti
4249
defaultMenuIsOpen={false}
4350
defaultValue={undefined}
4451
name="labels"
4552
options={labels.map(label => ({label, value: `${FIELD_LABELS}.${label}`}))}
4653
className="parca-select-container text-sm rounded-md bg-white"
4754
classNamePrefix="parca-select"
48-
components={{
49-
// eslint-disable-next-line react/prop-types
50-
MenuList: ({children, innerProps}) => (
51-
<div
52-
className="overflow-y-auto"
53-
{...testId(TEST_IDS.GROUP_BY_SELECT_FLYOUT)}
54-
{...innerProps}
55-
// eslint-disable-next-line react/prop-types
56-
style={{...innerProps.style, height: '332px', maxHeight: '332px', fontSize: '14px'}}
57-
>
58-
{children}
59-
</div>
60-
),
61-
}}
55+
onRefresh={metadataRefetch}
56+
refreshTitle="Refresh label names"
57+
refreshTestId="group-by-refresh-button"
58+
menuTestId={TEST_IDS.GROUP_BY_SELECT_FLYOUT}
6259
value={groupBy
6360
.filter(l => l.startsWith(FIELD_LABELS))
6461
.map(l => ({value: l, label: l.slice(FIELD_LABELS.length + 1)}))}
6562
onChange={newValue => {
6663
setGroupByLabels(newValue.map(option => option.value));
6764
}}
6865
placeholder="Select labels..."
66+
isLoading={metadataLoading}
6967
styles={{
7068
menu: provided => ({
7169
...provided,

0 commit comments

Comments
 (0)