This is an automated email from the ASF dual-hosted git repository.
dengliming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 1bcd0397 fix: make table sorters compare the displayed field (#635)
(#674)
1bcd0397 is described below
commit 1bcd0397cb9986b0a27609a5535793f110bca6c6
Author: satyam-311 <[email protected]>
AuthorDate: Sun Sep 27 10:47:37 2026 +0530
fix: make table sorters compare the displayed field (#635) (#674)
Several column sorters compared a different field from the column being
sorted:
- Instance: lastHeartBeatTime, dateCreated and instanceState compared
instanceType.
- Plugin and NamespacePlugin: the sort column compared role.
Add small sorter helpers (sortByDate, sortByNumber) that compare the
column's own value with a date or numeric comparator, and order missing
or invalid values explicitly before present ones. Cover them with unit
tests.
Fixes #635
---
src/routes/System/Instance/index.js | 7 ++-
src/routes/System/NamespacePlugin/index.js | 3 +-
src/routes/System/Plugin/index.js | 3 +-
src/utils/sorter.js | 58 ++++++++++++++++++
src/utils/sorter.test.js | 94 ++++++++++++++++++++++++++++++
5 files changed, 160 insertions(+), 5 deletions(-)
diff --git a/src/routes/System/Instance/index.js
b/src/routes/System/Instance/index.js
index 805cc685..64009697 100644
--- a/src/routes/System/Instance/index.js
+++ b/src/routes/System/Instance/index.js
@@ -34,6 +34,7 @@ import * as echarts from "echarts";
import { resizableComponents } from "../../../utils/resizable";
import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils";
import AuthButton from "../../../utils/AuthButton";
+import { sortByDate, sortByNumber } from "../../../utils/sorter";
const { Text } = Typography;
@@ -409,7 +410,7 @@ export default class Instance extends Component {
ellipsis: true,
key: "lastHeartBeatTime",
width: 120,
- sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
+ sorter: sortByDate("lastHeartBeatTime"),
render: (text) => {
return (
<div style={{ color: "#1f640a" }}>
@@ -425,7 +426,7 @@ export default class Instance extends Component {
ellipsis: true,
key: "dateCreated",
width: 120,
- sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
+ sorter: sortByDate("dateCreated"),
render: (text) => {
return (
<div style={{ color: "#1f640a" }}>
@@ -441,7 +442,7 @@ export default class Instance extends Component {
ellipsis: true,
key: "instanceState",
width: 120,
- sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
+ sorter: sortByNumber("instanceState"),
render: (state) => {
if (state === 1) {
return (
diff --git a/src/routes/System/NamespacePlugin/index.js
b/src/routes/System/NamespacePlugin/index.js
index 0fc9f335..3e21e7f6 100644
--- a/src/routes/System/NamespacePlugin/index.js
+++ b/src/routes/System/NamespacePlugin/index.js
@@ -33,6 +33,7 @@ import { Link } from "dva/router";
import { resizableComponents } from "../../../utils/resizable";
import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils";
import AuthButton from "../../../utils/AuthButton";
+import { sortByNumber } from "../../../utils/sorter";
import { refreshAuthMenus } from "../../../utils/AuthRoute";
import {
getUpdateModal,
@@ -299,7 +300,7 @@ export default class NamespacePlugin extends Component {
ellipsis: true,
key: "sort",
width: 120,
- sorter: (a, b) => (a.role > b.role ? 1 : -1),
+ sorter: sortByNumber("sort"),
render: (text) => {
return <div style={{ color: "#014955" }}>{text}</div>;
},
diff --git a/src/routes/System/Plugin/index.js
b/src/routes/System/Plugin/index.js
index 7bfeccd7..dfdf4b31 100644
--- a/src/routes/System/Plugin/index.js
+++ b/src/routes/System/Plugin/index.js
@@ -36,6 +36,7 @@ import { resizableComponents } from
"../../../utils/resizable";
import AddModal from "./AddModal";
import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils";
import AuthButton from "../../../utils/AuthButton";
+import { sortByNumber } from "../../../utils/sorter";
import { refreshAuthMenus } from "../../../utils/AuthRoute";
import { getUpdateModal, updatePluginsEnabled } from "../../../utils/plugin";
@@ -377,7 +378,7 @@ export default class Plugin extends Component {
ellipsis: true,
key: "sort",
width: 120,
- sorter: (a, b) => (a.role > b.role ? 1 : -1),
+ sorter: sortByNumber("sort"),
render: (text) => {
return <div style={{ color: "#014955" }}>{text}</div>;
},
diff --git a/src/utils/sorter.js b/src/utils/sorter.js
new file mode 100644
index 00000000..57b9883e
--- /dev/null
+++ b/src/utils/sorter.js
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const isMissing = (value) =>
+ value === null || value === undefined || value === "";
+
+// Missing values are ordered before present values, so they stay together
+// at the start of an ascending sort and at the end of a descending sort.
+const compareWithMissing = (a, b, compare) => {
+ const aMissing = isMissing(a);
+ const bMissing = isMissing(b);
+ if (aMissing || bMissing) {
+ if (aMissing && bMissing) {
+ return 0;
+ }
+ return aMissing ? -1 : 1;
+ }
+ return compare(a, b);
+};
+
+export const compareNumber = (a, b) =>
+ compareWithMissing(a, b, (x, y) => {
+ const numberA = Number(x);
+ const numberB = Number(y);
+ const aInvalid = Number.isNaN(numberA);
+ const bInvalid = Number.isNaN(numberB);
+ if (aInvalid || bInvalid) {
+ if (aInvalid && bInvalid) {
+ return 0;
+ }
+ return aInvalid ? -1 : 1;
+ }
+ return numberA - numberB;
+ });
+
+export const compareDate = (a, b) =>
+ compareWithMissing(a, b, (x, y) =>
+ compareNumber(new Date(x).getTime(), new Date(y).getTime()),
+ );
+
+export const sortByNumber = (field) => (a, b) =>
+ compareNumber(a[field], b[field]);
+
+export const sortByDate = (field) => (a, b) => compareDate(a[field], b[field]);
diff --git a/src/utils/sorter.test.js b/src/utils/sorter.test.js
new file mode 100644
index 00000000..cfbb822e
--- /dev/null
+++ b/src/utils/sorter.test.js
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { compareDate, compareNumber, sortByDate, sortByNumber } from
"./sorter";
+
+describe("compareNumber", () => {
+ it("orders numbers and numeric strings by value", () => {
+ expect(["42", 8, "100", 0].sort(compareNumber)).toEqual([
+ 0,
+ 8,
+ "42",
+ "100",
+ ]);
+ });
+
+ it("orders missing values before present values", () => {
+ // Array#sort always moves undefined to the end without calling the
+ // comparator, so undefined is checked directly below.
+ expect([3, null, 1, ""].sort(compareNumber)).toEqual([null, "", 1, 3]);
+ expect(compareNumber(undefined, 1)).toBeLessThan(0);
+ expect(compareNumber(1, undefined)).toBeGreaterThan(0);
+ });
+
+ it("treats two missing values as equal", () => {
+ expect(compareNumber(null, undefined)).toBe(0);
+ });
+});
+
+describe("compareDate", () => {
+ it("orders date strings and timestamps chronologically", () => {
+ const dates = [
+ "2026-09-26 10:00:00",
+ 1735689600000, // 2025-01-01T00:00:00Z
+ "2024-05-01T08:30:00Z",
+ ];
+ expect([...dates].sort(compareDate)).toEqual([
+ "2024-05-01T08:30:00Z",
+ 1735689600000,
+ "2026-09-26 10:00:00",
+ ]);
+ });
+
+ it("orders missing and invalid dates before valid dates", () => {
+ expect(["2026-01-01", null, "not a date"].sort(compareDate)).toEqual([
+ null,
+ "not a date",
+ "2026-01-01",
+ ]);
+ });
+});
+
+describe("column sorters", () => {
+ it("sorts rows by the requested numeric field, ignoring other fields", () =>
{
+ const rows = [
+ { role: "a", sort: 300 },
+ { role: "b", sort: 10 },
+ { role: "c", sort: 200 },
+ ];
+ expect([...rows].sort(sortByNumber("sort")).map((row) =>
row.sort)).toEqual(
+ [10, 200, 300],
+ );
+ });
+
+ it("sorts rows by the requested date field, ignoring other fields", () => {
+ const rows = [
+ { instanceType: "http", lastHeartBeatTime: "2026-09-03 00:00:00" },
+ { instanceType: "http", lastHeartBeatTime: "2026-09-01 00:00:00" },
+ { instanceType: "http", lastHeartBeatTime: "2026-09-02 00:00:00" },
+ ];
+ expect(
+ [...rows]
+ .sort(sortByDate("lastHeartBeatTime"))
+ .map((row) => row.lastHeartBeatTime),
+ ).toEqual([
+ "2026-09-01 00:00:00",
+ "2026-09-02 00:00:00",
+ "2026-09-03 00:00:00",
+ ]);
+ });
+});