@parca/profile 0.16.213 → 0.16.215

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.215](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.214...@parca/profile@0.16.215) (2023-07-20)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## 0.16.214 (2023-07-20)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.16.213](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.212...@parca/profile@0.16.213) (2023-07-18)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -21,6 +21,7 @@ interface IcicleGraphArrowProps {
21
21
  curPath: string[];
22
22
  setCurPath: (path: string[]) => void;
23
23
  navigateTo?: NavigateFunction;
24
+ sortBy: string;
24
25
  }
25
26
  export declare const IcicleGraphArrow: React.NamedExoticComponent<IcicleGraphArrowProps>;
26
27
  export default IcicleGraphArrow;
@@ -31,13 +31,12 @@ export const FIELD_CHILDREN = 'children';
31
31
  export const FIELD_LABELS = 'labels';
32
32
  export const FIELD_CUMULATIVE = 'cumulative';
33
33
  export const FIELD_DIFF = 'diff';
34
- export const IcicleGraphArrow = memo(function IcicleGraphArrow({ table, total, filtered, width, setCurPath, curPath, sampleUnit, navigateTo, }) {
34
+ export const IcicleGraphArrow = memo(function IcicleGraphArrow({ table, total, filtered, width, setCurPath, curPath, sampleUnit, navigateTo, sortBy, }) {
35
35
  const dispatch = useAppDispatch();
36
36
  const [colorProfile] = useUserPreference(USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key);
37
37
  const isDarkMode = useAppSelector(selectDarkMode);
38
38
  const [height, setHeight] = useState(0);
39
39
  const [hoveringRow, setHoveringRow] = useState(null);
40
- const sortBy = FIELD_FUNCTION_NAME; // TODO: make this configurable via UI
41
40
  const svg = useRef(null);
42
41
  const ref = useRef(null);
43
42
  const currentSearchString = selectQueryParam('search_string') ?? '';
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { Table } from 'apache-arrow';
2
3
  import { Flamegraph } from '@parca/client';
3
4
  import { type NavigateFunction } from '@parca/utilities';
@@ -13,7 +14,7 @@ interface ProfileIcicleGraphProps {
13
14
  setNewCurPath: (path: string[]) => void;
14
15
  navigateTo?: NavigateFunction;
15
16
  loading: boolean;
16
- setActionButtons?: (buttons: JSX.Element) => void;
17
+ setActionButtons?: (buttons: React.JSX.Element) => void;
17
18
  }
18
19
  declare const ProfileIcicleGraph: ({ graph, table, total, filtered, curPath, setNewCurPath, sampleUnit, navigateTo, loading, setActionButtons, }: ProfileIcicleGraphProps) => JSX.Element;
19
20
  export default ProfileIcicleGraph;
@@ -11,17 +11,52 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
- import { useEffect, useMemo } from 'react';
15
- import { Button } from '@parca/components';
14
+ import { Fragment, useCallback, useEffect, useMemo } from 'react';
15
+ import { Menu, Transition } from '@headlessui/react';
16
+ import { Icon } from '@iconify/react';
17
+ import { Button, Select, useURLState } from '@parca/components';
16
18
  import { useContainerDimensions } from '@parca/hooks';
17
19
  import { divide, selectQueryParam } from '@parca/utilities';
18
20
  import DiffLegend from '../components/DiffLegend';
19
21
  import IcicleGraph from './IcicleGraph';
20
- import IcicleGraphArrow from './IcicleGraphArrow';
22
+ import IcicleGraphArrow, { FIELD_CUMULATIVE, FIELD_DIFF, FIELD_FUNCTION_NAME, FIELD_LABELS, } from './IcicleGraphArrow';
21
23
  const numberFormatter = new Intl.NumberFormat('en-US');
22
- const ProfileIcicleGraph = ({ graph, table, total, filtered, curPath, setNewCurPath, sampleUnit, navigateTo, loading, setActionButtons, }) => {
24
+ const GroupAndSortActionButtons = ({ navigateTo }) => {
25
+ const [storeSortBy = FIELD_FUNCTION_NAME, setStoreSortBy] = useURLState({
26
+ param: 'sort_by',
27
+ navigateTo,
28
+ });
29
+ const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
30
+ const [storeGroupBy = [FIELD_FUNCTION_NAME], setStoreGroupBy] = useURLState({
31
+ param: 'group_by',
32
+ navigateTo,
33
+ });
34
+ const setGroupBy = useCallback((keys) => {
35
+ setStoreGroupBy(keys);
36
+ }, [setStoreGroupBy]);
37
+ const groupBy = useMemo(() => {
38
+ if (storeGroupBy !== undefined) {
39
+ if (typeof storeGroupBy === 'string') {
40
+ return [storeGroupBy];
41
+ }
42
+ return storeGroupBy;
43
+ }
44
+ return [FIELD_FUNCTION_NAME];
45
+ }, [storeGroupBy]);
46
+ const toggleGroupBy = useCallback((key) => {
47
+ groupBy.includes(key)
48
+ ? setGroupBy(groupBy.filter(v => v !== key)) // remove
49
+ : setGroupBy([...groupBy, key]); // add
50
+ }, [groupBy, setGroupBy]);
51
+ return (_jsxs(_Fragment, { children: [_jsx(GroupByDropdown, { groupBy: groupBy, toggleGroupBy: toggleGroupBy }), _jsx(SortBySelect, { compareMode: compareMode, sortBy: storeSortBy, setSortBy: setStoreSortBy })] }));
52
+ };
53
+ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({ graph, table, total, filtered, curPath, setNewCurPath, sampleUnit, navigateTo, loading, setActionButtons, }) {
23
54
  const compareMode = selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
24
55
  const { ref, dimensions } = useContainerDimensions();
56
+ const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
57
+ param: 'sort_by',
58
+ navigateTo,
59
+ });
25
60
  const [totalFormatted, totalUnfilteredFormatted, isTrimmed, trimmedFormatted, trimmedPercentage, isFiltered, filteredPercentage,] = useMemo(() => {
26
61
  if (graph === undefined) {
27
62
  return ['0', '0', false, '0', '0', false, '0', '0'];
@@ -45,8 +80,8 @@ const ProfileIcicleGraph = ({ graph, table, total, filtered, curPath, setNewCurP
45
80
  if (setActionButtons === undefined) {
46
81
  return;
47
82
  }
48
- setActionButtons(_jsx(_Fragment, { children: _jsx(Button, { color: "neutral", onClick: () => setNewCurPath([]), disabled: curPath.length === 0, variant: "neutral", children: "Reset View" }) }));
49
- }, [setNewCurPath, curPath, setActionButtons]);
83
+ setActionButtons(_jsx("div", { className: "flex w-full justify-end gap-2 pb-2", children: _jsxs("div", { className: "flex w-full items-center justify-between space-x-2", children: [table !== undefined && _jsx(GroupAndSortActionButtons, { navigateTo: navigateTo }), _jsxs("div", { children: [_jsx("label", { className: "inline-block" }), _jsx(Button, { color: "neutral", onClick: () => setNewCurPath([]), disabled: curPath.length === 0, variant: "neutral", children: "Reset View" })] })] }) }));
84
+ }, [navigateTo, table, curPath, setNewCurPath, setActionButtons]);
50
85
  if (graph === undefined && table === undefined)
51
86
  return _jsx("div", { children: "no data..." });
52
87
  if (total === 0n && !loading)
@@ -54,6 +89,56 @@ const ProfileIcicleGraph = ({ graph, table, total, filtered, curPath, setNewCurP
54
89
  if (isTrimmed) {
55
90
  console.info(`Trimmed ${trimmedFormatted} (${trimmedPercentage}%) too small values.`);
56
91
  }
57
- return (_jsxs("div", { className: "relative", children: [compareMode && _jsx(DiffLegend, {}), _jsxs("div", { ref: ref, className: "min-h-48", children: [graph !== undefined && (_jsx(IcicleGraph, { width: dimensions?.width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo })), table !== undefined && (_jsx(IcicleGraphArrow, { width: dimensions?.width, table: table, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }));
92
+ return (_jsxs("div", { className: "relative", children: [compareMode && _jsx(DiffLegend, {}), _jsxs("div", { ref: ref, className: "min-h-48", children: [graph !== undefined && (_jsx(IcicleGraph, { width: dimensions?.width, graph: graph, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo })), table !== undefined && (_jsx(IcicleGraphArrow, { width: dimensions?.width, table: table, total: total, filtered: filtered, curPath: curPath, setCurPath: setNewCurPath, sampleUnit: sampleUnit, navigateTo: navigateTo, sortBy: storeSortBy }))] }), _jsxs("p", { className: "my-2 text-xs", children: ["Showing ", totalFormatted, ' ', isFiltered ? (_jsxs("span", { children: ["(", filteredPercentage, "%) filtered of ", totalUnfilteredFormatted, ' '] })) : (_jsx(_Fragment, {})), "values.", ' '] })] }));
93
+ };
94
+ const groupByOptions = [
95
+ {
96
+ value: FIELD_FUNCTION_NAME,
97
+ label: 'Function Name',
98
+ description: 'Stacktraces are grouped by function names.',
99
+ },
100
+ {
101
+ value: FIELD_LABELS,
102
+ label: 'Labels',
103
+ description: 'Stacktraces are grouped by pprof labels.',
104
+ },
105
+ ];
106
+ const GroupByDropdown = ({ groupBy, toggleGroupBy, }) => {
107
+ const label = groupBy.length === 0
108
+ ? 'Nothing'
109
+ : groupBy.length === 1
110
+ ? groupByOptions.find(option => option.value === groupBy[0])?.label
111
+ : 'Multiple';
112
+ return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Group" }), _jsxs(Menu, { as: "div", className: "relative text-left", children: [_jsx("div", { children: _jsxs(Menu.Button, { className: "relative w-full cursor-default rounded-md border bg-gray-50 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", children: [_jsx("span", { className: "ml-3 block overflow-x-hidden text-ellipsis", children: label }), _jsx("span", { className: "pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400", children: _jsx(Icon, { icon: "heroicons:chevron-down-20-solid", "aria-hidden": "true" }) })] }) }), _jsx(Transition, { as: Fragment, leave: "transition ease-in duration-100", leaveFrom: "opacity-100", leaveTo: "opacity-0", children: _jsx(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", children: _jsx("div", { className: "p-4", children: _jsx("fieldset", { children: _jsx("div", { className: "space-y-5", children: groupByOptions.map(({ value, label, description }) => (_jsxs("div", { className: "relative flex items-start", children: [_jsx("div", { className: "flex h-6 items-center", children: _jsx("input", { id: value, name: value, type: "checkbox", className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600", checked: groupBy.includes(value), onChange: () => {
113
+ toggleGroupBy(value);
114
+ } }) }), _jsxs("div", { className: "ml-3 text-sm leading-6", children: [_jsx("label", { htmlFor: value, className: "font-medium text-gray-900", children: label }), _jsx("p", { className: "text-gray-500", children: description })] })] }, value))) }) }) }) }) })] })] }));
115
+ };
116
+ const SortBySelect = ({ sortBy, setSortBy, compareMode, }) => {
117
+ return (_jsxs("div", { children: [_jsx("label", { className: "text-sm", children: "Sort" }), _jsx(Select, { items: [
118
+ {
119
+ key: FIELD_FUNCTION_NAME,
120
+ disabled: false,
121
+ element: {
122
+ active: _jsx(_Fragment, { children: "Function" }),
123
+ expanded: (_jsx(_Fragment, { children: _jsx("span", { children: "Function" }) })),
124
+ },
125
+ },
126
+ {
127
+ key: FIELD_CUMULATIVE,
128
+ disabled: false,
129
+ element: {
130
+ active: _jsx(_Fragment, { children: "Cumulative" }),
131
+ expanded: (_jsx(_Fragment, { children: _jsx("span", { children: "Cumulative" }) })),
132
+ },
133
+ },
134
+ {
135
+ key: FIELD_DIFF,
136
+ disabled: !compareMode,
137
+ element: {
138
+ active: _jsx(_Fragment, { children: "Diff" }),
139
+ expanded: (_jsx(_Fragment, { children: _jsx("span", { children: "Diff" }) })),
140
+ },
141
+ },
142
+ ], selectedKey: sortBy, onSelection: key => setSortBy(key), placeholder: 'Sort By', primary: false, disabled: false })] }));
58
143
  };
59
144
  export default ProfileIcicleGraph;
@@ -17,12 +17,14 @@ import { QueryRequest_ReportType } from '@parca/client';
17
17
  import { useGrpcMetadata, useParcaContext, useURLState } from '@parca/components';
18
18
  import { USER_PREFERENCES, useUIFeatureFlag, useUserPreference } from '@parca/hooks';
19
19
  import { saveAsBlob } from '@parca/utilities';
20
+ import { FIELD_FUNCTION_NAME } from './ProfileIcicleGraph/IcicleGraphArrow';
20
21
  import { ProfileView } from './ProfileView';
21
22
  import { useQuery } from './useQuery';
22
23
  import { downloadPprof } from './utils';
23
24
  export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, }) => {
24
25
  const metadata = useGrpcMetadata();
25
26
  const [dashboardItems = ['icicle']] = useURLState({ param: 'dashboard_items', navigateTo });
27
+ const [groupBy = [FIELD_FUNCTION_NAME]] = useURLState({ param: 'group_by', navigateTo });
26
28
  const [enableTrimming] = useUserPreference(USER_PREFERENCES.ENABLE_GRAPH_TRIMMING.key);
27
29
  const [arrowFlamegraphEnabled] = useUIFeatureFlag('flamegraph-arrow');
28
30
  const [pprofDownloading, setPprofDownloading] = useState(false);
@@ -40,9 +42,12 @@ export const ProfileViewWithData = ({ queryClient, profileSource, navigateTo, })
40
42
  const reportType = arrowFlamegraphEnabled
41
43
  ? QueryRequest_ReportType.FLAMEGRAPH_ARROW
42
44
  : QueryRequest_ReportType.FLAMEGRAPH_TABLE;
45
+ // make sure we get a string[]
46
+ const groupByParam = typeof groupBy === 'string' ? [groupBy] : groupBy;
43
47
  const { isLoading: flamegraphLoading, response: flamegraphResponse, error: flamegraphError, } = useQuery(queryClient, profileSource, reportType, {
44
48
  skip: !dashboardItems.includes('icicle'),
45
49
  nodeTrimThreshold,
50
+ groupBy: groupByParam,
46
51
  });
47
52
  const { perf } = useParcaContext();
48
53
  const { isLoading: topTableLoading, response: topTableResponse, error: topTableError, } = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP, {
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- /*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.invisible{visibility:hidden}.absolute{position:absolute}.relative{position:relative}.-inset-2{top:-.5rem;right:-.5rem;bottom:-.5rem;left:-.5rem}.left-\[25px\]{left:25px}.left-0{left:0}.top-\[-46px\]{top:-46px}.right-0{right:0}.z-50{z-index:50}.z-10{z-index:10}.m-auto{margin:auto}.m-2{margin:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-20{margin-top:5rem;margin-bottom:5rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mb-2{margin-bottom:.5rem}.mr-6{margin-right:1.5rem}.mr-1{margin-right:.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-fit{height:-moz-fit-content;height:fit-content}.h-10{height:2.5rem}.h-full{height:100%}.h-1{height:.25rem}.h-\[80vh\]{height:80vh}.h-4{height:1rem}.max-h-\[400px\]{max-height:400px}.min-h-52{min-height:13rem}.min-h-48{min-height:12rem}.w-full{width:100%}.w-auto{width:auto}.w-1\/4{width:25%}.w-3\/4{width:75%}.w-\[500px\]{width:500px}.w-40{width:10rem}.w-1\/2{width:50%}.w-8{width:2rem}.w-4{width:1rem}.w-16{width:4rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-\[420px\]{width:420px}.min-w-\[300px\]{min-width:300px}.max-w-\[500px\]{max-width:500px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.translate-y-1{--tw-translate-y:0.25rem}.translate-y-0,.translate-y-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.\!items-center{align-items:center!important}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-1{gap:.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-r-0{border-right-width:0}.border-l-0{border-left-width:0}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.fill-transparent{fill:#0000}.fill-current{fill:currentColor}.stroke-white{stroke:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-10{padding:2.5rem}.p-4{padding:1rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.pr-0{padding-right:0}.pl-3{padding-left:.75rem}.pr-9{padding-right:2.25rem}.pt-2{padding-top:.5rem}.pb-4{padding-bottom:1rem}.pr-2{padding-right:.5rem}.pl-2{padding-left:.5rem}.pb-2{padding-bottom:.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.\!text-indigo-600{--tw-text-opacity:1!important;color:rgb(79 70 229/var(--tw-text-opacity))!important}.opacity-100{opacity:1}.opacity-0{opacity:0}.opacity-90{opacity:.9}.opacity-50{opacity:.5}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\]{--tw-shadow:0 0 10px 2px #0000004d;--tw-shadow-colored:0 0 10px 2px var(--tw-shadow-color)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\],.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.outline-none{outline:2px solid #0000;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-150{transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-indigo-800:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(55 48 163/var(--tw-ring-opacity))}.group:hover .group-hover\:flex{display:flex}[class~=theme-dark] .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}[class~=theme-dark] .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:stroke-gray-700{stroke:#374151}[class~=theme-dark] .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}[class~=theme-dark] .dark\:\!text-indigo-400{--tw-text-opacity:1!important;color:rgb(129 140 248/var(--tw-text-opacity))!important}@media (min-width:640px){.sm\:inline{display:inline}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:1024px){.lg\:w-1\/2{width:50%}}
1
+ /*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.absolute{position:absolute}.relative{position:relative}.-inset-2{top:-.5rem;right:-.5rem;bottom:-.5rem;left:-.5rem}.inset-y-0{top:0;bottom:0}.left-\[25px\]{left:25px}.left-0{left:0}.top-\[-46px\]{top:-46px}.right-0{right:0}.z-50{z-index:50}.z-10{z-index:10}.m-auto{margin:auto}.m-2{margin:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-20{margin-top:5rem;margin-bottom:5rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.ml-3{margin-left:.75rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mb-2{margin-bottom:.5rem}.mr-6{margin-right:1.5rem}.mr-1{margin-right:.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-fit{height:-moz-fit-content;height:fit-content}.h-10{height:2.5rem}.h-6{height:1.5rem}.h-4{height:1rem}.h-full{height:100%}.h-1{height:.25rem}.h-\[80vh\]{height:80vh}.max-h-\[400px\]{max-height:400px}.min-h-52{min-height:13rem}.min-h-48{min-height:12rem}.w-full{width:100%}.w-auto{width:auto}.w-1\/4{width:25%}.w-3\/4{width:75%}.w-\[500px\]{width:500px}.w-4{width:1rem}.w-40{width:10rem}.w-1\/2{width:50%}.w-8{width:2rem}.w-16{width:4rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-\[420px\]{width:420px}.min-w-\[300px\]{min-width:300px}.min-w-\[400px\]{min-width:400px}.max-w-\[500px\]{max-width:500px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.translate-y-1{--tw-translate-y:0.25rem}.translate-y-0,.translate-y-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.\!items-center{align-items:center!important}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-1{gap:.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem*var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-hidden{overflow-x:hidden}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-none{border-radius:0}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-r-0{border-right-width:0}.border-l-0{border-left-width:0}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.fill-transparent{fill:#0000}.fill-current{fill:currentColor}.stroke-white{stroke:#fff}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-10{padding:2.5rem}.p-1{padding:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.pr-0{padding-right:0}.pl-3{padding-left:.75rem}.pr-9{padding-right:2.25rem}.pt-2{padding-top:.5rem}.pb-4{padding-bottom:1rem}.pr-2{padding-right:.5rem}.pl-2{padding-left:.5rem}.pb-2{padding-bottom:.5rem}.pr-10{padding-right:2.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-medium{font-weight:500}.leading-6{line-height:1.5rem}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.\!text-indigo-600{--tw-text-opacity:1!important;color:rgb(79 70 229/var(--tw-text-opacity))!important}.opacity-100{opacity:1}.opacity-0{opacity:0}.opacity-90{opacity:.9}.opacity-50{opacity:.5}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\]{--tw-shadow:0 0 10px 2px #0000004d;--tw-shadow-colored:0 0 10px 2px var(--tw-shadow-color)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\],.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid #0000;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-150{transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-800:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(55 48 163/var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.group:hover .group-hover\:flex{display:flex}[class~=theme-dark] .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}[class~=theme-dark] .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:stroke-gray-700{stroke:#374151}[class~=theme-dark] .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}[class~=theme-dark] .dark\:\!text-indigo-400{--tw-text-opacity:1!important;color:rgb(129 140 248/var(--tw-text-opacity))!important}[class~=theme-dark] .dark\:ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}[class~=theme-dark] .dark\:ring-opacity-20{--tw-ring-opacity:0.2}@media (min-width:640px){.sm\:inline{display:inline}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:1024px){.lg\:w-1\/2{width:50%}}
@@ -9,6 +9,7 @@ export interface IQueryResult {
9
9
  interface UseQueryOptions {
10
10
  skip?: boolean;
11
11
  nodeTrimThreshold?: number;
12
+ groupBy?: string[];
12
13
  }
13
14
  export declare const useQuery: (client: QueryServiceClient, profileSource: ProfileSource, reportType: QueryRequest_ReportType, options?: UseQueryOptions) => IQueryResult;
14
15
  export {};
package/dist/useQuery.js CHANGED
@@ -16,11 +16,14 @@ export const useQuery = (client, profileSource, reportType, options) => {
16
16
  const { skip = false } = options ?? {};
17
17
  const metadata = useGrpcMetadata();
18
18
  const { data, isLoading, error } = useGrpcQuery({
19
- key: ['query', profileSource, reportType, options?.nodeTrimThreshold],
19
+ key: ['query', profileSource, reportType, options?.nodeTrimThreshold, options?.groupBy],
20
20
  queryFn: async () => {
21
21
  const req = profileSource.QueryRequest();
22
22
  req.reportType = reportType;
23
23
  req.nodeTrimThreshold = options?.nodeTrimThreshold;
24
+ req.groupBy = {
25
+ fields: options?.groupBy ?? [],
26
+ };
24
27
  const { response } = await client.query(req, { meta: metadata });
25
28
  return response;
26
29
  },
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.213",
3
+ "version": "0.16.215",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
- "@parca/client": "^0.16.80",
7
- "@parca/components": "^0.16.170",
6
+ "@parca/client": "^0.16.81",
7
+ "@parca/components": "^0.16.172",
8
8
  "@parca/dynamicsize": "^0.16.54",
9
- "@parca/hooks": "^0.0.15",
9
+ "@parca/hooks": "^0.0.16",
10
10
  "@parca/parser": "^0.16.55",
11
- "@parca/store": "^0.16.92",
12
- "@parca/utilities": "^0.0.21",
11
+ "@parca/store": "^0.16.94",
12
+ "@parca/utilities": "^0.0.23",
13
13
  "@tanstack/react-query": "^4.0.5",
14
14
  "@types/react-beautiful-dnd": "^13.1.3",
15
15
  "apache-arrow": "^12.0.0",
@@ -47,5 +47,5 @@
47
47
  "access": "public",
48
48
  "registry": "https://registry.npmjs.org/"
49
49
  },
50
- "gitHead": "13c773e7ee4bd01c09e30e23eaa1f02fd8e39417"
50
+ "gitHead": "452157d0adc6a0b7a2288ce9c787c4ddc2395ac1"
51
51
  }
@@ -58,6 +58,7 @@ interface IcicleGraphArrowProps {
58
58
  curPath: string[];
59
59
  setCurPath: (path: string[]) => void;
60
60
  navigateTo?: NavigateFunction;
61
+ sortBy: string;
61
62
  }
62
63
 
63
64
  export const IcicleGraphArrow = memo(function IcicleGraphArrow({
@@ -69,6 +70,7 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
69
70
  curPath,
70
71
  sampleUnit,
71
72
  navigateTo,
73
+ sortBy,
72
74
  }: IcicleGraphArrowProps): React.JSX.Element {
73
75
  const dispatch = useAppDispatch();
74
76
  const [colorProfile] = useUserPreference<ColorProfileName>(
@@ -78,7 +80,6 @@ export const IcicleGraphArrow = memo(function IcicleGraphArrow({
78
80
 
79
81
  const [height, setHeight] = useState(0);
80
82
  const [hoveringRow, setHoveringRow] = useState<number | null>(null);
81
- const sortBy = FIELD_FUNCTION_NAME; // TODO: make this configurable via UI
82
83
  const svg = useRef(null);
83
84
  const ref = useRef<SVGGElement>(null);
84
85
 
@@ -11,18 +11,25 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect, useMemo} from 'react';
14
+ import React, {Fragment, useCallback, useEffect, useMemo} from 'react';
15
15
 
16
+ import {Menu, Transition} from '@headlessui/react';
17
+ import {Icon} from '@iconify/react';
16
18
  import {Table} from 'apache-arrow';
17
19
 
18
20
  import {Flamegraph} from '@parca/client';
19
- import {Button} from '@parca/components';
21
+ import {Button, Select, useURLState} from '@parca/components';
20
22
  import {useContainerDimensions} from '@parca/hooks';
21
23
  import {divide, selectQueryParam, type NavigateFunction} from '@parca/utilities';
22
24
 
23
25
  import DiffLegend from '../components/DiffLegend';
24
26
  import IcicleGraph from './IcicleGraph';
25
- import IcicleGraphArrow from './IcicleGraphArrow';
27
+ import IcicleGraphArrow, {
28
+ FIELD_CUMULATIVE,
29
+ FIELD_DIFF,
30
+ FIELD_FUNCTION_NAME,
31
+ FIELD_LABELS,
32
+ } from './IcicleGraphArrow';
26
33
 
27
34
  const numberFormatter = new Intl.NumberFormat('en-US');
28
35
 
@@ -39,10 +46,61 @@ interface ProfileIcicleGraphProps {
39
46
  setNewCurPath: (path: string[]) => void;
40
47
  navigateTo?: NavigateFunction;
41
48
  loading: boolean;
42
- setActionButtons?: (buttons: JSX.Element) => void;
49
+ setActionButtons?: (buttons: React.JSX.Element) => void;
43
50
  }
44
51
 
45
- const ProfileIcicleGraph = ({
52
+ const GroupAndSortActionButtons = ({navigateTo}: {navigateTo?: NavigateFunction}): JSX.Element => {
53
+ const [storeSortBy = FIELD_FUNCTION_NAME, setStoreSortBy] = useURLState({
54
+ param: 'sort_by',
55
+ navigateTo,
56
+ });
57
+ const compareMode: boolean =
58
+ selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
59
+
60
+ const [storeGroupBy = [FIELD_FUNCTION_NAME], setStoreGroupBy] = useURLState({
61
+ param: 'group_by',
62
+ navigateTo,
63
+ });
64
+
65
+ const setGroupBy = useCallback(
66
+ (keys: string[]): void => {
67
+ setStoreGroupBy(keys);
68
+ },
69
+ [setStoreGroupBy]
70
+ );
71
+
72
+ const groupBy = useMemo(() => {
73
+ if (storeGroupBy !== undefined) {
74
+ if (typeof storeGroupBy === 'string') {
75
+ return [storeGroupBy];
76
+ }
77
+ return storeGroupBy;
78
+ }
79
+ return [FIELD_FUNCTION_NAME];
80
+ }, [storeGroupBy]);
81
+
82
+ const toggleGroupBy = useCallback(
83
+ (key: string): void => {
84
+ groupBy.includes(key)
85
+ ? setGroupBy(groupBy.filter(v => v !== key)) // remove
86
+ : setGroupBy([...groupBy, key]); // add
87
+ },
88
+ [groupBy, setGroupBy]
89
+ );
90
+
91
+ return (
92
+ <>
93
+ <GroupByDropdown groupBy={groupBy} toggleGroupBy={toggleGroupBy} />
94
+ <SortBySelect
95
+ compareMode={compareMode}
96
+ sortBy={storeSortBy as string}
97
+ setSortBy={setStoreSortBy}
98
+ />
99
+ </>
100
+ );
101
+ };
102
+
103
+ const ProfileIcicleGraph = function ProfileIcicleGraphNonMemo({
46
104
  graph,
47
105
  table,
48
106
  total,
@@ -53,11 +111,16 @@ const ProfileIcicleGraph = ({
53
111
  navigateTo,
54
112
  loading,
55
113
  setActionButtons,
56
- }: ProfileIcicleGraphProps): JSX.Element => {
114
+ }: ProfileIcicleGraphProps): JSX.Element {
57
115
  const compareMode: boolean =
58
116
  selectQueryParam('compare_a') === 'true' && selectQueryParam('compare_b') === 'true';
59
117
  const {ref, dimensions} = useContainerDimensions();
60
118
 
119
+ const [storeSortBy = FIELD_FUNCTION_NAME] = useURLState({
120
+ param: 'sort_by',
121
+ navigateTo,
122
+ });
123
+
61
124
  const [
62
125
  totalFormatted,
63
126
  totalUnfilteredFormatted,
@@ -94,18 +157,24 @@ const ProfileIcicleGraph = ({
94
157
  return;
95
158
  }
96
159
  setActionButtons(
97
- <>
98
- <Button
99
- color="neutral"
100
- onClick={() => setNewCurPath([])}
101
- disabled={curPath.length === 0}
102
- variant="neutral"
103
- >
104
- Reset View
105
- </Button>
106
- </>
160
+ <div className="flex w-full justify-end gap-2 pb-2">
161
+ <div className="flex w-full items-center justify-between space-x-2">
162
+ {table !== undefined && <GroupAndSortActionButtons navigateTo={navigateTo} />}
163
+ <div>
164
+ <label className="inline-block"></label>
165
+ <Button
166
+ color="neutral"
167
+ onClick={() => setNewCurPath([])}
168
+ disabled={curPath.length === 0}
169
+ variant="neutral"
170
+ >
171
+ Reset View
172
+ </Button>
173
+ </div>
174
+ </div>
175
+ </div>
107
176
  );
108
- }, [setNewCurPath, curPath, setActionButtons]);
177
+ }, [navigateTo, table, curPath, setNewCurPath, setActionButtons]);
109
178
 
110
179
  if (graph === undefined && table === undefined) return <div>no data...</div>;
111
180
 
@@ -141,6 +210,7 @@ const ProfileIcicleGraph = ({
141
210
  setCurPath={setNewCurPath}
142
211
  sampleUnit={sampleUnit}
143
212
  navigateTo={navigateTo}
213
+ sortBy={storeSortBy as string}
144
214
  />
145
215
  )}
146
216
  </div>
@@ -159,4 +229,147 @@ const ProfileIcicleGraph = ({
159
229
  );
160
230
  };
161
231
 
232
+ const groupByOptions = [
233
+ {
234
+ value: FIELD_FUNCTION_NAME,
235
+ label: 'Function Name',
236
+ description: 'Stacktraces are grouped by function names.',
237
+ },
238
+ {
239
+ value: FIELD_LABELS,
240
+ label: 'Labels',
241
+ description: 'Stacktraces are grouped by pprof labels.',
242
+ },
243
+ ];
244
+
245
+ const GroupByDropdown = ({
246
+ groupBy,
247
+ toggleGroupBy,
248
+ }: {
249
+ groupBy: string[];
250
+ toggleGroupBy: (key: string) => void;
251
+ }): React.JSX.Element => {
252
+ const label =
253
+ groupBy.length === 0
254
+ ? 'Nothing'
255
+ : groupBy.length === 1
256
+ ? groupByOptions.find(option => option.value === groupBy[0])?.label
257
+ : 'Multiple';
258
+
259
+ return (
260
+ <div>
261
+ <label className="text-sm">Group</label>
262
+ <Menu as="div" className="relative text-left">
263
+ <div>
264
+ <Menu.Button className="relative w-full cursor-default rounded-md border bg-gray-50 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">
265
+ <span className="ml-3 block overflow-x-hidden text-ellipsis">{label}</span>
266
+ <span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2 text-gray-400">
267
+ <Icon icon="heroicons:chevron-down-20-solid" aria-hidden="true" />
268
+ </span>
269
+ </Menu.Button>
270
+ </div>
271
+
272
+ <Transition
273
+ as={Fragment}
274
+ leave="transition ease-in duration-100"
275
+ leaveFrom="opacity-100"
276
+ leaveTo="opacity-0"
277
+ >
278
+ <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">
279
+ <div className="p-4">
280
+ <fieldset>
281
+ <div className="space-y-5">
282
+ {groupByOptions.map(({value, label, description}) => (
283
+ <div key={value} className="relative flex items-start">
284
+ <div className="flex h-6 items-center">
285
+ <input
286
+ id={value}
287
+ name={value}
288
+ type="checkbox"
289
+ className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
290
+ checked={groupBy.includes(value)}
291
+ onChange={() => {
292
+ toggleGroupBy(value);
293
+ }}
294
+ />
295
+ </div>
296
+ <div className="ml-3 text-sm leading-6">
297
+ <label htmlFor={value} className="font-medium text-gray-900">
298
+ {label}
299
+ </label>
300
+ <p className="text-gray-500">{description}</p>
301
+ </div>
302
+ </div>
303
+ ))}
304
+ </div>
305
+ </fieldset>
306
+ </div>
307
+ </Menu.Items>
308
+ </Transition>
309
+ </Menu>
310
+ </div>
311
+ );
312
+ };
313
+
314
+ const SortBySelect = ({
315
+ sortBy,
316
+ setSortBy,
317
+ compareMode,
318
+ }: {
319
+ sortBy: string;
320
+ setSortBy: (key: string) => void;
321
+ compareMode: boolean;
322
+ }): React.JSX.Element => {
323
+ return (
324
+ <div>
325
+ <label className="text-sm">Sort</label>
326
+ <Select
327
+ items={[
328
+ {
329
+ key: FIELD_FUNCTION_NAME,
330
+ disabled: false,
331
+ element: {
332
+ active: <>Function</>,
333
+ expanded: (
334
+ <>
335
+ <span>Function</span>
336
+ </>
337
+ ),
338
+ },
339
+ },
340
+ {
341
+ key: FIELD_CUMULATIVE,
342
+ disabled: false,
343
+ element: {
344
+ active: <>Cumulative</>,
345
+ expanded: (
346
+ <>
347
+ <span>Cumulative</span>
348
+ </>
349
+ ),
350
+ },
351
+ },
352
+ {
353
+ key: FIELD_DIFF,
354
+ disabled: !compareMode,
355
+ element: {
356
+ active: <>Diff</>,
357
+ expanded: (
358
+ <>
359
+ <span>Diff</span>
360
+ </>
361
+ ),
362
+ },
363
+ },
364
+ ]}
365
+ selectedKey={sortBy}
366
+ onSelection={key => setSortBy(key)}
367
+ placeholder={'Sort By'}
368
+ primary={false}
369
+ disabled={false}
370
+ />
371
+ </div>
372
+ );
373
+ };
374
+
162
375
  export default ProfileIcicleGraph;
@@ -20,6 +20,7 @@ import {useGrpcMetadata, useParcaContext, useURLState} from '@parca/components';
20
20
  import {USER_PREFERENCES, useUIFeatureFlag, useUserPreference} from '@parca/hooks';
21
21
  import {saveAsBlob, type NavigateFunction} from '@parca/utilities';
22
22
 
23
+ import {FIELD_FUNCTION_NAME} from './ProfileIcicleGraph/IcicleGraphArrow';
23
24
  import {ProfileSource} from './ProfileSource';
24
25
  import {ProfileView} from './ProfileView';
25
26
  import {useQuery} from './useQuery';
@@ -39,6 +40,7 @@ export const ProfileViewWithData = ({
39
40
  }: ProfileViewWithDataProps): JSX.Element => {
40
41
  const metadata = useGrpcMetadata();
41
42
  const [dashboardItems = ['icicle']] = useURLState({param: 'dashboard_items', navigateTo});
43
+ const [groupBy = [FIELD_FUNCTION_NAME]] = useURLState({param: 'group_by', navigateTo});
42
44
 
43
45
  const [enableTrimming] = useUserPreference<boolean>(USER_PREFERENCES.ENABLE_GRAPH_TRIMMING.key);
44
46
  const [arrowFlamegraphEnabled] = useUIFeatureFlag('flamegraph-arrow');
@@ -61,6 +63,9 @@ export const ProfileViewWithData = ({
61
63
  ? QueryRequest_ReportType.FLAMEGRAPH_ARROW
62
64
  : QueryRequest_ReportType.FLAMEGRAPH_TABLE;
63
65
 
66
+ // make sure we get a string[]
67
+ const groupByParam: string[] = typeof groupBy === 'string' ? [groupBy] : groupBy;
68
+
64
69
  const {
65
70
  isLoading: flamegraphLoading,
66
71
  response: flamegraphResponse,
@@ -68,6 +73,7 @@ export const ProfileViewWithData = ({
68
73
  } = useQuery(queryClient, profileSource, reportType, {
69
74
  skip: !dashboardItems.includes('icicle'),
70
75
  nodeTrimThreshold,
76
+ groupBy: groupByParam,
71
77
  });
72
78
  const {perf} = useParcaContext();
73
79
 
package/src/useQuery.tsx CHANGED
@@ -28,6 +28,7 @@ export interface IQueryResult {
28
28
  interface UseQueryOptions {
29
29
  skip?: boolean;
30
30
  nodeTrimThreshold?: number;
31
+ groupBy?: string[];
31
32
  }
32
33
 
33
34
  export const useQuery = (
@@ -39,11 +40,14 @@ export const useQuery = (
39
40
  const {skip = false} = options ?? {};
40
41
  const metadata = useGrpcMetadata();
41
42
  const {data, isLoading, error} = useGrpcQuery<QueryResponse | undefined>({
42
- key: ['query', profileSource, reportType, options?.nodeTrimThreshold],
43
+ key: ['query', profileSource, reportType, options?.nodeTrimThreshold, options?.groupBy],
43
44
  queryFn: async () => {
44
45
  const req = profileSource.QueryRequest();
45
46
  req.reportType = reportType;
46
47
  req.nodeTrimThreshold = options?.nodeTrimThreshold;
48
+ req.groupBy = {
49
+ fields: options?.groupBy ?? [],
50
+ };
47
51
 
48
52
  const {response} = await client.query(req, {meta: metadata});
49
53
  return response;