Fix webhook overview.

This commit is contained in:
James Cole committed 2026-09-12 06:50:50 +02:00
1 parent 6c9ba9c81b
commit 70bd60808a
10 files changed
+326 -223

No files matched your search

@@ -117,7 +117,6 @@ final class MessageController extends Controller
/** @var WebhookMessageTransformer $transformer */
$transformer = app(WebhookMessageTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($message, $transformer, self::RESOURCE_KEY);
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
@@ -0,0 +1,38 @@
/*
* get.js
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {api} from "../../../boot/axios";
export default class Get {
show(id, params){
return api.get('/api/v1/webhooks/' + parseInt(id), {params: params});
}
messages(id, params){
return api.get('/api/v1/webhooks/' + parseInt(id) + '/messages', {params: params});
}
message(webhook, message, params){
return api.get('/api/v1/webhooks/' + parseInt(webhook) + '/messages/' + parseInt(message), {params: params});
}
attempts(webhook, message, params){
return api.get('/api/v1/webhooks/' + parseInt(webhook) + '/messages/' + parseInt(message) + '/attempts', {params: params});
}
}
@@ -0,0 +1,28 @@
/*
* post.js
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {api} from "../../../boot/axios";
export default class Post {
triggerTransaction(webhook, transaction) {
let url = '/api/v1/webhooks';
return api.post('./api/v1/webhooks/' + webhook + '/trigger-transaction/' + transaction, {});
}
}
@@ -0,0 +1,28 @@
/*
* post.js
* Copyright (c) 2023 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {api} from "../../../boot/axios";
export default class Put {
put(submission, params) {
let url = '/api/v1/webhooks/' + parseInt(params.id);
return api.put(url, submission);
}
}
@@ -19,20 +19,20 @@
*/
import i18next from "i18next";
import Get from "../../../api/configuration/get.js";
function loadDeliveries() {
let result = [];
return axios.get('./api/v1/configuration/webhook.deliveries').then((response) => {
return (new Get).getByName('webhook.deliveries').then(response => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
if(Object.hasOwn(response.data.data.value, key)) {
result.push(
{
id: key,
name: i18next.t('firefly.webhook_delivery_' + key),
}
);
}
result.push(
{
id: key,
name: i18next.t('firefly.webhook_delivery_' + key),
}
);
}
return result;
}).catch((error) => {
@@ -19,20 +19,20 @@
*/
import i18next from "i18next";
import Get from "../../../api/configuration/get.js";
function loadResponses() {
let result = [];
return axios.get('./api/v1/configuration/webhook.responses').then((response) => {
return (new Get).getByName('webhook.responses').then(response => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
if(Object.hasOwn(response.data.data.value, key)) {
result.push(
{
id: key,
name: i18next.t('firefly.webhook_response_' + key),
}
);
}
result.push(
{
id: key,
name: i18next.t('firefly.webhook_response_' + key),
}
);
}
return result;
}).catch((error) => {
@@ -19,21 +19,21 @@
*/
import i18next from "i18next";
import Get from '../../../api/configuration/get.js';
export function loadTriggers() {
let result = [];
return axios.get('./api/v1/configuration/webhook.triggers').then((response) => {
//console.log(response.data.data);
return (new Get).getByName('webhook.triggers').then(response => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
if(Object.hasOwn(response.data.data.value, key)) {
result.push(
{
id: key,
name: i18next.t('firefly.webhook_trigger_' + key),
}
);
}
result.push(
{
id: key,
name: i18next.t('firefly.webhook_trigger_' + key),
}
);
}
return result;
}).catch((error) => {
+17 -27
View File
@@ -23,6 +23,10 @@ import sidebar from '../../pages/shared/sidebar.js';
import dates from '../shared/dates.js';
import format from "date-fns/format";
import i18next from "i18next";
import Post from '../../api/model/webhook/post.js';
import Get from '../../api/model/webhook/get.js';
import Put from '../../api/model/webhook/put.js';
import Alpine from 'alpinejs'
window.enableDates = false;
@@ -70,17 +74,9 @@ let show = function () {
}
let journalId = parseInt(prompt('Enter a transaction ID'));
if (journalId !== null && journalId > 0 && journalId <= 16777216) {
// console.log('OK 1');
this.disabledTrigger = true;
// disable button. Add informative message.
//let button = $('#triggerButton');
//button.prop('disabled', true).addClass('disabled');
this.success_message = this.$t('firefly.webhook_was_triggered');
// TODO actually trigger the webhook.
axios.post('./api/v1/webhooks/' + this.id + '/trigger-transaction/' + journalId, {});
//button.prop('disabled', false).removeClass('disabled');
// console.log('OK 2');
this.success_message = i18next.t('firefly.webhook_was_triggered');
(new Post).triggerTransaction(this.id, journalId);
// set a time-outs.
this.loading = true;
@@ -96,8 +92,7 @@ let show = function () {
},
downloadWebhook: function () {
axios.get('./api/v1/webhooks/' + this.id).then(response => {
// console.log(response.data.data.attributes);
(new Get).show(this.id).then(response => {
this.edit_url = './webhooks/edit/' + this.id;
this.delete_url = './webhooks/delete/' + this.id;
this.title = response.data.data.attributes.title;
@@ -106,9 +101,6 @@ let show = function () {
this.triggers = response.data.data.attributes.triggers;
this.responses = response.data.data.attributes.responses;
this.deliveries = response.data.data.attributes.deliveries;
console.log(response.data.data);
this.active = response.data.data.attributes.active;
this.url = response.data.data.attributes.url;
}).catch(error => {
@@ -117,13 +109,13 @@ let show = function () {
},
downloadWebhookMessages: function () {
this.messages = [];
axios.get('./api/v1/webhooks/' + this.id + '/messages').then(response => {
(new Get).messages(this.id, {}).then(response => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
if (Object.hasOwn(response.data.data,i)) {
let current = response.data.data[i];
this.messages.push({
id: current.id,
created_at: format(new Date(current.attributes.created_at), this.$t('config.date_time_fns')),
created_at: format(new Date(current.attributes.created_at), i18next.t('config.date_time_fns')),
uuid: current.attributes.uuid,
success: current.attributes.sent && !current.attributes.errored,
message: current.attributes.message,
@@ -134,27 +126,26 @@ let show = function () {
});
},
resetSecret: function () {
axios.put('./api/v1/webhooks/' + this.id, {secret: 'anything'}).then(() => {
(new Put).put({secret: 'anything'}, {id: this.id}).then(() => {
this.downloadWebhook();
});
},
showWebhookMessage: function (id) {
axios.get('./api/v1/webhooks/' + this.id + '/messages/' + id).then(response => {
$('#messageModal').modal('show');
(new Get).message(this.id, id, {}).then(response => {
this.message_content = response.data.data.attributes.message;
});
},
showWebhookAttempts: function (id) {
console.log('showWebhookAttempts', id);
this.message_attempts = [];
axios.get('./api/v1/webhooks/' + this.id + '/messages/' + id + '/attempts').then(response => {
$('#attemptModal').modal('show');
(new Get).attempts(this.id, id, {}).then(response => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
if (Object.hasOwn(response.data.data,i)) {
let current = response.data.data[i];
this.message_attempts.push({
id: current.id,
created_at: format(new Date(current.attributes.created_at), this.$t('config.date_time_fns')),
created_at: format(new Date(current.attributes.created_at), i18next.t('config.date_time_fns')),
logs: current.attributes.logs,
status_code: current.attributes.status_code,
response: current.attributes.response,
@@ -163,7 +154,6 @@ let show = function () {
}
});
},
}
};
@@ -179,7 +169,7 @@ function loadPage(comps) {
Object.keys(comps).forEach(comp => {
let data = comps[comp]();
Alpine.data(comp, () => data);
console.log(comp);
//console.log(comp);
});
Alpine.start();
}
+74 -70
View File
@@ -23,50 +23,54 @@
declare(strict_types=1);
return [
'buttons' => 'Buttons',
'icon' => 'Icon',
'id' => 'ID',
'create_date' => 'Created at',
'responds_when' => 'Responds when',
'responds_with' => 'Responds with',
'primary_currency' => 'Primary currency',
'update_date' => 'Updated at',
'updated_at' => 'Updated at',
'balance_before' => 'Balance before',
'balance_after' => 'Balance after',
'name' => 'Name',
'role' => 'Role',
'currentBalance' => 'Current balance',
'linked_to_rules' => 'Relevant rules',
'active' => 'Is active?',
'percentage' => 'pct.',
'recurring_transaction' => 'Recurring transaction',
'next_due' => 'Next due',
'transaction_type' => 'Type',
'running_balance' => 'Running balance',
'lastActivity' => 'Last activity',
'balanceDiff' => 'Balance difference',
'other_meta_data' => 'Other meta data',
'invited_at' => 'Invited at',
'expires' => 'Invitation expires',
'invited_by' => 'Invited by',
'invite_link' => 'Invite link',
'account_type' => 'Account type',
'created_at' => 'Created at',
'account' => 'Account',
'external_url' => 'External URL',
'matchingAmount' => 'Amount',
'destination' => 'Destination',
'source' => 'Source',
'next_expected_match' => 'Next expected match',
'automatch' => 'Auto match?',
'drag_and_drop' => 'Drag and drop',
'number' => 'Account number',
'current_balance' => 'Current balance',
'last_activity' => 'Last activity',
'amount_due' => 'Amount due',
'balance_difference' => 'Balance difference',
'menu' => 'Menu',
'buttons' => 'Buttons',
'icon' => 'Icon',
'id' => 'ID',
'create_date' => 'Created at',
'date_time' => 'Date and time',
'uuid' => 'UUID',
'success' => 'Success?',
'more_details' => 'More details',
'responds_when' => 'Responds when',
'responds_with' => 'Responds with',
'primary_currency' => 'Primary currency',
'update_date' => 'Updated at',
'updated_at' => 'Updated at',
'balance_before' => 'Balance before',
'balance_after' => 'Balance after',
'name' => 'Name',
'role' => 'Role',
'currentBalance' => 'Current balance',
'linked_to_rules' => 'Relevant rules',
'active' => 'Is active?',
'percentage' => 'pct.',
'recurring_transaction' => 'Recurring transaction',
'next_due' => 'Next due',
'transaction_type' => 'Type',
'running_balance' => 'Running balance',
'lastActivity' => 'Last activity',
'balanceDiff' => 'Balance difference',
'other_meta_data' => 'Other meta data',
'invited_at' => 'Invited at',
'expires' => 'Invitation expires',
'invited_by' => 'Invited by',
'invite_link' => 'Invite link',
'account_type' => 'Account type',
'created_at' => 'Created at',
'account' => 'Account',
'external_url' => 'External URL',
'matchingAmount' => 'Amount',
'destination' => 'Destination',
'source' => 'Source',
'next_expected_match' => 'Next expected match',
'automatch' => 'Auto match?',
'drag_and_drop' => 'Drag and drop',
'number' => 'Account number',
'current_balance' => 'Current balance',
'last_activity' => 'Last activity',
'amount_due' => 'Amount due',
'balance_difference' => 'Balance difference',
'menu' => 'Menu',
'repeat_freq' => 'Repeats',
'description' => 'Description',
@@ -134,30 +138,30 @@ return [
'account_at_bunq' => 'Account with bunq',
'file_name' => 'File name',
'file_size' => 'File size',
'file_type' => 'File type',
'attached_to' => 'Attached to',
'file_exists' => 'File exists',
'spectre_bank' => 'Bank',
'spectre_last_use' => 'Last login',
'spectre_status' => 'Status',
'bunq_payment_id' => 'bunq payment ID',
'repetitions' => 'Repetitions',
'title' => 'Title',
'transaction_s' => 'Transaction(s)',
'field' => 'Field',
'value' => 'Value',
'interest' => 'Interest',
'interest_period' => 'Interest period',
'liability_type' => 'Type of liability',
'liability_direction' => 'Liability in/out',
'end_date' => 'End date',
'payment_info' => 'Payment information',
'expected_info' => 'Next expected transaction',
'start_date' => 'Start date',
'trigger' => 'Trigger',
'response' => 'Response',
'delivery' => 'Delivery',
'url' => 'URL',
'secret' => 'Secret',
'file_size' => 'File size',
'file_type' => 'File type',
'attached_to' => 'Attached to',
'file_exists' => 'File exists',
'spectre_bank' => 'Bank',
'spectre_last_use' => 'Last login',
'spectre_status' => 'Status',
'bunq_payment_id' => 'bunq payment ID',
'repetitions' => 'Repetitions',
'title' => 'Title',
'transaction_s' => 'Transaction(s)',
'field' => 'Field',
'value' => 'Value',
'interest' => 'Interest',
'interest_period' => 'Interest period',
'liability_type' => 'Type of liability',
'liability_direction' => 'Liability in/out',
'end_date' => 'End date',
'payment_info' => 'Payment information',
'expected_info' => 'Next expected transaction',
'start_date' => 'Start date',
'trigger' => 'Trigger',
'response' => 'Response',
'delivery' => 'Delivery',
'url' => 'URL',
'secret' => 'Secret',
];
+113 -97
View File
@@ -5,7 +5,8 @@
<div class="row">
<div class="col-lg-12">
<div class="alert alert-success alert-dismissible" role="alert">
<button class="close" data-bs-dismiss="alert" type="button" aria-label="{{ __('firefly.close') }}"><span aria-hidden="true">&times;</span></button>
<button class="close" data-bs-dismiss="alert" type="button"
aria-label="{{ __('firefly.close') }}"><span aria-hidden="true">&times;</span></button>
<strong>{{ __("firefly.flash_success") }}</strong> <span x-html="success_message"></span>
</div>
</div>
@@ -72,11 +73,15 @@
</div>
<div class="card-footer">
<div class="btn-group pull-right">
<a :href=edit_url class="btn btn-default"><em class="bi bi-pencil"></em> {{ __('firefly.edit') }}</a>
<a :href=edit_url class="btn btn-secondary"><em
class="bi bi-pencil"></em> {{ __('firefly.edit') }}</a>
<template x-if="active">
<a id="triggerButton" href="#" @click="submitTest" :class="disabledTrigger ? 'btn btn-default disabled ' : 'btn btn-default'"><em class="bi bi-lightning"></em>{{ __('list.trigger') }}</a>
<a id="triggerButton" href="#" @click="submitTest"
:class="disabledTrigger ? 'btn btn-secondary disabled ' : 'btn btn-secondary'"><em
class="bi bi-lightning"></em>{{ __('list.trigger') }}</a>
</template>
<a :href=delete_url class="btn btn-danger"><em class="bi bi-trash"></em> {{ __('firefly.delete') }}</a>
<a :href=delete_url class="btn btn-danger"><em
class="bi bi-trash"></em> {{ __('firefly.delete') }}</a>
</div>
</div>
</div>
@@ -89,29 +94,30 @@
<div class="card-body p-0">
<table class="table table-hover">
<tbody>
<tr>
<td style="width:40%;"><strong>{{ __('list.url') }}</strong></td>
<td><input type="text" readonly class="form-control" :value=url></td>
</tr>
<tr>
<td style="width:40%;">
<strong>{{ __('list.secret') }}</strong>
</td>
<td>
<template x-if="show_secret">
<div>
<em style="cursor:pointer" class="bi bi-eye" @click="toggleSecret"></em>
<code x-text="secret"></code>
</div>
</template>
<template x-if="!show_secret">
<div>
<em style="cursor:pointer" class="bi bi-eye-slash" @click="toggleSecret"></em>
<code>********</code>
</div>
</template>
</td>
</tr>
<tr>
<td style="width:40%;"><strong>{{ __('list.url') }}</strong></td>
<td><input type="text" readonly class="form-control" :value=url></td>
</tr>
<tr>
<td style="width:40%;">
<strong>{{ __('list.secret') }}</strong>
</td>
<td>
<template x-if="show_secret">
<div>
<em style="cursor:pointer" class="bi bi-eye" @click="toggleSecret"></em>
<code x-text="secret"></code>
</div>
</template>
<template x-if="!show_secret">
<div>
<em style="cursor:pointer" class="bi bi-eye-slash"
@click="toggleSecret"></em>
<code>********</code>
</div>
</template>
</td>
</tr>
</tbody>
</table>
</div>
@@ -136,9 +142,9 @@
</div>
<div class="card-body">
<template x-if="messages.length === 0 && !loading">
<p>
{{ __('firefly.no_webhook_messages') }}
</p>
<p>
{{ __('firefly.no_webhook_messages') }}
</p>
</template>
</div>
<template x-if="loading">
@@ -149,56 +155,60 @@
<span class="visually-hidden">{{ __('firefly.thinking') }}</span>
</div>
</span>
</div>
</div>
</div>
</template>
<template x-if="messages.length > 0 && !loading">
<div class="card-body p-0">
<table class="table table-hover">
<thead>
<tr>
<th>
Date and time
</th>
<th>
UID
</th>
<th>
Success?
</th>
<th>
More details
</th>
</tr>
</thead>
<tbody>
<template x-for="message in messages">
<tr>
<td>
<span x-text="message.created_at"></span>
</td>
<td>
<span x-text="message.uuid"></span>
</td>
<td>
<em class="bi bi-check text-success" x-show="message.success"></em>
<em class="bi bi-x text-danger" x-show="!message.success"></em>
</td>
<td>
<a @click="showWebhookMessage(message.id)" class="btn btn-default">
<em class="bi bi-envelope"></em>
{{ __('firefly.view_message') }}
</a>
<a @click="showWebhookAttempts(message.id)" class="btn btn-default">
<em class="bi bi-cloud-arrow-up"></em>
{{ __('firefly.view_attempts') }}
</a>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="card-body p-0">
<table class="table table-hover">
<thead>
<tr>
<th>
{{ __('list.date_time') }}
</th>
<th>
{{ __('list.uuid') }}
</th>
<th>
{{ __('list.success') }}
</th>
<th>
{{ __('list.more_details') }}
</th>
</tr>
</thead>
<tbody>
<template x-for="message in messages">
<tr>
<td>
<span x-text="message.created_at"></span>
</td>
<td>
<span x-text="message.uuid"></span>
</td>
<td>
<em class="bi bi-check text-success" x-show="message.success"></em>
<em class="bi bi-x text-danger" x-show="!message.success"></em>
</td>
<td>
<a
data-bs-toggle="modal" data-bs-target="#messageModal"
@click="showWebhookMessage(message.id)" class="btn btn-secondary">
<em class="bi bi-envelope"></em>
{{ __('firefly.view_message') }}
</a>
<button
data-bs-toggle="modal" data-bs-target="#attemptModal"
@click="showWebhookAttempts(message.id)" class="btn btn-secondary">
<em class="bi bi-cloud-arrow-up"></em>
{{ __('firefly.view_attempts') }}
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</div>
</div>
@@ -217,7 +227,8 @@
<textarea class="form-control" rows="10" readonly x-model="message_content"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-bs-dismiss="modal">{{ __('firefly.close') }}</button>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">{{ __('firefly.close') }}</button>
</div>
</div>
</div>
@@ -234,31 +245,36 @@
<p>
{{ __('firefly.attempt_content_help') }}
</p>
<p x-show="0===message_attempts.length">
<em>
{{ __('firefly.no_attempts') }}
</em>
</p>
<template x-for="message in message_attempts">
<div style="border:1px #eee solid;margin-bottom:0.5em;">
<strong>
<span x-text="i18next.t('firefly.webhook_attempt_at', {moment: message.created_at})"></span>
<span class="text-danger">(<span x-text="message.status_code"></span>)</span>
</strong>
<template x-show="0===message_attempts.length">
<p>
{{ __('firefly.logs') }}: <br/>
<textarea class="form-control" rows="5" readonly x-model="message.logs"></textarea>
<em>
{{ __('firefly.no_attempts') }}
</em>
</p>
<p v-if="null !== message.response">
{{ __('firefly.response') }}: <br/>
<textarea class="form-control" rows="5" readonly x-model="message.response"></textarea>
</p>
</div>
</template>
<template x-for="message in message_attempts">
<div style="border:1px #eee solid;margin-bottom:0.5em;">
<strong>
<span
x-text="i18next.t('firefly.webhook_attempt_at', {moment: message.created_at})"></span>
<span class="text-danger">(<span x-text="message.status_code"></span>)</span>
</strong>
<p>
{{ __('firefly.logs') }}: <br/>
<textarea class="form-control" rows="5" readonly x-model="message.logs"></textarea>
</p>
<p v-if="null !== message.response">
{{ __('firefly.response') }}: <br/>
<textarea class="form-control" rows="5" readonly
x-model="message.response"></textarea>
</p>
</div>
</template>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-bs-dismiss="modal">{{ __('firefly.close') }}</button>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">{{ __('firefly.close') }}</button>
</div>
</div>
</div>