commit ee3f5c57b3e226e8ae9e38bb896f9c3579804a8e
parent cfd0c0749714878539589ec7e4ac446076384e5b
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 2 Aug 2021 11:22:48 -0300
making purge instance feature
Diffstat:
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>;
}