This is an automated email from the ASF dual-hosted git repository.

twice pushed a commit to branch webui-middleware
in repository https://gitbox.apache.org/repos/asf/kvrocks-controller.git

commit c68ae3a2d26efb5caff043743fad5fa76852d303
Author: PragmaTwice <[email protected]>
AuthorDate: Wed Oct 8 18:21:13 2025 +0800

    feat(webui): use nextjs middleware to dynamically redirect API calls
---
 webui/next.config.mjs                              | 19 +----
 webui/package.json                                 | 83 ++++++++++++----------
 .../[namespace]/clusters/[cluster]/page.tsx        |  5 +-
 .../[cluster]/shards/[shard]/nodes/[node]/page.tsx | 13 ++--
 .../clusters/[cluster]/shards/[shard]/page.tsx     | 13 ++--
 webui/src/app/namespaces/[namespace]/page.tsx      |  5 +-
 webui/src/app/page.tsx                             |  2 +-
 webui/src/middleware.ts                            | 20 ++++++
 webui/tsconfig.json                                | 64 ++++++++++-------
 9 files changed, 125 insertions(+), 99 deletions(-)

diff --git a/webui/next.config.mjs b/webui/next.config.mjs
index 4a75f77..920cfcd 100644
--- a/webui/next.config.mjs
+++ b/webui/next.config.mjs
@@ -17,25 +17,10 @@
  * under the License.
  */
 
-import { PHASE_DEVELOPMENT_SERVER } from "next/constants.js";
-
-const apiPrefix = "/api/v1";
-const devHost = "127.0.0.1:9379";
-const prodHost = "production-api.yourdomain.com";
-
 const nextConfig = (phase, { defaultConfig }) => {
-    const isDev = phase === PHASE_DEVELOPMENT_SERVER;
-    const host = isDev ? devHost : prodHost;
-
     return {
-        async rewrites() {
-            return [
-                {
-                    source: `${apiPrefix}/:slug*`,
-                    destination: `http://${host}${apiPrefix}/:slug*`,
-                },
-            ];
-        },
+        reactStrictMode: true,
+        output: 'standalone',
     };
 };
 
diff --git a/webui/package.json b/webui/package.json
index 77e7828..79ade28 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -1,41 +1,46 @@
 {
-    "name": "kvrocks-controller-ui",
-    "version": "0.1.0",
-    "private": true,
-    "scripts": {
-        "dev": "next dev",
-        "build": "next build",
-        "start": "next start",
-        "lint": "next lint"
-    },
-    "dependencies": {
-        "@emotion/react": "^11.11.3",
-        "@emotion/styled": "^11.11.0",
-        "@fortawesome/fontawesome-svg-core": "^6.6.0",
-        "@fortawesome/free-brands-svg-icons": "^6.6.0",
-        "@fortawesome/free-solid-svg-icons": "^6.6.0",
-        "@fortawesome/react-fontawesome": "^0.2.2",
-        "@mui/icons-material": "^5.15.7",
-        "@mui/material": "^5.15.5",
-        "@types/js-yaml": "^4.0.9",
-        "axios": "^1.6.7",
-        "js-yaml": "^4.1.0",
-        "next": "^14.2.29",
-        "react": "^18",
-        "react-dom": "^18"
-    },
-    "devDependencies": {
-        "@types/node": "^20",
-        "@types/react": "^18",
-        "@types/react-dom": "^18",
-        "autoprefixer": "^10.0.1",
-        "eslint": "^8",
-        "eslint-config-next": "14.1.0",
-        "eslint-config-prettier": "^10.1.1",
-        "postcss": "^8",
-        "prettier": "^3.5.3",
-        "prettier-plugin-tailwindcss": "^0.6.11",
-        "tailwindcss": "^3.3.0",
-        "typescript": "^5"
-    }
+  "name": "kvrocks-controller-ui",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev --turbopack",
+    "build": "next build",
+    "start": "next start",
+    "lint": "next lint",
+    "deploy": "next build && cp -r .next/static .next/standalone/.next/ && cp 
-r public .next/standalone/"
+  },
+  "dependencies": {
+    "@emotion/react": "^11.11.3",
+    "@emotion/styled": "^11.11.0",
+    "@fortawesome/fontawesome-svg-core": "^6.6.0",
+    "@fortawesome/free-brands-svg-icons": "^6.6.0",
+    "@fortawesome/free-solid-svg-icons": "^6.6.0",
+    "@fortawesome/react-fontawesome": "^0.2.2",
+    "@mui/icons-material": "^5.15.7",
+    "@mui/material": "^5.15.5",
+    "@types/js-yaml": "^4.0.9",
+    "axios": "^1.6.7",
+    "js-yaml": "^4.1.0",
+    "next": "15.5.4",
+    "react": "19.2.0",
+    "react-dom": "19.2.0"
+  },
+  "devDependencies": {
+    "@types/node": "^20",
+    "@types/react": "19.2.2",
+    "@types/react-dom": "19.2.1",
+    "autoprefixer": "^10.0.1",
+    "eslint": "^8",
+    "eslint-config-next": "15.5.4",
+    "eslint-config-prettier": "^10.1.1",
+    "postcss": "^8",
+    "prettier": "^3.5.3",
+    "prettier-plugin-tailwindcss": "^0.6.11",
+    "tailwindcss": "^3.3.0",
+    "typescript": "^5"
+  },
+  "overrides": {
+    "@types/react": "19.2.2",
+    "@types/react-dom": "19.2.1"
+  }
 }
diff --git a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
index 5132a1c..fd763cf 100644
--- a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
+++ b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/page.tsx
@@ -38,7 +38,7 @@ import {
     Divider,
 } from "@mui/material";
 import { ClusterSidebar } from "../../../../ui/sidebar";
-import { useState, useEffect } from "react";
+import { useState, useEffect, use } from "react";
 import { listShards, listNodes, fetchCluster, deleteShard } from 
"@/app/lib/api";
 import { AddShardCard, ResourceCard } from "@/app/ui/createCard";
 import Link from "next/link";
@@ -95,7 +95,8 @@ type FilterOption =
     | "with-importing";
 type SortOption = "index-asc" | "index-desc" | "nodes-desc" | "nodes-asc";
 
-export default function Cluster({ params }: { params: { namespace: string; 
cluster: string } }) {
+export default function Cluster(props: { params: Promise<{ namespace: string; 
cluster: string }> }) {
+    const params = use(props.params);
     const { namespace, cluster } = params;
     const [shardsData, setShardsData] = useState<ShardData[]>([]);
     const [resourceCounts, setResourceCounts] = useState<ResourceCounts>({
diff --git 
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
index 549053d..7cd6a6e 100644
--- 
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
+++ 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/nodes/[node]/page.tsx
@@ -22,7 +22,7 @@
 import { listNodes } from "@/app/lib/api";
 import { NodeSidebar } from "@/app/ui/sidebar";
 import { Box, Typography, Chip, Paper, Divider, Grid, Alert, IconButton } from 
"@mui/material";
-import { useEffect, useState } from "react";
+import { useEffect, useState, use } from "react";
 import { useRouter } from "next/navigation";
 import { LoadingSpinner } from "@/app/ui/loadingSpinner";
 import { truncateText } from "@/app/utils";
@@ -39,11 +39,12 @@ import NetworkCheckIcon from 
"@mui/icons-material/NetworkCheck";
 import SecurityIcon from "@mui/icons-material/Security";
 import LinkIcon from "@mui/icons-material/Link";
 
-export default function Node({
-    params,
-}: {
-    params: { namespace: string; cluster: string; shard: string; node: string 
};
-}) {
+export default function Node(
+    props: {
+        params: Promise<{ namespace: string; cluster: string; shard: string; 
node: string }>;
+    }
+) {
+    const params = use(props.params);
     const { namespace, cluster, shard, node } = params;
     const router = useRouter();
     const [nodeData, setNodeData] = useState<any[]>([]);
diff --git 
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
index e0d5b80..62a4e9f 100644
--- 
a/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
+++ 
b/webui/src/app/namespaces/[namespace]/clusters/[cluster]/shards/[shard]/page.tsx
@@ -37,7 +37,7 @@ import {
 import { ShardSidebar } from "@/app/ui/sidebar";
 import { fetchShard, deleteNode } from "@/app/lib/api";
 import { useRouter } from "next/navigation";
-import { useState, useEffect } from "react";
+import { useState, useEffect, use } from "react";
 import { AddNodeCard } from "@/app/ui/createCard";
 import Link from "next/link";
 import { LoadingSpinner } from "@/app/ui/loadingSpinner";
@@ -60,11 +60,12 @@ import DeleteIcon from "@mui/icons-material/Delete";
 import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
 import { FailoverDialog } from "@/app/ui/failoverDialog";
 
-export default function Shard({
-    params,
-}: {
-    params: { namespace: string; cluster: string; shard: string };
-}) {
+export default function Shard(
+    props: {
+        params: Promise<{ namespace: string; cluster: string; shard: string }>;
+    }
+) {
+    const params = use(props.params);
     const { namespace, cluster, shard } = params;
     const [nodesData, setNodesData] = useState<any>(null);
     const [loading, setLoading] = useState<boolean>(true);
diff --git a/webui/src/app/namespaces/[namespace]/page.tsx 
b/webui/src/app/namespaces/[namespace]/page.tsx
index e1ede3d..4927802 100644
--- a/webui/src/app/namespaces/[namespace]/page.tsx
+++ b/webui/src/app/namespaces/[namespace]/page.tsx
@@ -45,7 +45,7 @@ import {
 } from "@/app/lib/api";
 import Link from "next/link";
 import { useRouter, notFound } from "next/navigation";
-import { useState, useEffect } from "react";
+import { useState, useEffect, use } from "react";
 import { LoadingSpinner } from "@/app/ui/loadingSpinner";
 import StorageIcon from "@mui/icons-material/Storage";
 import FolderIcon from "@mui/icons-material/Folder";
@@ -106,7 +106,8 @@ type SortOption =
     | "nodes-desc"
     | "nodes-asc";
 
-export default function Namespace({ params }: { params: { namespace: string } 
}) {
+export default function Namespace(props: { params: Promise<{ namespace: string 
}> }) {
+    const params = use(props.params);
     const [clusterData, setClusterData] = useState<ClusterData[]>([]);
     const [resourceCounts, setResourceCounts] = useState<ResourceCounts>({
         clusters: 0,
diff --git a/webui/src/app/page.tsx b/webui/src/app/page.tsx
index cc049e4..64e157e 100644
--- a/webui/src/app/page.tsx
+++ b/webui/src/app/page.tsx
@@ -60,7 +60,7 @@ export default function Home() {
     const [scrollY, setScrollY] = useState(0);
     const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
     const [cursorVisible, setCursorVisible] = useState(true);
-    const requestRef = useRef<number>();
+    const requestRef = useRef<number>(undefined);
     const prevScrollY = useRef(0);
 
     const terminalRef = useRef({ lineIndex: 0, charIndex: 0 });
diff --git a/webui/src/middleware.ts b/webui/src/middleware.ts
new file mode 100644
index 0000000..150404b
--- /dev/null
+++ b/webui/src/middleware.ts
@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+export function middleware(req: NextRequest) {
+    const url = req.nextUrl.clone()
+
+    if (url.pathname.startsWith("/api/v1")) {
+        const host = process.env.KVCTL_API_HOST || "localhost:9379"
+        url.host = host;
+
+        return NextResponse.rewrite(url)
+    }
+
+    return NextResponse.next()
+}
+
+export const config = {
+    matcher: "/api/v1/:path*",
+    runtime: 'nodejs',
+}
diff --git a/webui/tsconfig.json b/webui/tsconfig.json
index a125ba4..8a9fe32 100644
--- a/webui/tsconfig.json
+++ b/webui/tsconfig.json
@@ -15,31 +15,43 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */
-
-{
-    "compilerOptions": {
-        "lib": ["dom", "dom.iterable", "esnext"],
-        "allowJs": true,
-        "skipLibCheck": true,
-        "strict": true,
-        "noEmit": true,
-        "esModuleInterop": true,
-        "module": "esnext",
-        "moduleResolution": "bundler",
-        "resolveJsonModule": true,
-        "isolatedModules": true,
-        "jsx": "preserve",
-        "incremental": true,
-        "plugins": [
-            {
-                "name": "next"
-            }
-        ],
-        "paths": {
-            "@/*": ["./src/*"]
-        }
+ */{
+  "compilerOptions": {
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "paths": {
+      "@/*": [
+        "./src/*"
+      ]
     },
-    "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
-    "exclude": ["node_modules"]
+    "target": "ES2017"
+  },
+  "include": [
+    "next-env.d.ts",
+    "**/*.ts",
+    "**/*.tsx",
+    ".next/types/**/*.ts"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
 }

Reply via email to