webhook api

This commit is contained in:
Sebastian 2023-01-27 15:08:03 -03:00
parent 1b2b5d62de
commit eebb85bef4
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
16 changed files with 1225 additions and 1 deletions

View File

@ -51,6 +51,9 @@ import TemplateCreatePage from "./paths/instance/templates/create/index.js";
import TemplateUsePage from "./paths/instance/templates/use/index.js";
import TemplateListPage from "./paths/instance/templates/list/index.js";
import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
import WebhookListPage from "./paths/instance/webhooks/list/index.js";
import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
import TransferListPage from "./paths/instance/transfers/list/index.js";
import InstanceUpdatePage, {
@ -87,6 +90,10 @@ export enum InstancePaths {
templates_update = "/templates/:tid/update",
templates_new = "/templates/new",
templates_use = "/templates/:tid/use",
webhooks_list = "/webhooks",
webhooks_update = "/webhooks/:tid/update",
webhooks_new = "/webhooks/new",
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
@ -389,6 +396,45 @@ export function InstanceRoutes({
route(InstancePaths.transfers_list);
}}
/>
{/**
* Webhooks pages
*/}
<Route
path={InstancePaths.webhooks_list}
component={WebhookListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
onCreate={() => {
route(InstancePaths.webhooks_new);
}}
onSelect={(id: string) => {
route(InstancePaths.webhooks_update.replace(":tid", id));
}}
/>
<Route
path={InstancePaths.webhooks_update}
component={WebhookUpdatePage}
onConfirm={() => {
route(InstancePaths.webhooks_list);
}}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.webhooks_list);
}}
/>
<Route
path={InstancePaths.webhooks_new}
component={WebhookCreatePage}
onConfirm={() => {
route(InstancePaths.webhooks_list);
}}
onBack={() => {
route(InstancePaths.webhooks_list);
}}
/>
{/**
* Templates pages
*/}

View File

@ -140,6 +140,16 @@ export function Sidebar({
<span class="menu-item-label">Reserves</span>
</a>
</li>
<li>
<a href={"/webhooks"} class="has-icon">
<span class="icon">
<i class="mdi mdi-newspaper" />
</span>
<span class="menu-item-label">
<i18n.Translate>Webhooks</i18n.Translate>
</span>
</a>
</li>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">

View File

@ -1360,6 +1360,82 @@ export namespace MerchantBackend {
}
}
namespace Webhooks {
interface WebhookAddDetails {
// Webhook ID to use.
webhook_id: string;
// The event of the webhook: why the webhook is used.
event_type: string;
// URL of the webhook where the customer will be redirected.
url: string;
// Method used by the webhook
http_method: string;
// Header template of the webhook
header_template?: string;
// Body template by the webhook
body_template?: string;
}
interface WebhookPatchDetails {
// The event of the webhook: why the webhook is used.
event_type: string;
// URL of the webhook where the customer will be redirected.
url: string;
// Method used by the webhook
http_method: string;
// Header template of the webhook
header_template?: string;
// Body template by the webhook
body_template?: string;
}
interface WebhookSummaryResponse {
// List of webhooks that are present in our backend.
webhooks: WebhookEntry[];
}
interface WebhookEntry {
// Webhook identifier, as found in the webhook.
webhook_id: string;
// The event of the webhook: why the webhook is used.
event_type: string;
}
interface WebhookDetails {
// The event of the webhook: why the webhook is used.
event_type: string;
// URL of the webhook where the customer will be redirected.
url: string;
// Method used by the webhook
http_method: string;
// Header template of the webhook
header_template?: string;
// Body template by the webhook
body_template?: string;
}
}
interface ContractTerms {
// Human-readable description of the whole purchase
summary: string;

View File

@ -115,6 +115,11 @@ interface useBackendInstanceRequestType {
position?: string,
delta?: number,
) => Promise<HttpResponseOk<T>>;
webhookFetcher: <T>(
path: string,
position?: string,
delta?: number,
) => Promise<HttpResponseOk<T>>;
}
interface useBackendBaseRequestType {
request: <T>(
@ -274,6 +279,23 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
[backend, token],
);
const webhookFetcher = useCallback(
function webhookFetcherImpl<T>(
path: string,
position?: string,
delta?: number,
): Promise<HttpResponseOk<T>> {
const params: any = {};
if (delta !== undefined) {
params.limit = delta;
}
if (position !== undefined) params.offset = position;
return requestHandler<T>(backend, path, { params, token });
},
[backend, token],
);
return {
request,
fetcher,
@ -283,5 +305,6 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
tipsDetailFetcher,
transferFetcher,
templateFetcher,
webhookFetcher,
};
}

View File

@ -0,0 +1,165 @@
/*
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/>
*/
import { MerchantBackend } from "../declaration.js";
import { useMatchMutate, useBackendInstanceRequest } from "./backend.js";
import useSWR from "swr";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
import { useEffect, useState } from "preact/hooks";
import {
HttpError,
HttpResponse,
HttpResponseOk,
HttpResponsePaginated,
} from "../utils/request.js";
export function useWebhookAPI(): WebhookAPI {
const mutateAll = useMatchMutate();
const { request } = useBackendInstanceRequest();
const createWebhook = async (
data: MerchantBackend.Webhooks.WebhookAddDetails,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`/private/webhooks`, {
method: "POST",
data,
});
await mutateAll(/.*private\/webhooks.*/);
return res;
};
const updateWebhook = async (
webhookId: string,
data: MerchantBackend.Webhooks.WebhookPatchDetails,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`/private/webhooks/${webhookId}`, {
method: "PATCH",
data,
});
await mutateAll(/.*private\/webhooks.*/);
return res;
};
const deleteWebhook = async (
webhookId: string,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`/private/webhooks/${webhookId}`, {
method: "DELETE",
});
await mutateAll(/.*private\/webhooks.*/);
return res;
};
return { createWebhook, updateWebhook, deleteWebhook };
}
export interface WebhookAPI {
createWebhook: (
data: MerchantBackend.Webhooks.WebhookAddDetails,
) => Promise<HttpResponseOk<void>>;
updateWebhook: (
id: string,
data: MerchantBackend.Webhooks.WebhookPatchDetails,
) => Promise<HttpResponseOk<void>>;
deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>;
}
export interface InstanceWebhookFilter {
//FIXME: add filter to the webhook list
position?: string;
}
export function useInstanceWebhooks(
args?: InstanceWebhookFilter,
updatePosition?: (id: string) => void,
): HttpResponsePaginated<MerchantBackend.Webhooks.WebhookSummaryResponse> {
const { webhookFetcher } = useBackendInstanceRequest();
const [pageAfter, setPageAfter] = useState(1);
const totalAfter = pageAfter * PAGE_SIZE;
const {
data: afterData,
error: afterError,
isValidating: loadingAfter,
} = useSWR<
HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>,
HttpError
>([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher);
const [lastAfter, setLastAfter] = useState<
HttpResponse<MerchantBackend.Webhooks.WebhookSummaryResponse>
>({ loading: true });
useEffect(() => {
if (afterData) setLastAfter(afterData);
}, [afterData]);
if (afterError) return afterError;
const isReachingEnd =
afterData && afterData.data.webhooks.length < totalAfter;
const isReachingStart = false;
const pagination = {
isReachingEnd,
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
if (afterData.data.webhooks.length < MAX_RESULT_SIZE) {
setPageAfter(pageAfter + 1);
} else {
const from = `${afterData.data.webhooks[afterData.data.webhooks.length - 1]
.webhook_id
}`;
if (from && updatePosition) updatePosition(from);
}
},
loadMorePrev: () => {
return
},
};
const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks;
if (loadingAfter)
return { loading: true, data: { webhooks } };
if (afterData) {
return { ok: true, data: { webhooks }, ...pagination };
}
return { loading: true };
}
export function useWebhookDetails(
webhookId: string,
): HttpResponse<MerchantBackend.Webhooks.WebhookDetails> {
const { webhookFetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>,
HttpError
>([`/private/webhooks/${webhookId}`], webhookFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
});
if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
if (error) return error;
return { loading: true };
}

View File

@ -180,7 +180,7 @@ function Table({
</button>
<button
class="button is-info is-small has-tooltip-left"
data-tooltip={i18n.str`delete selected templates from the database`}
data-tooltip={i18n.str`use template to create new order`}
onClick={() => onNewOrder(i)}
>
New order

View File

@ -0,0 +1,28 @@
/*
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 { CreatePage as TestedComponent } from "./CreatePage.js";
export default {
title: "Pages/Webhooks/Create",
component: TestedComponent,
};

View File

@ -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 { useTranslationContext } from "@gnu-taler/web-util/lib/index.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 { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
interface Props {
onCreate: (d: Entity) => Promise<void>;
onBack?: () => void;
}
const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>({});
const errors: FormErrors<Entity> = {
webhook_id: !state.webhook_id ? i18n.str`required` : undefined,
event_type: !state.event_type ? i18n.str`required` : undefined,
http_method: !state.http_method
? i18n.str`required`
: !validMethod.includes(state.http_method)
? i18n.str`should be one of "${validMethod.join(", ")}"`
: undefined,
url: !state.url ? i18n.str`required` : undefined,
};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
const submitForm = () => {
if (hasErrors) return Promise.reject();
return onCreate(state as any);
};
return (
<div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
<FormProvider
object={state}
valueHandler={setState}
errors={errors}
>
<Input<Entity>
name="webhook_id"
label={i18n.str`ID`}
tooltip={i18n.str`Webhook ID to use`}
/>
<Input<Entity>
name="event_type"
label={i18n.str`Event`}
tooltip={i18n.str`The event of the webhook: why the webhook is used`}
/>
<Input<Entity>
name="http_method"
label={i18n.str`Method`}
tooltip={i18n.str`Method used by the webhook`}
/>
<Input<Entity>
name="url"
label={i18n.str`URL`}
tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
/>
<Input<Entity>
name="header_template"
label={i18n.str`Header`}
inputType="multiline"
tooltip={i18n.str`Header template of the webhook`}
/>
<Input<Entity>
name="body_template"
inputType="multiline"
label={i18n.str`Body`}
tooltip={i18n.str`Body template by the webhook`}
/>
</FormProvider>
<div class="buttons is-right mt-5">
{onBack && (
<button class="button" onClick={onBack}>
<i18n.Translate>Cancel</i18n.Translate>
</button>
)}
<AsyncButton
disabled={hasErrors}
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
: "confirm operation"
}
onClick={submitForm}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</div>
</div>
<div class="column" />
</div>
</section>
</div>
);
}

View File

@ -0,0 +1,61 @@
/*
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/lib/index.browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import { useWebhookAPI } from "../../../../hooks/webhooks.js";
import { Notification } from "../../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
export type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
interface Props {
onBack?: () => void;
onConfirm: () => void;
}
export default function CreateWebhook({ onConfirm, onBack }: Props): VNode {
const { createWebhook } = useWebhookAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
return (
<>
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => {
return createWebhook(request)
.then(() => onConfirm())
.catch((error) => {
setNotif({
message: i18n.str`could not inform template`,
type: "ERROR",
description: error.message,
});
});
}}
/>
</>
);
}

View File

@ -0,0 +1,28 @@
/*
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 { FunctionalComponent, h } from "preact";
import { ListPage as TestedComponent } from "./ListPage.js";
export default {
title: "Pages/Templates/List",
component: TestedComponent,
};

View File

@ -0,0 +1,64 @@
/*
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 { MerchantBackend } from "../../../../declaration.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { CardTable } from "./Table.js";
export interface Props {
webhooks: MerchantBackend.Webhooks.WebhookEntry[];
onLoadMoreBefore?: () => void;
onLoadMoreAfter?: () => void;
onCreate: () => void;
onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void;
}
export function ListPage({
webhooks,
onCreate,
onDelete,
onSelect,
onLoadMoreBefore,
onLoadMoreAfter,
}: Props): VNode {
const form = { payto_uri: "" };
const { i18n } = useTranslationContext();
return (
<section class="section is-main-section">
<CardTable
webhooks={webhooks.map((o) => ({
...o,
id: String(o.webhook_id),
}))}
onCreate={onCreate}
onDelete={onDelete}
onSelect={onSelect}
onLoadMoreBefore={onLoadMoreBefore}
hasMoreBefore={!onLoadMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
hasMoreAfter={!onLoadMoreAfter}
/>
</section>
);
}

View File

@ -0,0 +1,225 @@
/*
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/lib/index.browser";
import { h, VNode } from "preact";
import { StateUpdater, useState } from "preact/hooks";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Webhooks.WebhookEntry;
interface Props {
webhooks: Entity[];
onDelete: (e: Entity) => void;
onSelect: (e: Entity) => void;
onCreate: () => void;
onLoadMoreBefore?: () => void;
hasMoreBefore?: boolean;
hasMoreAfter?: boolean;
onLoadMoreAfter?: () => void;
}
export function CardTable({
webhooks,
onCreate,
onDelete,
onSelect,
onLoadMoreAfter,
onLoadMoreBefore,
hasMoreAfter,
hasMoreBefore,
}: Props): VNode {
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
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-newspaper" />
</span>
<i18n.Translate>Webhooks</i18n.Translate>
</p>
<div class="card-header-icon" aria-label="more options">
<span
class="has-tooltip-left"
data-tooltip={i18n.str`add new webhooks`}
>
<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">
{webhooks.length > 0 ? (
<Table
instances={webhooks}
onDelete={onDelete}
onSelect={onSelect}
onNewOrder={(d) => {
console.log("test", d);
}}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
hasMoreAfter={hasMoreAfter}
hasMoreBefore={hasMoreBefore}
/>
) : (
<EmptyTable />
)}
</div>
</div>
</div>
</div>
);
}
interface TableProps {
rowSelection: string[];
instances: Entity[];
onDelete: (e: Entity) => void;
onNewOrder: (e: Entity) => void;
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
hasMoreBefore?: boolean;
hasMoreAfter?: boolean;
onLoadMoreAfter?: () => 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,
onLoadMoreAfter,
onDelete,
onNewOrder,
onSelect,
onLoadMoreBefore,
hasMoreAfter,
hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
return (
<div class="table-container">
{onLoadMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks before the first one`}
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore}
>
<i18n.Translate>load newer webhooks</i18n.Translate>
</button>
)}
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>
<i18n.Translate>ID</i18n.Translate>
</th>
<th>
<i18n.Translate>Event type</i18n.Translate>
</th>
<th />
</tr>
</thead>
<tbody>
{instances.map((i) => {
return (
<tr key={i.webhook_id}>
<td
onClick={(): void => onSelect(i)}
style={{ cursor: "pointer" }}
>
{i.webhook_id}
</td>
<td
onClick={(): void => onSelect(i)}
style={{ cursor: "pointer" }}
>
{i.event_type}
</td>
<td class="is-actions-cell right-sticky">
<div class="buttons is-right">
<button
class="button is-danger is-small has-tooltip-left"
data-tooltip={i18n.str`delete selected webhook from the database`}
onClick={() => onDelete(i)}
>
Delete
</button>
{/* <button
class="button is-info is-small has-tooltip-left"
data-tooltip={i18n.str`test webhook`}
onClick={() => onNewOrder(i)}
>
Test
</button> */}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{onLoadMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks after the last one`}
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter}
>
<i18n.Translate>load older webhooks</i18n.Translate>
</button>
)}
</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 webhooks yet, add more pressing the + sign
</i18n.Translate>
</p>
</div>
);
}

View File

@ -0,0 +1,95 @@
/*
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/lib/index.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 { MerchantBackend } from "../../../../declaration.js";
import {
useInstanceWebhooks,
useWebhookAPI,
} from "../../../../hooks/webhooks.js";
import { HttpError } from "../../../../utils/request.js";
import { Notification } from "../../../../utils/types.js";
import { ListPage } from "./ListPage.js";
interface Props {
onUnauthorized: () => VNode;
onLoadError: (error: HttpError) => VNode;
onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
}
export default function ListWebhooks({
onUnauthorized,
onLoadError,
onCreate,
onSelect,
onNotFound,
}: Props): VNode {
const [position, setPosition] = useState<string | undefined>(undefined);
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { deleteWebhook } = useWebhookAPI();
const result = useInstanceWebhooks({ position }, (id) => setPosition(id));
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} />
<ListPage
webhooks={result.data.webhooks}
onLoadMoreBefore={
result.isReachingStart ? result.loadMorePrev : undefined
}
onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
onCreate={onCreate}
onSelect={(e) => {
onSelect(e.webhook_id);
}}
onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) =>
deleteWebhook(e.webhook_id)
.then(() =>
setNotif({
message: i18n.str`webhook delete successfully`,
type: "SUCCESS",
}),
)
.catch((error) =>
setNotif({
message: i18n.str`could not delete the webhook`,
type: "ERROR",
description: error.message,
}),
)
}
/>
</Fragment>
);
}

View File

@ -0,0 +1,32 @@
/*
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 { UpdatePage as TestedComponent } from "./UpdatePage.js";
export default {
title: "Pages/Templates/Update",
component: TestedComponent,
argTypes: {
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};

View File

@ -0,0 +1,146 @@
/*
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/lib/index.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 { Input } from "../../../../components/form/Input.js";
import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
interface Props {
onUpdate: (d: Entity) => Promise<void>;
onBack?: () => void;
webhook: Entity;
}
const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"];
export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>(webhook);
const errors: FormErrors<Entity> = {
event_type: !state.event_type ? i18n.str`required` : undefined,
http_method: !state.http_method
? i18n.str`required`
: !validMethod.includes(state.http_method)
? i18n.str`should be one of "${validMethod.join(", ")}"`
: undefined,
url: !state.url ? i18n.str`required` : undefined,
};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
const submitForm = () => {
if (hasErrors) return Promise.reject();
return onUpdate(state as any);
};
return (
<div>
<section class="section">
<section class="hero is-hero-bar">
<div class="hero-body">
<div class="level">
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
Webhook: <b>{webhook.id}</b>
</span>
</div>
</div>
</div>
</div>
</section>
<hr />
<section class="section is-main-section">
<div class="columns">
<div class="column is-four-fifths">
<FormProvider
object={state}
valueHandler={setState}
errors={errors}
>
<Input<Entity>
name="event_type"
label={i18n.str`Event`}
tooltip={i18n.str`The event of the webhook: why the webhook is used`}
/>
<Input<Entity>
name="http_method"
label={i18n.str`Method`}
tooltip={i18n.str`Method used by the webhook`}
/>
<Input<Entity>
name="url"
label={i18n.str`URL`}
tooltip={i18n.str`URL of the webhook where the customer will be redirected`}
/>
<Input<Entity>
name="header_template"
label={i18n.str`Header`}
inputType="multiline"
tooltip={i18n.str`Header template of the webhook`}
/>
<Input<Entity>
name="body_template"
inputType="multiline"
label={i18n.str`Body`}
tooltip={i18n.str`Body template by the webhook`}
/>
</FormProvider>
<div class="buttons is-right mt-5">
{onBack && (
<button class="button" onClick={onBack}>
<i18n.Translate>Cancel</i18n.Translate>
</button>
)}
<AsyncButton
disabled={hasErrors}
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
: "confirm operation"
}
onClick={submitForm}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</div>
</div>
</div>
</section>
</section>
</div>
);
}

View File

@ -0,0 +1,85 @@
/*
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/lib/index.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 { MerchantBackend, WithId } from "../../../../declaration.js";
import {
useWebhookAPI,
useWebhookDetails,
} from "../../../../hooks/webhooks.js";
import { HttpError } from "../../../../utils/request.js";
import { Notification } from "../../../../utils/types.js";
import { UpdatePage } from "./UpdatePage.js";
export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
interface Props {
onBack?: () => void;
onConfirm: () => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
onLoadError: (e: HttpError) => VNode;
tid: string;
}
export default function UpdateWebhook({
tid,
onConfirm,
onBack,
onUnauthorized,
onNotFound,
onLoadError,
}: Props): VNode {
const { updateWebhook } = useWebhookAPI();
const result = useWebhookDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
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} />
<UpdatePage
webhook={{ ...result.data, id: tid }}
onBack={onBack}
onUpdate={(data) => {
return updateWebhook(tid, data)
.then(onConfirm)
.catch((error) => {
setNotif({
message: i18n.str`could not update template`,
type: "ERROR",
description: error.message,
});
});
}}
/>
</Fragment>
);
}