error notifications

This commit is contained in:
Sebastian 2021-11-04 15:18:30 -03:00
parent ae0a35df2b
commit 9ba0e8597d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
6 changed files with 84 additions and 29 deletions

View File

@ -26,7 +26,7 @@ import { useAsync } from "../hooks/async";
type Props = { type Props = {
children: ComponentChildren; children: ComponentChildren;
disabled: boolean; disabled?: boolean;
onClick?: () => Promise<void>; onClick?: () => Promise<void>;
[rest: string]: any; [rest: string]: any;
}; };

View File

@ -0,0 +1,59 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
export interface Notification {
message: string;
description?: string | VNode;
type: MessageType;
}
export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
interface Props {
notifications: Notification[];
removeNotification?: (n: Notification) => void;
}
function messageStyle(type: MessageType): string {
switch (type) {
case "INFO": return "message is-info";
case "WARN": return "message is-warning";
case "ERROR": return "message is-danger";
case "SUCCESS": return "message is-success";
default: return "message"
}
}
export function Notifications({ notifications, removeNotification }: Props): VNode {
return <div class="block">
{notifications.map((n,i) => <article key={i} class={messageStyle(n.type)}>
<div class="message-header">
<p>{n.message}</p>
<button class="delete" onClick={() => removeNotification && removeNotification(n)} />
</div>
{n.description && <div class="message-body">
{n.description}
</div>}
</article>)}
</div>
}

View File

@ -15,15 +15,18 @@ export function RecoveryFinishedScreen(): VNode {
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
return <div>invalid state</div> return <div>invalid state</div>
} }
const encodedSecret = reducer.currentReducerState.core_secret?.value const encodedSecret = reducer.currentReducerState.core_secret
if (!encodedSecret) { if (!encodedSecret) {
return <AnastasisClientFrame title="Recovery Problem" hideNav> return <AnastasisClientFrame title="Recovery Problem" hideNav>
<p> <p>
Secret not found Secret not found
</p> </p>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div>
</AnastasisClientFrame> </AnastasisClientFrame>
} }
const secret = bytesToString(decodeCrock(encodedSecret)) const secret = bytesToString(decodeCrock(encodedSecret.value))
return ( return (
<AnastasisClientFrame title="Recovery Finished" hideNav> <AnastasisClientFrame title="Recovery Finished" hideNav>
<p> <p>

View File

@ -6,6 +6,7 @@ import {
ChallengeFeedbackStatus, ChallengeFeedbackStatus,
ChallengeInfo, ChallengeInfo,
} from "../../../../anastasis-core/lib"; } from "../../../../anastasis-core/lib";
import { AsyncButton } from "../../components/AsyncButton";
import { TextInput } from "../../components/fields/TextInput"; import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
@ -106,8 +107,8 @@ export function SolveScreen(): VNode {
? SolveUndefinedEntry ? SolveUndefinedEntry
: dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry; : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
function onNext(): void { async function onNext(): Promise<void> {
reducer?.transition("solve_challenge", { answer }); return reducer?.transition("solve_challenge", { answer });
} }
function onCancel(): void { function onCancel(): void {
reducer?.back(); reducer?.back();
@ -136,9 +137,9 @@ export function SolveScreen(): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
<button class="button is-info" onClick={onNext}> <AsyncButton onClick={onNext} disabled={false}>
Confirm Confirm
</button> </AsyncButton>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );

View File

@ -25,10 +25,10 @@ export function StartScreen(): VNode {
<span>Recover a secret</span> <span>Recover a secret</span>
</button> </button>
<button class="button"> {/* <button class="button">
<div class="icon"><i class="mdi mdi-file" /></div> <div class="icon"><i class="mdi mdi-file" /></div>
<span>Restore a session</span> <span>Restore a session</span>
</button> </button> */}
</div> </div>
</div> </div>

View File

@ -15,6 +15,7 @@ import {
} from "preact/hooks"; } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton"; import { AsyncButton } from "../../components/AsyncButton";
import { Menu } from "../../components/menu"; import { Menu } from "../../components/menu";
import { Notifications } from "../../components/Notifications";
import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis"; import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
import { import {
AnastasisReducerApi, AnastasisReducerApi,
@ -96,18 +97,11 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
return <p>Fatal: Reducer must be in context.</p>; return <p>Fatal: Reducer must be in context.</p>;
} }
const next = async (): Promise<void> => { const next = async (): Promise<void> => {
return new Promise(async (res, rej) => { if (props.onNext) {
try { await props.onNext();
if (props.onNext) { } else {
await props.onNext(); await reducer.transition("next", {});
} else { }
await reducer.transition("next", {});
}
res()
} catch {
rej()
}
})
}; };
const handleKeyPress = ( const handleKeyPress = (
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>, e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
@ -120,8 +114,8 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
<Menu title="Anastasis" /> <Menu title="Anastasis" />
<div class="home" onKeyPress={(e) => handleKeyPress(e)}> <div class="home" onKeyPress={(e) => handleKeyPress(e)}>
<h1 class="title">{props.title}</h1> <h1 class="title">{props.title}</h1>
<ErrorBanner />
<section class="section is-main-section"> <section class="section is-main-section">
<ErrorBanner />
{props.children} {props.children}
{!props.hideNav ? ( {!props.hideNav ? (
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
@ -242,13 +236,11 @@ const AnastasisClientImpl: FunctionalComponent = () => {
function ErrorBanner(): VNode | null { function ErrorBanner(): VNode | null {
const reducer = useAnastasisContext(); const reducer = useAnastasisContext();
if (!reducer || !reducer.currentError) return null; if (!reducer || !reducer.currentError) return null;
return ( return (<Notifications removeNotification={reducer.dismissError} notifications={[{
<div id="error"> type: "ERROR",
<p>Error: {JSON.stringify(reducer.currentError)}</p> message: `Error code: ${reducer.currentError.code}`,
<button onClick={() => reducer.dismissError()}> description: reducer.currentError.hint
Dismiss Error }]} />
</button>
</div>
); );
} }