commit 59e4d7e1873e701ef6041219bce0e0431e659de5 parent dd2bfd6e703704c75322b21ff0044daa3fb3de3f Author: Sebastian <sebasjm@gmail.com> Date: Mon, 22 Feb 2021 01:28:39 -0300 refactor router Diffstat:
39 files changed, 1315 insertions(+), 926 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -27,14 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - remove checkbox from auth token, use button (manage auth) - prepend payto:// to account - validate on change everything - - remove footer + <!-- - remove footer --> - implement proper error handling - - PATCH payto uri not working as expeced: re-enable, creating with multiple uris - replace Yup and type definition with a taler-library for the purpose (first wait Florian to refactor wallet core) - add more doc style comments - - check the field names in forms dont break spaces - - update spanish lang - save every auth token of different instances - configure eslint - configure prettier diff --git a/packages/frontend/src/components/auth/index.tsx b/packages/frontend/src/components/auth/index.tsx @@ -20,40 +20,41 @@ */ import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useBackend } from "../../hooks"; +import { useContext, useState } from "preact/hooks"; +import { BackendContext } from "../../context/backend"; interface Props { - onConfirm?: () => void; + onConfirm: (backend: string, token?: string) => void; } -export function LoginPage({ onConfirm }: Props): VNode { - const [backend, setBackend] = useBackend() - const [url, setUrl] = useState(backend.url) - const [token, setToken] = useState(backend.token) - const [changeToken, setChangeToken] = useState(false) +export function LoginModal({ onConfirm }: Props): VNode { + const backend = useContext(BackendContext) + const [updatingToken, setUpdatingToken] = useState(false) + const [token, setToken] = useState(backend.token || '') + const [url, setURL] = useState(backend.url) + const toggleUpdatingToken = (): void => setUpdatingToken(v => !v) return <div class="modal is-active is-clipped"> <div class="modal-background" /> <div class="modal-card"> <header class="modal-card-head"> - <p class="modal-card-title">Authentication required</p> + <p class="modal-card-title">Login required</p> </header> <section class="modal-card-body"> Please enter your auth token. Token should have "secret-token:" and start with Bearer or ApiKey <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Change Token</label> + <label class="label">Updte token</label> </div> <div class="field-body"> <div class="field has-addons"> <label class="b-checkbox checkbox"> - <input type="checkbox" checked={changeToken} onClick={(): void => setChangeToken(!changeToken)} /> + <input type="checkbox" checked={updatingToken} onClick={toggleUpdatingToken} /> <span class="check" /> </label> <p class="control is-expanded"> - <input class="input" type="text" placeholder={changeToken ? "set new token" : "hidden token value"} disabled={!changeToken} name="id" value={token} onInput={(e): void => setToken(e?.currentTarget.value)} /> + <input class="input" type="text" placeholder={updatingToken ? "set new token" : "hidden token value"} disabled={!updatingToken} name="id" value={token} onInput={(e): void => setToken(e?.currentTarget.value)} /> </p> </div> </div> @@ -65,16 +66,15 @@ export function LoginPage({ onConfirm }: Props): VNode { <div class="field-body"> <div class="field"> <p class="control is-expanded"> - <input class="input" type="text" placeholder="set new url" name="id" value={url} onInput={(e): void => setUrl(e?.currentTarget.value)} /> + <input class="input" type="text" placeholder="set new url" name="id" value={url} onInput={(e): void => setURL(e?.currentTarget.value)} /> </p> </div> </div> </div> </section> - <footer class="modal-card-foot"> + <footer class="modal-card-foot " style={{justifyContent: 'flex-end'}}> <button class="button is-info" onClick={(): void => { - setBackend({ token, url }); - onConfirm && onConfirm(); + onConfirm(url, updatingToken && token ? token : undefined); }} >Confirm</button> </footer> </div> diff --git a/packages/frontend/src/components/footer/index.tsx b/packages/frontend/src/components/footer/index.tsx @@ -1,49 +0,0 @@ -/* - 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 function Footer(): VNode { - return ( - <footer class="footer"> - <div class="container-fluid"> - <div class="level"> - <div class="level-left"> - <div class="level-item">copyleft</div> - <div class="level-item"> - <a href="https://taler.net/" style="height: 20px"> - Taler - </a> - </div> - </div> - <div class="level-right"> - <div class="level-item"> - <div class="logo"> - sebasjm - </div> - </div> - </div> - </div> - </div> - </footer> - ) -} - diff --git a/packages/frontend/src/components/modal/index.tsx b/packages/frontend/src/components/modal/index.tsx @@ -43,8 +43,8 @@ export function ConfirmModal({ active, description, onCancel, onConfirm, childre {children} </section> <footer class="modal-card-foot"> - <button class="button " onClick={onCancel} ><Text id="cancel" /></button> - <button class={danger ? "button is-danger " : "button is-info "} onClick={onConfirm} ><Text id="confirm" /></button> + <button class="button " onClick={onCancel} ><Text id="text.cancel" /></button> + <button class={danger ? "button is-danger " : "button is-info "} onClick={onConfirm} ><Text id="text.confirm" /></button> </footer> </div> <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> diff --git a/packages/frontend/src/components/navbar/index.tsx b/packages/frontend/src/components/navbar/index.tsx @@ -20,8 +20,6 @@ */ import { h, VNode } from 'preact'; -import { useState } from 'preact/hooks'; -import { LoginPage } from '../auth'; import { translations } from '../../i18n' // TODO: Fix compilation problem // import * as logo from '../../assets/logo.jpeg'; @@ -29,10 +27,10 @@ import { translations } from '../../i18n' interface Props { lang: string; setLang: (l: string) => void; + onLogout: () => void; } -export function NavigationBar({ lang, setLang }: Props): VNode { - const [showLogin, setShowLogin] = useState(false) +export function NavigationBar({ lang, setLang, onLogout }: Props): VNode { return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation"> <div class="navbar-brand"> <a class="navbar-item" href="https://taler.net"> @@ -62,11 +60,10 @@ export function NavigationBar({ lang, setLang }: Props): VNode { </div> </div> <div class="navbar-item"> - <button class="button is-primary" onClick={(): void => setShowLogin(true)}>Change access</button> + <button class="button is-primary" onClick={(): void => onLogout()}>Log out</button> </div> </div> </div> - {showLogin && <LoginPage onConfirm={(): void => setShowLogin(false)} />} </nav> ); } \ No newline at end of file diff --git a/packages/frontend/src/components/notifications/Notifications.stories.tsx b/packages/frontend/src/components/notifications/Notifications.stories.tsx @@ -26,40 +26,29 @@ import { Notifications } from './index' export default { title: 'Components/Notification', component: Notifications, + argTypes: { + removeNotification: { action: 'removeNotification' }, + }, }; -export const NotificationInfo = (): VNode => { - return <div> - <Notifications notifications={[{ - messageId: 'unauthorized', - type: 'INFO', - }]} /> - </div> -}; - -export const NotificationWarn = (): VNode => { - return <div> - <Notifications notifications={[{ - messageId: 'unauthorized', - type: 'WARN', - }]} /> - </div> -}; - -export const NotificationError = (): VNode => { - return <div> - <Notifications notifications={[{ - messageId: 'unauthorized', - type: 'ERROR', - }]} /> - </div> -}; - -export const NotificationSuccess = (): VNode => { - return <div> - <Notifications notifications={[{ - messageId: 'unauthorized', - type: 'SUCCESS', - }]} /> - </div> -}; +export const Info = (a: any) => <Notifications {...a} />; +Info.args = { + notifications: [{ + messageId: 'unauthorized', + type: 'INFO', + }] +} +export const Warn = (a: any) => <Notifications {...a} />; +Warn.args = { + notifications: [{ + messageId: 'unauthorized', + type: 'WARN', + }] +} +export const Error = (a: any) => <Notifications {...a} />; +Error.args = { + notifications: [{ + messageId: 'unauthorized', + type: 'ERROR', + }] +} diff --git a/packages/frontend/src/components/notifications/index.tsx b/packages/frontend/src/components/notifications/index.tsx @@ -25,6 +25,7 @@ import { MessageType, Notification } from "../../declaration"; interface Props { notifications: Notification[]; + removeNotification: (n: Notification) => void; } function messageStyle(type: MessageType): string { @@ -37,11 +38,12 @@ function messageStyle(type: MessageType): string { } } -export function Notifications({ notifications }: Props): VNode { +export function Notifications({ notifications, removeNotification }: Props): VNode { return <div class="toast"> {notifications.map(n => <article class={messageStyle(n.type)}> <div class="message-header"> <p><Text id={`notification.${n.messageId}.title`} /> </p> + <button class="delete" onClick={()=> removeNotification(n)} /> </div> <div class="message-body"> <Text id={`notification.${n.messageId}.description`} fields={n.params} /> diff --git a/packages/frontend/src/components/yup/YupField.tsx b/packages/frontend/src/components/yup/YupField.tsx @@ -23,8 +23,10 @@ import { h, VNode } from "preact"; import { Text, useText } from "preact-i18n"; import { StateUpdater, useContext, useState } from "preact/hooks"; import { intervalToDuration, formatDuration } from 'date-fns' +import { BackendContext, ConfigContext } from '../../context/backend'; -function readableDuration(duration: number): string { +function readableDuration(duration?: number): string { + if (!duration) return "" return formatDuration(intervalToDuration({ start: 0, end: duration })) } @@ -55,8 +57,6 @@ interface Props { valueHandler: StateUpdater<any>; info: any; } -import { ConfigContext } from '../../context/backend'; - export function YupField({ name, field, errors, object, valueHandler, info }: Props): VNode { const updateField = (f: string) => (v: string): void => valueHandler((prev: any) => ({ ...prev, [f]: v })) const values = { @@ -65,24 +65,27 @@ export function YupField({ name, field, errors, object, valueHandler, info }: Pr value: object && object[field], onChange: updateField(field) } + const backend = useContext(BackendContext) const config = useContext(ConfigContext) switch (info.meta?.type) { - case 'group': return <YupObjectInput name={name} - info={info} errors={errors} - value={object && object[field]} - onChange={(updater: any): void => valueHandler((prev: any) => ({ ...prev, [field]: updater(prev[field]) }))} - /> + case 'group': { + return <YupObjectInput name={name} + info={info} errors={errors} + value={object && object[field]} + onChange={(updater: any): void => valueHandler((prev: any) => ({ ...prev, [field]: updater(prev[field]) }))} + /> + } case 'array': return <YupInputArray {...values} />; case 'amount': { if (config.currency) { return <YupInputWithAddon {...values} addon={config.currency} onChange={(v: string): void => values.onChange(`${config.currency}:${v}`)} value={values.value?.split(':')[1]} /> - } - return <YupInput {...values} />; + } + return <YupInput {...values} />; } - case 'url': return <YupInputWithAddon {...values} addon={`${config.backendURL}/private/instances/`} />; + case 'url': return <YupInputWithAddon {...values} addon={`${backend.url}/private/instances/`} />; case 'secured': return <YupInputSecured {...values} />; - case 'duration': return <YupInputWithAddon addon={readableDuration(values.value?.d_ms)} atTheEnd {...values} value={`${values.value?.d_ms / 1000 || ''}`} onChange={(v: string): void => values.onChange({ d_ms: (parseInt(v, 10) * 1000) || undefined } as any)} />; + case 'duration': return <YupInputWithAddon {...values} addon={readableDuration(values.value?.d_ms)} atTheEnd value={`${values.value?.d_ms / 1000 || ''}`} onChange={(v: string): void => values.onChange({ d_ms: (parseInt(v, 10) * 1000) || undefined } as any)} />; default: return <YupInput {...values} />; } diff --git a/packages/frontend/src/context/backend.ts b/packages/frontend/src/context/backend.ts @@ -1,11 +1,29 @@ import { createContext } from 'preact' -interface GlobalContext { - backendURL: string; +export interface BackendContextType { + url: string; + token?: string; + changeBackend: (url: string) => void; + clearToken: () => void; + updateToken: (token:string) => void; + lang: string; + setLang: (lang: string) => void; +} + +export interface ConfigContextType { currency?: string; } -export const ConfigContext = createContext<GlobalContext>({ - backendURL: '', - currency: '', +export const BackendContext = createContext<BackendContextType>({ + url: '', + lang: 'en', + token: undefined, + changeBackend: () => null, + clearToken: () => null, + updateToken: () => null, + setLang: () => null, +}) + +export const ConfigContext = createContext<ConfigContextType>({ + currency: undefined, }) diff --git a/packages/frontend/src/hooks/backend.ts b/packages/frontend/src/hooks/backend.ts @@ -14,14 +14,16 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** - * - * @author Sebastian Javier Marchano (sebasjm) - */ +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ import useSWR, { mutate } from 'swr'; import axios from 'axios' import { MerchantBackend } from '../declaration'; +import { useContext } from 'preact/hooks'; +import { BackendContext } from '../context/backend'; type HttpResponse<T> = HttpResponseOk<T> | HttpResponseError<T>; @@ -30,82 +32,101 @@ interface HttpResponseOk<T> { } interface HttpResponseError<T> { data: undefined; - needsAuth: boolean; + unauthorized: boolean; error: Error; } type Methods = 'get' | 'post' | 'patch' | 'delete' | 'put'; -async function request(url: string, method?: Methods, data?: any): Promise<any> { - const backend = localStorage.getItem('backend-url') - const token = localStorage.getItem('backend-token') - const headers = token ? { Authorization: `${token}` } : undefined +interface RequestOptions { + method?: Methods; + token?: string; + data?: any; +} + +async function request(url: string, options: RequestOptions = {}): Promise<any> { + const headers = options.token ? { Authorization: `${options.token}` } : undefined try { const res = await axios({ - method: method || 'get', - url: `${backend}${url}`, + method: options.method || 'get', + url, responseType: 'json', headers, - data + data: options.data }) return res.data } catch (e) { const info = e.response?.data const status = e.response?.status - throw { info, status, error:e, backend, hasToken: !!token } + throw { info, status, error: e, backend: url, hasToken: !!options.token } } } -async function fetcher(url: string): Promise<any> { - return request(url, 'get') +function fetcher(url: string, token: string, backend: string) { + return request(`${backend}${url}`, { token }) } -interface WithCreate<T> { - create: (data: T) => Promise<void>; -} -interface WithUpdate<T> { - update: (id: string, data: T) => Promise<void>; -} -interface WithDelete { - delete: (id: string) => Promise<void>; +interface BackendMutateAPI { + createInstance: (data: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; + updateInstance: (id: string, data: MerchantBackend.Instances.InstanceReconfigurationMessage) => Promise<void>; + deleteInstance: (id: string) => Promise<void>; } -export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> & WithCreate<MerchantBackend.Instances.InstanceConfigurationMessage> { - const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse>('/private/instances', fetcher) +export function useBackendMutateAPI(): BackendMutateAPI { + const { url, token } = useContext(BackendContext) - const create = async (instance: MerchantBackend.Instances.InstanceConfigurationMessage): Promise<void> => { - await request('/private/instances', 'post', instance) + const createInstance = async (instance: MerchantBackend.Instances.InstanceConfigurationMessage): Promise<void> => { + await request(`${url}/private/instances`, { + method: 'post', + token, + data: instance + }) mutate('/private/instances') } - - return { data, needsAuth: error?.status === 401, error, create } -} - -export function useBackendInstance(id: string | null): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> & WithUpdate<MerchantBackend.Instances.InstanceReconfigurationMessage> & WithDelete { - const { data, error } = useSWR<MerchantBackend.Instances.QueryInstancesResponse>(id ? `/private/instances/${id}` : null, fetcher) - - const update = async (updateId: string, instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { - await request(`/private/instances/${updateId}`, 'patch', instance) + const updateInstance = async (updateId: string, instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { + await request(`${url}/private/instances/${updateId}`, { + method: 'patch', + token, + data: instance + }) mutate('/private/instances', null) mutate(`/private/instances/${updateId}`, null) }; - const _delete = async (deleteId: string): Promise<void> => { - await request(`/private/instances/${deleteId}`, 'delete') + const deleteInstance = async (deleteId: string): Promise<void> => { + await request(`${url}/private/instances/${deleteId}`, { + method: 'delete', + token, + }) mutate('/private/instances', null) mutate(`/private/instances/${deleteId}`, null) } - return { data, needsAuth: error?.status === 401, error, update, delete: _delete } + return { createInstance, updateInstance, deleteInstance } +} + +export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { + const { url, token } = useContext(BackendContext) + const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse>(['/private/instances', token, url], fetcher) + + return { data, unauthorized: error?.status === 401, error } +} + +export function useBackendInstance(id: string | null): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { + const { url, token } = useContext(BackendContext) + const { data, error } = useSWR<MerchantBackend.Instances.QueryInstancesResponse>(id ? [`/private/instances/${id}`, token, url] : null, fetcher) + + return { data, unauthorized: error?.status === 401, error } } -export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> { - const { data, error } = useSWR<MerchantBackend.VersionResponse>(`/config`, fetcher) +export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> { + const { url, token } = useContext(BackendContext) + const { data, error } = useSWR<MerchantBackend.VersionResponse>(['/config', token, url], fetcher) - return { data, needsAuth: error?.status === 401, error } + return { data, unauthorized: error?.status === 401, error } } diff --git a/packages/frontend/src/hooks/index.ts b/packages/frontend/src/hooks/index.ts @@ -19,29 +19,52 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { StateUpdater, useState } from "preact/hooks"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; import { mutate } from 'swr'; -interface State { - token?: string; - url: string; +export function useBackendURL(): [string, StateUpdater<string>] { + return useNotNullLocalStorage('backend-url', window.location.origin) +} +export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] { + return useLocalStorage('backend-token') } -export function useBackend(): [State, StateUpdater<State>] { - const [url, setUrl] = useLocalStorage('backend-url', window.location.origin) - const [token, setToken] = useLocalStorage('backend-token') - - const updater: StateUpdater<State> = (value: State | ((value: State) => State)) => { - const valueToStore = value instanceof Function ? value({ token, url: url || window.location.origin }) : value; - setUrl(valueToStore.url) - setToken(valueToStore.token) +export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>, VoidFunction] { + const [token, setToken] = useLocalStorage(`backend-token-${id}`) + const [ids, setIds] = useLocalStorage(`backend-token-ids`) - mutate('/private/instances', null) + function clearAllTokens() { + // TODO: refactor this + ids?.split(',').map(i => localStorage.removeItem(`backend-token-${i}`)) } - return [{ token, url: url || window.location.origin }, updater] + useEffect(() => { + setIds((ids: string | undefined): string | undefined => { + if (!ids) return ids + const all = ids.split(',') + if (all.includes(id)) return ids + return all.concat(id).filter(Boolean).join(',') + }) + }, [id, setIds]) + + return [token, setToken, clearAllTokens] } +// export function useBackend(): [State, StateUpdater<State>] { +// const [url, setUrl] = useLocalStorage('backend-url', window.location.origin) +// const [token, setToken] = useLocalStorage('backend-token') + +// const updater: StateUpdater<State> = (value: State | ((value: State) => State)) => { +// const valueToStore = value instanceof Function ? value({ token, url: url || window.location.origin }) : value; +// setUrl(valueToStore.url) +// setToken(valueToStore.token) + +// mutate('/private/instances', null) +// } + +// return [{ token, url: url || window.location.origin }, updater] +// } + export function useLang(): [string, StateUpdater<string>] { return useNotNullLocalStorage('lang-preference', typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : 'en') } diff --git a/packages/frontend/src/hooks/notifications.ts b/packages/frontend/src/hooks/notifications.ts @@ -14,10 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** - * - * @author Sebastian Javier Marchano (sebasjm) - */ +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ import { useState } from "preact/hooks"; import { Notification } from '../declaration'; @@ -25,16 +25,24 @@ import { Notification } from '../declaration'; interface Result { notifications: Notification[]; pushNotification: (n: Notification) => void; + removeNotification: (n: Notification) => void; } +type NotificationWithDate = Notification & { since: Date } + export function useNotifications(timeout = 3000): Result { - const [notifications, setNotifications] = useState<(Notification & { since: Date })[]>([]) + const [notifications, setNotifications] = useState<(NotificationWithDate)[]>([]) + const pushNotification = (n: Notification): void => { const entry = { ...n, since: new Date() } setNotifications(ns => [...ns, entry]) - setTimeout(() => { + if (n.type !== 'ERROR') setTimeout(() => { setNotifications(ns => ns.filter(x => x.since !== entry.since)) }, timeout) } - return { notifications, pushNotification } + + const removeNotification = (notif: Notification) => { + setNotifications((ns: NotificationWithDate[]) => ns.filter(n => n !== notif)) + } + return { notifications, pushNotification, removeNotification } } diff --git a/packages/frontend/src/i18n/index.ts b/packages/frontend/src/i18n/index.ts @@ -150,8 +150,6 @@ export const translations = { description: 'the delete process completed' }, }, - cancel: 'cancel', - confirm: 'confirm', fields: { instance: { id: { @@ -271,6 +269,10 @@ export const translations = { list_of_configured_instances: 'List of configured instances', create_new_instance: 'Create new instance', + cancel: 'cancel', + confirm: 'confirm', + delete: 'delete', + update: 'update', instance: { empty_list: 'No instance configured yet, setup one pressing the + button', } diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx @@ -22,43 +22,200 @@ import "./scss/main.scss" import { h, VNode } from 'preact'; -import { Route, Router } from 'preact-router'; +import { useContext, useEffect } from "preact/hooks"; +import { Route, Router, route } from 'preact-router'; import { IntlProvider } from 'preact-i18n'; -import { Footer } from './components/footer'; +import { Notification } from "./declaration"; import { Sidebar } from './components/sidebar'; import { NavigationBar } from './components/navbar'; import { Notifications } from './components/notifications'; import { translations } from './i18n'; -import { useBackend, useLang } from './hooks'; - -import NotFoundPage from './routes/notfound'; -import Instances from './routes/instances'; +import { useBackendURL, useBackendDefaultToken, useLang, useBackendInstanceToken } from './hooks'; import { useNotifications } from "./hooks/notifications"; -import { ConfigContext } from './context/backend'; +import { BackendContext } from './context/backend'; import { useBackendConfig } from "./hooks/backend"; -export default function App(): VNode { - const { notifications, pushNotification } = useNotifications() +import NotFoundPage from './routes/notfound'; +import Login from './routes/login'; +import Instances from './routes/instances/list'; +import Create from "./routes/instances/create"; +import Details from "./routes/instances/details"; +import Update from "./routes/instances/update"; + +enum RootPages { + root = '/', + instances = '/instances', + new = '/new', + instance_id_route = '/instance/:id/:rest*', +} + +enum InstancePages { + details = '/instance/:id/', + update = '/instance/:id/update', +} + +function Redirect({ to }: { to: string }): null { + useEffect(() => { + route(to, true) + }) + return null +} + + + +function AppRouting(): VNode { + const { notifications, pushNotification, removeNotification } = useNotifications() + const { lang, setLang, clearToken, changeBackend, updateToken } = useContext(BackendContext) + const backendConfig = useBackendConfig(); + + const error = backendConfig.data || backendConfig.error + + useEffect(() =>{ + pushNotification({ + messageId: 'error', + type: 'ERROR', + params: error + }) + }, [error, pushNotification]) + + return <div id="app"> + <NavigationBar lang={lang} setLang={setLang} onLogout={clearToken} /> + <Sidebar /> + <Notifications notifications={notifications} removeNotification={removeNotification} /> + {!backendConfig.data ? + <Route default + component={Login} + + onConfirm={(url: string, token?: string) => { + changeBackend(url) + if (token) updateToken(token) + route(RootPages.instances) + }} /> : + <Route default component={AppReady} pushNotification={pushNotification} /> + } + </div> +} + +function AppReady({ pushNotification }: { pushNotification: (n: Notification) => void }): VNode { + const { changeBackend, updateToken } = useContext(BackendContext) + + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url) + if (token) updateToken(token) + } + + return <Router> + <Route path={RootPages.root} component={Redirect} to={RootPages.instances} /> + + <Route path={RootPages.instances} + component={Instances} + + onCreate={() => { + route(RootPages.new) + }} + + onUpdate={(id: string): void => { + route(`/instance/${id}/update`) + }} + + onUnauthorized={() => <Login onConfirm={updateLoginStatus} />} + + onError={(error: Error) => { + pushNotification({ messageId: 'error', params: error, type: 'ERROR' }) + return <div /> + }} + /> + + <Route path={RootPages.new} + component={Create} + onBack={() => route(RootPages.instances)} + + onConfirm={() => { + pushNotification({ messageId: 'create_success', type: 'SUCCESS' }) + route(RootPages.instances) + }} + + onError={(error: any) => { + pushNotification({ messageId: 'create_error', type: 'ERROR', params: error }) + }} + /> + + <Route path={RootPages.instance_id_route} component={SubPages} pushNotification={pushNotification} /> + + <Route default component={NotFoundPage} /> + + </Router> +} + +function useBackendContextState() { const [lang, setLang] = useLang() - const [{url: backendURL}] = useBackend(); - const { data } = useBackendConfig(); + const [url, changeBackend] = useBackendURL(); + const [token, updateToken] = useBackendDefaultToken(); + const clearToken = () => updateToken(undefined) + + return { url, token, changeBackend, clearToken, updateToken, lang, setLang } +} +export default function Application(): VNode { + const state = useBackendContextState() return ( - <ConfigContext.Provider value={{backendURL, currency: data && data.currency}}> - - <IntlProvider definition={(translations as any)[lang] || translations.en}> - <div id="app"> - <NavigationBar lang={lang} setLang={setLang} /> - <Sidebar /> - <Notifications notifications={notifications} /> - <Router> - <Route path="/" component={Instances} pushNotification={pushNotification} /> - <Route default component={NotFoundPage} /> - </Router> - <Footer /> - </div> + <BackendContext.Provider value={state}> + <IntlProvider definition={(translations as any)[state.lang] || translations.en}> + <AppRouting /> </IntlProvider > - </ConfigContext.Provider> + </BackendContext.Provider> ); +} +interface SubPagesProps { + id: string; + pushNotification: (n: Notification) => void; +} + +function SubPages({ id, pushNotification }: SubPagesProps): VNode { + const [, updateToken] = useBackendInstanceToken(id); + const { changeBackend } = useContext(BackendContext) + + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url) + if (token) updateToken(token) + } + + return <Router> + <Route path={InstancePages.details} + component={Details} + onUnauthorized={() => <Login onConfirm={updateLoginStatus} />} + onUpdate={() => { + route(`/instance/${id}/update`) + }} + onLoadError={(e: Error) => { + pushNotification({ messageId: 'update_load_error', type: 'ERROR', params: e }) + route(`/instance/${id}/`) + return <div /> + }} + + /> + + <Route path={InstancePages.update} + component={Update} + onUnauthorized={() => <Login onConfirm={updateLoginStatus} />} + onLoadError={(e: Error) => { + pushNotification({ messageId: 'update_load_error', type: 'ERROR', params: e }) + route(`/instance/${id}/`) + return <div /> + }} + onBack={() => { + route(`/instance/${id}/`) + }} + onConfirm={() => { + pushNotification({ messageId: 'create_success', type: 'SUCCESS' }) + route(`/instance/${id}/`) + }} + onUpdateError={(e: Error) => { + pushNotification({ messageId: 'update_error', type: 'ERROR', params: e }) + }} + /> + + <Route default component={NotFoundPage} /> + </Router> } \ No newline at end of file diff --git a/packages/frontend/src/routes/instances/CardTable.tsx b/packages/frontend/src/routes/instances/CardTable.tsx @@ -1,99 +0,0 @@ -/* - 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"; -import { Text } from "preact-i18n"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend, WidthId as WithId } from "../../declaration"; -import { EmptyTable } from "./EmptyTable"; -import { Table } from "./Table"; - -interface Props { - instances: MerchantBackend.Instances.Instance[]; - onSelect: (id: string | null, action: 'UPDATE' | 'DELETE') => void; - onCreate: () => void; - selected: MerchantBackend.Instances.QueryInstancesResponse & WithId | undefined; -} - -interface Actions { - element: MerchantBackend.Instances.Instance; - type: 'DELETE' | 'UPDATE'; -} - -function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { - return value !== null && value !== undefined; -} - -function buildActions(intances: MerchantBackend.Instances.Instance[], selected: string[], action: 'DELETE'): Actions[] { - return selected.map(id => intances.find(i => i.id === id)) - .filter(notEmpty) - .map(id => ({ element: id, type: action })) -} - -export function CardTable({ instances, onCreate, onSelect, selected }: Props): VNode { - const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); - const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { - onSelect(actionQueue[0].element.id, 'DELETE') - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onSelect]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { - onSelect(actionQueue[0].element.id, 'UPDATE') - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onSelect]) - - - return <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-account-multiple" /></span><Text id="text.instances" /></p> - - <div class="card-header-icon" aria-label="more options"> - - <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} - type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > - <span class="icon"><i class="mdi mdi-trash-can" /></span> - </button> - </div> - <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> - </div> - - </header> - <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} /> : - <EmptyTable /> - } - </div> - </div> - </div> - </div> -} -\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/CreatePage.tsx b/packages/frontend/src/routes/instances/CreatePage.tsx @@ -1,114 +0,0 @@ -/* - 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"; -import { useState } from "preact/hooks"; -import { MerchantBackend } from "../../declaration"; -import * as yup from 'yup'; -import { YupField } from "../../components/yup/YupField" -import { InstanceCreateSchema as schema } from '../../schemas' -import { Text } from "preact-i18n"; - -interface Props { - onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => void; - isLoading: boolean; - goBack: () => void; -} - -interface KeyValue { - [key: string]: string; -} - -function with_defaults(): Partial<MerchantBackend.Instances.InstanceConfigurationMessage> { - return { - default_pay_delay: { d_ms: 1000 }, - default_wire_fee_amortization: 10, - default_wire_transfer_delay: { d_ms: 2000 }, - }; -} - -export function CreatePage({ onCreate, isLoading, goBack }: Props): VNode { - const [value, valueHandler] = useState(with_defaults()) - const [errors, setErrors] = useState<KeyValue>({}) - - const submit = (): void => { - try { - schema.validateSync(value, { abortEarly: false }) - onCreate(schema.cast(value) as MerchantBackend.Instances.InstanceConfigurationMessage); - goBack() - } catch (err) { - const errors = err.inner as yup.ValidationError[] - const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) - setErrors(pathMessages) - } - } - return <div> - <section class="section is-title-bar"> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <ul> - <li><Text id="text.merchant" /></li> - <li><Text id="text.instances" /></li> - </ul> - </div> - </div> - </div> - </section> - - <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - <Text id="text.create_new_instances" /> - </h1> - </div> - </div> - <div class="level-right" style="display: none;"> - <div class="level-item" /> - </div> - </div> - </div> - </section> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-two-thirds"> - {Object.keys(schema.fields) - .map(f => <YupField name={f} - field={f} errors={errors} object={value} - valueHandler={valueHandler} info={schema.fields[f].describe()} - />)} - <div class="buttons is-right"> - <button class="button" onClick={goBack} ><Text id="cancel" /></button> - <button class="button is-success" onClick={submit} ><Text id="confirm" /></button> - </div> - </div> - <div class="column" /> - </div> - </section> - - </div> -} -\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/DeleteModal.tsx b/packages/frontend/src/routes/instances/DeleteModal.tsx @@ -1,37 +0,0 @@ -/* - 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"; -import { MerchantBackend, WidthId } from "../../declaration"; -import { ConfirmModal } from "../../components/modal"; - -interface Props { - element: MerchantBackend.Instances.QueryInstancesResponse & WidthId; - onCancel: () => void; - onConfirm: (i: MerchantBackend.Instances.QueryInstancesResponse & WidthId) => void; -} - -export function DeleteModal({ element, onCancel, onConfirm }: Props): VNode { - return <ConfirmModal description="delete_instance" danger active onCancel={onCancel} onConfirm={() => onConfirm(element)}> - <p>This will permanently delete instance "{element.name}" with id <b>{element.id}</b></p> - <p>Please confirm this action</p> - </ConfirmModal> -} diff --git a/packages/frontend/src/routes/instances/Table.tsx b/packages/frontend/src/routes/instances/Table.tsx @@ -1,84 +0,0 @@ -/* - 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" -import { Text } from "preact-i18n" -import { StateUpdater } from "preact/hooks" -import { MerchantBackend } from "../../declaration" - -interface Props { - rowSelection: string[]; - instances: MerchantBackend.Instances.Instance[]; - onSelect: (id: string | null, action: 'UPDATE' | 'DELETE') => void; - rowSelectionHandler: StateUpdater<string[]>; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) -} - -export function Table({ rowSelection, rowSelectionHandler, instances, onSelect }: Props): VNode { - return ( - <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={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> - <span class="check" /> - </label> - </th> - <th><Text id="fields.instance.id.label" /></th> - <th><Text id="fields.instance.name.label" /></th> - <th><Text id="fields.instance.merchant_pub.label" /></th> - <th><Text id="fields.instance.payment_targets.label" /></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={(): void => 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={(): void => onSelect(i.id, 'UPDATE')}> - <span class="icon"><i class="mdi mdi-eye" /></span> - </button> - <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onSelect(i.id, 'DELETE')}> - <span class="icon"><i class="mdi mdi-trash-can" /></span> - </button> - </div> - </td> - </tr> - })} - - </tbody> - </table>) -} -\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/UpdatePage.tsx b/packages/frontend/src/routes/instances/UpdatePage.tsx @@ -1,121 +0,0 @@ -/* - 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"; -import { useState } from "preact/hooks"; -import { MerchantBackend, WidthId } from "../../declaration"; -import * as yup from 'yup'; -import { YupField } from "../../components/yup/YupField" -import { InstanceUpdateSchema as schema } from '../../schemas' -import { Text } from "preact-i18n"; - -interface Props { - onUpdate: (id: string, d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void; - selected: MerchantBackend.Instances.QueryInstancesResponse & WidthId; - isLoading: boolean; - goBack: () => void; -} - -interface KeyValue { - [key: string]: string; -} - -function convert(from: MerchantBackend.Instances.QueryInstancesResponse): MerchantBackend.Instances.InstanceReconfigurationMessage { - const { accounts, ...rest } = from - const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) - const defaults = { - default_wire_fee_amortization: 1, - default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour - default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours - } - return { ...defaults, ...rest, payto_uris }; -} - -export function UpdatePage({ onUpdate, isLoading, selected, goBack }: Props): VNode { - const [value, valueHandler] = useState(convert(selected)) - const [errors, setErrors] = useState<KeyValue>({}) - - const submit = (): void => { - try { - schema.validateSync(value, { abortEarly: false }) - onUpdate(selected.id, schema.cast(value)); - goBack() - } catch (err) { - const errors = err.inner as yup.ValidationError[] - const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) - setErrors(pathMessages) - } - } - - return <div> - <section class="section is-title-bar"> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <ul> - <li><Text id="text.merchant" /></li> - <li><Text id="text.instances" /></li> - </ul> - </div> - </div> - </div> - </section> - - <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - <Text id="text.create_new_instances" /> - </h1> - </div> - </div> - <div class="level-right" style="display: none;"> - <div class="level-item" /> - </div> - </div> - </div> - </section> - - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-two-thirds"> - {Object.keys(schema.fields) - .map(f => <YupField name={f} - field={f} errors={errors} object={value} - valueHandler={valueHandler} info={schema.fields[f].describe()} - />)} - <div class="buttons is-right"> - <button class="button" onClick={goBack} ><Text id="cancel" /></button> - <button class="button is-success" onClick={submit} ><Text id="confirm" /></button> - </div> - </div> - <div class="column" /> - </div> - </section> - - </div> - - // </ConfirmModal> -} -\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/View.tsx b/packages/frontend/src/routes/instances/View.tsx @@ -1,92 +0,0 @@ -/* - 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"; -import { useState } from "preact/hooks"; -import { MerchantBackend, WidthId } from "../../declaration"; -import { CardTable } from './CardTable'; -import { DeleteModal } from './DeleteModal' -import { Text } from "preact-i18n"; - -interface Props { - instances: MerchantBackend.Instances.Instance[]; - onCreate: (s: boolean) => void; - onUpdate: (s: boolean) => void; - onDelete: (id: string) => void; - onSelect: (id: string | null) => void; - selected: MerchantBackend.Instances.QueryInstancesResponse & WidthId | undefined; - isLoading: boolean; -} - -export function View({ instances, isLoading, onCreate, onDelete, onSelect, onUpdate, selected }: Props): VNode { - const [action, setAction] = useState<'UPDATE' | 'DELETE' | null>(null) - - const onSelectAction = (id: string | null, action?: 'UPDATE' | 'DELETE'): void => { - onSelect(id) - setAction(action || null) - if (action === 'UPDATE') onUpdate(true) - } - - return <div id="app"> - <section class="section is-title-bar"> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <ul> - <li><Text id="text.merchant" /></li> - <li><Text id="text.instances" /></li> - </ul> - </div> - </div> - </div> - </section> - - <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - <Text id="text.list_of_configured_instances" /> - </h1> - </div> - </div> - <div class="level-right" style="display: none;"> - <div class="level-item" /> - </div> - </div> - </div> - </section> - <section class="section is-main-section"> - <CardTable instances={instances} onSelect={onSelectAction} selected={selected} onCreate={(): void => onCreate(true)} /> - </section> - - {selected && action === 'DELETE' ? - <DeleteModal element={selected} onCancel={(): void => onSelectAction(null)} onConfirm={(i): void => { - onDelete(i.id) - onSelectAction(null); - }} - /> - : null} - - </div > -} -\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/Create.stories.tsx b/packages/frontend/src/routes/instances/create/Create.stories.tsx diff --git a/packages/frontend/src/routes/instances/create/CreatePage.tsx b/packages/frontend/src/routes/instances/create/CreatePage.tsx @@ -0,0 +1,114 @@ +/* + 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"; +import { useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import * as yup from 'yup'; +import { YupField } from "../../../components/yup/YupField" +import { InstanceCreateSchema as schema } from '../../../schemas' +import { Text } from "preact-i18n"; + +interface Props { + onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => void; + isLoading: boolean; + onBack: () => void; +} + +interface KeyValue { + [key: string]: string; +} + +function with_defaults(): Partial<MerchantBackend.Instances.InstanceConfigurationMessage> { + return { + default_pay_delay: { d_ms: 1000 }, + default_wire_fee_amortization: 10, + default_wire_transfer_delay: { d_ms: 2000 }, + }; +} + +export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { + const [value, valueHandler] = useState(with_defaults()) + const [errors, setErrors] = useState<KeyValue>({}) + + const submit = (): void => { + try { + schema.validateSync(value, { abortEarly: false }) + onCreate(schema.cast(value) as MerchantBackend.Instances.InstanceConfigurationMessage); + onBack() + } catch (err) { + const errors = err.inner as yup.ValidationError[] + const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) + setErrors(pathMessages) + } + } + return <div> + <section class="section is-title-bar"> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <ul> + <li><Text id="text.merchant" /></li> + <li><Text id="text.instances" /></li> + </ul> + </div> + </div> + </div> + </section> + + <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + <Text id="text.create_new_instances" /> + </h1> + </div> + </div> + <div class="level-right" style="display: none;"> + <div class="level-item" /> + </div> + </div> + </div> + </section> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + {Object.keys(schema.fields) + .map(f => <YupField name={f} + field={f} errors={errors} object={value} + valueHandler={valueHandler} info={schema.fields[f].describe()} + />)} + <div class="buttons is-right"> + <button class="button" onClick={onBack} ><Text id="text.cancel" /></button> + <button class="button is-success" onClick={submit} ><Text id="text.confirm" /></button> + </div> + </div> + <div class="column" /> + </div> + </section> + + </div> +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/create/index.tsx b/packages/frontend/src/routes/instances/create/index.tsx @@ -0,0 +1,21 @@ +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../declaration"; +import { useBackendMutateAPI } from "../../../hooks/backend"; +import { CreatePage } from "./CreatePage"; + +interface Props { + onBack: () => void; + onConfirm: () => void; + onError: (error: any) => void; +} + +export default function Create({ onBack, onConfirm, onError }: Props): VNode { + const { createInstance } = useBackendMutateAPI(); + + return <CreatePage + onBack={onBack} + isLoading={false} + onCreate={(d: MerchantBackend.Instances.InstanceConfigurationMessage): Promise<void> => { + return createInstance(d).then(onConfirm).catch(onError) + }} /> +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/details/DetailPage.tsx b/packages/frontend/src/routes/instances/details/DetailPage.tsx @@ -0,0 +1,108 @@ +/* + 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"; +import { useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import { YupField } from "../../../components/yup/YupField" +import { InstanceUpdateSchema as schema } from '../../../schemas' +import { Text } from "preact-i18n"; + +interface Props { + onUpdate: () => void; + onDelete: () => void; + selected: MerchantBackend.Instances.QueryInstancesResponse; + id: string, + isLoading: boolean; +} + +interface KeyValue { + [key: string]: string; +} + +function convert(from: MerchantBackend.Instances.QueryInstancesResponse): MerchantBackend.Instances.InstanceReconfigurationMessage { + const { accounts, ...rest } = from + const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) + const defaults = { + default_wire_fee_amortization: 1, + default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour + default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours + } + return { ...defaults, ...rest, payto_uris }; +} + +export function DetailPage({ onUpdate, isLoading, selected, onDelete }: Props): VNode { + const [value, valueHandler] = useState(convert(selected)) + const [errors, setErrors] = useState<KeyValue>({}) + + return <div> + <section class="section is-title-bar"> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <ul> + <li><Text id="text.merchant" /></li> + <li><Text id="text.instances" /></li> + </ul> + </div> + </div> + </div> + </section> + + <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + <Text id="text.create_new_instances" /> + </h1> + </div> + </div> + <div class="level-right" style="display: none;"> + <div class="level-item" /> + </div> + </div> + </div> + </section> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + {Object.keys(schema.fields) + .map(f => <YupField name={f} + field={f} errors={errors} object={value} + valueHandler={valueHandler} info={schema.fields[f].describe()} + />)} + <div class="buttons is-right"> + <button class="button is-danger" onClick={() => onDelete()} ><Text id="text.delete" /></button> + <button class="button is-success" onClick={() => onUpdate()} ><Text id="text.update" /></button> + </div> + </div> + <div class="column" /> + </div> + </section> + + </div> + +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/details/index.tsx b/packages/frontend/src/routes/instances/details/index.tsx @@ -0,0 +1,52 @@ +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Notification } from "../../../declaration"; +import { useBackendInstance, useBackendMutateAPI } from "../../../hooks/backend"; +import { DeleteModal } from "../list/DeleteModal"; +import { DetailPage } from "./DetailPage"; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (e: Error) => VNode; + onUpdate: () => void; + id: string; + pushNotification: (n: Notification) => void; +} + +export default function Detail({ onUpdate, onLoadError, onUnauthorized, id, pushNotification }: Props): VNode { + const details = useBackendInstance(id) + const [deleting, setDeleting] = useState<boolean>(false) + + const { deleteInstance } = useBackendMutateAPI() + + if (!details.data) { + if (details.unauthorized) return onUnauthorized() + if (details.error) return onLoadError(details.error) + return <div> + loading .... + </div> + } + + return <Fragment> + <DetailPage + isLoading={false} + selected={details.data} id={id} + onUpdate={onUpdate} + onDelete={() => setDeleting(true) } + /> + {deleting && <DeleteModal + element={{name: details.data.name, id }} + onCancel={() => setDeleting(false) } + onConfirm={async (id: string): Promise<void> => { + try { + await deleteInstance(id) + pushNotification({ messageId: 'delete_success', type: 'SUCCESS' }) + } catch (error) { + pushNotification({ messageId: 'delete_error', type: 'ERROR', params: error }) + } + setDeleting(false) + }} + />} + + </Fragment> +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/index.tsx b/packages/frontend/src/routes/instances/index.tsx @@ -1,97 +0,0 @@ -/* - 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'; -import { View } from './View'; -import { LoginPage } from '../../components/auth'; -import { useBackendInstance, useBackendInstances } from '../../hooks/backend'; -import { useEffect, useState } from 'preact/hooks'; -import { Notification } from '../../declaration'; -import { CreatePage } from './CreatePage'; -import { UpdatePage } from './UpdatePage'; - -interface Props { - pushNotification: (n: Notification) => void; -} - -export default function Instances({ pushNotification }: Props): VNode { - const list = useBackendInstances() - const [selectedId, select] = useState<string | null>(null) - const details = useBackendInstance(selectedId) - const [create, setCreate] = useState<boolean>(false) - const [update, setUpdate] = useState<boolean>(false) - - - const requiresToken = (!list.data && list.needsAuth) || (selectedId != null && !details.data && details.needsAuth) - const isLoadingTheList = (!list.data && !list.error) - const isLoadingTheDetails = (!details.data && !details.error) - - const genericError = !list.data && list.error || !details.data && details.error - - useEffect(() => { - if (requiresToken) { - pushNotification({ messageId: 'unauthorized', type: 'ERROR' }) - } else if (genericError) { - pushNotification({ messageId: 'error', params: genericError, type: 'ERROR' }) - } - }, [requiresToken, genericError]) - - - if (requiresToken) { - return <LoginPage /> - } - - if (create) { - return <CreatePage - goBack={() => setCreate(false)} - isLoading={false} - - onCreate={(d): Promise<void> => list.create(d) - .then((): void => pushNotification({ messageId: 'create_success', type: 'SUCCESS' })) - .catch((error): void => pushNotification({ messageId: 'create_error', type: 'ERROR', params: error })) - } - /> - } - - if (update && details.data && selectedId) { - return <UpdatePage - goBack={() => setUpdate(false)} - isLoading={false} - selected={{ ...details.data, id: selectedId }} - onUpdate={(id, d): Promise<void> => details.update(id, d) - .then((): void => pushNotification({ messageId: 'update_success', type: 'SUCCESS' })) - .catch((error): void => pushNotification({ messageId: 'update_error', type: 'ERROR', params: error })) - } - /> - } - - return <View instances={list.data?.instances || []} - isLoading={isLoadingTheList || isLoadingTheDetails} - onCreate={setCreate} - onUpdate={setUpdate} - onDelete={(id): Promise<void> => details.delete(id) - .then((): void => pushNotification({ messageId: 'delete_success', type: 'SUCCESS' })) - .catch((error): void => pushNotification({ messageId: 'delete_error', type: 'ERROR', params: error })) - } - onSelect={select} - selected={!details.data || !selectedId ? undefined : { ...details.data, id: selectedId }} - />; -} diff --git a/packages/frontend/src/routes/instances/list/CardTable.tsx b/packages/frontend/src/routes/instances/list/CardTable.tsx @@ -0,0 +1,100 @@ +/* + 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"; +import { Text } from "preact-i18n"; +import { useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import { EmptyTable } from "./EmptyTable"; +import { Table } from "./Table"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + onCreate: () => void; + selected?: boolean; +} + +interface Actions { + element: MerchantBackend.Instances.Instance; + type: 'DELETE' | 'UPDATE'; +} + +function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +function buildActions(intances: MerchantBackend.Instances.Instance[], selected: string[], action: 'DELETE'): Actions[] { + return selected.map(id => intances.find(i => i.id === id)) + .filter(notEmpty) + .map(id => ({ element: id, type: action })) +} + +export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: Props): VNode { + const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); + const [rowSelection, rowSelectionHandler] = useState<string[]>([]) + + useEffect(() => { + if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { + onDelete(actionQueue[0].element) + actionQueueHandler(actionQueue.slice(1)) + } + }, [actionQueue, selected, onDelete]) + + useEffect(() => { + if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { + onUpdate(actionQueue[0].element.id) + actionQueueHandler(actionQueue.slice(1)) + } + }, [actionQueue, selected, onUpdate]) + + + return <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-account-multiple" /></span><Text id="text.instances" /></p> + + <div class="card-header-icon" aria-label="more options"> + + <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} + type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > + <span class="icon"><i class="mdi mdi-trash-can" /></span> + </button> + </div> + <div class="card-header-icon" aria-label="more options"> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </div> + + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? + <Table instances={instances} onUpdate={onUpdate} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : + <EmptyTable /> + } + </div> + </div> + </div> + </div> +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/list/DeleteModal.tsx b/packages/frontend/src/routes/instances/list/DeleteModal.tsx @@ -0,0 +1,36 @@ +/* + 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"; +import { ConfirmModal } from "../../../components/modal"; + +interface Props { + element: {id: string, name: string}; + onCancel: () => void; + onConfirm: (id: string) => void; +} + +export function DeleteModal({ element, onCancel, onConfirm }: Props): VNode { + return <ConfirmModal description="delete_instance" danger active onCancel={onCancel} onConfirm={() => onConfirm(element.id)}> + <p>This will permanently delete instance "{element.name}" with id <b>{element.id}</b></p> + <p>Please confirm this action</p> + </ConfirmModal> +} diff --git a/packages/frontend/src/routes/instances/EmptyTable.tsx b/packages/frontend/src/routes/instances/list/EmptyTable.tsx diff --git a/packages/frontend/src/routes/instances/list/Table.tsx b/packages/frontend/src/routes/instances/list/Table.tsx @@ -0,0 +1,85 @@ +/* + 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" +import { Text } from "preact-i18n" +import { StateUpdater } from "preact/hooks" +import { MerchantBackend } from "../../../declaration" + +interface Props { + rowSelection: string[]; + instances: MerchantBackend.Instances.Instance[]; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + rowSelectionHandler: StateUpdater<string[]>; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) +} + +export function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete }: Props): VNode { + return ( + <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={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> + <span class="check" /> + </label> + </th> + <th><Text id="fields.instance.id.label" /></th> + <th><Text id="fields.instance.name.label" /></th> + <th><Text id="fields.instance.merchant_pub.label" /></th> + <th><Text id="fields.instance.payment_targets.label" /></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={(): void => 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={(): void => onUpdate(i.id)}> + <span class="icon"><i class="mdi mdi-eye" /></span> + </button> + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> + <span class="icon"><i class="mdi mdi-trash-can" /></span> + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table>) +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/View.stories.tsx b/packages/frontend/src/routes/instances/list/View.stories.tsx diff --git a/packages/frontend/src/routes/instances/list/View.tsx b/packages/frontend/src/routes/instances/list/View.tsx @@ -0,0 +1,74 @@ +/* + 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"; +import { MerchantBackend } from "../../../declaration"; +import { CardTable } from './CardTable'; +import { Text } from "preact-i18n"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onCreate: () => void; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + selected?: boolean; + isLoading: boolean; +} + +export function View({ instances, isLoading, onCreate, onDelete, onUpdate, selected }: Props): VNode { + + return <div id="app"> + <section class="section is-title-bar"> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <ul> + <li><Text id="text.merchant" /></li> + <li><Text id="text.instances" /></li> + </ul> + </div> + </div> + </div> + </section> + + <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + <Text id="text.list_of_configured_instances" /> + </h1> + </div> + </div> + <div class="level-right" style="display: none;"> + <div class="level-item" /> + </div> + </div> + </div> + </section> + <section class="section is-main-section"> + <CardTable instances={instances} onDelete={onDelete} onUpdate={onUpdate} selected={selected} onCreate={onCreate} /> + </section> + + </div > +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/list/index.tsx b/packages/frontend/src/routes/instances/list/index.tsx @@ -0,0 +1,108 @@ +/* + 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 { Fragment, h, VNode } from 'preact'; +import { View } from './View'; +import { useBackendInstances, useBackendMutateAPI } from '../../../hooks/backend'; +import { useState } from 'preact/hooks'; +import { MerchantBackend, Notification } from '../../../declaration'; +import { DeleteModal } from './DeleteModal'; +interface Props { + pushNotification: (n: Notification) => void; + onUnauthorized: () => VNode; + onError: (e: Error) => VNode; + onCreate: () => void; + onUpdate: (id: string) => void; +} + +export default function Instances({ pushNotification, onUnauthorized, onError, onCreate, onUpdate }: Props): VNode { + const list = useBackendInstances() + const [deleting, setDeleting] = useState<MerchantBackend.Instances.Instance | null>(null) + // const details = useBackendInstance(selectedId) + // const [create, setCreate] = useState<boolean>(false) + // const [update, setUpdate] = useState<boolean>(false) + const { deleteInstance } = useBackendMutateAPI() + + // || (selectedId != null && !details.data && details.unauthorized) + const isLoadingTheList = (!list.data && !list.error) + // const isLoadingTheDetails = (!details.data && !details.error) + const error = !list.data && list.error + // || !details.data && details.error + + if (!list.data) { + if (list.unauthorized) return onUnauthorized() + if (list.error) return onError(list.error) + } + + // if (create) { + // return <CreatePage + // goBack={() => setCreate(false)} + // isLoading={false} + + // onCreate={(d): Promise<void> => list.create(d) + // .then((): void => pushNotification({ messageId: 'create_success', type: 'SUCCESS' })) + // .catch((error): void => pushNotification({ messageId: 'create_error', type: 'ERROR', params: error })) + // } + // /> + // } + + // if (update && details.data && selectedId) { + // return <UpdatePage + // goBack={() => setUpdate(false)} + // isLoading={false} + // selected={{ ...details.data, id: selectedId }} + // onUpdate={(id, d): Promise<void> => details.update(id, d) + // .then((): void => pushNotification({ messageId: 'update_success', type: 'SUCCESS' })) + // .catch((error): void => pushNotification({ messageId: 'update_error', type: 'ERROR', params: error })) + // } + // /> + // } + // {selected && action === 'DELETE' ? + + // : null} + + // (id): Promise<void> => deleteInstance(id) + // .then((): void => pushNotification({ messageId: 'delete_success', type: 'SUCCESS' })) + // .catch((error): void => pushNotification({ messageId: 'delete_error', type: 'ERROR', params: error })) + + return <Fragment> + <View instances={list.data?.instances || []} + isLoading={isLoadingTheList} + onDelete={setDeleting} + onCreate={onCreate} + onUpdate={onUpdate} + selected={!!deleting} + /> + {deleting && <DeleteModal + element={deleting} + onCancel={() => setDeleting(null) } + onConfirm={async (id: string): Promise<void> => { + try { + await deleteInstance(id) + pushNotification({ messageId: 'delete_success', type: 'SUCCESS' }) + } catch (e) { + pushNotification({ messageId: 'delete_error', type: 'ERROR', params: error }) + } + setDeleting(null) + }} + />} + </Fragment>; +} diff --git a/packages/frontend/src/routes/instances/update/UpdatePage.tsx b/packages/frontend/src/routes/instances/update/UpdatePage.tsx @@ -0,0 +1,122 @@ +/* + 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"; +import { useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import * as yup from 'yup'; +import { YupField } from "../../../components/yup/YupField" +import { InstanceUpdateSchema as schema } from '../../../schemas' +import { Text } from "preact-i18n"; + +interface Props { + onUpdate: (id: string, d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void; + selected: MerchantBackend.Instances.QueryInstancesResponse; + id: string, + isLoading: boolean; + onBack: () => void; +} + +interface KeyValue { + [key: string]: string; +} + +function convert(from: MerchantBackend.Instances.QueryInstancesResponse): MerchantBackend.Instances.InstanceReconfigurationMessage { + const { accounts, ...rest } = from + const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) + const defaults = { + default_wire_fee_amortization: 1, + default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour + default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours + } + return { ...defaults, ...rest, payto_uris }; +} + +export function UpdatePage({ onUpdate, isLoading, selected, id, onBack }: Props): VNode { + const [value, valueHandler] = useState(convert(selected)) + const [errors, setErrors] = useState<KeyValue>({}) + + const submit = (): void => { + try { + schema.validateSync(value, { abortEarly: false }) + onUpdate(id, schema.cast(value)); + onBack() + } catch (err) { + const errors = err.inner as yup.ValidationError[] + const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) + setErrors(pathMessages) + } + } + + return <div> + <section class="section is-title-bar"> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <ul> + <li><Text id="text.merchant" /></li> + <li><Text id="text.instances" /></li> + </ul> + </div> + </div> + </div> + </section> + + <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <h1 class="title"> + <Text id="text.create_new_instances" /> + </h1> + </div> + </div> + <div class="level-right" style="display: none;"> + <div class="level-item" /> + </div> + </div> + </div> + </section> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + {Object.keys(schema.fields) + .map(f => <YupField name={f} + field={f} errors={errors} object={value} + valueHandler={valueHandler} info={schema.fields[f].describe()} + />)} + <div class="buttons is-right"> + <button class="button" onClick={onBack} ><Text id="text.cancel" /></button> + <button class="button is-success" onClick={submit} ><Text id="text.confirm" /></button> + </div> + </div> + <div class="column" /> + </div> + </section> + + </div> + + // </ConfirmModal> +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/update/index.tsx b/packages/frontend/src/routes/instances/update/index.tsx @@ -0,0 +1,37 @@ +import { h, VNode } from "preact"; +import { MerchantBackend } from "../../../declaration"; +import { useBackendInstance, useBackendMutateAPI } from "../../../hooks/backend"; +import { UpdatePage } from "./UpdatePage"; + +interface Props { + onBack: () => void; + onConfirm: () => void; + pushNotification: (n: Notification) => void; + + onUnauthorized: () => VNode; + onLoadError: (e: Error) => VNode; + onUpdateError: (e: Error) => void; + + id: string; +} + +export default function Update({ onBack, onConfirm, onLoadError, onUpdateError, onUnauthorized, id }: Props): VNode { + const { updateInstance } = useBackendMutateAPI(); + const details = useBackendInstance(id) + + if (!details.data) { + if (details.unauthorized) return onUnauthorized() + if (details.error) return onLoadError(details.error) + return <div> + loading .... + </div> + } + + return <UpdatePage + onBack={onBack} + isLoading={false} + selected={details.data} id={id} + onUpdate={(id: string, d: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { + return updateInstance(id, d).then(onConfirm).catch(onUpdateError) + }} /> +} +\ No newline at end of file diff --git a/packages/frontend/src/routes/login/index.tsx b/packages/frontend/src/routes/login/index.tsx @@ -0,0 +1,9 @@ +import { h, VNode } from "preact"; +import { LoginModal } from '../../components/auth'; + +interface Props { + onConfirm: (url: string, token?: string) => void; +} +export default function LoginPage({onConfirm}: Props):VNode { + return <LoginModal onConfirm={onConfirm} /> +} +\ No newline at end of file diff --git a/packages/frontend/src/scss/main.scss b/packages/frontend/src/scss/main.scss @@ -48,17 +48,16 @@ @import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css"; .toast { - position: fixed; - right: 10px; - top: 10px; + position: absolute; + width: 60%; + margin-left: 10%; + margin-right: 10%; z-index: 999; display: flex; flex-direction: column; padding: 15px; - text-align: right; - align-items: flex-end; - width: auto; + text-align: center; pointer-events: none; } diff --git a/packages/frontend/tests/header.test.tsx b/packages/frontend/tests/header.test.tsx @@ -20,14 +20,13 @@ */ import { h } from 'preact'; -import { Footer } from '../src/components/footer'; +import { Sidebar } from '../src/components/sidebar'; // See: https://github.com/preactjs/enzyme-adapter-preact-pure import { shallow } from 'enzyme'; -describe('Initial Test of the Footer', () => { - test('Footer renders an anchor with Taler text', () => { - const context = shallow(<Footer />); - expect(context.find('a').text()).toBe('Taler'); - expect(context.find('footer').length).toBe(1); +describe('Initial Test of the Sidebar', () => { + test('Sidbar renders anchors with text', () => { + const context = shallow(<Sidebar />); + expect(context.find('a').map( a => a.text())).toEqual(["Instances", "Details", "Orders", "Inventory", "Tipping", "About"]); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -97,7 +97,7 @@ importers: typedoc: ^0.20.25 typescript: ^4.1.3 yup: ^0.32.8 -lockfileVersion: 5.1 +lockfileVersion: 5.2 packages: /@babel/code-frame/7.12.11: dependencies: @@ -1546,7 +1546,7 @@ packages: jest-haste-map: 26.6.2 jest-message-util: 26.6.2 jest-regex-util: 26.0.0 - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 jest-resolve-dependencies: 26.6.3 jest-runner: 26.6.3 jest-runtime: 26.6.3 @@ -1616,7 +1616,7 @@ packages: istanbul-lib-source-maps: 4.0.0 istanbul-reports: 3.0.2 jest-haste-map: 26.6.2 - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 jest-util: 26.6.2 jest-worker: 26.6.2 slash: 3.0.0 @@ -1833,7 +1833,7 @@ packages: '@prefresh/core': 0.8.1_preact@10.5.12 '@prefresh/utils': 0.3.1 preact: 10.5.12 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true peerDependencies: preact: ^10.4.0 @@ -2536,7 +2536,7 @@ packages: unfetch: 4.2.0 url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0 util-deprecate: 1.0.2 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-dev-middleware: 3.7.3_webpack@4.46.0 webpack-filter-warnings-plugin: 1.2.1_webpack@4.46.0 webpack-hot-middleware: 2.25.0 @@ -2646,7 +2646,7 @@ packages: unfetch: 4.2.0 url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0 util-deprecate: 1.0.2 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-dev-middleware: 3.7.3_webpack@4.46.0 webpack-filter-warnings-plugin: 1.2.1_webpack@4.46.0 webpack-hot-middleware: 2.25.0 @@ -4150,7 +4150,7 @@ packages: dependencies: chalk: 2.4.1 deepcopy: 1.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true peerDependencies: webpack: ^4.28.4 @@ -4217,7 +4217,7 @@ packages: loader-utils: 1.4.0 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 8.9' @@ -5564,7 +5564,7 @@ packages: find-cache-dir: 3.3.1 schema-utils: 2.7.1 serialize-javascript: 4.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-sources: 1.4.3 dev: true engines: @@ -5730,7 +5730,7 @@ packages: p-limit: 2.3.0 schema-utils: 1.0.0 serialize-javascript: 4.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-log: 2.0.0 dev: true engines: @@ -5752,7 +5752,7 @@ packages: resolution: integrity: sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA== /core-js/2.6.12: - deprecated: 'core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.' + deprecated: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3. dev: true requiresBuild: true resolution: @@ -5985,7 +5985,7 @@ packages: postcss-value-parser: 4.1.0 schema-utils: 2.7.1 semver: 6.3.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 8.9.0' @@ -6676,7 +6676,7 @@ packages: /dotenv-webpack/1.8.0_webpack@4.46.0: dependencies: dotenv-defaults: 1.1.1 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true peerDependencies: webpack: ^1 || ^2 || ^3 || ^4 @@ -7603,7 +7603,7 @@ packages: dependencies: loader-utils: 2.0.0 schema-utils: 3.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 10.13.0' @@ -8679,7 +8679,7 @@ packages: tapable: 1.1.3 toposort: 1.0.7 util.promisify: 1.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>=6.9' @@ -8698,7 +8698,7 @@ packages: pretty-error: 2.1.2 tapable: 1.1.3 util.promisify: 1.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>=6.9' @@ -9801,7 +9801,7 @@ packages: jest-get-type: 26.3.0 jest-jasmine2: 26.6.3 jest-regex-util: 26.0.0 - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 jest-util: 26.6.2 jest-validate: 26.6.2 micromatch: 4.0.2 @@ -9974,7 +9974,7 @@ packages: integrity: sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== /jest-pnp-resolver/1.2.2_jest-resolve@26.6.2: dependencies: - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 dev: true engines: node: '>=6' @@ -10022,7 +10022,7 @@ packages: node: '>= 10.14.2' resolution: integrity: sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== - /jest-resolve/26.6.2_jest-resolve@26.6.2: + /jest-resolve/26.6.2: dependencies: '@jest/types': 26.6.2 chalk: 4.1.0 @@ -10035,8 +10035,6 @@ packages: dev: true engines: node: '>= 10.14.2' - peerDependencies: - jest-resolve: '*' resolution: integrity: sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== /jest-runner/26.6.3: @@ -10055,7 +10053,7 @@ packages: jest-haste-map: 26.6.2 jest-leak-detector: 26.6.2 jest-message-util: 26.6.2 - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 jest-runtime: 26.6.3 jest-util: 26.6.2 jest-worker: 26.6.2 @@ -10088,7 +10086,7 @@ packages: jest-message-util: 26.6.2 jest-mock: 26.6.2 jest-regex-util: 26.0.0 - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 jest-snapshot: 26.6.2 jest-util: 26.6.2 jest-validate: 26.6.2 @@ -10124,7 +10122,7 @@ packages: jest-haste-map: 26.6.2 jest-matcher-utils: 26.6.2 jest-message-util: 26.6.2 - jest-resolve: 26.6.2_jest-resolve@26.6.2 + jest-resolve: 26.6.2 natural-compare: 1.4.0 pretty-format: 26.6.2 semver: 7.3.4 @@ -11132,7 +11130,7 @@ packages: loader-utils: 1.4.0 normalize-url: 1.9.1 schema-utils: 1.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-sources: 1.4.3 dev: true engines: @@ -11795,7 +11793,7 @@ packages: dependencies: cssnano: 4.1.10 last-call-webpack-plugin: 3.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true peerDependencies: webpack: ^4.0.0 @@ -12803,7 +12801,7 @@ packages: update-notifier: 4.1.3 url-loader: 4.1.1_file-loader@6.2.0+webpack@4.46.0 validate-npm-package-name: 3.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-bundle-analyzer: 3.9.0 webpack-dev-server: 3.11.2_webpack@4.46.0 webpack-fix-style-only-entries: 0.5.2 @@ -12962,7 +12960,7 @@ packages: dependencies: chalk: 3.0.0 progress: 2.0.3 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true peerDependencies: webpack: ^1.3.0 || ^2 || ^3 || ^4 || ^5 @@ -13225,7 +13223,7 @@ packages: dependencies: loader-utils: 2.0.0 schema-utils: 3.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 10.13.0' @@ -13873,7 +13871,7 @@ packages: request-promise-core: 1.1.4_request@2.88.2 stealthy-require: 1.1.1 tough-cookie: 2.5.0 - deprecated: 'request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142' + deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 dev: true engines: node: '>=0.12.0' @@ -13903,7 +13901,7 @@ packages: tough-cookie: 2.5.0 tunnel-agent: 0.6.0 uuid: 3.4.0 - deprecated: 'request has been deprecated, see https://github.com/request/request/issues/3142' + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 dev: true engines: node: '>= 6' @@ -13968,7 +13966,7 @@ packages: resolution: integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== /resolve-url/0.2.1: - deprecated: 'https://github.com/lydell/resolve-url#deprecated' + deprecated: https://github.com/lydell/resolve-url#deprecated dev: true resolution: integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= @@ -14552,7 +14550,7 @@ packages: minimatch: 3.0.4 pretty-bytes: 5.5.0 util.promisify: 1.1.1 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true peerDependencies: webpack: '*' @@ -15099,7 +15097,7 @@ packages: dependencies: loader-utils: 2.0.0 schema-utils: 2.7.1 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 8.9.0' @@ -15321,7 +15319,7 @@ packages: serialize-javascript: 4.0.0 source-map: 0.6.1 terser: 4.8.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-sources: 1.4.3 worker-farm: 1.7.0 dev: true @@ -15341,7 +15339,7 @@ packages: serialize-javascript: 4.0.0 source-map: 0.6.1 terser: 4.8.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-sources: 1.4.3 dev: true engines: @@ -16015,7 +16013,7 @@ packages: resolution: integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== /urix/0.1.0: - deprecated: 'Please see https://github.com/lydell/urix#deprecated' + deprecated: Please see https://github.com/lydell/urix#deprecated dev: true resolution: integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= @@ -16025,7 +16023,7 @@ packages: loader-utils: 2.0.0 mime-types: 2.1.29 schema-utils: 3.0.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 10.13.0' @@ -16368,7 +16366,7 @@ packages: mime: 2.5.2 mkdirp: 0.5.5 range-parser: 1.2.1 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-log: 2.0.0 dev: true engines: @@ -16408,7 +16406,7 @@ packages: strip-ansi: 3.0.1 supports-color: 6.1.0 url: 0.11.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-dev-middleware: 3.7.3_webpack@4.46.0 webpack-log: 2.0.0 ws: 6.2.1 @@ -16427,7 +16425,7 @@ packages: integrity: sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== /webpack-filter-warnings-plugin/1.2.1_webpack@4.46.0: dependencies: - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 dev: true engines: node: '>= 4.3 < 5.0.0 || >= 5.10' @@ -16482,7 +16480,7 @@ packages: dev: true resolution: integrity: sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA== - /webpack/4.46.0_webpack@4.46.0: + /webpack/4.46.0: dependencies: '@webassemblyjs/ast': 1.9.0 '@webassemblyjs/helper-module-context': 1.9.0 @@ -16506,14 +16504,12 @@ packages: tapable: 1.1.3 terser-webpack-plugin: 1.4.5_webpack@4.46.0 watchpack: 1.7.5 - webpack: 4.46.0_webpack@4.46.0 webpack-sources: 1.4.3 dev: true engines: node: '>=6.11.5' hasBin: true peerDependencies: - webpack: '*' webpack-cli: '*' webpack-command: '*' peerDependenciesMeta: @@ -16745,7 +16741,7 @@ packages: fast-json-stable-stringify: 2.1.0 source-map-url: 0.4.1 upath: 1.2.0 - webpack: 4.46.0_webpack@4.46.0 + webpack: 4.46.0 webpack-sources: 1.4.3 workbox-build: 5.1.4 dev: true