Repository: provectus/kafka-ui
Issue: Consumers: Topic list: Implement sorting
Labels: type/enhancement
good first issue
scope/frontend
status/accepted

<!--

Don't forget to check for existing issues/discussions regarding your proposal. We might already have it.
https://github.com/provectus/kafka-ui/issues
https://github.com/provectus/kafka-ui/discussions

-->

**Describe the bug**
<!--(A clear and concise description of what the bug is.)-->
No sorting on visible values is applied, so the list looks as in random order (Partition would be the logical suggestion). Of the "Partition", "Consumer ID", "Host", "Messages behind", "Current offset", "End offset" columns

**Expected behavior**
<!--
(A clear and concise description of what you expected to happen)
-->
At least the first 4 columns can be valid candidates for sorting via click on the column headers in a nested table.

**Set up**
<!--
How do you run the app? Please provide as much info as possible:
1. App version (docker image version or check commit hash in the top left corner in UI)
2. Helm chart version, if you use one
3. Any IAAC configs

We might close the issue without further explanation if you don't provide such information.
-->
[f4e6afe](https://github.com/provectus/kafka-ui/commit/f4e6afe)

**Steps to Reproduce**
<!-- We'd like you to provide an example setup (via docker-compose, helm, etc.) 
to reproduce the problem, especially with a complex setups. -->
1. Log in to Kafka UI and navigate to the Consumers page.
2. Open consumer group details of the topic with the large number of partitions.

**Screenshots**
<!--
(If applicable, add screenshots to help explain your problem)
-->
![image](https://user-images.githubusercontent.com/112083452/209823239-fdac8303-c64a-4ecf-89ca-01938a4e76a5.png)

**Additional context**
<!--
(Add any other context about the problem here)
-->
@Haarolean required to open this as a separate issue from the #2651 

Issue comments:

Pull request review comments:
* `const ipV4ToNum = (ip?: string) => {`  undefined can be written like this

* Here the typescript can automatically infer your return type you don't need to specify the return type.
Updated

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx
index 6637821020a..b234fb8e192 100644
--- a/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx
+++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/TopicContents/TopicContents.tsx
@@ -1,6 +1,6 @@
 import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
-import { ConsumerGroupTopicPartition } from 'generated-sources';
+import { ConsumerGroupTopicPartition, SortOrder } from 'generated-sources';
 import React from 'react';
 
 import { ContentBox, TopicContentWrapper } from './TopicContent.styled';
@@ -9,7 +9,125 @@ interface Props {
   consumers: ConsumerGroupTopicPartition[];
 }
 
+type OrderByKey = keyof ConsumerGroupTopicPartition;
+interface Headers {
+  title: string;
+  orderBy: OrderByKey | undefined;
+}
+
+const TABLE_HEADERS_MAP: Headers[] = [
+  { title: 'Partition', orderBy: 'partition' },
+  { title: 'Consumer ID', orderBy: 'consumerId' },
+  { title: 'Host', orderBy: 'host' },
+  { title: 'Messages Behind', orderBy: 'messagesBehind' },
+  { title: 'Current Offset', orderBy: 'currentOffset' },
+  { title: 'End offset', orderBy: 'endOffset' },
+];
+
+const ipV4ToNum = (ip?: string) => {
+  if (typeof ip === 'string' && ip.length !== 0) {
+    const withoutSlash = ip.indexOf('/') !== -1 ? ip.slice(1) : ip;
+    return Number(
+      withoutSlash
+        .split('.')
+        .map((octet) => `000${octet}`.slice(-3))
+        .join('')
+    );
+  }
+  return 0;
+};
+
+type ComparatorFunction<T> = (
+  valueA: T,
+  valueB: T,
+  order: SortOrder,
+  property?: keyof T
+) => number;
+
+const numberComparator: ComparatorFunction<ConsumerGroupTopicPartition> = (
+  valueA,
+  valueB,
+  order,
+  property
+) => {
+  if (property !== undefined) {
+    return order === SortOrder.ASC
+      ? Number(valueA[property]) - Number(valueB[property])
+      : Number(valueB[property]) - Number(valueA[property]);
+  }
+  return 0;
+};
+
+const ipComparator: ComparatorFunction<ConsumerGroupTopicPartition> = (
+  valueA,
+  valueB,
+  order
+) =>
+  order === SortOrder.ASC
+    ? ipV4ToNum(valueA.host) - ipV4ToNum(valueB.host)
+    : ipV4ToNum(valueB.host) - ipV4ToNum(valueA.host);
+
+const consumerIdComparator: ComparatorFunction<ConsumerGroupTopicPartition> = (
+  valueA,
+  valueB,
+  order
+) => {
+  if (valueA.consumerId && valueB.consumerId) {
+    if (order === SortOrder.ASC) {
+      if (valueA.consumerId?.toLowerCase() > valueB.consumerId?.toLowerCase()) {
+        return 1;
+      }
+    }
+
+    if (order === SortOrder.DESC) {
+      if (valueB.consumerId?.toLowerCase() > valueA.consumerId?.toLowerCase()) {
+        return -1;
+      }
+    }
+  }
+
+  return 0;
+};
+
 const TopicContents: React.FC<Props> = ({ consumers }) => {
+  const [orderBy, setOrderBy] = React.useState<OrderByKey>('partition');
+  const [sortOrder, setSortOrder] = React.useState<SortOrder>(SortOrder.DESC);
+
+  const handleOrder = React.useCallback((columnName: string | null) => {
+    if (typeof columnName === 'string') {
+      setOrderBy(columnName as OrderByKey);
+      setSortOrder((prevOrder) =>
+        prevOrder === SortOrder.DESC ? SortOrder.ASC : SortOrder.DESC
+      );
+    }
+  }, []);
+
+  const sortedConsumers = React.useMemo(() => {
+    if (orderBy && sortOrder) {
+      const isNumberProperty =
+        orderBy === 'partition' ||
+        orderBy === 'currentOffset' ||
+        orderBy === 'endOffset' ||
+        orderBy === 'messagesBehind';
+
+      let comparator: ComparatorFunction<ConsumerGroupTopicPartition>;
+      if (isNumberProperty) {
+        comparator = numberComparator;
+      }
+
+      if (orderBy === 'host') {
+        comparator = ipComparator;
+      }
+
+      if (orderBy === 'consumerId') {
+        comparator = consumerIdComparator;
+      }
+
+      return consumers.sort((a, b) => comparator(a, b, sortOrder, orderBy));
+    }
+    return consumers;
+  }, [orderBy, sortOrder, consumers]);
+
   return (
     <TopicContentWrapper>
       <td colSpan={3}>
@@ -17,16 +135,20 @@ const TopicContents: React.FC<Props> = ({ consumers }) => {
           <Table isFullwidth>
             <thead>
               <tr>
-                <TableHeaderCell title="Partition" />
-                <TableHeaderCell title="Consumer ID" />
-                <TableHeaderCell title="Host" />
-                <TableHeaderCell title="Messages behind" />
-                <TableHeaderCell title="Current offset" />
-                <TableHeaderCell title="End offset" />
+                {TABLE_HEADERS_MAP.map((header) => (
+                  <TableHeaderCell
+                    key={header.orderBy}
+                    title={header.title}
+                    orderBy={orderBy}
+                    sortOrder={sortOrder}
+                    orderValue={header.orderBy}
+                    handleOrderBy={handleOrder}
+                  />
+                ))}
               </tr>
             </thead>
             <tbody>
-              {consumers.map((consumer) => (
+              {sortedConsumers.map((consumer) => (
                 <tr key={consumer.partition}>
                   <td>{consumer.partition}</td>
                   <td>{consumer.consumerId}</td>


Test patch: