diff --git a/dashboard/components/BackPressureTable.tsx b/dashboard/components/BackPressureTable.tsx index 9629a429d9f95..241fde0ddc13c 100644 --- a/dashboard/components/BackPressureTable.tsx +++ b/dashboard/components/BackPressureTable.tsx @@ -24,11 +24,11 @@ import { Th, Thead, Tr, - useToast, } from "@chakra-ui/react" import { sortBy } from "lodash" import Head from "next/head" import { Fragment, useEffect, useState } from "react" +import useErrorToast from "../hook/useErrorToast" import { getActorBackPressures } from "../pages/api/metric" import RateBar from "./RateBar" import { Metrics } from "./metrics" @@ -44,7 +44,7 @@ export default function BackPressureTable({ }) { const [backPressuresMetrics, setBackPressuresMetrics] = useState() - const toast = useToast() + const toast = useErrorToast() useEffect(() => { async function doFetch() { @@ -58,14 +58,7 @@ export default function BackPressureTable({ setBackPressuresMetrics(metrics) await new Promise((resolve) => setTimeout(resolve, 5000)) // refresh every 5 secs } catch (e: any) { - toast({ - title: "Error Occurred", - description: e.toString(), - status: "error", - duration: 5000, - isClosable: true, - }) - console.error(e) + toast(e, "warning") break } } diff --git a/dashboard/components/FragmentGraph.tsx b/dashboard/components/FragmentGraph.tsx index e188310910638..891a6d21df4b0 100644 --- a/dashboard/components/FragmentGraph.tsx +++ b/dashboard/components/FragmentGraph.tsx @@ -20,7 +20,7 @@ import { generateBoxLinks, layout, } from "../lib/layout" -import { PlanNodeDatum } from "../pages/streaming_plan" +import { PlanNodeDatum } from "../pages/fragment_graph" import BackPressureTable from "./BackPressureTable" const ReactJson = loadable(() => import("react-json-view")) @@ -218,7 +218,7 @@ export default function FragmentGraph({ text .attr("fill", "black") - .text(({ id }) => `Fragment #${id}`) + .text(({ id }) => `Fragment ${id}`) .attr("font-family", "inherit") .attr("text-anchor", "end") .attr("dy", ({ height }) => height - actorMarginY + 12) diff --git a/dashboard/components/Layout.tsx b/dashboard/components/Layout.tsx index c161eefa20a33..a0bc7acd730d4 100644 --- a/dashboard/components/Layout.tsx +++ b/dashboard/components/Layout.tsx @@ -31,6 +31,7 @@ import { UrlObject } from "url" import { IconArrowRightCircle, IconArrowRightCircleFill, + IconBoxArrowUpRight, IconServer, } from "../components/utils/icons" @@ -41,11 +42,13 @@ function NavButton({ children, leftIcon, leftIconActive, + external, }: { href: string | UrlObject children?: React.ReactNode leftIcon?: React.ReactElement leftIconActive?: React.ReactElement + external?: boolean }) { const router = useRouter() const [match, setMatch] = useState(false) @@ -55,19 +58,22 @@ function NavButton({ return () => {} }, [href, router.asPath]) + const icon = + leftIcon || (external ? : ) + const activeIcon = + leftIconActive || + leftIcon || + (external ? undefined : ) + return ( - + @@ -83,6 +89,14 @@ function NavTitle({ children }: { children: React.ReactNode }) { ) } +function Section({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + function Layout({ children }: { children: React.ReactNode }) { return ( @@ -110,10 +124,12 @@ function Layout({ children }: { children: React.ReactNode }) { - }> - Cluster Overview - - +
+ }> + Cluster Overview + +
+
Catalog Sources Tables @@ -124,28 +140,32 @@ function Layout({ children }: { children: React.ReactNode }) { Internal Tables Sinks Views - - +
+
Streaming - Graph - Fragments - - + Dependency Graph + Fragment Graph +
+
Batch Batch Tasks - - +
+
Explain Distributed Plan - - +
+
Debug Await Tree Dump Heap Profiling - Diagnose - - - Settings + + Diagnose + +
+
+ Settings + Settings +
diff --git a/dashboard/components/Relations.tsx b/dashboard/components/Relations.tsx index 33df34b753b7a..acdba0e2608c2 100644 --- a/dashboard/components/Relations.tsx +++ b/dashboard/components/Relations.tsx @@ -33,7 +33,6 @@ import { Thead, Tr, useDisclosure, - useToast, } from "@chakra-ui/react" import loadable from "@loadable/component" import Head from "next/head" @@ -41,6 +40,7 @@ import Head from "next/head" import Link from "next/link" import { Fragment, useEffect, useState } from "react" import Title from "../components/Title" +import useErrorToast from "../hook/useErrorToast" import extractColumnInfo from "../lib/extractInfo" import { Relation, StreamingJob } from "../pages/api/streaming" import { @@ -121,7 +121,7 @@ export function Relations( getRelations: () => Promise, extraColumns: Column[] ) { - const toast = useToast() + const toast = useErrorToast() const [relationList, setRelationList] = useState([]) useEffect(() => { @@ -129,14 +129,7 @@ export function Relations( try { setRelationList(await getRelations()) } catch (e: any) { - toast({ - title: "Error Occurred", - description: e.toString(), - status: "error", - duration: 5000, - isClosable: true, - }) - console.error(e) + toast(e) } } doFetch() diff --git a/dashboard/components/utils/icons.tsx b/dashboard/components/utils/icons.tsx index a3e32af20fbd4..8bef660526c95 100644 --- a/dashboard/components/utils/icons.tsx +++ b/dashboard/components/utils/icons.tsx @@ -26,3 +26,7 @@ export function IconArrowRightCircle() { export function IconArrowRightCircleFill() { return } + +export function IconBoxArrowUpRight() { + return +} diff --git a/dashboard/hook/useErrorToast.ts b/dashboard/hook/useErrorToast.ts new file mode 100644 index 0000000000000..1c86cc3084fbf --- /dev/null +++ b/dashboard/hook/useErrorToast.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2024 RisingWave Labs + * + * Licensed 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 { AlertStatus, useToast } from "@chakra-ui/react" +import { useCallback } from "react" + +export default function useErrorToast() { + const toast = useToast() + + return useCallback( + (e: any, status: AlertStatus = "error") => { + let title: string + let description: string | undefined + + if (e instanceof Error) { + title = e.message + description = e.cause?.toString() + } else { + title = e.toString() + } + + toast({ + title, + description, + status, + duration: 5000, + isClosable: true, + }) + + console.error(e) + }, + [toast] + ) +} diff --git a/dashboard/hook/useWindowSize.js b/dashboard/hook/useWindowSize.js deleted file mode 100644 index 2b5a35d0cd710..0000000000000 --- a/dashboard/hook/useWindowSize.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed 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 { useEffect, useState } from "react" - -export default function useWindowSize() { - const [size, setSize] = useState({ w: 1024, h: 768 }) - // setSize({ - // w: window.innerWidth, - // h: window.innerHeight - // }) - const setSizeOnEvent = () => { - setSize({ - w: window.innerWidth, - h: window.innerHeight, - }) - } - useEffect(() => { - window.addEventListener("resize", setSizeOnEvent) - return () => window.removeEventListener("resize", setSizeOnEvent) - }, []) - return size -} diff --git a/dashboard/pages/_app.tsx b/dashboard/pages/_app.tsx index e383c2d71fae4..35f46f222cd7c 100644 --- a/dashboard/pages/_app.tsx +++ b/dashboard/pages/_app.tsx @@ -30,7 +30,11 @@ function App({ Component, pageProps }: AppProps) { const [isLoading, setIsLoading] = useState(false) useEffect(() => { - router.events.on("routeChangeStart", () => setIsLoading(true)) + router.events.on("routeChangeStart", (url, { shallow }) => { + if (!shallow) { + setIsLoading(true) + } + }) router.events.on("routeChangeComplete", () => setIsLoading(false)) router.events.on("routeChangeError", () => setIsLoading(false)) }, [router.events]) diff --git a/dashboard/pages/api/api.ts b/dashboard/pages/api/api.ts index c5e62cb989378..4164172b89e76 100644 --- a/dashboard/pages/api/api.ts +++ b/dashboard/pages/api/api.ts @@ -23,7 +23,9 @@ class Api { return data } catch (e) { console.error(e) - throw Error("Failed to fetch " + url) + throw Error(`Failed to fetch ${url}`, { + cause: e, + }) } } } diff --git a/dashboard/pages/api/fetch.ts b/dashboard/pages/api/fetch.ts index 2ee2bbe9975d1..b545ca60dee25 100644 --- a/dashboard/pages/api/fetch.ts +++ b/dashboard/pages/api/fetch.ts @@ -15,12 +15,12 @@ * */ -import { useToast } from "@chakra-ui/react" import { useEffect, useState } from "react" +import useErrorToast from "../../hook/useErrorToast" export default function useFetch(fetchFn: () => Promise) { const [response, setResponse] = useState() - const toast = useToast() + const toast = useErrorToast() useEffect(() => { const fetchData = async () => { @@ -28,14 +28,7 @@ export default function useFetch(fetchFn: () => Promise) { const res = await fetchFn() setResponse(res) } catch (e: any) { - toast({ - title: "Error Occurred", - description: e.toString(), - status: "error", - duration: 5000, - isClosable: true, - }) - console.error(e) + toast(e) } } fetchData() diff --git a/dashboard/pages/await_tree.tsx b/dashboard/pages/await_tree.tsx index 1be88ab0f0c19..8a3e00d2f89f8 100644 --- a/dashboard/pages/await_tree.tsx +++ b/dashboard/pages/await_tree.tsx @@ -81,7 +81,7 @@ export default function AwaitTreeDump() { result = `${title}\n\n${actorTraces}\n${rpcTraces}` } catch (e: any) { - result = `${title}\n\nError: ${e.message}` + result = `${title}\n\nERROR: ${e.message}\n${e.cause}` } setDump(result) diff --git a/dashboard/pages/cluster.tsx b/dashboard/pages/cluster.tsx index 7b5fe1d07684d..5edc80d862dcd 100644 --- a/dashboard/pages/cluster.tsx +++ b/dashboard/pages/cluster.tsx @@ -23,7 +23,6 @@ import { SimpleGrid, Text, theme, - useToast, VStack, } from "@chakra-ui/react" import { clone, reverse, sortBy } from "lodash" @@ -32,6 +31,7 @@ import { Fragment, useCallback, useEffect, useState } from "react" import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from "recharts" import { Metrics, MetricsSample } from "../components/metrics" import Title from "../components/Title" +import useErrorToast from "../hook/useErrorToast" import { WorkerNode } from "../proto/gen/common" import { getClusterInfoComputeNode, @@ -142,7 +142,7 @@ export default function Cluster() { const [frontendList, setFrontendList] = useState([]) const [computeNodeList, setComputeNodeList] = useState([]) const [metrics, setMetrics] = useState() - const toast = useToast() + const toast = useErrorToast() useEffect(() => { async function doFetch() { @@ -150,14 +150,7 @@ export default function Cluster() { setFrontendList(await getClusterInfoFrontend()) setComputeNodeList(await getClusterInfoComputeNode()) } catch (e: any) { - toast({ - title: "Error Occurred", - description: e.toString(), - status: "error", - duration: 5000, - isClosable: true, - }) - console.error(e) + toast(e) } } doFetch() @@ -177,14 +170,7 @@ export default function Cluster() { setMetrics(metrics) await new Promise((resolve) => setTimeout(resolve, 5000)) // refresh every 5 secs } catch (e: any) { - toast({ - title: "Error Occurred", - description: e.toString(), - status: "error", - duration: 5000, - isClosable: true, - }) - console.error(e) + toast(e, "warning") break } } diff --git a/dashboard/pages/streaming_graph.tsx b/dashboard/pages/dependency_graph.tsx similarity index 87% rename from dashboard/pages/streaming_graph.tsx rename to dashboard/pages/dependency_graph.tsx index c2ec4eb731ae1..40a7d4c5c897a 100644 --- a/dashboard/pages/streaming_graph.tsx +++ b/dashboard/pages/dependency_graph.tsx @@ -15,7 +15,7 @@ * */ -import { Box, Button, Flex, Text, useToast, VStack } from "@chakra-ui/react" +import { Box, Button, Flex, Text, VStack } from "@chakra-ui/react" import { reverse, sortBy } from "lodash" import Head from "next/head" import Link from "next/link" @@ -23,8 +23,9 @@ import { useRouter } from "next/router" import { Fragment, useCallback, useEffect, useState } from "react" import { StreamGraph } from "../components/StreamGraph" import Title from "../components/Title" +import useErrorToast from "../hook/useErrorToast" import { ActorPoint } from "../lib/layout" -import { getRelations, Relation, relationIsStreamingJob } from "./api/streaming" +import { Relation, getRelations, relationIsStreamingJob } from "./api/streaming" const SIDEBAR_WIDTH = "200px" @@ -47,7 +48,7 @@ function buildDependencyAsEdges(list: Relation[]): ActorPoint[] { } export default function StreamingGraph() { - const toast = useToast() + const toast = useErrorToast() const [streamingJobList, setStreamingJobList] = useState() useEffect(() => { @@ -55,14 +56,7 @@ export default function StreamingGraph() { try { setStreamingJobList(await getRelations()) } catch (e: any) { - toast({ - title: "Error Occurred", - description: e.toString(), - status: "error", - duration: 5000, - isClosable: true, - }) - console.error(e) + toast(e) } } doFetch() @@ -83,7 +77,7 @@ export default function StreamingGraph() { const retVal = ( - Streaming Graph + Dependency Graph - All Nodes + Relations - + {streamingJobList?.map((r) => { const match = router.query.id === r.id.toString() return ( - +