From 9b1ab50ae86fcf52af6340463c5e988e981a00df Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 19 Oct 2024 17:54:36 -0700 Subject: [PATCH 1/3] initial progress --- webui/package-lock.json | 40 ++++++++ webui/package.json | 2 + webui/src/index.tsx | 8 +- webui/src/views/App.tsx | 144 ++++++++++++++++++++++----- webui/src/views/MainContentArea.tsx | 23 ++++- webui/src/views/SummaryDashboard.tsx | 6 +- 6 files changed, 189 insertions(+), 34 deletions(-) diff --git a/webui/package-lock.json b/webui/package-lock.json index e41d02e1..abbec241 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -25,6 +25,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-js-cron": "^5.0.1", + "react-router": "^6.27.0", + "react-router-dom": "^6.27.0", "react-virtualized": "^9.22.5", "recharts": "^2.12.7", "typescript": "^5.2.2" @@ -2186,6 +2188,14 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@swc/core": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.2.tgz", @@ -4924,6 +4934,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "dependencies": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", diff --git a/webui/package.json b/webui/package.json index 2687f167..f03206ea 100644 --- a/webui/package.json +++ b/webui/package.json @@ -29,6 +29,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-js-cron": "^5.0.1", + "react-router": "^6.27.0", + "react-router-dom": "^6.27.0", "react-virtualized": "^9.22.5", "recharts": "^2.12.7", "typescript": "^5.2.2" diff --git a/webui/src/index.tsx b/webui/src/index.tsx index 76b7eac1..5a912dee 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -34,8 +34,10 @@ el && ], }} > - - - + + + + + ); diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 81e6b965..cb3f1f90 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { Suspense, useEffect, useState } from "react"; import { ScheduleOutlined, DatabaseOutlined, @@ -9,7 +9,7 @@ import { LoadingOutlined, } from "@ant-design/icons"; import type { MenuProps } from "antd"; -import { Button, Layout, Menu, Spin, theme } from "antd"; +import { Button, Empty, Layout, Menu, Spin, theme } from "antd"; import { Config } from "../../gen/ts/v1/config_pb"; import { useAlertApi } from "../components/Alerts"; import { useShowModal } from "../components/ModalManager"; @@ -25,28 +25,134 @@ import _ from "lodash"; import { Code } from "@connectrpc/connect"; import { LoginModal } from "./LoginModal"; import { backrestService, setAuthToken } from "../api"; -import { MainContentArea, useSetContent } from "./MainContentArea"; -import { GettingStartedGuide } from "./GettingStartedGuide"; import { useConfig } from "../components/ConfigProvider"; import { shouldShowSettings } from "../state/configutil"; import { OpSelector } from "../../gen/ts/v1/service_pb"; import { colorForStatus } from "../state/flowdisplayaggregator"; import { getStatusForSelector } from "../state/logstate"; +import { + createHashRouter, + RouterProvider, + useNavigate, + useParams, +} from "react-router-dom"; +import { MainContentAreaTemplate } from "./MainContentArea"; const { Header, Sider } = Layout; +const SummaryDashboard = React.lazy(() => + import("./SummaryDashboard").then((m) => ({ + default: m.SummaryDashboard, + })) +); + +const GettingStartedGuide = React.lazy(() => + import("./GettingStartedGuide").then((m) => ({ + default: m.GettingStartedGuide, + })) +); + +const PlanView = React.lazy(() => + import("./PlanView").then((m) => ({ + default: m.PlanView, + })) +); + +const RepoView = React.lazy(() => + import("./RepoView").then((m) => ({ + default: m.RepoView, + })) +); + +const RepoViewContainer = () => { + const { repoId } = useParams(); + const [config, setConfig] = useConfig(); + + if (!config) { + return ; + } + + const repo = config.repos.find((r) => r.id === repoId); + if (!repo) { + return ; + } + + return ( + + + + ); +}; + +const PlanViewContainer = () => { + const { planId } = useParams(); + const [config, setConfig] = useConfig(); + + if (!config) { + return ; + } + + const plan = config.plans.find((p) => p.id === planId); + if (!plan) { + return ; + } + + return ( + + + + ); +}; + +const router = createHashRouter([ + { + path: "/", + element: ( + }> + + + ), + }, + { + path: "/getting-started", + element: ( + }> + + + ), + }, + { + path: "/plan/:planId", + element: ( + }> + + + ), + }, + { + path: "/repo/:repoId", + element: ( + }> + + + ), + }, +]); + export const App: React.FC = () => { const { token: { colorBgContainer, colorTextLightSolid }, } = theme.useToken(); const alertApi = useAlertApi()!; const showModal = useShowModal(); - const setContent = useSetContent(); + const navigate = useNavigate(); const [config, setConfig] = useConfig(); useEffect(() => { - showModal(); - backrestService .getConfig({}) .then((config) => { @@ -86,26 +192,12 @@ export const App: React.FC = () => { }, []); const showSummaryDashboard = async () => { - const { SummaryDashboard } = await import("./SummaryDashboard"); - setContent( - }> - - , - [ - { - title: "Summary Dashboard", - }, - ] - ); + navigate("/"); }; - useEffect(() => { - if (config === null) { - setContent(

Loading...

, []); - } else { - showSummaryDashboard(); - } - }, [config === null]); + if (!config) { + return ; + } const items = getSidenavItems(config); @@ -176,7 +268,7 @@ export const App: React.FC = () => { items={items} /> - + ); diff --git a/webui/src/views/MainContentArea.tsx b/webui/src/views/MainContentArea.tsx index fde233af..e8455503 100644 --- a/webui/src/views/MainContentArea.tsx +++ b/webui/src/views/MainContentArea.tsx @@ -14,7 +14,7 @@ interface ContentAreaState { type ContentAreaCtx = [ ContentAreaState, - (content: React.ReactNode, breadcrumbs: Breadcrumb[]) => void, + (content: React.ReactNode, breadcrumbs: Breadcrumb[]) => void ]; const ContentAreaContext = React.createContext([ @@ -58,6 +58,25 @@ export const useSetContent = () => { export const MainContentArea = () => { const { breadcrumbs, content } = React.useContext(ContentAreaContext)[0]; + + return ( + + {content ? ( + content + ) : ( + + )} + + ); +}; + +export const MainContentAreaTemplate = ({ + breadcrumbs, + children, +}: { + breadcrumbs: Breadcrumb[]; + children: React.ReactNode; +}) => { const { token: { colorBgContainer }, } = theme.useToken(); @@ -76,7 +95,7 @@ export const MainContentArea = () => { background: colorBgContainer, }} > - {content} + {children} ); diff --git a/webui/src/views/SummaryDashboard.tsx b/webui/src/views/SummaryDashboard.tsx index 3e89c5da..218dd0f3 100644 --- a/webui/src/views/SummaryDashboard.tsx +++ b/webui/src/views/SummaryDashboard.tsx @@ -103,7 +103,7 @@ export const SummaryDashboard = () => { Repos {summaryData && summaryData.repoSummaries.length > 0 ? ( summaryData.repoSummaries.map((summary) => ( - + )) ) : ( @@ -111,7 +111,7 @@ export const SummaryDashboard = () => { Plans {summaryData && summaryData.planSummaries.length > 0 ? ( summaryData.planSummaries.map((summary) => ( - + )) ) : ( @@ -184,7 +184,7 @@ const SummaryPanel = ({ recentBackups.status[idx] === OperationStatus.STATUS_PENDING; return ( - + Backup at {formatTime(entry.time)}{" "}
{isPending ? ( From 3a895fbd8238ee693006e0cf283c5bec80dbc1a6 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 19 Oct 2024 18:17:54 -0700 Subject: [PATCH 2/3] feat: use react-router to enable linking to webUI pages --- webui/src/components/OperationTree.tsx | 3 + webui/src/index.tsx | 5 +- webui/src/views/App.tsx | 125 ++++++++++++------------- 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index d428576b..87b9052a 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -393,6 +393,9 @@ const BackupViewContainer = ({ children }: { children: React.ReactNode }) => { // handle scroll events to keep the fixed container in view. const handleScroll = () => { + if (!ref.current) { + return; + } const refRect = ref.current!.getBoundingClientRect(); let wiggle = Math.max(refRect.height - window.innerHeight, 0); let topY = Math.max(ref.current!.getBoundingClientRect().top, 0); diff --git a/webui/src/index.tsx b/webui/src/index.tsx index 5a912dee..6d31550e 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -8,6 +8,7 @@ import "react-js-cron/dist/styles.css"; import { ConfigProvider as AntdConfigProvider, theme } from "antd"; import { ConfigContextProvider } from "./components/ConfigProvider"; import { MainContentProvider } from "./views/MainContentArea"; +import { HashRouter } from "react-router-dom"; const Root = ({ children }: { children: React.ReactNode }) => { return ( @@ -36,7 +37,9 @@ el && > - + + + diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index cb3f1f90..d43061da 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -32,7 +32,9 @@ import { colorForStatus } from "../state/flowdisplayaggregator"; import { getStatusForSelector } from "../state/logstate"; import { createHashRouter, + Route, RouterProvider, + Routes, useNavigate, useParams, } from "react-router-dom"; @@ -73,15 +75,17 @@ const RepoViewContainer = () => { } const repo = config.repos.find((r) => r.id === repoId); - if (!repo) { - return ; - } return ( - + {repo ? ( + + ) : ( + + )} ); }; @@ -95,54 +99,20 @@ const PlanViewContainer = () => { } const plan = config.plans.find((p) => p.id === planId); - if (!plan) { - return ; - } - return ( - + {plan ? ( + + ) : ( + + )} ); }; -const router = createHashRouter([ - { - path: "/", - element: ( - }> - - - ), - }, - { - path: "/getting-started", - element: ( - }> - - - ), - }, - { - path: "/plan/:planId", - element: ( - }> - - - ), - }, - { - path: "/repo/:repoId", - element: ( - }> - - - ), - }, -]); - export const App: React.FC = () => { const { token: { colorBgContainer, colorTextLightSolid }, @@ -191,16 +161,12 @@ export const App: React.FC = () => { }); }, []); - const showSummaryDashboard = async () => { - navigate("/"); - }; + const items = getSidenavItems(config); if (!config) { return ; } - const items = getSidenavItems(config); - return (
{ > { + navigate("/"); + }} > { items={items} /> - + + + }> + + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + ); @@ -276,9 +279,11 @@ export const App: React.FC = () => { const getSidenavItems = (config: Config | null): MenuProps["items"] => { const showModal = useShowModal(); - const setContent = useSetContent(); + const navigate = useNavigate(); - if (!config) return []; + if (!config) { + return; + } const configPlans = config.plans || []; const configRepos = config.repos || []; @@ -318,12 +323,7 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { ), onClick: async () => { - const { PlanView } = await import("./PlanView"); - - setContent(, [ - { title: "Plans" }, - { title: plan.id || "" }, - ]); + navigate(`/plan/${plan.id}`); }, }; }), @@ -364,12 +364,7 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { ), onClick: async () => { - const { RepoView } = await import("./RepoView"); - - setContent(, [ - { title: "Repos" }, - { title: repo.id || "" }, - ]); + navigate(`/repo/${repo.id}`); }, }; }), From c6cabb7556b5d380d4fa51f984040fea37b77f07 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Sat, 19 Oct 2024 18:23:57 -0700 Subject: [PATCH 3/3] fix npm check output --- webui/src/index.tsx | 5 +-- webui/src/views/App.tsx | 66 +++++++++++++--------------- webui/src/views/MainContentArea.tsx | 65 +-------------------------- webui/src/views/SummaryDashboard.tsx | 22 ++-------- 4 files changed, 37 insertions(+), 121 deletions(-) diff --git a/webui/src/index.tsx b/webui/src/index.tsx index 6d31550e..0634b599 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -7,16 +7,13 @@ import { ModalContextProvider } from "./components/ModalManager"; import "react-js-cron/dist/styles.css"; import { ConfigProvider as AntdConfigProvider, theme } from "antd"; import { ConfigContextProvider } from "./components/ConfigProvider"; -import { MainContentProvider } from "./views/MainContentArea"; import { HashRouter } from "react-router-dom"; const Root = ({ children }: { children: React.ReactNode }) => { return ( - - {children} - + {children} ); diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index d43061da..a4f1d70f 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -236,42 +236,38 @@ export const App: React.FC = () => { items={items} /> - - - }> + }> + + - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - + + } + /> + + + + } + /> + } /> + } /> + + + + } + /> + + ); diff --git a/webui/src/views/MainContentArea.tsx b/webui/src/views/MainContentArea.tsx index e8455503..87f9954b 100644 --- a/webui/src/views/MainContentArea.tsx +++ b/webui/src/views/MainContentArea.tsx @@ -1,75 +1,12 @@ import { Breadcrumb, Layout, Spin, theme } from "antd"; import { Content } from "antd/es/layout/layout"; -import React, { useState } from "react"; +import React from "react"; interface Breadcrumb { title: string; onClick?: () => void; } -interface ContentAreaState { - content: React.ReactNode | null; - breadcrumbs: Breadcrumb[]; -} - -type ContentAreaCtx = [ - ContentAreaState, - (content: React.ReactNode, breadcrumbs: Breadcrumb[]) => void -]; - -const ContentAreaContext = React.createContext([ - { - content: null, - breadcrumbs: [], - }, - (content, breadcrumbs) => {}, -]); - -export const MainContentProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const [state, setState] = useState({ - content: null, - breadcrumbs: [], - }); - - return ( - <> - { - setState({ content, breadcrumbs }); - }, - ]} - > - {children} - - - ); -}; - -export const useSetContent = () => { - const context = React.useContext(ContentAreaContext); - return context[1]; -}; - -export const MainContentArea = () => { - const { breadcrumbs, content } = React.useContext(ContentAreaContext)[0]; - - return ( - - {content ? ( - content - ) : ( - - )} - - ); -}; - export const MainContentAreaTemplate = ({ breadcrumbs, children, diff --git a/webui/src/views/SummaryDashboard.tsx b/webui/src/views/SummaryDashboard.tsx index 218dd0f3..1488e3f8 100644 --- a/webui/src/views/SummaryDashboard.tsx +++ b/webui/src/views/SummaryDashboard.tsx @@ -13,7 +13,6 @@ import { } from "antd"; import React, { useEffect, useState } from "react"; import { useConfig } from "../components/ConfigProvider"; -import { useSetContent } from "./MainContentArea"; import { SummaryDashboardResponse, SummaryDashboardResponse_Summary, @@ -38,29 +37,16 @@ import { import { colorForStatus } from "../state/flowdisplayaggregator"; import { OperationStatus } from "../../gen/ts/v1/operations_pb"; import { isMobile } from "../lib/browserutil"; +import { useNavigate } from "react-router"; export const SummaryDashboard = () => { const config = useConfig()[0]; - const setContent = useSetContent(); const alertApi = useAlertApi()!; + const navigate = useNavigate(); const [summaryData, setSummaryData] = useState(); - const showGettingStarted = async () => { - const { GettingStartedGuide } = await import("./GettingStartedGuide"); - setContent( - }> - - , - [ - { - title: "Getting Started", - }, - ] - ); - }; - useEffect(() => { // Fetch summary data const fetchData = async () => { @@ -73,7 +59,7 @@ export const SummaryDashboard = () => { const data = await backrestService.getSummaryDashboard({}); setSummaryData(data); } catch (e) { - alertApi.error("Failed to fetch summary data", e); + alertApi.error("Failed to fetch summary data: " + e); } }; @@ -89,7 +75,7 @@ export const SummaryDashboard = () => { } if (config.repos.length === 0 && config.plans.length === 0) { - showGettingStarted(); + navigate("/getting-started"); } }, [config]);