diff --git a/dashboard/mock-server.js b/dashboard/mock-server.js index ba10e464e54a..31d67bd87b77 100644 --- a/dashboard/mock-server.js +++ b/dashboard/mock-server.js @@ -16,8 +16,11 @@ */ const express = require("express") +const cors = require("cors") const app = express() +app.use(cors()) + app.listen(32333, () => { console.log("Server running on port 32333") }) diff --git a/dashboard/next.config.js b/dashboard/next.config.js index 17622bb79444..ca73de39b634 100644 --- a/dashboard/next.config.js +++ b/dashboard/next.config.js @@ -20,16 +20,6 @@ */ const nextConfig = { trailingSlash: true, - - rewrites: () => { - return [ - { - source: "/api/:path*", - // To test with a RisingWave Meta node, use "http://127.0.0.1:5691/api/:path*" - destination: "http://localhost:32333/:path*", - }, - ] - }, } module.exports = nextConfig diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 7ef31ee76bd7..bcb4687ce202 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -13,6 +13,7 @@ "@monaco-editor/react": "^4.4.6", "@types/d3": "^7.4.0", "@types/lodash": "^4.14.184", + "@uidotdev/usehooks": "^2.4.1", "base64url": "^3.0.1", "bootstrap-icons": "^1.9.1", "d3": "^7.6.1", @@ -42,6 +43,7 @@ "@types/node": "^18.7.14", "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^5.52.0", + "cors": "^2.8.5", "eslint": "^8.45.0", "eslint-config-next": "13.4.12", "eslint-config-prettier": "^8.8.0", @@ -3224,6 +3226,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@zag-js/element-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.1.0.tgz", @@ -4118,6 +4132,19 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -13342,6 +13369,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@uidotdev/usehooks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", + "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", + "requires": {} + }, "@zag-js/element-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.1.0.tgz", @@ -14004,6 +14037,16 @@ "integrity": "sha512-IeHpLwk3uoci37yoI2Laty59+YqH9x5uR65/yiA0ARAJrTrN4YU0rmauLWfvqOuk77SlNJXj2rM6oT/dBD87+A==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 67d2ff0ef171..18377b4ef25e 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -19,6 +19,7 @@ "@monaco-editor/react": "^4.4.6", "@types/d3": "^7.4.0", "@types/lodash": "^4.14.184", + "@uidotdev/usehooks": "^2.4.1", "base64url": "^3.0.1", "bootstrap-icons": "^1.9.1", "d3": "^7.6.1", @@ -48,6 +49,7 @@ "@types/node": "^18.7.14", "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^5.52.0", + "cors": "^2.8.5", "eslint": "^8.45.0", "eslint-config-next": "13.4.12", "eslint-config-prettier": "^8.8.0", diff --git a/dashboard/pages/api/api.ts b/dashboard/pages/api/api.ts index c5e62cb98937..0b9514ed1653 100644 --- a/dashboard/pages/api/api.ts +++ b/dashboard/pages/api/api.ts @@ -15,8 +15,34 @@ * */ +const PROD_API_ENDPOINT = "/api" +const MOCK_API_ENDPOINT = "http://localhost:32333" +const EXTERNAL_META_NODE_API_ENDPOINT = "http://localhost:5691/api" + +export const PREDEFINED_API_ENDPOINTS = [ + PROD_API_ENDPOINT, + MOCK_API_ENDPOINT, + EXTERNAL_META_NODE_API_ENDPOINT, +] + +export const DEFAULT_API_ENDPOINT: string = + process.env.NODE_ENV === "production" ? PROD_API_ENDPOINT : MOCK_API_ENDPOINT + +export const API_ENDPOINT_KEY = "risingwave.dashboard.api.endpoint" + class Api { - async get(url: string) { + urlFor(path: string) { + let apiEndpoint: string = ( + JSON.parse(localStorage.getItem(API_ENDPOINT_KEY) || "null") || + DEFAULT_API_ENDPOINT + ).replace(/\/+$/, "") // remove trailing slashes + + return `${apiEndpoint}${path}` + } + + async get(path: string) { + const url = this.urlFor(path) + try { const res = await fetch(url) const data = await res.json() diff --git a/dashboard/pages/api/cluster.ts b/dashboard/pages/api/cluster.ts index 6d8d4850529e..0efc08330938 100644 --- a/dashboard/pages/api/cluster.ts +++ b/dashboard/pages/api/cluster.ts @@ -18,19 +18,19 @@ import { WorkerNode } from "../../proto/gen/common" import api from "./api" export async function getClusterMetrics() { - const res = await api.get("/api/metrics/cluster") + const res = await api.get("/metrics/cluster") return res } export async function getClusterInfoFrontend() { - const res: WorkerNode[] = (await api.get("/api/clusters/1")).map( + const res: WorkerNode[] = (await api.get("/clusters/1")).map( WorkerNode.fromJSON ) return res } export async function getClusterInfoComputeNode() { - const res: WorkerNode[] = (await api.get("/api/clusters/2")).map( + const res: WorkerNode[] = (await api.get("/clusters/2")).map( WorkerNode.fromJSON ) return res diff --git a/dashboard/pages/api/metric.ts b/dashboard/pages/api/metric.ts index 55103881277c..c799e52e71a7 100644 --- a/dashboard/pages/api/metric.ts +++ b/dashboard/pages/api/metric.ts @@ -18,7 +18,7 @@ import { MetricsSample } from "../../components/metrics" import api from "./api" export async function getActorBackPressures() { - const res = await api.get("/api/metrics/actor/back_pressures") + const res = await api.get("/metrics/actor/back_pressures") return res } diff --git a/dashboard/pages/api/streaming.ts b/dashboard/pages/api/streaming.ts index 97d8a45582da..a77a165357b9 100644 --- a/dashboard/pages/api/streaming.ts +++ b/dashboard/pages/api/streaming.ts @@ -23,11 +23,11 @@ import { ColumnCatalog, Field } from "../../proto/gen/plan_common" import api from "./api" export async function getActors(): Promise { - return (await api.get("/api/actors")).map(ActorLocation.fromJSON) + return (await api.get("/actors")).map(ActorLocation.fromJSON) } export async function getFragments(): Promise { - let fragmentList: TableFragments[] = (await api.get("/api/fragments2")).map( + let fragmentList: TableFragments[] = (await api.get("/fragments2")).map( TableFragments.fromJSON ) fragmentList = sortBy(fragmentList, (x) => x.tableId) @@ -75,7 +75,7 @@ export async function getRelations() { async function getTableCatalogsInner( path: "tables" | "materialized_views" | "indexes" | "internal_tables" ) { - let list: Table[] = (await api.get(`/api/${path}`)).map(Table.fromJSON) + let list: Table[] = (await api.get(`/${path}`)).map(Table.fromJSON) list = sortBy(list, (x) => x.id) return list } @@ -97,21 +97,19 @@ export async function getInternalTables() { } export async function getSinks() { - let sinkList: Sink[] = (await api.get("/api/sinks")).map(Sink.fromJSON) + let sinkList: Sink[] = (await api.get("/sinks")).map(Sink.fromJSON) sinkList = sortBy(sinkList, (x) => x.id) return sinkList } export async function getSources() { - let sourceList: Source[] = (await api.get("/api/sources")).map( - Source.fromJSON - ) + let sourceList: Source[] = (await api.get("/sources")).map(Source.fromJSON) sourceList = sortBy(sourceList, (x) => x.id) return sourceList } export async function getViews() { - let views: View[] = (await api.get("/api/views")).map(View.fromJSON) + let views: View[] = (await api.get("/views")).map(View.fromJSON) views = sortBy(views, (x) => x.id) return views } diff --git a/dashboard/pages/await_tree.tsx b/dashboard/pages/await_tree.tsx index 1be88ab0f0c1..4d5f428c3ec6 100644 --- a/dashboard/pages/await_tree.tsx +++ b/dashboard/pages/await_tree.tsx @@ -67,7 +67,7 @@ export default function AwaitTreeDump() { try { const response: StackTraceResponse = StackTraceResponse.fromJSON( - await api.get(`/api/monitor/await_tree/${computeNodeId}`) + await api.get(`/monitor/await_tree/${computeNodeId}`) ) const actorTraces = _(response.actorTraces) diff --git a/dashboard/pages/heap_profiling.tsx b/dashboard/pages/heap_profiling.tsx index 88b737dc4f92..a457d8482459 100644 --- a/dashboard/pages/heap_profiling.tsx +++ b/dashboard/pages/heap_profiling.tsx @@ -75,7 +75,7 @@ export default function HeapProfiling() { try { let list: ListHeapProfilingResponse = ListHeapProfilingResponse.fromJSON( - await api.get(`/api/monitor/list_heap_profile/${computeNodeId}`) + await api.get(`/monitor/list_heap_profile/${computeNodeId}`) ) setProfileList(list) } catch (e: any) { @@ -119,7 +119,7 @@ export default function HeapProfiling() { }, [selectedProfileList]) async function dumpProfile() { - api.get(`/api/monitor/dump_heap_profile/${computeNodeId}`) + api.get(`/monitor/dump_heap_profile/${computeNodeId}`) getProfileList(computeNodes, computeNodeId) } @@ -149,7 +149,7 @@ export default function HeapProfiling() { try { let analyzeFilePathBase64 = base64url(analyzeFilePath) let resObj = await fetch( - `/api/monitor/analyze/${computeNodeId}/${analyzeFilePathBase64}` + `/monitor/analyze/${computeNodeId}/${analyzeFilePathBase64}` ).then(async (res) => ({ filename: res.headers.get("content-disposition"), blob: await res.blob(), diff --git a/dashboard/pages/settings.tsx b/dashboard/pages/settings.tsx index 8301aa227fe1..3214bbdf5c74 100644 --- a/dashboard/pages/settings.tsx +++ b/dashboard/pages/settings.tsx @@ -16,11 +16,28 @@ */ import { Box, FormControl, FormLabel, Input, VStack } from "@chakra-ui/react" +import { useIsClient, useLocalStorage } from "@uidotdev/usehooks" import Head from "next/head" import { Fragment } from "react" import Title from "../components/Title" +import { + API_ENDPOINT_KEY, + DEFAULT_API_ENDPOINT, + PREDEFINED_API_ENDPOINTS, +} from "./api/api" export default function Settings() { + const isClient = useIsClient() + return isClient && +} + +// Local storage is only available on the client side. +function ClientSettings() { + const [apiEndpoint, saveApiEndpoint] = useLocalStorage( + API_ENDPOINT_KEY, + DEFAULT_API_ENDPOINT + ) + return ( @@ -31,15 +48,16 @@ export default function Settings() { RisingWave Meta Node HTTP API - - - - Grafana HTTP API - - - - Prometheus HTTP API - + saveApiEndpoint(event.target.value)} + list="predefined" + /> + + {PREDEFINED_API_ENDPOINTS.map((endpoint) => ( +