@parca/profile 0.8.0 → 0.9.2
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 +20 -0
- package/package.json +5 -5
- package/src/IcicleGraph.tsx +3 -8
- package/src/ProfileView.styles.css +3 -0
- package/src/ProfileView.tsx +116 -27
- package/src/TopTable.styles.css +7 -0
- package/src/TopTable.tsx +188 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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.9.2](https://github.com/parca-dev/parca/compare/ui-v0.9.1...ui-v0.9.2) (2022-02-22)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.9.1](https://github.com/parca-dev/parca/compare/ui-v0.9.0...ui-v0.9.1) (2022-02-21)
|
|
11
|
+
|
|
12
|
+
## [0.8.2](https://github.com/parca-dev/parca/compare/ui-v0.8.1...ui-v0.8.2) (2022-02-14)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @parca/profile
|
|
15
|
+
|
|
16
|
+
# [0.9.0](https://github.com/parca-dev/parca/compare/ui-v0.8.3...ui-v0.9.0) (2022-02-16)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @parca/profile
|
|
19
|
+
|
|
20
|
+
## [0.8.2](https://github.com/parca-dev/parca/compare/ui-v0.8.1...ui-v0.8.2) (2022-02-14)
|
|
21
|
+
|
|
22
|
+
# [0.8.0](https://github.com/parca-dev/parca/compare/ui-v0.7.13...ui-v0.8.0) (2022-01-31)
|
|
23
|
+
|
|
24
|
+
**Note:** Version bump only for package @parca/profile
|
|
25
|
+
|
|
6
26
|
# [0.8.0](https://github.com/parca-dev/parca/compare/ui-v0.7.13...ui-v0.8.0) (2022-01-31)
|
|
7
27
|
|
|
8
28
|
**Note:** Version bump only for package @parca/profile
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@parca/client": "^0.
|
|
7
|
-
"@parca/dynamicsize": "^0.
|
|
8
|
-
"@parca/parser": "^0.
|
|
6
|
+
"@parca/client": "^0.9.1",
|
|
7
|
+
"@parca/dynamicsize": "^0.9.0",
|
|
8
|
+
"@parca/parser": "^0.9.0",
|
|
9
9
|
"d3-scale": "^4.0.2"
|
|
10
10
|
},
|
|
11
11
|
"main": "src/index.tsx",
|
|
@@ -19,5 +19,5 @@
|
|
|
19
19
|
"access": "public",
|
|
20
20
|
"registry": "https://registry.npmjs.org/"
|
|
21
21
|
},
|
|
22
|
-
"gitHead": "
|
|
22
|
+
"gitHead": "544d4819ace2db4570e2780876108636860ff0d2"
|
|
23
23
|
}
|
package/src/IcicleGraph.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import {pointer} from 'd3-selection';
|
|
|
4
4
|
import {scaleLinear} from 'd3-scale';
|
|
5
5
|
import {Flamegraph, FlamegraphNode, FlamegraphRootNode} from '@parca/client';
|
|
6
6
|
import {usePopper} from 'react-popper';
|
|
7
|
-
import {valueFormatter} from '@parca/functions';
|
|
7
|
+
import {getLastItem, valueFormatter} from '@parca/functions';
|
|
8
8
|
|
|
9
9
|
const RowHeight = 20;
|
|
10
10
|
|
|
@@ -106,13 +106,6 @@ function diffColor(diff: number, cumulative: number): string {
|
|
|
106
106
|
return color;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
function getLastItem(thePath: string): string {
|
|
110
|
-
const index = thePath.lastIndexOf('/');
|
|
111
|
-
if (index === -1) return thePath;
|
|
112
|
-
|
|
113
|
-
return thePath.substring(index + 1);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
109
|
export function nodeLabel(node: FlamegraphNode.AsObject): string {
|
|
117
110
|
if (node.meta === undefined) return '<unknown>';
|
|
118
111
|
const mapping = `${
|
|
@@ -280,6 +273,7 @@ const FlamegraphNodeTooltipTableRows = ({
|
|
|
280
273
|
function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0) {
|
|
281
274
|
const domRect = contextElement.getBoundingClientRect();
|
|
282
275
|
return () =>
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
283
277
|
({
|
|
284
278
|
width: 0,
|
|
285
279
|
height: 0,
|
|
@@ -292,6 +286,7 @@ function generateGetBoundingClientRect(contextElement: Element, x = 0, y = 0) {
|
|
|
292
286
|
|
|
293
287
|
const virtualElement = {
|
|
294
288
|
getBoundingClientRect: () =>
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
295
290
|
({
|
|
296
291
|
width: 0,
|
|
297
292
|
height: 0,
|
package/src/ProfileView.tsx
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import React, {useEffect, useState
|
|
2
|
-
// import ProfileSVG from './ProfileSVG'
|
|
3
|
-
// import ProfileTop from './ProfileTop'
|
|
1
|
+
import React, {useEffect, useState} from 'react';
|
|
4
2
|
import {CalcWidth} from '@parca/dynamicsize';
|
|
5
|
-
import
|
|
6
|
-
import {ProfileSource} from './ProfileSource';
|
|
3
|
+
import {parseParams} from '@parca/functions';
|
|
7
4
|
import {QueryRequest, QueryResponse, QueryServiceClient, ServiceError} from '@parca/client';
|
|
8
|
-
import Card from '../../../app/web/src/components/ui/Card';
|
|
9
5
|
import Button from '@parca/web/src/components/ui/Button';
|
|
10
6
|
import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
|
|
11
7
|
|
|
8
|
+
import ProfileIcicleGraph from './ProfileIcicleGraph';
|
|
9
|
+
import {ProfileSource} from './ProfileSource';
|
|
10
|
+
import Card from '../../../app/web/src/components/ui/Card';
|
|
11
|
+
import TopTable from './TopTable';
|
|
12
|
+
|
|
13
|
+
import './ProfileView.styles.css';
|
|
14
|
+
|
|
15
|
+
type NavigateFunction = (path: string, queryParams: any) => void;
|
|
16
|
+
|
|
12
17
|
interface ProfileViewProps {
|
|
13
18
|
queryClient: QueryServiceClient;
|
|
14
19
|
profileSource: ProfileSource;
|
|
20
|
+
navigateTo?: NavigateFunction;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export interface IQueryResult {
|
|
@@ -52,9 +58,16 @@ export const useQuery = (
|
|
|
52
58
|
return result;
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
export const ProfileView = ({
|
|
61
|
+
export const ProfileView = ({
|
|
62
|
+
queryClient,
|
|
63
|
+
profileSource,
|
|
64
|
+
navigateTo,
|
|
65
|
+
}: ProfileViewProps): JSX.Element => {
|
|
66
|
+
const router = parseParams(window.location.search);
|
|
67
|
+
const currentViewFromURL = router.currentProfileView as string;
|
|
56
68
|
const [curPath, setCurPath] = useState<string[]>([]);
|
|
57
69
|
const {response, error} = useQuery(queryClient, profileSource);
|
|
70
|
+
const [currentView, setCurrentView] = useState<string | undefined>(currentViewFromURL);
|
|
58
71
|
|
|
59
72
|
if (error != null) {
|
|
60
73
|
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
@@ -100,7 +113,7 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
|
|
|
100
113
|
e.preventDefault();
|
|
101
114
|
|
|
102
115
|
const req = profileSource.QueryRequest();
|
|
103
|
-
req.setReportType(QueryRequest.ReportType.
|
|
116
|
+
req.setReportType(QueryRequest.ReportType.REPORT_TYPE_PPROF);
|
|
104
117
|
|
|
105
118
|
queryClient.query(
|
|
106
119
|
req,
|
|
@@ -108,6 +121,10 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
|
|
|
108
121
|
error: ServiceError | null,
|
|
109
122
|
responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
|
|
110
123
|
) => {
|
|
124
|
+
if (error != null) {
|
|
125
|
+
console.error('Error while querying', error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
111
128
|
if (responseMessage !== null) {
|
|
112
129
|
const bytes = responseMessage.getPprof();
|
|
113
130
|
const blob = new Blob([bytes], {type: 'application/octet-stream'});
|
|
@@ -116,14 +133,14 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
|
|
|
116
133
|
link.href = window.URL.createObjectURL(blob);
|
|
117
134
|
link.download = 'profile.pb.gz';
|
|
118
135
|
link.click();
|
|
136
|
+
} else {
|
|
137
|
+
console.error(error);
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
);
|
|
122
141
|
};
|
|
123
142
|
|
|
124
|
-
const resetIcicleGraph = (
|
|
125
|
-
setCurPath([]);
|
|
126
|
-
};
|
|
143
|
+
const resetIcicleGraph = () => setCurPath([]);
|
|
127
144
|
|
|
128
145
|
const setNewCurPath = (path: string[]) => {
|
|
129
146
|
if (!arrayEquals(curPath, path)) {
|
|
@@ -131,31 +148,103 @@ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX
|
|
|
131
148
|
}
|
|
132
149
|
};
|
|
133
150
|
|
|
151
|
+
const switchProfileView = (view: string) => {
|
|
152
|
+
if (!navigateTo) return;
|
|
153
|
+
|
|
154
|
+
setCurrentView(view);
|
|
155
|
+
|
|
156
|
+
navigateTo('/', {...router, ...{currentProfileView: view}});
|
|
157
|
+
};
|
|
158
|
+
|
|
134
159
|
return (
|
|
135
160
|
<>
|
|
136
161
|
<div className="py-3">
|
|
137
162
|
<Card>
|
|
138
163
|
<Card.Body>
|
|
139
|
-
<div className="flex
|
|
140
|
-
<div className="w-
|
|
141
|
-
<
|
|
142
|
-
|
|
164
|
+
<div className="flex py-3 w-full">
|
|
165
|
+
<div className="w-2/5 flex space-x-4">
|
|
166
|
+
<div>
|
|
167
|
+
<Button color="neutral" onClick={downloadPProf}>
|
|
168
|
+
Download pprof
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="flex ml-auto">
|
|
174
|
+
<div className="mr-3">
|
|
175
|
+
<Button
|
|
176
|
+
color="neutral"
|
|
177
|
+
onClick={resetIcicleGraph}
|
|
178
|
+
disabled={curPath.length === 0}
|
|
179
|
+
additionalClasses="whitespace-nowrap text-ellipsis"
|
|
180
|
+
>
|
|
181
|
+
Reset View
|
|
182
|
+
</Button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<Button
|
|
186
|
+
color={`${currentView === 'table' ? 'primary' : 'neutral'}`}
|
|
187
|
+
additionalClasses={`rounded-tr-none rounded-br-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons`}
|
|
188
|
+
onClick={() => switchProfileView('table')}
|
|
189
|
+
>
|
|
190
|
+
Table
|
|
191
|
+
</Button>
|
|
192
|
+
|
|
193
|
+
<Button
|
|
194
|
+
color={`${currentView === 'both' ? 'primary' : 'neutral'}`}
|
|
195
|
+
additionalClasses={`rounded-tl-none rounded-tr-none rounded-bl-none rounded-br-none border-l-0 border-r-0 w-auto px-8 whitespace-nowrap no-outline-on-buttons no-outline-on-buttons text-ellipsis`}
|
|
196
|
+
onClick={() => switchProfileView('both')}
|
|
197
|
+
>
|
|
198
|
+
Both
|
|
199
|
+
</Button>
|
|
200
|
+
|
|
201
|
+
<Button
|
|
202
|
+
color={`${currentView === 'icicle' ? 'primary' : 'neutral'}`}
|
|
203
|
+
additionalClasses={`rounded-tl-none rounded-bl-none w-auto px-8 whitespace-nowrap text-ellipsis no-outline-on-buttons`}
|
|
204
|
+
onClick={() => switchProfileView('icicle')}
|
|
205
|
+
>
|
|
206
|
+
Icicle Graph
|
|
143
207
|
</Button>
|
|
144
208
|
</div>
|
|
209
|
+
</div>
|
|
145
210
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
211
|
+
<div className="flex space-x-4">
|
|
212
|
+
{currentView === 'icicle' && (
|
|
213
|
+
<div className="w-full">
|
|
214
|
+
<CalcWidth throttle={300} delay={2000}>
|
|
215
|
+
<ProfileIcicleGraph
|
|
216
|
+
curPath={curPath}
|
|
217
|
+
setNewCurPath={setNewCurPath}
|
|
218
|
+
graph={response.getFlamegraph()?.toObject()}
|
|
219
|
+
/>
|
|
220
|
+
</CalcWidth>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{currentView === 'table' && (
|
|
225
|
+
<div className="w-full">
|
|
226
|
+
<TopTable queryClient={queryClient} profileSource={profileSource} />
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{currentView === 'both' && (
|
|
231
|
+
<>
|
|
232
|
+
<div className="w-1/2">
|
|
233
|
+
<TopTable queryClient={queryClient} profileSource={profileSource} />
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div className="w-1/2">
|
|
237
|
+
<CalcWidth throttle={300} delay={2000}>
|
|
238
|
+
<ProfileIcicleGraph
|
|
239
|
+
curPath={curPath}
|
|
240
|
+
setNewCurPath={setNewCurPath}
|
|
241
|
+
graph={response.getFlamegraph()?.toObject()}
|
|
242
|
+
/>
|
|
243
|
+
</CalcWidth>
|
|
244
|
+
</div>
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
151
247
|
</div>
|
|
152
|
-
<CalcWidth throttle={300} delay={2000}>
|
|
153
|
-
<ProfileIcicleGraph
|
|
154
|
-
curPath={curPath}
|
|
155
|
-
setNewCurPath={setNewCurPath}
|
|
156
|
-
graph={response.getFlamegraph()?.toObject()}
|
|
157
|
-
/>
|
|
158
|
-
</CalcWidth>
|
|
159
248
|
</Card.Body>
|
|
160
249
|
</Card>
|
|
161
250
|
</div>
|
package/src/TopTable.tsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, {useEffect, useState} from 'react';
|
|
2
|
+
|
|
3
|
+
import {ProfileSource} from './ProfileSource';
|
|
4
|
+
import {QueryRequest, QueryResponse, QueryServiceClient, ServiceError} from '@parca/client';
|
|
5
|
+
import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
|
|
6
|
+
import {getLastItem, valueFormatter} from '@parca/functions';
|
|
7
|
+
|
|
8
|
+
import './TopTable.styles.css';
|
|
9
|
+
|
|
10
|
+
interface ProfileViewProps {
|
|
11
|
+
queryClient: QueryServiceClient;
|
|
12
|
+
profileSource: ProfileSource;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IQueryResult {
|
|
16
|
+
response: QueryResponse | null;
|
|
17
|
+
error: ServiceError | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Arrow = ({direction}: {direction: string | undefined}) => {
|
|
21
|
+
return (
|
|
22
|
+
<svg
|
|
23
|
+
className={`${direction !== undefined ? 'fill-[#161616] dark:fill-[#ffffff]' : ''}`}
|
|
24
|
+
fill="#777d87"
|
|
25
|
+
height="10"
|
|
26
|
+
viewBox="0 0 11 10"
|
|
27
|
+
width="11"
|
|
28
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
29
|
+
>
|
|
30
|
+
<path clip-rule="evenodd" d="m.573997 0 5.000003 10 5-10h-9.999847z" fill-rule="evenodd" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const useSortableData = (
|
|
36
|
+
response: QueryResponse | null,
|
|
37
|
+
config = {key: 'cumulative', direction: 'desc'}
|
|
38
|
+
) => {
|
|
39
|
+
const [sortConfig, setSortConfig] = React.useState<{key: string; direction: string} | null>(
|
|
40
|
+
config
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const rawTableReport = response?.toObject().top?.listList;
|
|
44
|
+
|
|
45
|
+
const items = rawTableReport?.map(node => ({
|
|
46
|
+
...node,
|
|
47
|
+
name: node.meta?.pb_function?.name,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const sortedItems = React.useMemo(() => {
|
|
51
|
+
if (!items) return;
|
|
52
|
+
|
|
53
|
+
let sortableItems = [...items];
|
|
54
|
+
if (sortConfig !== null) {
|
|
55
|
+
sortableItems.sort((a, b) => {
|
|
56
|
+
if (a[sortConfig.key] < b[sortConfig.key]) {
|
|
57
|
+
return sortConfig.direction === 'asc' ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
if (a[sortConfig.key] > b[sortConfig.key]) {
|
|
60
|
+
return sortConfig.direction === 'asc' ? 1 : -1;
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return sortableItems;
|
|
66
|
+
}, [items, sortConfig]);
|
|
67
|
+
|
|
68
|
+
const requestSort = key => {
|
|
69
|
+
let direction = 'desc';
|
|
70
|
+
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'desc') {
|
|
71
|
+
direction = 'asc';
|
|
72
|
+
}
|
|
73
|
+
setSortConfig({key, direction});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {items: sortedItems, requestSort, sortConfig};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const useQuery = (
|
|
80
|
+
client: QueryServiceClient,
|
|
81
|
+
profileSource: ProfileSource
|
|
82
|
+
): IQueryResult => {
|
|
83
|
+
const [result, setResult] = useState<IQueryResult>({
|
|
84
|
+
response: null,
|
|
85
|
+
error: null,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const req = profileSource.QueryRequest();
|
|
90
|
+
req.setReportType(QueryRequest.ReportType.REPORT_TYPE_TOP);
|
|
91
|
+
|
|
92
|
+
client.query(
|
|
93
|
+
req,
|
|
94
|
+
(
|
|
95
|
+
error: ServiceError | null,
|
|
96
|
+
responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
|
|
97
|
+
) => {
|
|
98
|
+
setResult({
|
|
99
|
+
response: responseMessage,
|
|
100
|
+
error: error,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}, [client, profileSource]);
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const TopTable = ({queryClient, profileSource}: ProfileViewProps): JSX.Element => {
|
|
110
|
+
const {response, error} = useQuery(queryClient, profileSource);
|
|
111
|
+
const {items, requestSort, sortConfig} = useSortableData(response);
|
|
112
|
+
|
|
113
|
+
const unit = response?.toObject().top?.unit as string;
|
|
114
|
+
|
|
115
|
+
if (error != null) {
|
|
116
|
+
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const total = response?.toObject().top?.listList.length;
|
|
120
|
+
if (total === 0) return <>Profile has no samples</>;
|
|
121
|
+
|
|
122
|
+
const getClassNamesFor = name => {
|
|
123
|
+
if (!sortConfig) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
return sortConfig.key === name ? sortConfig.direction : undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
<div className="w-full">
|
|
132
|
+
<table className="iciclegraph-table table-auto text-left w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
133
|
+
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
134
|
+
<tr>
|
|
135
|
+
<th
|
|
136
|
+
className="text-sm cursor-pointer pt-2 pb-2 pl-2"
|
|
137
|
+
onClick={() => requestSort('name')}
|
|
138
|
+
>
|
|
139
|
+
Name
|
|
140
|
+
<span className={`inline-block align-middle ml-2 ${getClassNamesFor('name')}`}>
|
|
141
|
+
<Arrow direction={getClassNamesFor('name')} />
|
|
142
|
+
</span>
|
|
143
|
+
</th>
|
|
144
|
+
<th
|
|
145
|
+
className="text-left text-sm cursor-pointer pt-2 pb-2"
|
|
146
|
+
onClick={() => requestSort('flat')}
|
|
147
|
+
>
|
|
148
|
+
Flat
|
|
149
|
+
<span className={`inline-block align-middle ml-2 ${getClassNamesFor('flat')}`}>
|
|
150
|
+
<Arrow direction={getClassNamesFor('flat')} />
|
|
151
|
+
</span>
|
|
152
|
+
</th>
|
|
153
|
+
<th
|
|
154
|
+
className="text-right text-sm cursor-pointer pt-2 pb-2 pr-2"
|
|
155
|
+
onClick={() => requestSort('cumulative')}
|
|
156
|
+
>
|
|
157
|
+
Cumulative
|
|
158
|
+
<span
|
|
159
|
+
className={`inline-block align-middle ml-2 ${getClassNamesFor('cumulative')}`}
|
|
160
|
+
>
|
|
161
|
+
<Arrow direction={getClassNamesFor('cumulative')} />
|
|
162
|
+
</span>
|
|
163
|
+
</th>
|
|
164
|
+
</tr>
|
|
165
|
+
</thead>
|
|
166
|
+
<tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-900 dark:divide-gray-700">
|
|
167
|
+
{items?.map((report, index) => (
|
|
168
|
+
<tr key={index} className="hover:bg-[#62626212] dark:hover:bg-[#ffffff12]">
|
|
169
|
+
<td className="text-xs py-1.5 pl-2 min-w-[150px] max-w-[450px]">
|
|
170
|
+
{report.meta?.mapping?.file !== '' && [getLastItem(report.meta?.mapping?.file)]}{' '}
|
|
171
|
+
{report.meta?.pb_function?.name}
|
|
172
|
+
</td>
|
|
173
|
+
<td className="text-xs min-w-[150px] max-w-[150px] py-1.5text-right">
|
|
174
|
+
{valueFormatter(report.flat, unit, 2)}
|
|
175
|
+
</td>
|
|
176
|
+
<td className="text-xs min-w-[150px] max-w-[150px] py-1.5 text-right pr-2">
|
|
177
|
+
{valueFormatter(report.cumulative, unit, 2)}
|
|
178
|
+
</td>
|
|
179
|
+
</tr>
|
|
180
|
+
))}
|
|
181
|
+
</tbody>
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default TopTable;
|