aboutsummaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/paths/admin/list
diff options
context:
space:
mode:
authorNic Eigel <nic@eigel.ch>2024-01-14 15:18:12 +0100
committerNic Eigel <nic@eigel.ch>2024-01-14 15:18:12 +0100
commit7a201c3b885c5d23bf0fd0f3da32379a49b30c38 (patch)
tree13f35c4761087b0e6adce39153be5ca03c5c846b /packages/auditor-backoffice-ui/src/paths/admin/list
parent2be9142ac5f944fbc03186b22ca67e6020187c92 (diff)
downloadwallet-core-7a201c3b885c5d23bf0fd0f3da32379a49b30c38.tar.xz
adding auditor-backoffice-ui
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths/admin/list')
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx287
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx90
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx110
-rw-r--r--packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx140
4 files changed, 627 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
new file mode 100644
index 000000000..885a351d2
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -0,0 +1,287 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+
+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 } = useTranslationContext();
+
+ 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>
+ <i18n.Translate>Instances</i18n.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"),
+ )
+ }
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ </div>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`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 {
+ const { i18n } = useTranslationContext();
+ 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>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Name</i18n.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)}
+ >
+ <i18n.Translate>Edit</i18n.Translate>
+ </button>
+ {!i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal is-outlined"
+ type="button"
+ onClick={(): void => onDelete(i)}
+ >
+ <i18n.Translate>Delete</i18n.Translate>
+ </button>
+ )}
+ {i.deleted && (
+ <button
+ class="button is-small is-danger jb-modal"
+ type="button"
+ onClick={(): void => onPurge(i)}
+ >
+ <i18n.Translate>Purge</i18n.Translate>
+ </button>
+ )}
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+}
+
+function EmptyTable(): VNode {
+ const { i18n } = useTranslationContext();
+ 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>
+ <i18n.Translate>
+ There is no instances yet, add more pressing the + sign
+ </i18n.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(
+ instances: MerchantBackend.Instances.Instance[],
+ selected: string[],
+ action: "DELETE",
+): Actions[] {
+ return selected
+ .map((id) => instances.find((i) => i.id === id))
+ .filter(notEmpty)
+ .map((id) => ({ element: id, type: action }));
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
new file mode 100644
index 000000000..e0f5d5430
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.stories.tsx
@@ -0,0 +1,90 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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.js";
+
+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/auditor-backoffice-ui/src/paths/admin/list/View.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
new file mode 100644
index 000000000..b59112338
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/View.tsx
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { MerchantBackend } from "../../../declaration.js";
+import { CardTable as CardTableActive } from "./TableActive.js";
+
+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 } = useTranslationContext();
+
+ const showingInstances = showIsDeleted
+ ? instances.filter((i) => i.deleted)
+ : showIsActive
+ ? instances.filter((i) => !i.deleted)
+ : instances;
+
+ return (
+ <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.str`Only show active instances`}
+ >
+ <a onClick={() => setShow("active")}>
+ <i18n.Translate>Active</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showIsDeleted}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Only show deleted instances`}
+ >
+ <a onClick={() => setShow("deleted")}>
+ <i18n.Translate>Deleted</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={showAll}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`Show all instances`}
+ >
+ <a onClick={() => setShow(null)}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <CardTableActive
+ instances={showingInstances}
+ onDelete={onDelete}
+ onPurge={onPurge}
+ setInstanceName={setInstanceName}
+ onUpdate={onUpdate}
+ selected={selected}
+ onCreate={onCreate}
+ />
+ </section>
+ );
+}
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
new file mode 100644
index 000000000..2f839291b
--- /dev/null
+++ b/packages/auditor-backoffice-ui/src/paths/admin/list/index.tsx
@@ -0,0 +1,140 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 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 {
+ ErrorType,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
+import { NotificationCard } from "../../../components/menu/index.js";
+import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
+import { Notification } from "../../../utils/types.js";
+import { View } from "./View.js";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+
+interface Props {
+ onCreate: () => void;
+ onUpdate: (id: string) => void;
+ instances: MerchantBackend.Instances.Instance[];
+ onUnauthorized: () => VNode;
+ onNotFound: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => 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 } = useTranslationContext();
+
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.Unauthorized
+ )
+ return onUnauthorized();
+ if (
+ result.type === ErrorType.CLIENT &&
+ result.status === HttpStatusCode.NotFound
+ )
+ return onNotFound();
+ 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.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`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.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`,
+ type: "SUCCESS",
+ });
+ } catch (error) {
+ setNotif({
+ message: i18n.str`Failed to purge instance`,
+ type: "ERROR",
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setPurging(null);
+ }}
+ />
+ )}
+ </Fragment>
+ );
+}