Updated A LOT
This commit is contained in:
162
src/components/WebhookSettings.vue
Normal file
162
src/components/WebhookSettings.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<v-card class="span-12 panel-pad">
|
||||
<h2 class="section-title mb-1">Alert webhook</h2>
|
||||
<p class="quiet text-body-2 mb-3">
|
||||
Send every newly raised alert to an external HTTP endpoint as a JSON object (one POST per alert).
|
||||
Delivery runs on the alert cycle alongside (or instead of) email. Leave the secret blank to keep the stored value.
|
||||
</p>
|
||||
<div class="form-grid">
|
||||
<v-switch v-model="form.webhook_enabled" label="Enable webhook delivery" color="primary" density="compact" hide-details />
|
||||
<div />
|
||||
<v-text-field v-model="form.webhook_url" class="full" label="Webhook URL" placeholder="https://example.com/hooks/mixedassets" />
|
||||
<v-text-field
|
||||
v-model="form.webhook_secret"
|
||||
type="password"
|
||||
:label="settings.webhook_secret_set ? 'Signing secret (unchanged)' : 'Signing secret (optional)'"
|
||||
hint="If set, each request is signed with HMAC-SHA256 in the X-MixedAssets-Signature header"
|
||||
persistent-hint
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
v-if="settings.webhook_secret_set"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
prepend-icon="mdi-key-remove"
|
||||
@click="clearSecret"
|
||||
>Clear secret</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-row mt-3">
|
||||
<v-btn variant="tonal" prepend-icon="mdi-webhook" :loading="testing" :disabled="!form.webhook_url" @click="sendTest">Send test event</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" @click="save">Save webhook</v-btn>
|
||||
</div>
|
||||
<v-alert v-if="status" class="mt-3" density="compact" :type="statusType">{{ status }}</v-alert>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
<div class="quiet text-caption mb-1">Example payload delivered per alert:</div>
|
||||
<pre class="payload-sample">{{ samplePayload }}</pre>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiRequest } from '../services/api';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
token: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
form: {
|
||||
webhook_enabled: false,
|
||||
webhook_url: '',
|
||||
webhook_secret: ''
|
||||
},
|
||||
saving: false,
|
||||
testing: false,
|
||||
status: '',
|
||||
statusType: 'success',
|
||||
samplePayload: JSON.stringify({
|
||||
event: 'alert',
|
||||
id: 142,
|
||||
type: 'pm_overdue',
|
||||
severity: 'critical',
|
||||
title: 'PM overdue: Lift quarterly service',
|
||||
message: 'Lift quarterly service on FA-1042 was due 2026-05-20.',
|
||||
due_date: '2026-05-20',
|
||||
status: 'open',
|
||||
reference_type: 'asset_pm',
|
||||
reference_id: 88,
|
||||
asset_id: 1042,
|
||||
asset_code: 'FA-1042',
|
||||
created_at: '2026-06-04T12:00:00.000Z',
|
||||
sent_at: '2026-06-04T12:05:00.000Z'
|
||||
}, null, 2)
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async api(path, options = {}) {
|
||||
return apiRequest(path, this.token, options);
|
||||
},
|
||||
async load() {
|
||||
const data = await (await this.api('/api/notifications/settings')).json();
|
||||
this.settings = data.settings;
|
||||
this.form = {
|
||||
webhook_enabled: data.settings.webhook_enabled,
|
||||
webhook_url: data.settings.webhook_url,
|
||||
webhook_secret: ''
|
||||
};
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
this.status = '';
|
||||
try {
|
||||
const payload = {
|
||||
webhook_enabled: this.form.webhook_enabled,
|
||||
webhook_url: this.form.webhook_url
|
||||
};
|
||||
if (this.form.webhook_secret) payload.webhook_secret = this.form.webhook_secret;
|
||||
const data = await (await this.api('/api/notifications/settings', { method: 'PUT', body: JSON.stringify(payload) })).json();
|
||||
this.settings = data.settings;
|
||||
this.form.webhook_secret = '';
|
||||
this.statusType = 'success';
|
||||
this.status = 'Webhook settings saved.';
|
||||
} catch (error) {
|
||||
this.statusType = 'error';
|
||||
this.status = error.message;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async clearSecret() {
|
||||
this.status = '';
|
||||
try {
|
||||
const data = await (await this.api('/api/notifications/settings', { method: 'PUT', body: JSON.stringify({ webhook_secret_clear: true }) })).json();
|
||||
this.settings = data.settings;
|
||||
this.form.webhook_secret = '';
|
||||
this.statusType = 'success';
|
||||
this.status = 'Signing secret cleared.';
|
||||
} catch (error) {
|
||||
this.statusType = 'error';
|
||||
this.status = error.message;
|
||||
}
|
||||
},
|
||||
async sendTest() {
|
||||
this.testing = true;
|
||||
this.status = '';
|
||||
try {
|
||||
// Persist the current URL/secret first so the test hits what the user typed.
|
||||
await this.save();
|
||||
const result = await (await this.api('/api/notifications/webhook-test', { method: 'POST', body: JSON.stringify({}) })).json();
|
||||
this.statusType = 'success';
|
||||
this.status = `Test event delivered (HTTP ${result.status}).`;
|
||||
} catch (error) {
|
||||
this.statusType = 'error';
|
||||
this.status = `Test failed: ${error.message}`;
|
||||
} finally {
|
||||
this.testing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.payload-sample {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(127, 127, 127, 0.1);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user