From cda9244e8e4f3bd0fdafcac1245e1dbefbdb3350 Mon Sep 17 00:00:00 2001 From: Samuel N Cui Date: Tue, 13 Dec 2022 20:02:29 +0800 Subject: [PATCH] feat: frontend use router --- cmd/tape-httpd/main.go | 13 +++---- executor/job_archive_exe.go | 4 +- frontend/package.json | 6 ++- frontend/pnpm-lock.yaml | 74 +++++++++++++++++++++++++++++++++++++ frontend/src/app.tsx | 69 ++++++++++++++++------------------ frontend/src/file.tsx | 34 ++++++++++++----- frontend/src/jobs.tsx | 17 ++++++++- frontend/src/main.tsx | 5 ++- frontend/vite.config.ts | 1 - tools/recover.go | 6 ++- 10 files changed, 167 insertions(+), 62 deletions(-) diff --git a/cmd/tape-httpd/main.go b/cmd/tape-httpd/main.go index a743d06..92b6dbc 100644 --- a/cmd/tape-httpd/main.go +++ b/cmd/tape-httpd/main.go @@ -96,15 +96,14 @@ func main() { fs := http.FileServer(http.Dir("./frontend/assets")) mux.Handle("/assets/", http.StripPrefix("/assets/", fs)) - indexBuf, err := ioutil.ReadFile("./frontend/index.html") - if err != nil { - panic(err) - } - - indexBuf = bytes.ReplaceAll(indexBuf, []byte("%%API_BASE%%"), []byte(fmt.Sprintf("%s/services", conf.Domain))) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + indexBuf, err := ioutil.ReadFile("./frontend/index.html") + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write(indexBuf) + w.Write(bytes.ReplaceAll(indexBuf, []byte("%%API_BASE%%"), []byte(fmt.Sprintf("%s/services", conf.Domain)))) }) srv := &http.Server{ diff --git a/executor/job_archive_exe.go b/executor/job_archive_exe.go index bd42ded..61a7e4f 100644 --- a/executor/job_archive_exe.go +++ b/executor/job_archive_exe.go @@ -84,7 +84,7 @@ func (a *jobArchiveExecutor) handle(ctx context.Context, param *entity.JobArchiv } tools.Working() - go tools.Wrap(ctx, func() { + go tools.WrapWithLogger(ctx, a.logger, func() { defer tools.Done() if err := a.makeTape(tools.ShutdownContext, p.Device, p.Barcode, p.Name); err != nil { a.logger.WithContext(ctx).WithError(err).Errorf("make type has error, barcode= '%s' name= '%s'", p.Barcode, p.Name) @@ -246,7 +246,7 @@ func (a *jobArchiveExecutor) makeTape(ctx context.Context, device, barcode, name a.logger.WithContext(ctx).WithError(err).Warnf("open report file fail, barcode= '%s'", barcode) } else { defer reportFile.Close() - tools.Wrap(ctx, func() { + tools.WrapWithLogger(ctx, a.logger, func() { reportFile.Write([]byte(report.ToJSONString(false))) }) } diff --git a/frontend/package.json b/frontend/package.json index cb1ca11..0f54657 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,12 +26,16 @@ "fast-text-encoding": "^1.0.6", "filesize": "^10.0.5", "format-duration": "^2.0.0", + "localforage": "^1.10.0", + "match-sorter": "^6.3.1", "moment": "^2.29.4", "react": "^18.2.0", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "^18.2.0", - "react-is": "^18.2.0" + "react-is": "^18.2.0", + "react-router-dom": "^6.4.5", + "sort-by": "^1.2.0" }, "devDependencies": { "@protobuf-ts/plugin": "^2.8.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index cd5d74c..a842130 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -23,6 +23,8 @@ specifiers: filesize: ^10.0.5 format-duration: ^2.0.0 less: ^4.1.3 + localforage: ^1.10.0 + match-sorter: ^6.3.1 moment: ^2.29.4 prettier: 2.7.1 react: ^18.2.0 @@ -30,6 +32,8 @@ specifiers: react-dnd-html5-backend: ^11.1.3 react-dom: ^18.2.0 react-is: ^18.2.0 + react-router-dom: ^6.4.5 + sort-by: ^1.2.0 tsdef: ^0.0.14 typescript: ^4.6.4 vite: ^3.1.0 @@ -51,12 +55,16 @@ dependencies: fast-text-encoding: 1.0.6 filesize: 10.0.5 format-duration: 2.0.0 + localforage: 1.10.0 + match-sorter: 6.3.1 moment: 2.29.4 react: 18.2.0 react-dnd: 11.1.3_biqbaboplfbrettd7655fr4n2y react-dnd-html5-backend: 11.1.3 react-dom: 18.2.0_react@18.2.0 react-is: 18.2.0 + react-router-dom: 6.4.5_biqbaboplfbrettd7655fr4n2y + sort-by: 1.2.0 devDependencies: '@protobuf-ts/plugin': 2.8.2 @@ -1025,6 +1033,11 @@ packages: reselect: 4.1.6 dev: false + /@remix-run/router/1.0.5: + resolution: {integrity: sha512-my0Mycd+jruq/1lQuO5LBB6WTlL/e8DTCYWp44DfMTDcXz8DcTlgF0ISaLsGewt+ctHN+yA8xMq3q/N7uWJPug==} + engines: {node: '>=14'} + dev: false + /@types/classnames/2.3.1: resolution: {integrity: sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==} deprecated: This is a stub types definition. classnames provides its own type definitions, so you do not need this installed. @@ -1739,6 +1752,10 @@ packages: dev: true optional: true + /immediate/3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /immer/9.0.15: resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==} dev: false @@ -1935,10 +1952,22 @@ packages: - supports-color dev: true + /lie/3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: false + /localforage/1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: false @@ -1967,6 +1996,13 @@ packages: dev: true optional: true + /match-sorter/6.3.1: + resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==} + dependencies: + '@babel/runtime': 7.20.6 + remove-accents: 0.4.2 + dev: false + /memoize-one/5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} dev: false @@ -2029,6 +2065,11 @@ packages: engines: {node: '>= 10.12.0'} dev: false + /object-path/0.6.0: + resolution: {integrity: sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==} + engines: {node: '>=0.8.0'} + dev: false + /parent-module/1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2225,6 +2266,29 @@ packages: engines: {node: '>=0.10.0'} dev: true + /react-router-dom/6.4.5_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-a7HsgikBR0wNfroBHcZUCd9+mLRqZS8R5U1Z1mzLWxFXEkUT3vR1XXmSIVoVpxVX8Bar0nQYYYc9Yipq8dWwAA==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.0.5 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-router: 6.4.5_react@18.2.0 + dev: false + + /react-router/6.4.5_react@18.2.0: + resolution: {integrity: sha512-1RQJ8bM70YEumHIlNUYc6mFfUDoWa5EgPDenK/fq0bxD8DYpQUi/S6Zoft+9DBrh2xmtg92N5HMAJgGWDhKJ5Q==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.0.5 + react: 18.2.0 + dev: false + /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -2294,6 +2358,10 @@ packages: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false + /remove-accents/0.4.2: + resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + dev: false + /reselect/4.1.6: resolution: {integrity: sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==} dev: false @@ -2360,6 +2428,12 @@ packages: nanoid: 2.1.11 dev: false + /sort-by/1.2.0: + resolution: {integrity: sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==} + dependencies: + object-path: 0.6.0 + dev: false + /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 345579e..aed35bf 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -1,6 +1,5 @@ -import { useEffect } from "react"; -import { useState, useCallback } from "react"; -import { ChangeEvent } from "react"; +import { Fragment, useCallback, ChangeEvent } from "react"; +import { Routes, Route, Link, useNavigate, Navigate, useLocation } from "react-router-dom"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; @@ -13,62 +12,58 @@ import { JobsBrowser, JobsType } from "./jobs"; import "./app.less"; import { sleep } from "./api"; import { Nullable } from "tsdef"; +import { Job } from "./entity"; +import { useEffect } from "react"; +import { useState } from "react"; // import reactLogo from './assets/react.svg' // React logo const theme = createTheme({}); -const typeToElement = (type: string) => { - switch (type) { - case FileBrowserType: - return ; - case BackupType: - return ; - case JobsType: - return ; - default: - return null; - } +const Delay = ({ inner }: { inner: JSX.Element }) => { + const [ok, setOK] = useState(false); + useEffect(() => { + setOK(false); + (async () => { + await sleep(0); + setOK(true); + })(); + return () => { + setOK(false); + }; + }, [inner]); + + return ok ? inner : null; }; const App = () => { - const [tabValue, setTabValue] = useState(FileBrowserType); - const [inner, setInner] = useState>(null); - - const setType = useCallback( - (newValue: string) => { - (async () => { - setTabValue(newValue); - setInner(null); - await sleep(0); - setInner(typeToElement(newValue)); - })(); - }, - [setTabValue, setInner] - ); - + const location = useLocation(); + const navigate = useNavigate(); const handleTabChange = useCallback( (_: ChangeEvent<{}>, newValue: string) => { - setType(newValue); + navigate("/" + newValue); }, - [setTabValue] + [navigate] ); - useEffect(() => { - setType(FileBrowserType); - }, []); - return (
- + + + + } />} /> + } />} /> + } />} /> + } /> + + - {inner}
); }; diff --git a/frontend/src/file.tsx b/frontend/src/file.tsx index fb7b20f..6f243d3 100644 --- a/frontend/src/file.tsx +++ b/frontend/src/file.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useMemo, useCallback } from "react"; import Grid from "@mui/material/Grid"; import Box from "@mui/material/Box"; -import { FullFileBrowser, FileBrowserHandle, FileArray } from "chonky"; +import { FullFileBrowser, FileBrowserProps, FileBrowserHandle, FileArray } from "chonky"; import { ChonkyActions, ChonkyFileActionData } from "chonky"; import "./app.less"; @@ -32,7 +32,7 @@ const useDualSide = () => { return { instances, refreshAll }; }; -const useFileBrowser = (refreshAll: () => Promise, openDetailModel: (detail: FileGetReply) => void) => { +const useFileBrowser = (storageKey: string, refreshAll: () => Promise, openDetailModel: (detail: FileGetReply) => void) => { const [files, setFiles] = useState(Array(1).fill(null)); const [folderChain, setFolderChan] = useState([Root]); const currentID = useMemo(() => { @@ -48,15 +48,28 @@ const useFileBrowser = (refreshAll: () => Promise, openDetailModel: (detai return last.id; }, [folderChain]); - const openFolder = useCallback((id: string) => { - (async () => { - const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id) }).response, cli.fileListParents({ id: BigInt(id) }).response]); + const openFolder = useCallback(async (id: string) => { + const [file, folderChain] = await Promise.all([cli.fileGet({ id: BigInt(id) }).response, cli.fileListParents({ id: BigInt(id) }).response]); - setFiles(convertFiles(file.children)); - setFolderChan([Root, ...convertFiles(folderChain.parents)]); + setFiles(convertFiles(file.children)); + setFolderChan([Root, ...convertFiles(folderChain.parents)]); + localStorage.setItem(storageKey, id); + }, []); + useEffect(() => { + (async () => { + const storagedID = localStorage.getItem(storageKey); + if (storagedID) { + try { + await openFolder(storagedID); + return; + } catch (e) { + console.log("open storaged id fail, err= ", e); + } + } + + openFolder(Root.id); })(); }, []); - useEffect(() => openFolder(Root.id), []); const onFileAction = useCallback( (data: ChonkyFileActionData) => { @@ -157,8 +170,8 @@ export const FileBrowser = () => { const { instances, refreshAll } = useDualSide(); const { detail, openDetailModel, closeDetailModel } = useDetailModal(); - const leftProps = useFileBrowser(refreshAll, openDetailModel); - const rightProps = useFileBrowser(refreshAll, openDetailModel); + const leftProps = useFileBrowser("file_browser:left:current_id", refreshAll, openDetailModel); + const rightProps = useFileBrowser("file_browser:right:current_id", refreshAll, openDetailModel); useEffect(() => { Object.values(instances).map((inst) => inst.current?.requestFileAction(ChonkyActions.ToggleHiddenFiles, {})); @@ -167,6 +180,7 @@ export const FileBrowser = () => { }, 10000); return () => clearInterval(interval); }, []); + useEffect(() => {}); return ( diff --git a/frontend/src/jobs.tsx b/frontend/src/jobs.tsx index 37b12fe..e4d3e65 100644 --- a/frontend/src/jobs.tsx +++ b/frontend/src/jobs.tsx @@ -348,10 +348,18 @@ const ViewLogDialog = ({ jobID }: { jobID: bigint }) => { const LogConsole = ({ jobId }: { jobId: bigint }) => { const [log, setLog] = useState(""); + const bottom = useRef(null); const refreshLog = useCallback(async () => { const reply = await cli.jobGetLog({ jobId, offset: BigInt(log.length) }).response; setLog(log + new TextDecoder().decode(reply.logs)); - }, [log, setLog]); + + if (log.length === 0 && reply.logs.length > 0 && bottom && bottom.current) { + await sleep(10); + (bottom.current as HTMLElement).scrollIntoView(true); + await sleep(10); + (bottom.current as HTMLElement).parentElement?.scrollBy(0, 100); + } + }, [log, setLog, bottom]); useEffect(() => { let closed = false; (async () => { @@ -366,7 +374,12 @@ const LogConsole = ({ jobId }: { jobId: bigint }) => { }; }, [refreshLog]); - return
{log || "loading..."}
; + return ( + +
{log || "loading..."}
+
+ + ); }; const ArchiveViewFilesDialog = ({ sources }: { sources: SourceState[] }) => { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f7a9b63..f57e866 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; import App from "./app"; import "./index.css"; import "./init"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + + ); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b53ad6d..7222466 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,7 +4,6 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig(({ command, mode }) => { const env = loadEnv(mode, process.cwd(), ""); - console.log(env); return { plugins: [react()], diff --git a/tools/recover.go b/tools/recover.go index dbc82e8..93c4a41 100644 --- a/tools/recover.go +++ b/tools/recover.go @@ -9,6 +9,10 @@ import ( ) func Wrap(ctx context.Context, f func()) { + WrapWithLogger(ctx, logrus.StandardLogger(), f) +} + +func WrapWithLogger(ctx context.Context, logger *logrus.Logger, f func()) { defer func() { e := recover() if e == nil { @@ -23,7 +27,7 @@ func Wrap(ctx context.Context, f func()) { err = fmt.Errorf("%v", err) } - logrus.WithContext(ctx).WithError(err).Errorf("panic: %s", debug.Stack()) + logger.WithContext(ctx).WithError(err).Errorf("panic: %s", debug.Stack()) }() f()