diff options
author | Nic Eigel <nic@eigel.ch> | 2024-01-14 15:18:12 +0100 |
---|---|---|
committer | Nic Eigel <nic@eigel.ch> | 2024-01-14 15:18:12 +0100 |
commit | 7a201c3b885c5d23bf0fd0f3da32379a49b30c38 (patch) | |
tree | 13f35c4761087b0e6adce39153be5ca03c5c846b /packages/auditor-backoffice-ui/src/paths/admin | |
parent | 2be9142ac5f944fbc03186b22ca67e6020187c92 (diff) | |
download | wallet-core-7a201c3b885c5d23bf0fd0f3da32379a49b30c38.tar.xz |
adding auditor-backoffice-ui
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths/admin')
10 files changed, 1167 insertions, 0 deletions
diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx new file mode 100644 index 000000000..91b6b4b56 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/Create.stories.tsx @@ -0,0 +1,57 @@ +/* + 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, VNode, FunctionalComponent } from "preact"; +import { ConfigContextProvider } from "../../../context/config.js"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Instance/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => ( + <ConfigContextProvider + value={{ + currency: "ARS", + version: "1", + }} + > + <Component {...args} /> + </ConfigContextProvider> + ); + r.args = props; + return r; +} + +export const Example = createExample(TestedComponent, {}); +// export const Example = (a: any): VNode => <CreatePage {...a} />; +// Example.args = { +// isLoading: false +// } diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx new file mode 100644 index 000000000..d13b7e929 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -0,0 +1,257 @@ +/* + 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 { AsyncButton } from "../../../components/exception/AsyncButton.js"; +import { + FormErrors, + FormProvider, +} from "../../../components/form/FormProvider.js"; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; +import { MerchantBackend } from "../../../declaration.js"; +import { INSTANCE_ID_REGEX } from "../../../utils/constants.js"; +import { undefinedIfEmpty } from "../../../utils/table.js"; +import { SetTokenNewInstanceModal } from "../../../components/modal/index.js"; +import { Duration } from "@gnu-taler/taler-util"; + +export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceConfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { + auth_token?: string; + default_pay_delay: Duration, + default_wire_transfer_delay: Duration, +}; + +interface Props { + onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; + onBack?: () => void; + forceId?: string; +} + +function with_defaults(id?: string): Partial<Entity> { + return { + id, + // accounts: [], + user_type: "business", + use_stefan: true, + default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours + default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days + }; +} + +export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { + const [value, valueHandler] = useState(with_defaults(forceId)); + const [isTokenSet, updateIsTokenSet] = useState<boolean>(false); + const [isTokenDialogActive, updateIsTokenDialogActive] = + useState<boolean>(false); + + const { i18n } = useTranslationContext(); + + const errors: FormErrors<Entity> = { + id: !value.id + ? i18n.str`required` + : !INSTANCE_ID_REGEX.test(value.id) + ? i18n.str`is not valid` + : undefined, + name: !value.name ? i18n.str`required` : undefined, + + user_type: !value.user_type + ? i18n.str`required` + : value.user_type !== "business" && value.user_type !== "individual" + ? i18n.str`should be business or individual` + : undefined, + // accounts: + // !value.accounts || !value.accounts.length + // ? i18n.str`required` + // : undefinedIfEmpty( + // value.accounts.map((p) => { + // return !PAYTO_REGEX.test(p.payto_uri) + // ? i18n.str`is not valid` + // : undefined; + // }), + // ), + default_pay_delay: !value.default_pay_delay + ? i18n.str`required` + : !!value.default_wire_transfer_delay && + value.default_wire_transfer_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? + i18n.str`pay delay can't be greater than wire transfer delay` : undefined, + default_wire_transfer_delay: !value.default_wire_transfer_delay + ? i18n.str`required` + : undefined, + address: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n.str`max 7 lines` + : undefined, + }), + jurisdiction: undefinedIfEmpty({ + address_lines: + value.address?.address_lines && value.address?.address_lines.length > 7 + ? i18n.str`max 7 lines` + : undefined, + }), + }; + + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); + + const submit = (): Promise<void> => { + // use conversion instead of this + const newToken = value.auth_token; + value.auth_token = undefined; + value.auth = newToken === null || newToken === undefined + ? { method: "external" } + : { method: "token", token: `secret-token:${newToken}` }; + if (!value.address) value.address = {}; + if (!value.jurisdiction) value.jurisdiction = {}; + // remove above use conversion + // schema.validateSync(value, { abortEarly: false }) + value.default_pay_delay = Duration.toTalerProtocolDuration(value.default_pay_delay!) as any + value.default_wire_transfer_delay = Duration.toTalerProtocolDuration(value.default_wire_transfer_delay!) as any + // delete value.default_pay_delay; + // delete value.default_wire_transfer_delay; + + return onCreate(value as any as MerchantBackend.Instances.InstanceConfigurationMessage); + }; + + function updateToken(token: string | null) { + valueHandler((old) => ({ + ...old, + auth_token: token === null ? undefined : token, + })); + } + + return ( + <div> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + {isTokenDialogActive && ( + <SetTokenNewInstanceModal + onCancel={() => { + updateIsTokenDialogActive(false); + updateIsTokenSet(false); + }} + onClear={() => { + updateToken(null); + updateIsTokenDialogActive(false); + updateIsTokenSet(true); + }} + onConfirm={(newToken) => { + updateToken(newToken); + updateIsTokenDialogActive(false); + updateIsTokenSet(true); + }} + /> + )} + </div> + <div class="column" /> + </div> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider<Entity> + errors={errors} + object={value} + valueHandler={valueHandler} + > + <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} /> + </FormProvider> + + <div class="level"> + <div class="level-item has-text-centered"> + <h1 class="title"> + <button + class={ + !isTokenSet + ? "button is-danger has-tooltip-bottom" + : !value.auth_token + ? "button has-tooltip-bottom" + : "button is-info has-tooltip-bottom" + } + data-tooltip={i18n.str`change authorization configuration`} + onClick={() => updateIsTokenDialogActive(true)} + > + <div class="icon is-centered"> + <i class="mdi mdi-lock-reset" /> + </div> + <span> + <i18n.Translate>Set access token</i18n.Translate> + </span> + </button> + </h1> + </div> + </div> + <div class="level"> + <div class="level-item has-text-centered"> + {!isTokenSet ? ( + <p class="is-size-6"> + <i18n.Translate> + Access token is not yet configured. This instance can't be + created. + </i18n.Translate> + </p> + ) : value.auth_token === undefined ? ( + <p class="is-size-6"> + <i18n.Translate> + No access token. Authorization must be handled externally. + </i18n.Translate> + </p> + ) : ( + <p class="is-size-6"> + <i18n.Translate> + Access token is set. Authorization is handled by the + merchant backend. + </i18n.Translate> + </p> + )} + </div> + </div> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <AsyncButton + onClick={submit} + disabled={hasErrors || !isTokenSet} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields and choose authorization method` + : "confirm operation" + } + > + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx new file mode 100644 index 000000000..c620c6482 --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx @@ -0,0 +1,74 @@ +/* + 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, VNode } from "preact"; +import { CreatedSuccessfully } from "../../../components/notifications/CreatedSuccessfully.js"; +import { Entity } from "./index.js"; + +export function InstanceCreatedSuccessfully({ + entity, + onConfirm, +}: { + entity: Entity; + onConfirm: () => void; +}): VNode { + return ( + <CreatedSuccessfully onConfirm={onConfirm}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.id} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Business Name</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.name} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Access token</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + {entity.auth.method === "external" && "external"} + {entity.auth.method === "token" && ( + <input class="input" readonly value={entity.auth.token} /> + )} + </p> + </div> + </div> + </div> + </CreatedSuccessfully> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx new file mode 100644 index 000000000..23f41ecff --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/index.tsx @@ -0,0 +1,82 @@ +/* + 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 { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../../components/menu/index.js"; +import { AccessToken, MerchantBackend } from "../../../declaration.js"; +import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js"; +import { Notification } from "../../../utils/types.js"; +import { CreatePage } from "./CreatePage.js"; +import { useCredentialsChecker } from "../../../hooks/backend.js"; +import { useBackendContext } from "../../../context/backend.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + forceId?: string; +} +export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; + +export default function Create({ onBack, onConfirm, forceId }: Props): VNode { + const { createInstance } = useAdminAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const { requestNewLoginToken } = useCredentialsChecker() + const { url: backendURL, updateToken } = useBackendContext() + + return ( + <Fragment> + <NotificationCard notification={notif} /> + + <CreatePage + onBack={onBack} + forceId={forceId} + onCreate={async ( + d: MerchantBackend.Instances.InstanceConfigurationMessage, + ) => { + try { + await createInstance(d) + if (d.auth.token) { + const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken) + if (resp.valid) { + const { token, expiration } = resp + updateToken({ token, expiration }); + } else { + updateToken(undefined) + } + } + onConfirm(); + } catch (ex) { + if (ex instanceof Error) { + setNotif({ + message: i18n.str`Failed to create instance`, + type: "ERROR", + description: ex.message, + }); + } else { + console.error(ex) + } + } + }} + /> + </Fragment> + ); +} diff --git a/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx new file mode 100644 index 000000000..0012f9b9b --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/create/stories.tsx @@ -0,0 +1,52 @@ +/* + 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, VNode, FunctionalComponent } from "preact"; +import { ConfigContextProvider } from "../../../context/config.js"; +import { CreatePage as TestedComponent } from "./CreatePage.js"; + +export default { + title: "Pages/Instance/Create", + component: TestedComponent, + argTypes: { + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, +}; + +function createExample<Props>( + Internal: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const component = (args: any) => ( + <ConfigContextProvider + value={{ + currency: "TESTKUDOS", + version: "1", + }} + > + <Internal {...(props as any)} /> + </ConfigContextProvider> + ); + return { component, props }; +} + +export const Example = createExample(TestedComponent, {}); diff --git a/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts new file mode 100644 index 000000000..fdae1a24d --- /dev/null +++ b/packages/auditor-backoffice-ui/src/paths/admin/index.stories.ts @@ -0,0 +1,18 @@ +/* + 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/> + */ + +// export * as list from "./list/stories.js"; +export * as create from "./create/stories.js"; 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> + ); +} |