This is an automated email from the ASF dual-hosted git repository.
twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks-controller.git
The following commit(s) were added to refs/heads/unstable by this push:
new d9f8c40 feat(webui): use nextjs middleware to dynamically redirect
API calls (#360)
d9f8c40 is described below
commit d9f8c40db01c9d14c5dc179177291127490f4b59
Author: Twice <[email protected]>
AuthorDate: Wed Oct 8 18:39:22 2025 +0800
feat(webui): use nextjs middleware to dynamically redirect API calls (#360)
* feat(webui): use nextjs middleware to dynamically redirect API calls
* lint
---
webui/next.config.mjs | 19 ++----------------
webui/package.json | 23 +++++++++++++---------
.../[namespace]/clusters/[cluster]/page.tsx | 7 +++++--
.../[cluster]/shards/[shard]/nodes/[node]/page.tsx | 9 ++++-----
.../clusters/[cluster]/shards/[shard]/page.tsx | 9 ++++-----
webui/src/app/namespaces/[namespace]/page.tsx | 5 +++--
webui/src/app/page.tsx | 2 +-
webui/src/middleware.ts | 20 +++++++++++++++++++
webui/tsconfig.json | 7 +++----
9 files changed, 56 insertions(+), 45 deletions(-)
diff --git a/webui/next.config.mjs b/webui/next.config.mjs
index 4a75f77..fd8c840 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..67f62c9 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -3,10 +3,11 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
+ "dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "deploy": "next build && cp -r .next/static .next/standalone/.next/ &&
cp -r public .next/standalone/"
},
"dependencies": {
"@emotion/react": "^11.11.3",
@@ -20,22 +21,26 @@
"@types/js-yaml": "^4.0.9",
"axios": "^1.6.7",
"js-yaml": "^4.1.0",
- "next": "^14.2.29",
- "react": "^18",
- "react-dom": "^18"
+ "next": "15.5.4",
+ "react": "19.2.0",
+ "react-dom": "19.2.0"
},
"devDependencies": {
"@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
+ "@types/react": "19.2.2",
+ "@types/react-dom": "19.2.1",
"autoprefixer": "^10.0.1",
"eslint": "^8",
- "eslint-config-next": "14.1.0",
+ "eslint-config-next": "15.5.4",
"eslint-config-prettier": "^10.1.1",
"postcss": "^8",
- "prettier": "^3.5.3",
+ "prettier": "^3.6.2",
"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..6288541 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,10 @@ 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..b785cb4 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,10 @@ 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..a7406a6 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,10 @@ 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..f83dd6b
--- /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..8ec9a25 100644
--- a/webui/tsconfig.json
+++ b/webui/tsconfig.json
@@ -15,9 +15,7 @@
* 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,
@@ -38,7 +36,8 @@
],
"paths": {
"@/*": ["./src/*"]
- }
+ },
+ "target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]