@parca/profile 0.16.334 → 0.16.336

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileIcicleGraph/ActionButtons/GroupByDropdown.d.ts +6 -0
  3. package/dist/ProfileIcicleGraph/ActionButtons/GroupByDropdown.js +57 -0
  4. package/dist/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.d.ts +10 -0
  5. package/dist/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.js +23 -0
  6. package/dist/ProfileIcicleGraph/ActionButtons/SortBySelect.d.ts +7 -0
  7. package/dist/ProfileIcicleGraph/ActionButtons/SortBySelect.js +44 -0
  8. package/dist/ProfileIcicleGraph/index.d.ts +2 -1
  9. package/dist/ProfileIcicleGraph/index.js +19 -86
  10. package/dist/ProfileMetricsGraph/index.js +10 -8
  11. package/dist/ProfileView/VisualizationPanel.js +1 -1
  12. package/dist/ProfileView/index.js +6 -30
  13. package/dist/SourceView/index.js +5 -4
  14. package/dist/Table/ColumnsVisibility.d.ts +9 -0
  15. package/dist/Table/ColumnsVisibility.js +22 -0
  16. package/dist/Table/index.d.ts +13 -0
  17. package/dist/Table/index.js +13 -12
  18. package/dist/styles.css +1 -1
  19. package/package.json +6 -5
  20. package/src/ProfileIcicleGraph/ActionButtons/GroupByDropdown.tsx +127 -0
  21. package/src/ProfileIcicleGraph/ActionButtons/RuntimeFilterDropdown.tsx +123 -0
  22. package/src/ProfileIcicleGraph/ActionButtons/SortBySelect.tsx +80 -0
  23. package/src/ProfileIcicleGraph/index.tsx +123 -341
  24. package/src/ProfileMetricsGraph/index.tsx +37 -21
  25. package/src/ProfileView/VisualizationPanel.tsx +19 -8
  26. package/src/ProfileView/index.tsx +52 -81
  27. package/src/SourceView/index.tsx +23 -8
  28. package/src/Table/ColumnsVisibility.tsx +87 -0
  29. package/src/Table/index.tsx +53 -86
  30. package/dist/QueryBrowser/index.d.ts +0 -12
  31. package/dist/QueryBrowser/index.js +0 -51
@@ -11,28 +11,30 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import React, {Fragment, useCallback, useEffect, useMemo} from 'react';
14
+ import React, {useCallback, useEffect, useMemo} from 'react';
15
15
 
16
- import {Menu, Transition} from '@headlessui/react';
17
16
  import {Icon} from '@iconify/react';
17
+ import {AnimatePresence, motion} from 'framer-motion';
18
18
 
19
19
  import {Flamegraph, FlamegraphArrow} from '@parca/client';
20
- import {Button, Select, useParcaContext, useURLState} from '@parca/components';
20
+ import {
21
+ Button,
22
+ IcicleActionButtonPlaceholder,
23
+ IcicleGraphSkeleton,
24
+ IconButton,
25
+ useParcaContext,
26
+ useURLState,
27
+ } from '@parca/components';
21
28
  import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
22
29
  import {capitalizeOnlyFirstLetter, divide, type NavigateFunction} from '@parca/utilities';
23
30
 
24
31
  import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
25
32
  import DiffLegend from '../components/DiffLegend';
33
+ import GroupByDropdown from './ActionButtons/GroupByDropdown';
34
+ import RuntimeFilterDropdown from './ActionButtons/RuntimeFilterDropdown';
35
+ import SortBySelect from './ActionButtons/SortBySelect';
26
36
  import IcicleGraph from './IcicleGraph';
27
- import IcicleGraphArrow, {
28
- FIELD_CUMULATIVE,
29
- FIELD_DIFF,
30
- FIELD_FUNCTION_FILE_NAME,
31
- FIELD_FUNCTION_NAME,
32
- FIELD_LABELS,
33
- FIELD_LOCATION_ADDRESS,
34
- FIELD_MAPPING_FILE,
35
- } from './IcicleGraphArrow';
37
+ import IcicleGraphArrow, {FIELD_FUNCTION_NAME} from './IcicleGraphArrow';
36
38
 
37
39
  const numberFormatter = new Intl.NumberFormat('en-US');
38
40
 
@@ -51,13 +53,20 @@ interface ProfileIcicleGraphProps {
51
53
  loading: boolean;
52
54
  setActionButtons?: (buttons: React.JSX.Element) => void;
53
55
  error?: any;
56
+ isHalfScreen: boolean;
54
57
  }
55
58
 
56
59
  const ErrorContent = ({errorMessage}: {errorMessage: string}): JSX.Element => {
57
60
  return <div className="flex justify-center p-10">{errorMessage}</div>;
58
61
  };
59
62
 
60
- const ShowHideLegendButton = ({navigateTo}: {navigateTo?: NavigateFunction}): JSX.Element => {
63
+ const ShowHideLegendButton = ({
64
+ navigateTo,
65
+ isHalfScreen,
66
+ }: {
67
+ navigateTo?: NavigateFunction;
68
+ isHalfScreen: boolean;
69
+ }): JSX.Element => {
61
70
  const [colorStackLegend, setStoreColorStackLegend] = useURLState({
62
71
  param: 'color_stack_legend',
63
72
  navigateTo,
@@ -81,14 +90,25 @@ const ShowHideLegendButton = ({navigateTo}: {navigateTo?: NavigateFunction}): JS
81
90
  return (
82
91
  <>
83
92
  {colorProfileName === 'default' || compareMode ? null : (
84
- <Button
85
- className="gap-2"
86
- variant="neutral"
87
- onClick={() => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true')}
88
- >
89
- {isColorStackLegendEnabled ? 'Hide legend' : 'Show legend'}
90
- <Icon icon={isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye'} width={20} />
91
- </Button>
93
+ <>
94
+ {isHalfScreen ? (
95
+ <IconButton
96
+ className="rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center !py-2 !px-3 cursor-pointer min-h-[38px]"
97
+ icon={isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye'}
98
+ toolTipText={isColorStackLegendEnabled ? 'Hide legend' : 'Show legend'}
99
+ onClick={() => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true')}
100
+ />
101
+ ) : (
102
+ <Button
103
+ className="gap-2 w-max"
104
+ variant="neutral"
105
+ onClick={() => setColorStackLegend(isColorStackLegendEnabled ? 'false' : 'true')}
106
+ >
107
+ {isColorStackLegendEnabled ? 'Hide legend' : 'Show legend'}
108
+ <Icon icon={isColorStackLegendEnabled ? 'ph:eye-closed' : 'ph:eye'} width={20} />
109
+ </Button>
110
+ )}
111
+ </>
92
112
  )}
93
113
  </>
94
114
  );
@@ -173,110 +193,6 @@ const GroupAndSortActionButtons = ({navigateTo}: {navigateTo?: NavigateFunction}
173
193
  );
174
194
  };
175
195
 
176
- const RuntimeToggle = ({
177
- id,
178
- state,
179
- toggle,
180
- label,
181
- description,
182
- }: {
183
- id: string;
184
- state: boolean;
185
- toggle: () => void;
186
- label: string;
187
- description: string;
188
- }): JSX.Element => {
189
- return (
190
- <div key={id} className="relative flex items-start">
191
- <div className="flex h-6 items-center">
192
- <input
193
- id={id}
194
- name={id}
195
- type="checkbox"
196
- className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
197
- checked={state}
198
- onChange={() => toggle()}
199
- />
200
- </div>
201
- <div className="ml-3 text-sm leading-6">
202
- <label htmlFor={id} className="font-medium text-gray-900 dark:text-gray-200">
203
- {label}
204
- </label>
205
- <p className="text-gray-500 dark:text-gray-400">{description}</p>
206
- </div>
207
- </div>
208
- );
209
- };
210
-
211
- const RuntimeFilterDropdown = ({
212
- showRuntimeRuby,
213
- toggleShowRuntimeRuby,
214
- showRuntimePython,
215
- toggleShowRuntimePython,
216
- showInterpretedOnly,
217
- toggleShowInterpretedOnly,
218
- }: {
219
- showRuntimeRuby: boolean;
220
- toggleShowRuntimeRuby: () => void;
221
- showRuntimePython: boolean;
222
- toggleShowRuntimePython: () => void;
223
- showInterpretedOnly: boolean;
224
- toggleShowInterpretedOnly: () => void;
225
- }): React.JSX.Element => {
226
- return (
227
- <div>
228
- <label className="text-sm">Runtimes</label>
229
- <Menu as="div" className="relative text-left">
230
- <div>
231
- <Menu.Button className="relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-10 text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm">
232
- <span className="ml-3 block overflow-x-hidden text-ellipsis">Runtimes</span>
233
- <span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400">
234
- <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
235
- </span>
236
- </Menu.Button>
237
- </div>
238
-
239
- <Transition
240
- as={Fragment}
241
- leave="transition ease-in duration-100"
242
- leaveFrom="opacity-100"
243
- leaveTo="opacity-0"
244
- >
245
- <Menu.Items 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">
246
- <div className="p-4">
247
- <fieldset>
248
- <div className="space-y-5">
249
- <RuntimeToggle
250
- id="show-runtime-ruby"
251
- state={showRuntimeRuby}
252
- toggle={toggleShowRuntimeRuby}
253
- label="Ruby"
254
- description="Show Ruby runtime functions."
255
- />
256
- <RuntimeToggle
257
- id="show-runtime-python"
258
- state={showRuntimePython}
259
- toggle={toggleShowRuntimePython}
260
- label="Python"
261
- description="Show Python runtime functions."
262
- />
263
- <RuntimeToggle
264
- id="show-interpreted-only"
265
- state={showInterpretedOnly}
266
- toggle={toggleShowInterpretedOnly}
267
- label="Interpreted Only"
268
- description="Show only interpreted functions."
269
- />
270
- </div>
271
- </fieldset>
272
- </div>
273
- </Menu.Items>
274
- </Transition>
275
- </Menu>
276
- </div>
277
- );
278
- };
279
-
280
196
  const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
281
197
  graph,
282
198
  arrow,
@@ -290,8 +206,9 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
290
206
  setActionButtons,
291
207
  error,
292
208
  width,
209
+ isHalfScreen,
293
210
  }: ProfileIcicleGraphProps): JSX.Element {
294
- const {loader, onError, authenticationErrorMessage} = useParcaContext();
211
+ const {onError, authenticationErrorMessage, isDarkMode} = useParcaContext();
295
212
  const {compareMode} = useProfileViewContext();
296
213
 
297
214
  const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
@@ -330,28 +247,50 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
330
247
  }, [graph, arrow, filtered, total]);
331
248
 
332
249
  useEffect(() => {
250
+ if (loading && setActionButtons !== undefined) {
251
+ setActionButtons(<IcicleActionButtonPlaceholder isHalfScreen={isHalfScreen} />);
252
+ return;
253
+ }
254
+
333
255
  if (setActionButtons === undefined) {
334
256
  return;
335
257
  }
258
+
336
259
  setActionButtons(
337
260
  <div className="flex w-full justify-end gap-2 pb-2">
338
261
  <div className="ml-2 flex w-full flex-col items-start justify-between gap-2 md:flex-row md:items-end">
339
262
  {arrow !== undefined && <GroupAndSortActionButtons navigateTo={navigateTo} />}
340
- <ShowHideLegendButton navigateTo={navigateTo} />
341
- <Button
342
- variant="neutral"
343
- onClick={() => setNewCurPath([])}
344
- disabled={curPath.length === 0}
345
- >
346
- Reset View
347
- </Button>
263
+ <ShowHideLegendButton isHalfScreen={isHalfScreen} navigateTo={navigateTo} />
264
+ {isHalfScreen ? (
265
+ <IconButton
266
+ icon="system-uicons:reset"
267
+ disabled={curPath.length === 0}
268
+ toolTipText="Reset View"
269
+ onClick={() => setNewCurPath([])}
270
+ className="rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 items-center flex border border-gray-200 dark:border-gray-600 dark:text-white justify-center py-2 px-3 cursor-pointer min-h-[38px]"
271
+ />
272
+ ) : (
273
+ <Button
274
+ variant="neutral"
275
+ className="gap-2 w-max"
276
+ onClick={() => setNewCurPath([])}
277
+ disabled={curPath.length === 0}
278
+ >
279
+ Reset View
280
+ <Icon icon="system-uicons:reset" width={20} />
281
+ </Button>
282
+ )}
348
283
  </div>
349
284
  </div>
350
285
  );
351
- }, [navigateTo, arrow, curPath, setNewCurPath, setActionButtons]);
286
+ }, [navigateTo, arrow, curPath, setNewCurPath, setActionButtons, loading, isHalfScreen]);
352
287
 
353
288
  if (loading) {
354
- return <div className="h-96">{loader}</div>;
289
+ return (
290
+ <div className="h-auto overflow-clip">
291
+ <IcicleGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
292
+ </div>
293
+ );
355
294
  }
356
295
 
357
296
  if (error != null) {
@@ -375,212 +314,55 @@ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
375
314
  }
376
315
 
377
316
  return (
378
- <div className="relative">
379
- {compareMode ? <DiffLegend /> : null}
380
- <div className="min-h-48">
381
- {graph !== undefined && (
382
- <IcicleGraph
383
- width={width}
384
- graph={graph}
385
- total={total}
386
- filtered={filtered}
387
- curPath={curPath}
388
- setCurPath={setNewCurPath}
389
- sampleUnit={sampleUnit}
390
- navigateTo={navigateTo}
391
- />
392
- )}
393
- {arrow !== undefined && (
394
- <IcicleGraphArrow
395
- width={width}
396
- arrow={arrow}
397
- total={total}
398
- filtered={filtered}
399
- curPath={curPath}
400
- setCurPath={setNewCurPath}
401
- sampleUnit={sampleUnit}
402
- navigateTo={navigateTo}
403
- sortBy={storeSortBy as string}
404
- />
405
- )}
406
- </div>
407
- <p className="my-2 text-xs">
408
- Showing {totalFormatted}{' '}
409
- {isFiltered ? (
410
- <span>
411
- ({filteredPercentage}%) filtered of {totalUnfilteredFormatted}{' '}
412
- </span>
413
- ) : (
414
- <></>
415
- )}
416
- values.{' '}
417
- </p>
418
- </div>
419
- );
420
- };
421
-
422
- const groupByOptions = [
423
- {
424
- value: FIELD_FUNCTION_NAME,
425
- label: 'Function Name',
426
- description: 'Stacktraces are grouped by function names.',
427
- disabled: true,
428
- },
429
- {
430
- value: FIELD_LABELS,
431
- label: 'Labels',
432
- description: 'Stacktraces are grouped by pprof labels.',
433
- disabled: false,
434
- },
435
- {
436
- value: FIELD_FUNCTION_FILE_NAME,
437
- label: 'Filename',
438
- description: 'Stacktraces are grouped by filenames.',
439
- disabled: false,
440
- },
441
- {
442
- value: FIELD_LOCATION_ADDRESS,
443
- label: 'Address',
444
- description: 'Stacktraces are grouped by addresses.',
445
- disabled: false,
446
- },
447
- {
448
- value: FIELD_MAPPING_FILE,
449
- label: 'Binary',
450
- description: 'Stacktraces are grouped by binaries.',
451
- disabled: false,
452
- },
453
- ];
454
-
455
- const GroupByDropdown = ({
456
- groupBy,
457
- toggleGroupBy,
458
- }: {
459
- groupBy: string[];
460
- toggleGroupBy: (key: string) => void;
461
- }): React.JSX.Element => {
462
- const label =
463
- groupBy.length === 0
464
- ? 'Nothing'
465
- : groupBy.length === 1
466
- ? groupByOptions.find(option => option.value === groupBy[0])?.label
467
- : 'Multiple';
468
-
469
- return (
470
- <div>
471
- <label className="text-sm">Group</label>
472
- <Menu as="div" className="relative text-left">
473
- <div>
474
- <Menu.Button className="relative w-full cursor-default rounded-md border bg-white py-2 pl-3 pr-10 text-left text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-900 sm:text-sm">
475
- <span className="ml-3 block overflow-x-hidden text-ellipsis">{label}</span>
476
- <span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400">
477
- <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
478
- </span>
479
- </Menu.Button>
317
+ <AnimatePresence>
318
+ <motion.div
319
+ className="relative h-full w-full"
320
+ key="icicle-graph-loaded"
321
+ initial={{opacity: 0}}
322
+ animate={{opacity: 1}}
323
+ transition={{duration: 0.5}}
324
+ >
325
+ {compareMode ? <DiffLegend /> : null}
326
+ <div className="min-h-48">
327
+ {graph !== undefined && (
328
+ <IcicleGraph
329
+ width={width}
330
+ graph={graph}
331
+ total={total}
332
+ filtered={filtered}
333
+ curPath={curPath}
334
+ setCurPath={setNewCurPath}
335
+ sampleUnit={sampleUnit}
336
+ navigateTo={navigateTo}
337
+ />
338
+ )}
339
+ {arrow !== undefined && (
340
+ <IcicleGraphArrow
341
+ width={width}
342
+ arrow={arrow}
343
+ total={total}
344
+ filtered={filtered}
345
+ curPath={curPath}
346
+ setCurPath={setNewCurPath}
347
+ sampleUnit={sampleUnit}
348
+ navigateTo={navigateTo}
349
+ sortBy={storeSortBy as string}
350
+ />
351
+ )}
480
352
  </div>
481
-
482
- <Transition
483
- as={Fragment}
484
- leave="transition ease-in duration-100"
485
- leaveFrom="opacity-100"
486
- leaveTo="opacity-0"
487
- >
488
- <Menu.Items 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">
489
- <div className="p-4">
490
- <fieldset>
491
- <div className="space-y-5">
492
- {groupByOptions.map(({value, label, description, disabled}) => (
493
- <div key={value} className="relative flex items-start">
494
- <div className="flex h-6 items-center">
495
- <input
496
- id={value}
497
- name={value}
498
- type="checkbox"
499
- disabled={disabled}
500
- className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
501
- checked={groupBy.includes(value)}
502
- onChange={() => toggleGroupBy(value)}
503
- />
504
- </div>
505
- <div className="ml-3 text-sm leading-6">
506
- <label
507
- htmlFor={value}
508
- className="font-medium text-gray-900 dark:text-gray-200"
509
- >
510
- {label}
511
- </label>
512
- <p className="text-gray-500 dark:text-gray-400">{description}</p>
513
- </div>
514
- </div>
515
- ))}
516
- </div>
517
- </fieldset>
518
- </div>
519
- </Menu.Items>
520
- </Transition>
521
- </Menu>
522
- </div>
523
- );
524
- };
525
-
526
- const SortBySelect = ({
527
- sortBy,
528
- setSortBy,
529
- compareMode,
530
- }: {
531
- sortBy: string;
532
- setSortBy: (key: string) => void;
533
- compareMode: boolean;
534
- }): React.JSX.Element => {
535
- return (
536
- <div>
537
- <label className="text-sm">Sort</label>
538
- <Select
539
- items={[
540
- {
541
- key: FIELD_FUNCTION_NAME,
542
- disabled: false,
543
- element: {
544
- active: <>Function</>,
545
- expanded: (
546
- <>
547
- <span>Function</span>
548
- </>
549
- ),
550
- },
551
- },
552
- {
553
- key: FIELD_CUMULATIVE,
554
- disabled: false,
555
- element: {
556
- active: <>Cumulative</>,
557
- expanded: (
558
- <>
559
- <span>Cumulative</span>
560
- </>
561
- ),
562
- },
563
- },
564
- {
565
- key: FIELD_DIFF,
566
- disabled: !compareMode,
567
- element: {
568
- active: <>Diff</>,
569
- expanded: (
570
- <>
571
- <span>Diff</span>
572
- </>
573
- ),
574
- },
575
- },
576
- ]}
577
- selectedKey={sortBy}
578
- onSelection={key => setSortBy(key)}
579
- placeholder={'Sort By'}
580
- primary={false}
581
- disabled={false}
582
- />
583
- </div>
353
+ <p className="my-2 text-xs">
354
+ Showing {totalFormatted}{' '}
355
+ {isFiltered ? (
356
+ <span>
357
+ ({filteredPercentage}%) filtered of {totalUnfilteredFormatted}{' '}
358
+ </span>
359
+ ) : (
360
+ <></>
361
+ )}
362
+ values.{' '}
363
+ </p>
364
+ </motion.div>
365
+ </AnimatePresence>
584
366
  );
585
367
  };
586
368
 
@@ -14,9 +14,15 @@
14
14
  import {useEffect, useState} from 'react';
15
15
 
16
16
  import {RpcError} from '@protobuf-ts/runtime-rpc';
17
+ import {AnimatePresence, motion} from 'framer-motion';
17
18
 
18
19
  import {Duration, Label, QueryRangeResponse, QueryServiceClient, Timestamp} from '@parca/client';
19
- import {DateTimeRange, useGrpcMetadata, useParcaContext} from '@parca/components';
20
+ import {
21
+ DateTimeRange,
22
+ MetricsGraphSkeleton,
23
+ useGrpcMetadata,
24
+ useParcaContext,
25
+ } from '@parca/components';
20
26
  import {Query} from '@parca/parser';
21
27
  import {capitalizeOnlyFirstLetter, getStepDuration} from '@parca/utilities';
22
28
 
@@ -127,8 +133,8 @@ const ProfileMetricsGraph = ({
127
133
  }: ProfileMetricsGraphProps): JSX.Element => {
128
134
  const {isLoading, response, error} = useQueryRange(queryClient, queryExpression, from, to);
129
135
  const isLoaderVisible = useDelayedLoader(isLoading);
130
- const {loader, onError, perf, authenticationErrorMessage} = useParcaContext();
131
- const {width, height, margin} = useMetricsGraphDimensions(comparing);
136
+ const {onError, perf, authenticationErrorMessage, isDarkMode} = useParcaContext();
137
+ const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
132
138
 
133
139
  useEffect(() => {
134
140
  if (error !== null) {
@@ -147,11 +153,13 @@ const ProfileMetricsGraph = ({
147
153
  const series = response?.series;
148
154
  const dataAvailable = series !== null && series !== undefined && series?.length > 0;
149
155
 
150
- if (isLoaderVisible || (isLoading && !dataAvailable)) {
151
- return <>{loader}</>;
156
+ const metricsGraphLoading = isLoaderVisible || (isLoading && !dataAvailable);
157
+
158
+ if (metricsGraphLoading) {
159
+ return <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />;
152
160
  }
153
161
 
154
- if (error !== null) {
162
+ if (!metricsGraphLoading && error !== null) {
155
163
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
156
164
  return <ErrorContent errorMessage={authenticationErrorMessage} />;
157
165
  }
@@ -165,21 +173,29 @@ const ProfileMetricsGraph = ({
165
173
  };
166
174
 
167
175
  return (
168
- <div className="h-full w-full">
169
- <MetricsGraph
170
- data={series}
171
- from={from}
172
- to={to}
173
- profile={profile as MergedProfileSelection}
174
- setTimeRange={setTimeRange}
175
- onSampleClick={handleSampleClick}
176
- addLabelMatcher={addLabelMatcher}
177
- sampleUnit={Query.parse(queryExpression).profileType().sampleUnit}
178
- height={height}
179
- width={width}
180
- margin={margin}
181
- />
182
- </div>
176
+ <AnimatePresence>
177
+ <motion.div
178
+ className="h-full w-full"
179
+ key="metrics-graph-loaded"
180
+ initial={{display: 'none', opacity: 0}}
181
+ animate={{display: 'block', opacity: 1}}
182
+ transition={{duration: 0.5}}
183
+ >
184
+ <MetricsGraph
185
+ data={series}
186
+ from={from}
187
+ to={to}
188
+ profile={profile as MergedProfileSelection}
189
+ setTimeRange={setTimeRange}
190
+ onSampleClick={handleSampleClick}
191
+ addLabelMatcher={addLabelMatcher}
192
+ sampleUnit={Query.parse(queryExpression).profileType().sampleUnit}
193
+ height={height}
194
+ width={width}
195
+ margin={margin}
196
+ />
197
+ </motion.div>
198
+ </AnimatePresence>
183
199
  );
184
200
  }
185
201
 
@@ -51,22 +51,33 @@ export const VisualizationPanel = React.memo(function VisualizationPanel({
51
51
 
52
52
  return (
53
53
  <>
54
- <div className="flex w-full items-start justify-end gap-2 pb-2">
55
- <div className="flex w-full items-start justify-between flex-col-reverse md:flex-row">
56
- <div className="flex items-start">
54
+ <div className="flex w-full items-center justify-end gap-2 pb-2 min-h-[78px]">
55
+ <div
56
+ className={cx(
57
+ 'flex w-full justify-between flex-col-reverse md:flex-row',
58
+ isMultiPanelView && dashboardItem === 'icicle' ? 'items-end gap-x-2' : 'items-end'
59
+ )}
60
+ >
61
+ <div className="flex items-center">
57
62
  <div
58
63
  className={cx(isMultiPanelView ? '' : 'hidden', 'flex items-center')}
59
64
  {...dragHandleProps}
60
65
  >
61
66
  <Icon className="text-xl" icon="material-symbols:drag-indicator" />
62
67
  </div>
63
- <div>{actionButtons}</div>
68
+ <div className="flex gap-2">{actionButtons}</div>
64
69
  </div>
65
- <div className="flex flex-col items-end gap-4">
70
+ <div
71
+ className={cx(
72
+ 'flex flex-row items-center gap-4',
73
+ isMultiPanelView && dashboardItem === 'icicle' && 'pb-[10px]'
74
+ )}
75
+ >
66
76
  <ViewSelector defaultValue={dashboardItem} navigateTo={navigateTo} position={index} />
67
- <div className="px-2">
68
- {dashboardItem === 'icicle' && flamegraphHint != null ? flamegraphHint : null}
69
- </div>
77
+
78
+ {dashboardItem === 'icicle' && flamegraphHint != null ? (
79
+ <div className="px-2">{flamegraphHint}</div>
80
+ ) : null}
70
81
  </div>
71
82
  </div>
72
83
  {isMultiPanelView && (