From 174cf3d39db501693eb6cee306ad1859af95ec72 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Thu, 12 Sep 2024 16:02:18 -0700 Subject: [PATCH] flash error messages (#369) --- frontend/app/app.less | 41 ++++++++++++++++++++ frontend/app/app.tsx | 75 +++++++++++++++++++++++++++++++++++- frontend/app/store/global.ts | 20 ++++++++++ frontend/app/theme.less | 1 + frontend/types/custom.d.ts | 9 +++++ frontend/wave.ts | 2 + 6 files changed, 147 insertions(+), 1 deletion(-) diff --git a/frontend/app/app.less b/frontend/app/app.less index cea0b5e32..58a5d19f6 100644 --- a/frontend/app/app.less +++ b/frontend/app/app.less @@ -100,3 +100,44 @@ a { transition-delay: none !important; } } + +.flash-error-container { + position: absolute; + right: 10px; + bottom: 10px; + z-index: var(--zindex-flash-error-container); + display: flex; + flex-direction: column; + gap: 10px; + + .flash-error { + background: var(--error-color); + color: var(--main-text-color); + border-radius: 4px; + padding: 10px; + display: flex; + flex-direction: column; + width: 280px; + border: 1px solid transparent; + max-height: 100px; + cursor: pointer; + + .flash-error-scroll { + overflow-y: auto; + display: flex; + flex-direction: column; + } + + &.hovered { + border: 1px solid var(--main-text-color); + } + + .flash-error-title { + font-weight: bold; + margin-bottom: 5px; + } + + .flash-error-message { + } + } +} diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index dbc774280..9f5c5da12 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -4,7 +4,7 @@ import { useWaveObjectValue } from "@/app/store/wos"; import { Workspace } from "@/app/workspace/workspace"; import { ContextMenuModel } from "@/store/contextmenu"; -import { PLATFORM, WOS, atoms, getApi, globalStore, useSettingsPrefixAtom } from "@/store/global"; +import { PLATFORM, WOS, atoms, getApi, globalStore, removeFlashError, useSettingsPrefixAtom } from "@/store/global"; import { appHandleKeyDown } from "@/store/keymodel"; import { getWebServerEndpoint } from "@/util/endpoints"; import { getElemAsStr } from "@/util/focusutil"; @@ -251,6 +251,78 @@ const AppKeyHandlers = () => { return null; }; +const FlashError = () => { + const flashErrors = jotai.useAtomValue(atoms.flashErrors); + const [hoveredId, setHoveredId] = React.useState(null); + const [ticker, setTicker] = React.useState(0); + + React.useEffect(() => { + if (flashErrors.length == 0 || hoveredId != null) { + return; + } + const now = Date.now(); + for (let ferr of flashErrors) { + if (ferr.expiration == null || ferr.expiration < now) { + removeFlashError(ferr.id); + } + } + setTimeout(() => setTicker(ticker + 1), 1000); + }, [flashErrors, ticker, hoveredId]); + + if (flashErrors.length == 0) { + return null; + } + + function copyError(id: string) { + const ferr = flashErrors.find((f) => f.id === id); + if (ferr == null) { + return; + } + let text = ""; + if (ferr.title != null) { + text += ferr.title; + } + if (ferr.message != null) { + if (text.length > 0) { + text += "\n"; + } + text += ferr.message; + } + navigator.clipboard.writeText(text); + } + + function convertNewlinesToBreaks(text) { + return text.split("\n").map((part, index) => ( + + {part} +
+
+ )); + } + + return ( +
+ {flashErrors.map((err, idx) => ( +
copyError(err.id)} + onMouseEnter={() => setHoveredId(err.id)} + onMouseLeave={() => setHoveredId(null)} + title="Click to Copy Error Message" + > +
+ {err.title != null ?
{err.title}
: null} + {err.message != null ? ( +
{convertNewlinesToBreaks(err.message)}
+ ) : null} +
+
+ ))} +
+ ); +}; + const AppInner = () => { const prefersReducedMotion = jotai.useAtomValue(atoms.prefersReducedMotionAtom); const client = jotai.useAtomValue(atoms.client); @@ -281,6 +353,7 @@ const AppInner = () => { + ); }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index cf205ded9..4b352d56d 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -148,6 +148,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const connStatuses = Array.from(ConnStatusMap.values()).map((atom) => get(atom)); return connStatuses; }); + const flashErrorsAtom = atom([]); atoms = { // initialized in wave.ts (will not be null inside of application) windowId: windowIdAtom, @@ -167,6 +168,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { typeAheadModalAtom, modalOpen, allConnStatus: allConnStatusAtom, + flashErrors: flashErrorsAtom, }; } @@ -504,6 +506,22 @@ function getConnStatusAtom(conn: string): PrimitiveAtom { return rtn; } +function pushFlashError(ferr: FlashErrorType) { + if (ferr.expiration == null) { + ferr.expiration = Date.now() + 5000; + } + ferr.id = crypto.randomUUID(); + globalStore.set(atoms.flashErrors, (prev) => { + return [...prev, ferr]; + }); +} + +function removeFlashError(id: string) { + globalStore.set(atoms.flashErrors, (prev) => { + return prev.filter((ferr) => ferr.id !== id); + }); +} + export { atoms, counterInc, @@ -524,8 +542,10 @@ export { loadConnStatus, openLink, PLATFORM, + pushFlashError, refocusNode, registerBlockComponentModel, + removeFlashError, setNodeFocus, setPlatform, subscribeToConnEvents, diff --git a/frontend/app/theme.less b/frontend/app/theme.less index 4fbe73471..6eeefb252 100644 --- a/frontend/app/theme.less +++ b/frontend/app/theme.less @@ -57,6 +57,7 @@ --zindex-layout-overlay-container: 4; --zindex-layout-magnified-node: 5; --zindex-block-mask-inner: 10; + --zindex-flash-error-container: 550; // z-indexes in xterm.css // xterm-helpers: 5 diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 9f7916d79..9118844ad 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -23,6 +23,7 @@ declare global { typeAheadModalAtom: jotai.PrimitiveAtom; modalOpen: jotai.PrimitiveAtom; allConnStatus: jotai.Atom; + flashErrors: jotai.PrimitiveAtom; }; type WritableWaveObjectAtom = jotai.WritableAtom; @@ -273,6 +274,14 @@ declare global { connName: string; baseDir: string; }; + + type FlashErrorType = { + id: string; + icon: string; + title: string; + message: string; + expiration: number; + }; } export {}; diff --git a/frontend/wave.ts b/frontend/wave.ts index 6043e0030..a9564367c 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -22,6 +22,7 @@ import { initGlobal, initGlobalWaveEventSubs, loadConnStatus, + pushFlashError, subscribeToConnEvents, } from "@/store/global"; import * as WOS from "@/store/wos"; @@ -51,6 +52,7 @@ loadFonts(); (window as any).countersPrint = countersPrint; (window as any).countersClear = countersClear; (window as any).getLayoutModelForActiveTab = getLayoutModelForActiveTab; +(window as any).pushFlashError = pushFlashError; document.title = `The Next Wave (${windowId.substring(0, 8)})`;