diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/admin/list')
4 files changed, 472 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx new file mode 100644 index 000000000..928658910 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -0,0 +1,184 @@ +/* + 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; + onPurge: (id: MerchantBackend.Instances.Instance) => void; + onCreate: () => void; + selected?: boolean; + setInstanceName: (s: string) => void; +} + +export function CardTable({ instances, onCreate, onUpdate, onPurge, 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} onPurge={onPurge} 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; + onPurge: (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, 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"> + <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> + {!i.deleted && + <button class="button is-small is-danger jb-modal is-outlined" type="button" onClick={(): void => onDelete(i)}> + <Translate>Delete</Translate> + </button> + } + {i.deleted && + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onPurge(i)}> + <Translate>Purge</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/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx new file mode 100644 index 000000000..3da8c2e50 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx @@ -0,0 +1,82 @@ +/* + 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 } from 'preact'; +import { View } from './View'; + + +export default { + title: 'Pages/Instance/List', + component: View, + argTypes: { + onSelect: { action: 'onSelect' }, + }, +}; + +export const Empty = (a: any) => <View {...a} />; +Empty.args = { + instances: [] +} + +export const WithDefaultInstance = (a: any) => <View {...a} />; +WithDefaultInstance.args = { + instances: [{ + id: 'default', + name: 'the default instance', + merchant_pub: 'abcdef', + payment_targets: [] + }] +} + +export const WithFiveInstance = (a: any) => <View {...a} />; +WithFiveInstance.args = { + instances: [{ + id: 'first', + name: 'the first instance', + merchant_pub: 'abcdefgh', + payment_targets: ['asd'] + }, { + id: 'second', + name: 'the second instance', + merchant_pub: 'zxczxcz', + payment_targets: ['asd'] + }, { + id: 'third', + name: 'the third instance', + merchant_pub: 'QWEQWEWQE', + payment_targets: ['asd'] + }, { + id: 'other', + name: 'the other instance', + merchant_pub: 'FHJHGJGHJ', + payment_targets: ['asd'] + }, { + id: 'another', + name: 'the another instance', + merchant_pub: 'abcd3423423efgh', + payment_targets: ['asd'] + }, { + id: 'last', + name: 'last instance', + merchant_pub: 'zxcvvbnm', + payment_targets: ['pay-to', 'asd'] + }] +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx new file mode 100644 index 000000000..a77a5a1bf --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -0,0 +1,80 @@ +/* + 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 as CardTableActive } from './TableActive'; +import { useState } from 'preact/hooks'; +import { Translate, useTranslator } from "../../../i18n"; + +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, onPurge, onUpdate, setInstanceName, selected }: Props): VNode { + const [show, setShow] = useState<"active" | "deleted" | null>("active"); + const showIsActive = show === 'active' ? "is-active" : '' + const showIsDeleted = show === 'deleted' ? "is-active" : '' + const showAll = show === null ? "is-active" : '' + const i18n = useTranslator() + + const showingInstances = showIsDeleted ? + instances.filter(i => i.deleted) : (showIsActive ? + instances.filter(i => !i.deleted) : + instances) + + return <div id="app"> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-two-thirds"> + <div class="tabs" style={{ overflow: 'inherit' }}> + <ul> + <li class={showIsActive}> + <div class="has-tooltip-right" data-tooltip={i18n`Only show active instances`}> + <a onClick={() => setShow("active")}><Translate>Active</Translate></a> + </div> + </li> + <li class={showIsDeleted}> + <div class="has-tooltip-right" data-tooltip={i18n`Only show deleted instances`}> + <a onClick={() => setShow("deleted")}><Translate>Deleted</Translate></a> + </div> + </li> + <li class={showAll}> + <div class="has-tooltip-right" data-tooltip={i18n`Show all instances`}> + <a onClick={() => setShow(null)}><Translate>All</Translate></a> + </div> + </li> + </ul> + </div> + </div> + </div> + <CardTableActive instances={showingInstances} onDelete={onDelete} onPurge={onPurge} setInstanceName={setInstanceName} onUpdate={onUpdate} selected={selected} onCreate={onCreate} /> + </section> + + </div > +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx new file mode 100644 index 000000000..c5609fd10 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -0,0 +1,126 @@ +/* + 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 { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; +import { NotificationCard } from "../../../components/menu"; +import { DeleteModal, PurgeModal } from "../../../components/modal"; +import { MerchantBackend } from "../../../declaration"; +import { HttpError } from "../../../hooks/backend"; +import { useAdminAPI, useBackendInstances } from "../../../hooks/instance"; +import { useTranslator } from "../../../i18n"; +import { Notification } from "../../../utils/types"; +import { View } from "./View"; + +interface Props { + onCreate: () => void; + onUpdate: (id: string) => void; + instances: MerchantBackend.Instances.Instance[]; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (error: HttpError) => VNode; + setInstanceName: (s: string) => void; +} + +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 [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(); + if (result.loading) return <Loading />; + 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} + /> + {deleting && ( + <DeleteModal + element={deleting} + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteInstance(deleting.id); + // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + setNotif({ + message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n`Failed to delete instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + // 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 "${purging.name}" (ID: ${purging.id}) has been disabled`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n`Failed to purge instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setPurging(null); + }} + /> + )} + </Fragment> + ); +} |