merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit ee3f5c57b3e226e8ae9e38bb896f9c3579804a8e
parent cfd0c0749714878539589ec7e4ac446076384e5b
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon,  2 Aug 2021 11:22:48 -0300

making purge instance feature

Diffstat:
Mpackages/frontend/src/components/modal/index.tsx | 9+++++++++
Mpackages/frontend/src/hooks/admin.ts | 12+++++++++++-
Dpackages/frontend/src/paths/admin/list/Table.tsx | 175-------------------------------------------------------------------------------
Apackages/frontend/src/paths/admin/list/TableActive.tsx | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/admin/list/TableDeleted.tsx | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/paths/admin/list/View.tsx | 12+++++++++---
Mpackages/frontend/src/paths/admin/list/index.tsx | 43++++++++++++++++++++++++++++++++++++++++---
7 files changed, 367 insertions(+), 182 deletions(-)

diff --git a/packages/frontend/src/components/modal/index.tsx b/packages/frontend/src/components/modal/index.tsx @@ -125,7 +125,16 @@ interface DeleteModalProps { export function DeleteModal({ element, onCancel, onConfirm }: DeleteModalProps): VNode { return <ConfirmModal description="delete_instance" danger active onCancel={onCancel} onConfirm={() => onConfirm(element.id)}> + <p>This will deactivate the instance "{element.name}" with id <b>{element.id}</b></p> + <p>The merchant will not be able to create order or make refunds anymore.</p> + <p>Please confirm this action</p> + </ConfirmModal> +} + +export function PurgeModal({ element, onCancel, onConfirm }: DeleteModalProps): 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>The information being deleted also include tax records, make sure you have backups.</p> <p>Please confirm this action</p> </ConfirmModal> } diff --git a/packages/frontend/src/hooks/admin.ts b/packages/frontend/src/hooks/admin.ts @@ -40,10 +40,20 @@ export function useAdminAPI(): AdminAPI { mutateAll(/@"\/private\/instances"@/); }; - return { createInstance, deleteInstance }; + const purgeInstance = async (id: string): Promise<void> => { + await request(`${url}/private/instances/${id}?purge=YES`, { + method: 'delete', + token, + }); + + mutateAll(/@"\/private\/instances"@/); + }; + + return { createInstance, deleteInstance, purgeInstance }; } export interface AdminAPI { createInstance: (data: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; deleteInstance: (id: string) => Promise<void>; + purgeInstance: (id: string) => Promise<void>; } diff --git a/packages/frontend/src/paths/admin/list/Table.tsx b/packages/frontend/src/paths/admin/list/Table.tsx @@ -1,175 +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 { StateUpdater, useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../declaration"; -import { Translate, useTranslator } from "../../../i18n"; - -interface Props { - instances: MerchantBackend.Instances.Instance[]; - onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onCreate: () => void; - selected?: boolean; - setInstanceName: (s: string) => void; -} - -export function CardTable({ instances, onCreate, onUpdate, setInstanceName, 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]) - - const i18n = useTranslator() - - return <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-desktop-mac" /></span><Translate>Instances</Translate></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'))} > - <Translate>Delete</Translate> - </button> - </div> - <div class="card-header-icon" aria-label="more options"> - <span class="has-tooltip-left" data-tooltip={i18n`add new instance`}> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> - </span> - </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} setInstanceName={setInstanceName} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : - <EmptyTable /> - } - </div> - </div> - </div> - </div> -} -interface TableProps { - rowSelection: string[]; - instances: MerchantBackend.Instances.Instance[]; - onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - rowSelectionHandler: StateUpdater<string[]>; - setInstanceName: (s:string) => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) -} - -function Table({ rowSelection, rowSelectionHandler, setInstanceName, instances, onUpdate, onDelete }: TableProps): VNode { - return ( - <div class="table-container"> - <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><Translate>ID</Translate></th> - <th><Translate>Name</Translate></th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr key={i.id}> - <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><a href={`#/orders?instance=${i.id}`} onClick={(e) => { - setInstanceName(i.id); - }}>{i.id}</a></td> - <td >{i.name}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button class="button is-small is-success jb-modal" type="button" onClick={(): void => onUpdate(i.id)}> - <Translate>Edit</Translate> - </button> - <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> - <Translate>Delete</Translate> - </button> - </div> - </td> - </tr> - })} - - </tbody> - </table> - </div> - ) -} - -function EmptyTable(): VNode { - return <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> - </p> - <p><Translate>There is no instances yet, add more pressing the + sign</Translate></p> - </div> -} - - -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 })) -} - - diff --git a/packages/frontend/src/paths/admin/list/TableActive.tsx b/packages/frontend/src/paths/admin/list/TableActive.tsx @@ -0,0 +1,175 @@ +/* + 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 { StateUpdater, useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import { Translate, useTranslator } from "../../../i18n"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + onCreate: () => void; + selected?: boolean; + setInstanceName: (s: string) => void; +} + +export function CardTable({ instances, onCreate, onUpdate, setInstanceName, 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]) + + const i18n = useTranslator() + + return <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-desktop-mac" /></span><Translate>Instances</Translate></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'))} > + <Translate>Delete</Translate> + </button> + </div> + <div class="card-header-icon" aria-label="more options"> + <span class="has-tooltip-left" data-tooltip={i18n`add new instance`}> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </span> + </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} setInstanceName={setInstanceName} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : + <EmptyTable /> + } + </div> + </div> + </div> + </div> +} +interface TableProps { + rowSelection: string[]; + instances: MerchantBackend.Instances.Instance[]; + onUpdate: (id: string) => void; + onDelete: (id: MerchantBackend.Instances.Instance) => void; + rowSelectionHandler: StateUpdater<string[]>; + setInstanceName: (s:string) => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) +} + +function Table({ rowSelection, rowSelectionHandler, setInstanceName, instances, onUpdate, onDelete }: TableProps): VNode { + return ( + <div class="table-container"> + <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><Translate>ID</Translate></th> + <th><Translate>Name</Translate></th> + <th /> + </tr> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <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><a href={`#/orders?instance=${i.id}`} onClick={(e) => { + setInstanceName(i.id); + }}>{i.id}</a></td> + <td >{i.name}</td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button class="button is-small is-success jb-modal" type="button" onClick={(): void => onUpdate(i.id)}> + <Translate>Edit</Translate> + </button> + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> + <Translate>Disable</Translate> + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table> + </div> + ) +} + +function EmptyTable(): VNode { + return <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> + </p> + <p><Translate>There is no instances yet, add more pressing the + sign</Translate></p> + </div> +} + + +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 })) +} + + diff --git a/packages/frontend/src/paths/admin/list/TableDeleted.tsx b/packages/frontend/src/paths/admin/list/TableDeleted.tsx @@ -0,0 +1,123 @@ +/* + 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 { StateUpdater, useEffect, useState } from "preact/hooks"; +import { MerchantBackend } from "../../../declaration"; +import { Translate, useTranslator } from "../../../i18n"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onPurge: (id: MerchantBackend.Instances.Instance) => void; +} + +export function CardTable({ instances, onPurge }: Props): VNode { + + const i18n = useTranslator() + + return <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-desktop-mac" /></span><Translate>Disabled instances</Translate></p> + + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? + <Table instances={instances} onPurge={onPurge} /> : + <EmptyTable /> + } + </div> + </div> + </div> + </div> +} +interface TableProps { + instances: MerchantBackend.Instances.Instance[]; + onPurge: (id: MerchantBackend.Instances.Instance) => void; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) +} + +function Table({ instances, onPurge }: TableProps): VNode { + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th class="is-checkbox-cell"> + </th> + <th><Translate>ID</Translate></th> + <th><Translate>Name</Translate></th> + <th /> + </tr> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td class="is-checkbox-cell"> + </td> + <td>{i.id}</td> + <td >{i.name}</td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onPurge(i)}> + <Translate>Delete</Translate> + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table> + </div> + ) +} + +function EmptyTable(): VNode { + return <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> + </p> + <p><Translate>There is no instances yet, add more pressing the + sign</Translate></p> + </div> +} + + +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 })) +} + + diff --git a/packages/frontend/src/paths/admin/list/View.tsx b/packages/frontend/src/paths/admin/list/View.tsx @@ -21,23 +21,29 @@ import { h, VNode } from "preact"; import { MerchantBackend } from "../../../declaration"; -import { CardTable } from './Table'; +import { CardTable as CardTableActive } from './TableActive'; +import { CardTable as CardTableDeleted } from './TableDeleted'; interface Props { instances: MerchantBackend.Instances.Instance[]; onCreate: () => void; onUpdate: (id: string) => void; onDelete: (id: MerchantBackend.Instances.Instance) => void; + onPurge: (id: MerchantBackend.Instances.Instance) => void; selected?: boolean; setInstanceName: (s:string) => void; } -export function View({ instances, onCreate, onDelete, onUpdate, setInstanceName, selected }: Props): VNode { +export function View({ instances, onCreate, onDelete, onPurge, onUpdate, setInstanceName, selected }: Props): VNode { return <div id="app"> <section class="section is-main-section"> - <CardTable instances={instances} onDelete={onDelete} setInstanceName={setInstanceName} onUpdate={onUpdate} selected={selected} onCreate={onCreate} /> + <CardTableActive instances={instances.filter(i => !i.deleted)} onDelete={onDelete} setInstanceName={setInstanceName} onUpdate={onUpdate} selected={selected} onCreate={onCreate} /> + </section> + + <section class="section is-main-section"> + <CardTableDeleted instances={instances.filter(i => i.deleted)} onPurge={onPurge} /> </section> </div > diff --git a/packages/frontend/src/paths/admin/list/index.tsx b/packages/frontend/src/paths/admin/list/index.tsx @@ -22,11 +22,14 @@ import { Fragment, h, VNode } from 'preact'; import { useState } from 'preact/hooks'; import { Loading } from '../../../components/exception/loading'; -import { DeleteModal } from '../../../components/modal'; +import { NotificationCard } from '../../../components/menu'; +import { DeleteModal, PurgeModal } from '../../../components/modal'; import { MerchantBackend } from '../../../declaration'; import { useAdminAPI } from "../../../hooks/admin"; import { HttpError } from '../../../hooks/backend'; import { useBackendInstances } from '../../../hooks/instance'; +import { useTranslator } from '../../../i18n'; +import { Notification } from '../../../utils/types'; import { View } from './View'; interface Props { @@ -42,7 +45,10 @@ interface Props { export default function Instances({ onUnauthorized, onLoadError, onNotFound, onCreate, onUpdate, setInstanceName }: Props): VNode { const result = useBackendInstances() const [deleting, setDeleting] = useState<MerchantBackend.Instances.Instance | null>(null) - const { deleteInstance } = useAdminAPI() + const [purging, setPurging] = useState<MerchantBackend.Instances.Instance | null>(null) + const { deleteInstance, purgeInstance } = useAdminAPI() + const [notif, setNotif] = useState<Notification | undefined>(undefined) + const i18n = useTranslator() if (result.clientError && result.isUnauthorized) return onUnauthorized() if (result.clientError && result.isNotfound) return onNotFound() @@ -50,9 +56,11 @@ export default function Instances({ onUnauthorized, onLoadError, onNotFound, onC if (!result.ok) return onLoadError(result) return <Fragment> + <NotificationCard notification={notif} /> <View instances={result.data.instances} onDelete={setDeleting} onCreate={onCreate} + onPurge={setPurging} onUpdate={onUpdate} setInstanceName={setInstanceName} selected={!!deleting} @@ -64,11 +72,40 @@ export default function Instances({ onUnauthorized, onLoadError, onNotFound, onC try { await deleteInstance(deleting.id) // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) - } catch (e) { + setNotif({ + message: i18n`Instance disable`, + type: 'SUCCESS' + }) + } catch (error) { + setNotif({ + message: i18n`Failed to disable instance`, + type: "ERROR", + description: error.message + }) // pushNotification({ message: 'delete_error', type: 'ERROR' }) } setDeleting(null) }} />} + {purging && <PurgeModal + element={purging} + onCancel={() => setPurging(null)} + onConfirm={async (): Promise<void> => { + try { + await purgeInstance(purging.id) + setNotif({ + message: i18n`Instance deleted`, + type: 'SUCCESS' + }) + } catch (error) { + setNotif({ + message: i18n`Failed to delete instance`, + type: "ERROR", + description: error.message + }) + } + setPurging(null) + }} + />} </Fragment>; }