This is an automated email from the ASF dual-hosted git repository. ppawar pushed a commit to branch ATLAS-5146 in repository https://gitbox.apache.org/repos/asf/atlas.git
commit cb7dbfa270b7a14af572820143f9447958fb8ab9 Author: Prasad Pawar <[email protected]> AuthorDate: Thu Oct 23 18:46:54 2025 +0530 ATLAS-5146: Statistics UI: Kafka topics should be listed in sorter order, along with average processing time field --- dashboard/src/styles/commonComponents.scss | 2 +- dashboard/src/views/Statistics/ServerStats.tsx | 89 ++++++++++++++++------ dashboard/src/views/Statistics/Statistics.tsx | 4 +- dashboardv2/public/css/scss/override.scss | 37 +++++++++ .../site/Statistics_Topic_Offset_table_tmpl.html | 13 ++-- .../public/js/views/site/StatisticsTimelineView.js | 59 +++++++++++--- dashboardv3/public/css/scss/override.scss | 35 +++++++++ .../site/Statistics_Topic_Offset_table_tmpl.html | 15 ++-- .../public/js/views/site/StatisticsTimelineView.js | 59 +++++++++++--- 9 files changed, 255 insertions(+), 58 deletions(-) diff --git a/dashboard/src/styles/commonComponents.scss b/dashboard/src/styles/commonComponents.scss index 93aade94c..d16457a8f 100644 --- a/dashboard/src/styles/commonComponents.scss +++ b/dashboard/src/styles/commonComponents.scss @@ -94,7 +94,7 @@ } .MuiDialog-paperWidthMd { - max-width: 800px !important; + max-width: 1110px !important; } .chip-items { diff --git a/dashboard/src/views/Statistics/ServerStats.tsx b/dashboard/src/views/Statistics/ServerStats.tsx index 32a9cc18a..5edc0532d 100644 --- a/dashboard/src/views/Statistics/ServerStats.tsx +++ b/dashboard/src/views/Statistics/ServerStats.tsx @@ -31,6 +31,7 @@ import { AccordionSummary } from "@components/muiComponents"; import { numberFormatWithComma } from "@utils/Helper"; +import { useMemo, useState } from "react"; import { useAppSelector } from "@hooks/reducerHook"; import { isEmpty } from "@utils/Utils"; import { stats } from "@utils/Enum"; @@ -81,6 +82,40 @@ const ServerStats = ({ selectedValue, currentMetricsData }: any) => { serverData?.Notification?.topicDetails ); + const [topicSort, setTopicSort] = useState<{ + key: string; + order: "asc" | "desc"; + }>({ key: "label", order: "asc" }); + + const handleSort = (key: string) => { + setTopicSort((prev) => { + const nextOrder = prev.key === key && prev.order === "asc" ? "desc" : "asc"; + return { key, order: nextOrder }; + }); + }; + + const sortedOffsetTableData = useMemo(() => { + const list = Array.isArray(offSetTabelData) ? [...offSetTabelData] : []; + const { key, order } = topicSort; + const dir = order === "asc" ? 1 : -1; + return list.sort((a: any, b: any) => { + if (key === "label") { + const av = String(a.label || "").toLowerCase(); + const bv = String(b.label || "").toLowerCase(); + if (av < bv) return -1 * dir; + if (av > bv) return 1 * dir; + return 0; + } + const aTopic = serverData?.Notification?.topicDetails?.[a.label] || {}; + const bTopic = serverData?.Notification?.topicDetails?.[b.label] || {}; + const av = Number(aTopic?.[key] ?? 0); + const bv = Number(bTopic?.[key] ?? 0); + if (av < bv) return -1 * dir; + if (av > bv) return 1 * dir; + return 0; + }); + }, [JSON.stringify(offSetTabelData), JSON.stringify(serverData?.Notification?.topicDetails), topicSort]); + let notificationTableHeader = [ "Count", "Avg", @@ -91,20 +126,13 @@ const ServerStats = ({ selectedValue, currentMetricsData }: any) => { // "Failed" ]; - let topicOffsetTableCol = [ - "Kafka Topic-Partition", - "Start Offset", - "Current Offset", - "Processed", - "Failed", - "Last Message Processed Time" - ]; let topciOffsetTableHeader = [ "offsetStart", "offsetCurrent", "processedMessageCount", "failedMessageCount", - "lastMessageProcessedTime" + "lastMessageProcessedTime", + "avgProcessingTime" ]; let tableCol = [ @@ -235,15 +263,28 @@ const ServerStats = ({ selectedValue, currentMetricsData }: any) => { <Table className="classificationTable" size="small"> <TableHead> <TableRow> - {topicOffsetTableCol.map((col) => { - return ( - <> - <TableCell align="left"> - <Typography fontWeight="600">{col}</Typography> - </TableCell> - </> - ); - })} + <TableCell align="left" onClick={() => handleSort("label")} sx={{ cursor: "pointer" }}> + <Typography fontWeight="600"> + Kafka Topic-Partition{topicSort.key === "label" ? (topicSort.order === "asc" ? " ▲" : " ▼") : ""} + </Typography> + </TableCell> + {topciOffsetTableHeader.map((key) => ( + <TableCell key={key} align="left" onClick={() => handleSort(key)} sx={{ cursor: "pointer" }}> + <Typography fontWeight="600"> + {( + { + offsetStart: "Start Offset", + offsetCurrent: "Current Offset", + processedMessageCount: "Processed", + failedMessageCount: "Failed", + lastMessageProcessedTime: "Last Message Processed Time", + avgProcessingTime: "Average message processing time" + } as any + )[key]} + {topicSort.key === key ? (topicSort.order === "asc" ? " ▲" : " ▼") : ""} + </Typography> + </TableCell> + ))} </TableRow> </TableHead> <TableBody> @@ -254,20 +295,22 @@ const ServerStats = ({ selectedValue, currentMetricsData }: any) => { </TableCell> </TableRow> ) : ( - offSetTabelData?.map((obj, index) => ( + sortedOffsetTableData?.map((obj, index) => ( <TableRow key={index}> <TableCell>{obj.label}</TableCell> {topciOffsetTableHeader.map((header) => { let returnVal = - serverData?.Notification?.topicDetails?.[ - obj.label - ]?.[header]; + serverData?.Notification?.topicDetails?.[obj.label]?.[header]; + const typeForKey = + header === "avgProcessingTime" + ? stats.Notification["lastMessageProcessedTime"] + : stats.Notification[header]; return ( <TableCell align="left"> {returnVal ? getStatsValue({ value: returnVal, - type: stats.Notification[header] + type: typeForKey }) : 0} </TableCell> diff --git a/dashboard/src/views/Statistics/Statistics.tsx b/dashboard/src/views/Statistics/Statistics.tsx index f6c0eecb6..6204a6d08 100644 --- a/dashboard/src/views/Statistics/Statistics.tsx +++ b/dashboard/src/views/Statistics/Statistics.tsx @@ -158,6 +158,7 @@ const Statistics = ({ <Autocomplete value={selectedValue} onChange={(_event: any, newValue: any) => { + if (!newValue) return; const { value } = newValue; setSelectedValue(newValue); if (value != "Current") { @@ -169,6 +170,7 @@ const Statistics = ({ ...metricStatsListOptions ]} getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option?.value === value?.value} disableClearable clearOnEscape={false} size="small" @@ -180,7 +182,7 @@ const Statistics = ({ fullWidth /> )} - defaultValue={"Current"} + defaultValue={{ label: "Current", value: "Current" }} sx={{ minWidth: "250px", backgroundColor: "#f6f7fb", diff --git a/dashboardv2/public/css/scss/override.scss b/dashboardv2/public/css/scss/override.scss index 26b706547..269530288 100644 --- a/dashboardv2/public/css/scss/override.scss +++ b/dashboardv2/public/css/scss/override.scss @@ -665,4 +665,41 @@ div.columnmanager-dropdown-container { } .search-tab-body{ padding: 0px 18px 18px 18px !important; +} + +th.sortable { + cursor: pointer; + user-select: none; + position: relative; + padding-right: 18px; +} + +table > thead > tr > th.sortable { + padding-right: 24px !important; +} +.sort-icon { + display: inline-block; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #bbb; +} +.sort-icon.asc { + border-top-color: #38bb9b; + transform: translateY(-50%) rotate(180deg); +} +.sort-icon.desc { + border-top-color: #38bb9b; +} + +th.sortable[data-sort-key="processedMessageCount"], +th.sortable[data-sort-key="failedMessageCount"] { + white-space: nowrap; + vertical-align: top; + text-align: center; } \ No newline at end of file diff --git a/dashboardv2/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html b/dashboardv2/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html index b4c8d8677..77fadb407 100644 --- a/dashboardv2/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html +++ b/dashboardv2/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html @@ -16,12 +16,13 @@ --> <thead> <tr> - <th>Kafka Topic-Partition</th> - <th>Start Offset</th> - <th>Current Offset</th> - <th>Processed</th> - <th>Failed</th> - <th>Last Message Processed Time</th> + <th data-sort-key="label" class="sortable">Kafka Topic-Partition <span class="sort-icon"></span></th> + <th data-sort-key="offsetStart" class="sortable">Start Offset <span class="sort-icon"></span></th> + <th data-sort-key="offsetCurrent" class="sortable">Current Offset <span class="sort-icon"></span></th> + <th data-sort-key="processedMessageCount" class="sortable">Processed <span class="sort-icon"></span></th> + <th data-sort-key="failedMessageCount" class="sortable">Failed <span class="sort-icon"></span></th> + <th data-sort-key="lastMessageProcessedTime" class="sortable">Last Message Processed Time <span class="sort-icon"></span></th> + <th data-sort-key="avgProcessingTime" class="sortable">Average message processing time <span class="sort-icon"></span></th> </tr> </thead> {{#if data}} diff --git a/dashboardv2/public/js/views/site/StatisticsTimelineView.js b/dashboardv2/public/js/views/site/StatisticsTimelineView.js index d7caa60e7..b6a604fe8 100644 --- a/dashboardv2/public/js/views/site/StatisticsTimelineView.js +++ b/dashboardv2/public/js/views/site/StatisticsTimelineView.js @@ -696,17 +696,56 @@ define(['require', return returnObj } - that.ui.offsetCard.html( - TopicOffsetTable({ - data: data.Notification.topicDetails, - tableHeader: ["offsetStart", "offsetCurrent", "processedMessageCount", "failedMessageCount", "lastMessageProcessedTime"], - tableCol: offsetTableColumn(data.Notification.topicDetails), - getTmplValue: function(argument, args) { - var returnVal = data.Notification.topicDetails[argument.label][args]; - return returnVal ? that.getValue({ value: returnVal, type: Enums.stats.Notification[args] }) : 0; + var sortState = { key: 'label', order: 'asc' }; + var renderTopicTable = function(){ + var rows = offsetTableColumn(data.Notification.topicDetails); + var dir = sortState.order === 'asc' ? 1 : -1; + rows = rows.sort(function(a, b){ + if (sortState.key === 'label') { + var av = (a.label || '').toLowerCase(); + var bv = (b.label || '').toLowerCase(); + if (av < bv) return -1 * dir; + if (av > bv) return 1 * dir; + return 0; } - }) - ) + var at = data.Notification.topicDetails[a.label] || {}; + var bt = data.Notification.topicDetails[b.label] || {}; + var avn = Number(at[sortState.key] || 0); + var bvn = Number(bt[sortState.key] || 0); + if (avn < bvn) return -1 * dir; + if (avn > bvn) return 1 * dir; + return 0; + }); + + that.ui.offsetCard.html( + TopicOffsetTable({ + data: data.Notification.topicDetails, + tableHeader: ["offsetStart", "offsetCurrent", "processedMessageCount", "failedMessageCount", "lastMessageProcessedTime", "avgProcessingTime"], + tableCol: rows, + getTmplValue: function(argument, args) { + var returnVal = data.Notification.topicDetails[argument.label][args]; + var typeKey = (args === 'avgProcessingTime') ? 'lastMessageProcessedTime' : args; + return returnVal ? that.getValue({ value: returnVal, type: Enums.stats.Notification[typeKey] }) : 0; + } + }) + ); + + var $ths = that.ui.offsetCard.find('th.sortable'); + $ths.find('.sort-icon').removeClass('asc desc'); + $ths.filter('[data-sort-key="' + sortState.key + '"]').find('.sort-icon').addClass(sortState.order); + + that.ui.offsetCard.find('th.sortable').off('click').on('click', function(){ + var key = $(this).data('sort-key'); + if (sortState.key === key) { + sortState.order = (sortState.order === 'asc') ? 'desc' : 'asc'; + } else { + sortState.key = key; + sortState.order = 'asc'; + } + renderTopicTable(); + }); + }; + renderTopicTable(); that.ui.notificationDetails.removeClass('hide'); } diff --git a/dashboardv3/public/css/scss/override.scss b/dashboardv3/public/css/scss/override.scss index 1f4a786f5..933fe45d0 100644 --- a/dashboardv3/public/css/scss/override.scss +++ b/dashboardv3/public/css/scss/override.scss @@ -636,4 +636,39 @@ div.columnmanager-dropdown-container { ul { list-style: disc } +} + +th.sortable { + cursor: pointer; + user-select: none; + position: relative; + padding-right: 18px; +} +table > thead > tr > th.sortable { + padding-right: 24px !important; +} +.sort-icon { + display: inline-block; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #bbb; +} +.sort-icon.asc { + border-top-color: #38bb9b; + transform: translateY(-50%) rotate(180deg); +} +.sort-icon.desc { + border-top-color: #38bb9b; +} +th.sortable[data-sort-key="processedMessageCount"], +th.sortable[data-sort-key="failedMessageCount"] { + white-space: nowrap; + vertical-align: top; + text-align: center; } \ No newline at end of file diff --git a/dashboardv3/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html b/dashboardv3/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html index b4c8d8677..37654cdcf 100644 --- a/dashboardv3/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html +++ b/dashboardv3/public/js/templates/site/Statistics_Topic_Offset_table_tmpl.html @@ -16,15 +16,16 @@ --> <thead> <tr> - <th>Kafka Topic-Partition</th> - <th>Start Offset</th> - <th>Current Offset</th> - <th>Processed</th> - <th>Failed</th> - <th>Last Message Processed Time</th> + <th data-sort-key="label" class="sortable">Kafka Topic-Partition <span class="sort-icon"></span></th> + <th data-sort-key="offsetStart" class="sortable">Start Offset <span class="sort-icon"></span></th> + <th data-sort-key="offsetCurrent" class="sortable">Current Offset <span class="sort-icon"></span></th> + <th data-sort-key="processedMessageCount" class="sortable">Processed <span class="sort-icon"></span></th> + <th data-sort-key="failedMessageCount" class="sortable">Failed <span class="sort-icon"></span></th> + <th data-sort-key="lastMessageProcessedTime" class="sortable">Last Message Processed Time <span class="sort-icon"></span></th> + <th data-sort-key="avgProcessingTime" class="sortable">Average message processing time <span class="sort-icon"></span></th> </tr> </thead> -{{#if data}} +{{#if tableCol}} <tbody> {{#each tableCol}} <tr> diff --git a/dashboardv3/public/js/views/site/StatisticsTimelineView.js b/dashboardv3/public/js/views/site/StatisticsTimelineView.js index d7caa60e7..b6a604fe8 100644 --- a/dashboardv3/public/js/views/site/StatisticsTimelineView.js +++ b/dashboardv3/public/js/views/site/StatisticsTimelineView.js @@ -696,17 +696,56 @@ define(['require', return returnObj } - that.ui.offsetCard.html( - TopicOffsetTable({ - data: data.Notification.topicDetails, - tableHeader: ["offsetStart", "offsetCurrent", "processedMessageCount", "failedMessageCount", "lastMessageProcessedTime"], - tableCol: offsetTableColumn(data.Notification.topicDetails), - getTmplValue: function(argument, args) { - var returnVal = data.Notification.topicDetails[argument.label][args]; - return returnVal ? that.getValue({ value: returnVal, type: Enums.stats.Notification[args] }) : 0; + var sortState = { key: 'label', order: 'asc' }; + var renderTopicTable = function(){ + var rows = offsetTableColumn(data.Notification.topicDetails); + var dir = sortState.order === 'asc' ? 1 : -1; + rows = rows.sort(function(a, b){ + if (sortState.key === 'label') { + var av = (a.label || '').toLowerCase(); + var bv = (b.label || '').toLowerCase(); + if (av < bv) return -1 * dir; + if (av > bv) return 1 * dir; + return 0; } - }) - ) + var at = data.Notification.topicDetails[a.label] || {}; + var bt = data.Notification.topicDetails[b.label] || {}; + var avn = Number(at[sortState.key] || 0); + var bvn = Number(bt[sortState.key] || 0); + if (avn < bvn) return -1 * dir; + if (avn > bvn) return 1 * dir; + return 0; + }); + + that.ui.offsetCard.html( + TopicOffsetTable({ + data: data.Notification.topicDetails, + tableHeader: ["offsetStart", "offsetCurrent", "processedMessageCount", "failedMessageCount", "lastMessageProcessedTime", "avgProcessingTime"], + tableCol: rows, + getTmplValue: function(argument, args) { + var returnVal = data.Notification.topicDetails[argument.label][args]; + var typeKey = (args === 'avgProcessingTime') ? 'lastMessageProcessedTime' : args; + return returnVal ? that.getValue({ value: returnVal, type: Enums.stats.Notification[typeKey] }) : 0; + } + }) + ); + + var $ths = that.ui.offsetCard.find('th.sortable'); + $ths.find('.sort-icon').removeClass('asc desc'); + $ths.filter('[data-sort-key="' + sortState.key + '"]').find('.sort-icon').addClass(sortState.order); + + that.ui.offsetCard.find('th.sortable').off('click').on('click', function(){ + var key = $(this).data('sort-key'); + if (sortState.key === key) { + sortState.order = (sortState.order === 'asc') ? 'desc' : 'asc'; + } else { + sortState.key = key; + sortState.order = 'asc'; + } + renderTopicTable(); + }); + }; + renderTopicTable(); that.ui.notificationDetails.removeClass('hide'); }
