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" + ] }
