Skip to content

Commit 2205569

Browse files
committed
ui: Add reusable RefreshButton component and add to group by dropdown
1 parent 41b1829 commit 2205569

File tree

11 files changed

+148
-82
lines changed

11 files changed

+148
-82
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
loading,
32+
sticky = false,
33+
}: RefreshButtonProps): JSX.Element => {
34+
return (
35+
<div
36+
className={cx(
37+
'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',
38+
sticky && 'sticky bottom-0 z-20 mt-auto'
39+
)}
40+
>
41+
<button
42+
onClick={e => {
43+
e.preventDefault();
44+
e.stopPropagation();
45+
onClick();
46+
}}
47+
disabled={disabled}
48+
className={cx(
49+
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
50+
disabled
51+
? 'cursor-wait opacity-50'
52+
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
53+
)}
54+
title={title}
55+
type="button"
56+
data-testid={testId}
57+
>
58+
<Icon
59+
icon="system-uicons:reset"
60+
className={cx('w-3 h-3 text-gray-500 dark:text-gray-400', disabled && 'animate-spin')}
61+
/>
62+
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
63+
</button>
64+
</div>
65+
);
66+
};
67+
68+
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/ProfileView/components/ActionButtons/GroupByDropdown.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,22 @@ interface GroupByControlsProps {
1919
groupBy: string[];
2020
labels: string[];
2121
setGroupByLabels: (labels: string[]) => void;
22+
metadataRefetch?: () => void;
2223
}
2324

24-
const GroupByControls: React.FC<GroupByControlsProps> = ({groupBy, labels, setGroupByLabels}) => {
25+
const GroupByControls: React.FC<GroupByControlsProps> = ({
26+
groupBy,
27+
labels,
28+
setGroupByLabels,
29+
metadataRefetch,
30+
}) => {
2531
return (
2632
<div className="relative flex" id="h-group-by-controls">
2733
<GroupByLabelsDropdown
2834
labels={labels}
2935
groupBy={groupBy}
3036
setGroupByLabels={setGroupByLabels}
37+
metadataRefetch={metadataRefetch}
3138
/>
3239
</div>
3340
);

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

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

14+
import {useCallback, useState} from 'react';
15+
1416
import Select from 'react-select';
1517

18+
import {RefreshButton} from '@parca/components';
1619
import {TEST_IDS, testId} from '@parca/test-utils';
1720

1821
import {FIELD_LABELS} from '../../../ProfileFlameGraph/FlameGraphArrow';
@@ -26,9 +29,28 @@ interface Props {
2629
labels: string[];
2730
groupBy: string[];
2831
setGroupByLabels: (labels: string[]) => void;
32+
metadataRefetch?: () => void;
2933
}
3034

31-
const GroupByLabelsDropdown = ({labels, groupBy, setGroupByLabels}: Props): JSX.Element => {
35+
const GroupByLabelsDropdown = ({
36+
labels,
37+
groupBy,
38+
setGroupByLabels,
39+
metadataRefetch,
40+
}: Props): JSX.Element => {
41+
const [isRefetching, setIsRefetching] = useState(false);
42+
43+
const handleRefetch = useCallback(async () => {
44+
if (metadataRefetch == null || isRefetching) return;
45+
46+
setIsRefetching(true);
47+
try {
48+
await metadataRefetch();
49+
} finally {
50+
setIsRefetching(false);
51+
}
52+
}, [metadataRefetch, isRefetching]);
53+
3254
return (
3355
<div className="flex flex-col relative" {...testId(TEST_IDS.GROUP_BY_CONTAINER)}>
3456
<div className="flex items-center justify-between">
@@ -48,14 +70,24 @@ const GroupByLabelsDropdown = ({labels, groupBy, setGroupByLabels}: Props): JSX.
4870
components={{
4971
// eslint-disable-next-line react/prop-types
5072
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}
73+
<div className="flex flex-col" style={{maxHeight: '332px'}}>
74+
<div
75+
className="overflow-y-auto flex-1"
76+
{...testId(TEST_IDS.GROUP_BY_SELECT_FLYOUT)}
77+
{...innerProps}
78+
// eslint-disable-next-line react/prop-types
79+
style={{...innerProps.style, fontSize: '14px'}}
80+
>
81+
{children}
82+
</div>
83+
{metadataRefetch != null && (
84+
<RefreshButton
85+
onClick={() => void handleRefetch()}
86+
disabled={isRefetching}
87+
title="Refresh label names"
88+
testId="group-by-refresh-button"
89+
/>
90+
)}
5991
</div>
6092
),
6193
}}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface VisualisationToolbarProps {
5454
setAlignFunctionName: (align: string) => void;
5555
colorBy: string;
5656
setColorBy: (colorBy: string) => void;
57+
metadataRefetch?: () => void;
5758
}
5859

5960
export interface TableToolbarProps {
@@ -147,6 +148,7 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
147148
setAlignFunctionName,
148149
colorBy,
149150
setColorBy,
151+
metadataRefetch,
150152
}) => {
151153
const {dashboardItems} = useDashboard();
152154

@@ -172,6 +174,7 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
172174
groupBy={groupBy}
173175
labels={groupByLabels}
174176
setGroupByLabels={setGroupByLabels}
177+
metadataRefetch={metadataRefetch}
175178
/>
176179

177180
<InvertCallStack />

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export const ProfileView = ({
155155
setAlignFunctionName={setAlignFunctionName}
156156
colorBy={colorBy}
157157
setColorBy={setColorBy}
158+
metadataRefetch={flamegraphData.metadataRefetch}
158159
/>
159160

160161
{isColorStackLegendEnabled && (

ui/packages/shared/profile/src/ProfileView/types/visualization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface FlamegraphData {
2424
metadataMappingFiles?: string[];
2525
metadataLoading: boolean;
2626
metadataLabels?: string[];
27+
metadataRefetch?: () => void;
2728
}
2829

2930
export interface TopTableData {

ui/packages/shared/profile/src/ProfileViewWithData.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,12 @@ export const ProfileViewWithData = ({
115115
isLoading: profileMetadataLoading,
116116
response: profileMetadataResponse,
117117
error: profileMetadataError,
118+
refetch: metadataRefetch,
118119
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.PROFILE_METADATA, {
119120
nodeTrimThreshold,
120121
groupBy,
121122
protoFilters,
123+
staleTime: 0,
122124
});
123125

124126
const {perf} = useParcaContext();
@@ -258,6 +260,7 @@ export const ProfileViewWithData = ({
258260
? profileMetadataResponse?.report?.profileMetadata?.labels
259261
: undefined,
260262
metadataLoading: profileMetadataLoading,
263+
metadataRefetch,
261264
}}
262265
flamechartData={{
263266
loading: flamechartLoading && profileMetadataLoading,

ui/packages/shared/profile/src/SimpleMatchers/Select.tsx

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {Icon} from '@iconify/react';
1717
import cx from 'classnames';
1818
import levenshtein from 'fast-levenshtein';
1919

20-
import {Button, DividerWithLabel, useParcaContext} from '@parca/components';
20+
import {Button, DividerWithLabel, RefreshButton, useParcaContext} from '@parca/components';
2121
import {TEST_IDS, testId} from '@parca/test-utils/dist/test-ids';
2222

2323
export interface SelectElement {
@@ -374,34 +374,13 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
374374
)}
375375
</div>
376376
{refetchValues !== undefined && loading !== true && (
377-
<div className="sticky bottom-0 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 z-20 mt-auto">
378-
<button
379-
onClick={e => {
380-
e.preventDefault();
381-
e.stopPropagation();
382-
void handleRefetch();
383-
}}
384-
disabled={isRefetching}
385-
className={cx(
386-
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
387-
isRefetching
388-
? 'cursor-wait opacity-50'
389-
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
390-
)}
391-
title="Refresh label values"
392-
type="button"
393-
{...testId(TEST_IDS.LABEL_VALUE_REFRESH_BUTTON)}
394-
>
395-
<Icon
396-
icon="system-uicons:reset"
397-
className={cx(
398-
'w-3 h-3 text-gray-500 dark:text-gray-400',
399-
isRefetching && 'animate-spin'
400-
)}
401-
/>
402-
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
403-
</button>
404-
</div>
377+
<RefreshButton
378+
onClick={() => void handleRefetch()}
379+
disabled={isRefetching}
380+
title="Refresh label values"
381+
testId={TEST_IDS.LABEL_VALUE_REFRESH_BUTTON}
382+
sticky={true}
383+
/>
405384
)}
406385
</div>
407386
</div>

0 commit comments

Comments
 (0)