|
199 | 199 | <div v-else class="tree-view-container"> |
200 | 200 | <div class="tree-header"> |
201 | 201 | <span class="tree-header-name">Name</span> |
202 | | - <span class="tree-header-cell">All</span> |
203 | | - <span class="tree-header-cell"> |
| 202 | + <span |
| 203 | + class="tree-header-cell sortable" |
| 204 | + @click="toggleTreeSort('all')" |
| 205 | + > |
| 206 | + All |
| 207 | + <v-icon |
| 208 | + v-if="treeSortKey === 'all'" |
| 209 | + size="12" |
| 210 | + class="sort-icon" |
| 211 | + > |
| 212 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 213 | + </v-icon> |
| 214 | + </span> |
| 215 | + <span |
| 216 | + class="tree-header-cell sortable" |
| 217 | + @click="toggleTreeSort('style')" |
| 218 | + > |
204 | 219 | <severity-icon :status="Severity.STYLE" :size="14" /> |
| 220 | + <v-icon |
| 221 | + v-if="treeSortKey === 'style'" |
| 222 | + size="12" |
| 223 | + class="sort-icon" |
| 224 | + > |
| 225 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 226 | + </v-icon> |
205 | 227 | </span> |
206 | | - <span class="tree-header-cell"> |
| 228 | + <span |
| 229 | + class="tree-header-cell sortable" |
| 230 | + @click="toggleTreeSort('low')" |
| 231 | + > |
207 | 232 | <severity-icon :status="Severity.LOW" :size="14" /> |
| 233 | + <v-icon |
| 234 | + v-if="treeSortKey === 'low'" |
| 235 | + size="12" |
| 236 | + class="sort-icon" |
| 237 | + > |
| 238 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 239 | + </v-icon> |
208 | 240 | </span> |
209 | | - <span class="tree-header-cell"> |
| 241 | + <span |
| 242 | + class="tree-header-cell sortable" |
| 243 | + @click="toggleTreeSort('medium')" |
| 244 | + > |
210 | 245 | <severity-icon :status="Severity.MEDIUM" :size="14" /> |
| 246 | + <v-icon |
| 247 | + v-if="treeSortKey === 'medium'" |
| 248 | + size="12" |
| 249 | + class="sort-icon" |
| 250 | + > |
| 251 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 252 | + </v-icon> |
211 | 253 | </span> |
212 | | - <span class="tree-header-cell"> |
| 254 | + <span |
| 255 | + class="tree-header-cell sortable" |
| 256 | + @click="toggleTreeSort('high')" |
| 257 | + > |
213 | 258 | <severity-icon :status="Severity.HIGH" :size="14" /> |
| 259 | + <v-icon |
| 260 | + v-if="treeSortKey === 'high'" |
| 261 | + size="12" |
| 262 | + class="sort-icon" |
| 263 | + > |
| 264 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 265 | + </v-icon> |
214 | 266 | </span> |
215 | | - <span class="tree-header-cell"> |
| 267 | + <span |
| 268 | + class="tree-header-cell sortable" |
| 269 | + @click="toggleTreeSort('critical')" |
| 270 | + > |
216 | 271 | <severity-icon :status="Severity.CRITICAL" :size="14" /> |
| 272 | + <v-icon |
| 273 | + v-if="treeSortKey === 'critical'" |
| 274 | + size="12" |
| 275 | + class="sort-icon" |
| 276 | + > |
| 277 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 278 | + </v-icon> |
217 | 279 | </span> |
218 | | - <span class="tree-header-cell"> |
| 280 | + <span |
| 281 | + class="tree-header-cell sortable" |
| 282 | + @click="toggleTreeSort('unreviewed')" |
| 283 | + > |
219 | 284 | <review-status-icon :status="0" :size="14" /> |
| 285 | + <v-icon |
| 286 | + v-if="treeSortKey === 'unreviewed'" |
| 287 | + size="12" |
| 288 | + class="sort-icon" |
| 289 | + > |
| 290 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 291 | + </v-icon> |
220 | 292 | </span> |
221 | | - <span class="tree-header-cell"> |
| 293 | + <span |
| 294 | + class="tree-header-cell sortable" |
| 295 | + @click="toggleTreeSort('confirmed')" |
| 296 | + > |
222 | 297 | <review-status-icon :status="1" :size="14" /> |
| 298 | + <v-icon |
| 299 | + v-if="treeSortKey === 'confirmed'" |
| 300 | + size="12" |
| 301 | + class="sort-icon" |
| 302 | + > |
| 303 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 304 | + </v-icon> |
223 | 305 | </span> |
224 | | - <span class="tree-header-cell"> |
| 306 | + <span |
| 307 | + class="tree-header-cell sortable" |
| 308 | + @click="toggleTreeSort('false_positive')" |
| 309 | + > |
225 | 310 | <review-status-icon :status="2" :size="14" /> |
| 311 | + <v-icon |
| 312 | + v-if="treeSortKey === 'false_positive'" |
| 313 | + size="12" |
| 314 | + class="sort-icon" |
| 315 | + > |
| 316 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 317 | + </v-icon> |
226 | 318 | </span> |
227 | | - <span class="tree-header-cell"> |
| 319 | + <span |
| 320 | + class="tree-header-cell sortable" |
| 321 | + @click="toggleTreeSort('intentional')" |
| 322 | + > |
228 | 323 | <review-status-icon :status="3" :size="14" /> |
| 324 | + <v-icon |
| 325 | + v-if="treeSortKey === 'intentional'" |
| 326 | + size="12" |
| 327 | + class="sort-icon" |
| 328 | + > |
| 329 | + {{ treeSortOrder === 'desc' ? 'mdi-arrow-down' : 'mdi-arrow-up' }} |
| 330 | + </v-icon> |
229 | 331 | </span> |
230 | 332 | </div> |
231 | 333 | <v-treeview |
232 | 334 | v-model:opened="openedTreeItems" |
233 | | - :items="treeItems" |
| 335 | + :items="sortedTreeItems" |
234 | 336 | item-value="fullPath" |
235 | 337 | open-on-click |
236 | 338 | density="compact" |
@@ -426,6 +528,8 @@ const openedTreeItems = ref([]); |
426 | 528 | const allReportsFileCounts = ref({}); |
427 | 529 | const fileSeverities = ref({}); |
428 | 530 | const treeItems = ref([]); |
| 531 | +const treeSortKey = ref(null); |
| 532 | +const treeSortOrder = ref(null); |
429 | 533 |
|
430 | 534 | const runIds = computed(function() { |
431 | 535 | return store.getters[`${namespace}/getRunIds`]; |
@@ -500,6 +604,37 @@ const formattedReports = computed(function() { |
500 | 604 | }); |
501 | 605 | }); |
502 | 606 |
|
| 607 | +function getTreeStatValue(item, statKey) { |
| 608 | + return statKey === "all" ? (item.findings || 0) : (item.stats[statKey] || 0); |
| 609 | +} |
| 610 | +
|
| 611 | +function sortTreeNodes(nodes) { |
| 612 | + if (!nodes) return []; |
| 613 | +
|
| 614 | + const _sorted = nodes.map(node => ({ ...node })); |
| 615 | + _sorted.forEach(node => { |
| 616 | + if (node.children && node.children.length > 0) { |
| 617 | + node.children = sortTreeNodes(node.children); |
| 618 | + } |
| 619 | + }); |
| 620 | +
|
| 621 | + if (treeSortKey.value && treeSortOrder.value) { |
| 622 | + _sorted.sort((a, b) => { |
| 623 | + const _aVal = getTreeStatValue(a, treeSortKey.value); |
| 624 | + const _bVal = getTreeStatValue(b, treeSortKey.value); |
| 625 | + return treeSortOrder.value === "desc" |
| 626 | + ? _bVal - _aVal |
| 627 | + : _aVal - _bVal; |
| 628 | + }); |
| 629 | + } |
| 630 | +
|
| 631 | + return _sorted; |
| 632 | +} |
| 633 | +
|
| 634 | +const sortedTreeItems = computed(function() { |
| 635 | + return sortTreeNodes(treeItems.value); |
| 636 | +}); |
| 637 | +
|
503 | 638 | watch( |
504 | 639 | [ page, itemsPerPage, sortBy ], |
505 | 640 | () => { |
@@ -540,6 +675,18 @@ function onTreeItemClick(item) { |
540 | 675 | viewMode.value = "table"; |
541 | 676 | } |
542 | 677 |
|
| 678 | +function toggleTreeSort(statKey) { |
| 679 | + if (treeSortKey.value !== statKey) { |
| 680 | + treeSortKey.value = statKey; |
| 681 | + treeSortOrder.value = "desc"; |
| 682 | + } else if (treeSortOrder.value === "desc") { |
| 683 | + treeSortOrder.value = "asc"; |
| 684 | + } else { |
| 685 | + treeSortKey.value = null; |
| 686 | + treeSortOrder.value = null; |
| 687 | + } |
| 688 | +} |
| 689 | +
|
543 | 690 | function i64ToNum(val) { |
544 | 691 | if (val == null) return 0; |
545 | 692 | if (typeof val === "number") return val; |
@@ -867,6 +1014,19 @@ body { |
867 | 1014 | width: 50px; |
868 | 1015 | text-align: center; |
869 | 1016 | flex-shrink: 0; |
| 1017 | +
|
| 1018 | + &.sortable { |
| 1019 | + cursor: pointer; |
| 1020 | + user-select: none; |
| 1021 | +
|
| 1022 | + &:hover { |
| 1023 | + color: rgb(var(--v-theme-primary)); |
| 1024 | + } |
| 1025 | + } |
| 1026 | +
|
| 1027 | + .sort-icon { |
| 1028 | + vertical-align: middle; |
| 1029 | + } |
870 | 1030 | } |
871 | 1031 |
|
872 | 1032 | .tree-row { |
|
0 commit comments