gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: notification handler,


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: notification handler, fixing types
Date: Thu, 11 Feb 2021 16:09:40 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new 7c17876  notification handler, fixing types
7c17876 is described below

commit 7c17876c4c6fd41f82586cc88556a63305c07097
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Feb 11 12:09:30 2021 -0300

    notification handler, fixing types
---
 package.json                                       |  2 +-
 src/components/hooks/backend.ts                    | 38 ++++-----
 .../notifications/Notifications.stories.tsx        | 49 +++++++++++
 src/components/notifications/index.tsx             | 31 +++++++
 src/declaration.d.ts                               |  8 ++
 src/hooks/notifications.ts                         | 19 +++++
 src/routes/index.tsx                               |  7 +-
 src/routes/instances/CreateModal.tsx               |  2 +-
 src/routes/instances/Table.tsx                     | 95 ++++++++++++----------
 src/routes/instances/UpdateModal.stories.tsx       |  2 +-
 src/routes/instances/UpdateModal.tsx               |  7 +-
 src/routes/instances/View.stories.tsx              |  6 +-
 src/routes/instances/View.tsx                      | 26 +-----
 src/routes/instances/index.tsx                     | 34 ++++++--
 src/scss/main.scss                                 | 17 ++++
 15 files changed, 240 insertions(+), 103 deletions(-)

diff --git a/package.json b/package.json
index b5cbbeb..0e3c39c 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
     "build": "preact build --no-sw --no-esm",
     "serve": "sirv build --port 8080 --cors --single --no-sw --no-esm",
     "dev": "preact watch --no-sw --no-esm",
-    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
     "test": "jest ./tests",
     "storybook": "start-storybook -p 6006"
   },
diff --git a/src/components/hooks/backend.ts b/src/components/hooks/backend.ts
index 4a1eebe..2f3fb27 100644
--- a/src/components/hooks/backend.ts
+++ b/src/components/hooks/backend.ts
@@ -13,10 +13,6 @@ interface HttpResponseError<T> {
   error: Error;
 }
 
-class AuthError extends Error {
-  public readonly isAuth = true
-}
-
 const BACKEND = process.env.BACKEND_ENDPOINT
 const TOKEN_KEY = 'backend-token'
 
@@ -26,20 +22,22 @@ async function request(url: string, method?: Methods, 
data?: object): Promise<an
   const token = localStorage.getItem(TOKEN_KEY)
   const headers = token ? { Authorization: `Bearer secret-token:${token}` } : 
undefined
 
-  const res = await axios({
-    method: method || 'get',
-    url: `${BACKEND}/private${url}`,
-    responseType: 'json',
-    headers,
-    data
-  })
-  if (res.status == 200 || res.status == 204) return res.data
-  if (res.status == 401) throw new AuthError()
-
-  const error = new Error('An error occurred while fetching the data.')
-  const info = res.data
-  const status = res.status
-  throw { info, status, ...error }
+  try {
+    const res = await axios({
+      method: method || 'get',
+      url: `${BACKEND}/private${url}`,
+      responseType: 'json',
+      headers,
+      data
+    })
+    return res.data
+  } catch (e) {
+    const error = new Error('An error occurred while fetching the data.')
+    const info = e.response.data
+    const status = e.response.status
+    throw { info, status, ...error }
+  }
+
 }
 
 async function fetcher(url: string): Promise<any> {
@@ -74,7 +72,7 @@ export function useBackendInstances(): 
HttpResponse<MerchantBackend.Instances.In
     globalMutate('/instances')
   }
 
-  return { data, needsAuth: error instanceof AuthError, error, create }
+  return { data, needsAuth: error?.status === 401, error, create }
 }
 
 export function useBackendInstance(id: string | null): 
HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> & 
WithUpdate<MerchantBackend.Instances.InstanceReconfigurationMessage> & 
WithDelete {
@@ -93,5 +91,5 @@ export function useBackendInstance(id: string | null): 
HttpResponse<MerchantBack
     globalMutate(`/instances/${deleteId}`, null)
   }
 
-  return { data, needsAuth: error instanceof AuthError, error, update, delete: 
_delete }
+  return { data, needsAuth: error?.status === 401, error, update, delete: 
_delete }
 }
diff --git a/src/components/notifications/Notifications.stories.tsx 
b/src/components/notifications/Notifications.stories.tsx
new file mode 100644
index 0000000..242432a
--- /dev/null
+++ b/src/components/notifications/Notifications.stories.tsx
@@ -0,0 +1,49 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import { h } from 'preact';
+import Notification from './index'
+
+
+export default {
+  title: 'Components/Notification',
+  component: Notification,
+};
+
+export const NotificationInfo = () => {
+  return <div>
+    <Notification notifications={[{
+      title: 'Title',
+      description: 'this is a message',
+      type: 'INFO'
+    }]} />
+  </div>
+};
+
+export const NotificationWarn = () => {
+  return <div>
+    <Notification notifications={[{
+      title: 'Title',
+      description: 'this is a message',
+      type: 'WARN'
+    }]} />
+  </div>
+};
+
+export const NotificationError = () => {
+  return <div>
+    <Notification notifications={[{
+      title: 'Title',
+      description: 'this is a message',
+      type: 'ERROR'
+    }]} />
+  </div>
+};
+
+export const NotificationSuccess = () => {
+  return <div>
+    <Notification notifications={[{
+      title: 'Title',
+      description: 'this is a message',
+      type: 'SUCCESS'
+    }]} />
+  </div>
+};
diff --git a/src/components/notifications/index.tsx 
b/src/components/notifications/index.tsx
new file mode 100644
index 0000000..906502f
--- /dev/null
+++ b/src/components/notifications/index.tsx
@@ -0,0 +1,31 @@
+import { h, VNode } from "preact";
+import { useEffect } from "preact/hooks";
+import { MessageType, Notification } from "../../declaration";
+
+interface Props {
+  notifications: Notification[];
+}
+
+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 default function Notifications({ notifications }: Props): VNode {
+  return <div class="toast">
+    {notifications.map(n => <article class={messageStyle(n.type)}>
+      <div class="message-header">
+        <p>{n.title}</p>
+        <button class="delete" aria-label="delete" />
+      </div>
+      <div class="message-body">
+        {n.description}
+      </div>
+    </article>)}
+  </div>
+}
\ No newline at end of file
diff --git a/src/declaration.d.ts b/src/declaration.d.ts
index 7b83773..ef46320 100644
--- a/src/declaration.d.ts
+++ b/src/declaration.d.ts
@@ -9,6 +9,14 @@ declare module "*.scss" {
     export default mapping;
 }
 
+interface Notification {
+    title: string;
+    description: string;
+    type: MessageType;
+  }
+  
+  type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
+  
 type EddsaPublicKey = string;
 type RelativeTime = Duration;
 interface Timestamp {
diff --git a/src/hooks/notifications.ts b/src/hooks/notifications.ts
new file mode 100644
index 0000000..0dc361f
--- /dev/null
+++ b/src/hooks/notifications.ts
@@ -0,0 +1,19 @@
+import { useState } from 'react';
+import { Notification } from '../declaration';
+
+interface Result {
+  notifications: Notification[];
+  pushNotification: (n: Notification) => void;
+}
+
+export function useNotifications(): Result {
+  const [notifications, setNotifications] = useState<(Notification & {since: 
Date})[]>([])
+  const pushNotification = (n: Notification): void => {
+    const entry = {...n, since: new Date() }
+    setNotifications(ns => [...ns, entry])
+    setTimeout(()=>{
+      setNotifications(ns => ns.filter(x => x.since !== entry.since))
+    }, 2000)
+  }
+  return {notifications, pushNotification}
+}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 1543a1a..fe190ae 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -8,6 +8,8 @@ import Sidebar from '../components/sidebar';
 import NavigationBar from '../components/navbar';
 import { useEffect } from 'preact/hooks';
 import InstanceDetail from './instanceDetail';
+import Notifications from '../components/notifications';
+import { useNotifications } from '../hooks/notifications';
 
 function Redirector({ to }: { path: string; to: string }): null {
   useEffect(() => {
@@ -16,13 +18,16 @@ function Redirector({ to }: { path: string; to: string }): 
null {
   return null;
 }
 
+
 export default function PageRouter(): VNode {
+  const {notifications, pushNotification} = useNotifications()
   return (
     <div id="app">
       <NavigationBar />
       <Sidebar />
+      <Notifications notifications={notifications} />
       <Router>
-        <Route path="/" component={Instances} />
+        <Route path="/" component={Instances} 
pushNotification={pushNotification} />
         <Route path="/i/:instance" component={InstanceDetail} />
         <NotFoundPage default />
       </Router>
diff --git a/src/routes/instances/CreateModal.tsx 
b/src/routes/instances/CreateModal.tsx
index 518af6d..e1b53da 100644
--- a/src/routes/instances/CreateModal.tsx
+++ b/src/routes/instances/CreateModal.tsx
@@ -73,7 +73,7 @@ export default function CreateModal({ active, onCancel, 
onConfirm }: Props): VNo
         <div class="field-body">
           <div class="field">
             <p class="control is-expanded has-icons-left">
-              <input class="input" type="text" 
placeholder={info?.meta?.placeholder} readonly={info?.meta?.readonly} name={f} 
value={value[f]} onChange={e => valueHandler(prev => ({ ...prev, [f]: 
e.currentTarget.value }))} />
+              <input class="input" type="text" 
placeholder={info?.meta?.placeholder} readonly={info?.meta?.readonly} name={f} 
value={value[f]} onChange={e => valueHandler((prev: any) => ({ ...prev, [f]: 
e.currentTarget.value }))} />
               {info?.meta?.help}
             </p>
             {errors[f] ? <p class="help is-danger">{errors[f]}</p> : null}
diff --git a/src/routes/instances/Table.tsx b/src/routes/instances/Table.tsx
index 7db153b..e6d1474 100644
--- a/src/routes/instances/Table.tsx
+++ b/src/routes/instances/Table.tsx
@@ -1,5 +1,5 @@
 import { h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { useEffect, useState, StateUpdater } from "preact/hooks";
 import { MerchantBackend, WidthId as WithId } from "../../declaration";
 import DeleteModal from './DeleteModal'
 import UpdateModal from './UpdateModal'
@@ -14,7 +14,7 @@ interface Props {
   selected: MerchantBackend.Instances.QueryInstancesResponse & WithId | 
undefined;
 }
 
-function toggleSelected<T>(id: T) {
+function toggleSelected<T>(id: T): (prev: T[]) => T[] {
   return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : 
prev.filter(e => e != id)
 }
 
@@ -40,50 +40,59 @@ const EmptyTable = () => <div class="content has-text-grey 
has-text-centered">
   <p>No instance configured yet, setup one pressing the + button </p>
 </div>
 
-const Table = ({ rowSelection, rowSelectionHandler, instances, onSelect, 
toBeDeletedHandler }) => <table class="table is-fullwidth is-striped 
is-hoverable is-fullwidth">
-  <thead>
-    <tr>
-      <th class="is-checkbox-cell">
-        <label class="b-checkbox checkbox">
-          <input type="checkbox" checked={rowSelection.length === 
instances.length} onClick={e => rowSelectionHandler(rowSelection.length === 
instances.length ? [] : instances.map(i => i.id))} />
-          <span class="check" />
-        </label>
-      </th>
-      <th>id</th>
-      <th>name</th>
-      <th>public key</th>
-      <th>payments</th>
-      <th />
-    </tr>
-  </thead>
-  <tbody>
-    {instances.map(i => {
-      return <tr>
-        <td class="is-checkbox-cell">
+interface TableProps {
+  rowSelection: string[];
+  instances: MerchantBackend.Instances.Instance[];
+  onSelect: (id: string | null) => void;
+  rowSelectionHandler: StateUpdater<string[]>;
+  toBeDeletedHandler:  StateUpdater<MerchantBackend.Instances.Instance | null>;
+}
+
+const Table = ({ rowSelection, rowSelectionHandler, instances, onSelect, 
toBeDeletedHandler }: TableProps): VNode => (
+  <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+    <thead>
+      <tr>
+        <th class="is-checkbox-cell">
           <label class="b-checkbox checkbox">
-            <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} 
onClick={e => rowSelectionHandler(toggleSelected(i.id))} />
+            <input type="checkbox" checked={rowSelection.length === 
instances.length} onClick={e => rowSelectionHandler(rowSelection.length === 
instances.length ? [] : instances.map(i => i.id))} />
             <span class="check" />
           </label>
-        </td>
-        <td >{i.id}</td>
-        <td >{i.name}</td>
-        <td >{i.merchant_pub}</td>
-        <td >{i.payment_targets}</td>
-        <td class="is-actions-cell">
-          <div class="buttons is-right">
-            <button class="button is-small is-primary" type="button" 
onClick={e => onSelect(i.id)}>
-              <span class="icon"><i class="mdi mdi-eye" /></span>
-            </button>
-            <button class="button is-small is-danger jb-modal" type="button" 
onClick={e => toBeDeletedHandler(i)}>
-              <span class="icon"><i class="mdi mdi-trash-can" /></span>
-            </button>
-          </div>
-        </td>
+        </th>
+        <th>id</th>
+        <th>name</th>
+        <th>public key</th>
+        <th>payments</th>
+        <th />
       </tr>
-    })}
+    </thead>
+    <tbody>
+      {instances.map(i => {
+        return <tr>
+          <td class="is-checkbox-cell">
+            <label class="b-checkbox checkbox">
+              <input type="checkbox" checked={rowSelection.indexOf(i.id) != 
-1} onClick={e => rowSelectionHandler(toggleSelected(i.id))} />
+              <span class="check" />
+            </label>
+          </td>
+          <td >{i.id}</td>
+          <td >{i.name}</td>
+          <td >{i.merchant_pub}</td>
+          <td >{i.payment_targets}</td>
+          <td class="is-actions-cell">
+            <div class="buttons is-right">
+              <button class="button is-small is-primary" type="button" 
onClick={e => onSelect(i.id)}>
+                <span class="icon"><i class="mdi mdi-eye" /></span>
+              </button>
+              <button class="button is-small is-danger jb-modal" type="button" 
onClick={e => toBeDeletedHandler(i)}>
+                <span class="icon"><i class="mdi mdi-trash-can" /></span>
+              </button>
+            </div>
+          </td>
+        </tr>
+      })}
 
-  </tbody>
-</table>
+    </tbody>
+  </table>)
 
 
 export default function CardTable({ instances, onCreate, onDelete, onSelect, 
onUpdate, selected }: Props): VNode {
@@ -121,8 +130,8 @@ export default function CardTable({ instances, onCreate, 
onDelete, onSelect, onU
     <div class="card-content">
       <div class="b-table has-pagination">
         <div class="table-wrapper has-mobile-cards">
-          {instances.length > 0 ? 
-            <Table instances={instances} onSelect={onSelect} 
rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} 
toBeDeletedHandler={toBeDeletedHandler} /> : 
+          {instances.length > 0 ?
+            <Table instances={instances} onSelect={onSelect} 
rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} 
toBeDeletedHandler={toBeDeletedHandler} /> :
             <EmptyTable />
           }
         </div>
diff --git a/src/routes/instances/UpdateModal.stories.tsx 
b/src/routes/instances/UpdateModal.stories.tsx
index db39d61..1ae5bca 100644
--- a/src/routes/instances/UpdateModal.stories.tsx
+++ b/src/routes/instances/UpdateModal.stories.tsx
@@ -13,7 +13,7 @@ export default {
   }
 };
 
-export const WithDefaultInstance = (a) => <UpdateModal {...a} />;
+export const WithDefaultInstance = (a: any) => <UpdateModal {...a} />;
 WithDefaultInstance.args = {
   element: {
     id: 'default',
diff --git a/src/routes/instances/UpdateModal.tsx 
b/src/routes/instances/UpdateModal.tsx
index f3f3bb8..d788a91 100644
--- a/src/routes/instances/UpdateModal.tsx
+++ b/src/routes/instances/UpdateModal.tsx
@@ -33,7 +33,7 @@ interface KeyValue {
 }
 
 export default function UpdateModal({ element, onCancel, onConfirm }: Props): 
VNode {
-  const copy = !element ? {} : Object.keys(schema.fields).reduce((prev,cur) => 
({...prev, [cur]: (element as any)[cur] }), {})
+  const copy: any = !element ? {} : 
Object.keys(schema.fields).reduce((prev,cur) => ({...prev, [cur]: (element as 
any)[cur] }), {})
 
   const [value, valueHandler] = useState(copy)
   const [errors, setErrors] = useState<KeyValue>({})
@@ -63,7 +63,10 @@ export default function UpdateModal({ element, onCancel, 
onConfirm }: Props): VN
         <div class="field-body">
           <div class="field">
             <p class="control is-expanded has-icons-left">
-              <input class="input" type="text" 
placeholder={info?.meta?.placeholder} readonly={info?.meta?.readonly} name={f} 
value={value[f]} onChange={e => valueHandler(prev => ({ ...prev, [f]: 
e.currentTarget.value }))} />
+              <input class="input" type="text" 
+                  placeholder={info?.meta?.placeholder} 
readonly={info?.meta?.readonly} 
+                  name={f} value={value[f]} 
+                  onChange={e => valueHandler((prev: any) => ({ ...prev, [f]: 
e.currentTarget.value }))} />
               {info?.meta?.help}
             </p>
             {errors[f] ? <p class="help is-danger">{errors[f]}</p> : null}
diff --git a/src/routes/instances/View.stories.tsx 
b/src/routes/instances/View.stories.tsx
index 208de17..5804152 100644
--- a/src/routes/instances/View.stories.tsx
+++ b/src/routes/instances/View.stories.tsx
@@ -13,12 +13,12 @@ export default {
   },
 };
 
-export const Empty = (a) => <View {...a} />;
+export const Empty = (a: any) => <View {...a} />;
 Empty.args = {
   instances: []
 }
 
-export const WithDefaultInstance = (a) => <View {...a} />;
+export const WithDefaultInstance = (a: any) => <View {...a} />;
 WithDefaultInstance.args = {
   instances: [{
     id: 'default',
@@ -28,7 +28,7 @@ WithDefaultInstance.args = {
   }]
 }
 
-export const WithTwoInstance = (a) => <View {...a} />;
+export const WithTwoInstance = (a: any) => <View {...a} />;
 WithTwoInstance.args = {
   instances: [{
     id: 'first',
diff --git a/src/routes/instances/View.tsx b/src/routes/instances/View.tsx
index 6d357af..95af162 100644
--- a/src/routes/instances/View.tsx
+++ b/src/routes/instances/View.tsx
@@ -9,31 +9,11 @@ interface Props {
   onDelete: (id: string) => void;
   onSelect: (id: string | null) => void;
   selected: MerchantBackend.Instances.QueryInstancesResponse & WidthId | 
undefined;
+  isLoading: boolean;
 }
 
-export default function View({ instances, onCreate, onDelete, onSelect, 
onUpdate, selected }: Props): VNode {
+export default function View({ instances, isLoading, onCreate, onDelete, 
onSelect, onUpdate, selected }: Props): VNode {
   return <div id="app">
-    <div class="toast">
-      <article class="message">
-        <div class="message-header">
-          <p>Normal message</p>
-          <button class="delete" aria-label="delete" />
-        </div>
-        <div class="message-body">
-          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-        </div>
-      </article>
-      <article class="message is-danger">
-        <div class="message-header">
-          <p>Normal message</p>
-          <button class="delete" aria-label="delete" />
-        </div>
-        <div class="message-body">
-          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
-        </div>
-      </article>
-    </div>
-    
     <section class="section is-title-bar">
 
       <div class="level">
@@ -47,7 +27,7 @@ export default function View({ instances, onCreate, onDelete, 
onSelect, onUpdate
         </div>
       </div>
     </section>
-    <section class="hero is-hero-bar">
+    <section class={ isLoading ? "hero is-hero-bar" : "hero is-hero-bar 
is-loading" }>
       <div class="hero-body">
         <div class="level">
           <div class="level-left">
diff --git a/src/routes/instances/index.tsx b/src/routes/instances/index.tsx
index b83eee0..feb98b6 100644
--- a/src/routes/instances/index.tsx
+++ b/src/routes/instances/index.tsx
@@ -2,21 +2,39 @@ import { h, VNode } from 'preact';
 import View from './View';
 import LoginPage from '../../components/auth/LoginPage';
 import { updateToken, useBackendInstance, useBackendInstances } from 
'../../components/hooks/backend';
-import { useState } from 'preact/hooks';
+import { useEffect, useState } from 'preact/hooks';
+import { Notification } from '../../declaration';
 
+interface Props {
+  pushNotification: (n: Notification) => void;
+}
 
-export default function Instances(): VNode {
-  const list    = useBackendInstances()
-  const [selectedId, select] = useState<string|null>(null)
+export default function Instances({ pushNotification }: Props): VNode {
+  const list = useBackendInstances()
+  const [selectedId, select] = useState<string | null>(null)
   const details = useBackendInstance(selectedId)
 
-  if (!list.data || (selectedId != null && !details.data)) {
+
+  const requiresToken = (!list.data && list.needsAuth) || (selectedId != null 
&& !details.data && details.needsAuth)
+  const isLoadingTheList = (!list.data && !list.error)
+  const isLoadingTheDetails = (!details.data && !details.error)
+  
+  useEffect(() => {
+    if (requiresToken) pushNotification({
+      title: `unauthorized access`,
+      description: 'backend has denied access',
+      type: 'ERROR'
+    })
+  }, [requiresToken])
+
+  if (requiresToken) {
     return <LoginPage onLogIn={updateToken} />
   }
 
-  return <View instances={list.data.instances} 
-    onCreate={list.create} onUpdate={details.update} 
+  return <View instances={list.data?.instances || []}
+    isLoading={isLoadingTheList || isLoadingTheDetails}
+    onCreate={list.create} onUpdate={details.update}
     onDelete={details.delete} onSelect={select}
-    selected={ !details.data || !selectedId ? undefined : {...details.data, 
id:selectedId} } 
+    selected={!details.data || !selectedId ? undefined : { ...details.data, 
id: selectedId }}
   />;
 }
diff --git a/src/scss/main.scss b/src/scss/main.scss
index 8f6ed75..5146c41 100644
--- a/src/scss/main.scss
+++ b/src/scss/main.scss
@@ -43,3 +43,20 @@
   white-space:pre-wrap;
   opacity:80%;
 }
+
+div {
+  &.is-loading {
+      position: relative;
+      pointer-events: none;
+      opacity: 0.5;
+      &:after {
+          // @include loader;
+          position: absolute;
+          top: calc(50% - 2.5em);
+          left: calc(50% - 2.5em);
+          width: 5em;
+          height: 5em;
+          border-width: 0.25em;
+      }
+  }
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]