flash error messages (#369)

This commit is contained in:
Mike Sawka 2024-09-12 16:02:18 -07:00 committed by GitHub
parent 936d4bfb30
commit 174cf3d39d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 147 additions and 1 deletions

View File

@ -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 {
}
}
}

View File

@ -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<string>(null);
const [ticker, setTicker] = React.useState<number>(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) => (
<React.Fragment key={index}>
{part}
<br />
</React.Fragment>
));
}
return (
<div className="flash-error-container">
{flashErrors.map((err, idx) => (
<div
key={idx}
className={clsx("flash-error", { hovered: hoveredId === err.id })}
onClick={() => copyError(err.id)}
onMouseEnter={() => setHoveredId(err.id)}
onMouseLeave={() => setHoveredId(null)}
title="Click to Copy Error Message"
>
<div className="flash-error-scroll">
{err.title != null ? <div className="flash-error-title">{err.title}</div> : null}
{err.message != null ? (
<div className="flash-error-message">{convertNewlinesToBreaks(err.message)}</div>
) : null}
</div>
</div>
))}
</div>
);
};
const AppInner = () => {
const prefersReducedMotion = jotai.useAtomValue(atoms.prefersReducedMotionAtom);
const client = jotai.useAtomValue(atoms.client);
@ -281,6 +353,7 @@ const AppInner = () => {
<DndProvider backend={HTML5Backend}>
<Workspace />
</DndProvider>
<FlashError />
</div>
);
};

View File

@ -148,6 +148,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
const connStatuses = Array.from(ConnStatusMap.values()).map((atom) => get(atom));
return connStatuses;
});
const flashErrorsAtom = atom<FlashErrorType[]>([]);
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<ConnStatus> {
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,

View File

@ -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

View File

@ -23,6 +23,7 @@ declare global {
typeAheadModalAtom: jotai.PrimitiveAtom<TypeAheadModalType>;
modalOpen: jotai.PrimitiveAtom<boolean>;
allConnStatus: jotai.Atom<ConnStatus[]>;
flashErrors: jotai.PrimitiveAtom<FlashErrorType[]>;
};
type WritableWaveObjectAtom<T extends WaveObj> = jotai.WritableAtom<T, [value: T], void>;
@ -273,6 +274,14 @@ declare global {
connName: string;
baseDir: string;
};
type FlashErrorType = {
id: string;
icon: string;
title: string;
message: string;
expiration: number;
};
}
export {};

View File

@ -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)})`;