feat(admin): migrate webhooks to vue 3 composable

pull/5698/head
Nicolas Giard 2 years ago
parent cc506a086d
commit 5a4a9df43a
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -0,0 +1,85 @@
const graphHelper = require('../../helpers/graph')
const _ = require('lodash')
/* global WIKI */
module.exports = {
Query: {
async hooks () {
return WIKI.models.hooks.query().orderBy('name')
},
async hookById (obj, args) {
return WIKI.models.hooks.query().findById(args.id)
}
},
Mutation: {
/**
* CREATE HOOK
*/
async createHook (obj, args) {
try {
// -> Validate inputs
if (!args.name || args.name.length < 1) {
throw WIKI.ERROR(new Error('Invalid Hook Name'), 'HookCreateInvalidName')
}
if (!args.events || args.events.length < 1) {
throw WIKI.ERROR(new Error('Invalid Hook Events'), 'HookCreateInvalidEvents')
}
if (!args.url || args.url.length < 8 || !args.url.startsWith('http')) {
throw WIKI.ERROR(new Error('Invalid Hook URL'), 'HookCreateInvalidURL')
}
// -> Create hook
const newHook = await WIKI.models.hooks.createHook(args)
return {
operation: graphHelper.generateSuccess('Hook created successfully'),
hook: newHook
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* UPDATE HOOK
*/
async updateHook (obj, args) {
try {
// -> Load hook
const hook = await WIKI.models.hooks.query().findById(args.id)
if (!hook) {
throw WIKI.ERROR(new Error('Invalid Hook ID'), 'HookInvalidId')
}
// -> Check for bad input
if (_.has(args.patch, 'name') && args.patch.name.length < 1) {
throw WIKI.ERROR(new Error('Invalid Hook Name'), 'HookCreateInvalidName')
}
if (_.has(args.patch, 'events') && args.patch.events.length < 1) {
throw WIKI.ERROR(new Error('Invalid Hook Events'), 'HookCreateInvalidEvents')
}
if (_.has(args.patch, 'url') && (_.trim(args.patch.url).length < 8 || !args.patch.url.startsWith('http'))) {
throw WIKI.ERROR(new Error('URL is invalid.'), 'HookInvalidURL')
}
// -> Update hook
await WIKI.models.hooks.query().findById(args.id).patch(args.patch)
return {
operation: graphHelper.generateSuccess('Hook updated successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* DELETE HOOK
*/
async deleteHook (obj, args) {
try {
await WIKI.models.hooks.deleteHook(args.id)
return {
operation: graphHelper.generateSuccess('Hook deleted successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
}
}
}

@ -0,0 +1,70 @@
# ===============================================
# WEBHOOKS
# ===============================================
extend type Query {
hooks: [Hook]
hookById(
id: UUID!
): Hook
}
extend type Mutation {
createHook(
name: String!
events: [String]!
url: String!
includeMetadata: Boolean!
includeContent: Boolean!
acceptUntrusted: Boolean!
authHeader: String
): HookCreateResponse
updateHook(
id: UUID!
patch: HookUpdateInput!
): DefaultResponse
deleteHook (
id: UUID!
): DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type Hook {
id: UUID
name: String
events: [String]
url: String
includeMetadata: Boolean
includeContent: Boolean
acceptUntrusted: Boolean
authHeader: String
state: HookState
lastErrorMessage: String
}
input HookUpdateInput {
name: String
events: [String]
url: String
includeMetadata: Boolean
includeContent: Boolean
acceptUntrusted: Boolean
authHeader: String
}
enum HookState {
pending
error
success
}
type HookCreateResponse {
operation: Operation
hook: Hook
}

@ -0,0 +1,44 @@
const Model = require('objection').Model
/* global WIKI */
/**
* Hook model
*/
module.exports = class Hook extends Model {
static get tableName () { return 'hooks' }
static get jsonAttributes () {
return ['events']
}
$beforeUpdate () {
this.updatedAt = new Date()
}
static async createHook (data) {
return WIKI.models.hooks.query().insertAndFetch({
name: data.name,
events: data.events,
url: data.url,
includeMetadata: data.includeMetadata,
includeContent: data.includeContent,
acceptUntrusted: data.acceptUntrusted,
authHeader: data.authHeader,
state: 'pending',
lastErrorMessage: null
})
}
static async updateHook (id, patch) {
return WIKI.models.hooks.query().findById(id).patch({
...patch,
state: 'pending',
lastErrorMessage: null
})
}
static async deleteHook (id) {
return WIKI.models.hooks.query().deleteById(id)
}
}

@ -1,92 +1,106 @@
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;') q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
span {{$t(`admin.webhooks.delete`)}} span {{t(`admin.webhooks.delete`)}}
q-card-section q-card-section
.text-body2 .text-body2
i18n-t(keypath='admin.webhooks.deleteConfirm') i18n-t(keypath='admin.webhooks.deleteConfirm')
template(v-slot:name) template(v-slot:name)
strong {{hook.name}} strong {{hook.name}}
.text-body2.q-mt-md .text-body2.q-mt-md
strong.text-negative {{$t(`admin.webhooks.deleteConfirmWarn`)}} strong.text-negative {{t(`admin.webhooks.deleteConfirmWarn`)}}
q-card-actions.card-actions q-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
unelevated unelevated
:label='$t(`common.actions.delete`)' :label='t(`common.actions.delete`)'
color='negative' color='negative'
padding='xs md' padding='xs md'
@click='confirm' @click='confirm'
:loading='state.isLoading'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
export default { // PROPS
props: {
hook: { const props = defineProps({
type: Object hook: {
} type: Object,
}, required: true
emits: ['ok', 'hide'], }
data () { })
return {
} // EMITS
},
methods: { defineEmits([
show () { ...useDialogPluginComponent.emits
this.$refs.dialog.show() ])
},
hide () { // QUASAR
this.$refs.dialog.hide()
}, const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
onDialogHide () { const $q = useQuasar()
this.$emit('hide')
}, // I18N
async confirm () {
try { const { t } = useI18n()
const resp = await this.$apollo.mutate({
mutation: gql` // DATA
mutation deleteHook ($id: UUID!) {
deleteHook(id: $id) { const state = reactive({
status { isLoading: false
succeeded })
message
} // METHODS
}
async function confirm () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation deleteHook ($id: UUID!) {
deleteHook(id: $id) {
operation {
succeeded
message
} }
`,
variables: {
id: this.hook.id
} }
})
if (resp?.data?.deleteHook?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.webhooks.deleteSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.deleteHook?.status?.message || 'An unexpected error occured.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', id: props.hook.id
message: err.message
})
} }
})
if (resp?.data?.deleteHook?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.webhooks.deleteSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.deleteHook?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
state.isLoading = false
} }
</script> </script>

@ -1,35 +1,35 @@
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 850px;') q-card(style='min-width: 850px;')
q-card-section.card-header q-card-section.card-header
template(v-if='hookId') template(v-if='props.hookId')
q-icon(name='img:/_assets/icons/fluent-pencil-drawing.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-pencil-drawing.svg', left, size='sm')
span {{$t(`admin.webhooks.edit`)}} span {{t(`admin.webhooks.edit`)}}
template(v-else) template(v-else)
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
span {{$t(`admin.webhooks.new`)}} span {{t(`admin.webhooks.new`)}}
//- STATE INFO BAR //- STATE INFO BAR
q-card-section.flex.items-center.bg-indigo.text-white(v-if='hookId && hook.state === `pending`') q-card-section.flex.items-center.bg-indigo.text-white(v-if='props.hookId && state.hook.state === `pending`')
q-spinner-clock.q-mr-sm( q-spinner-clock.q-mr-sm(
color='white' color='white'
size='xs' size='xs'
) )
.text-caption {{$t('admin.webhooks.statePendingHint')}} .text-caption {{t('admin.webhooks.statePendingHint')}}
q-card-section.flex.items-center.bg-positive.text-white(v-if='hookId && hook.state === `success`') q-card-section.flex.items-center.bg-positive.text-white(v-if='props.hookId && state.hook.state === `success`')
q-spinner-infinity.q-mr-sm( q-spinner-infinity.q-mr-sm(
color='white' color='white'
size='xs' size='xs'
) )
.text-caption {{$t('admin.webhooks.stateSuccessHint')}} .text-caption {{t('admin.webhooks.stateSuccessHint')}}
q-card-section.bg-negative.text-white(v-if='hookId && hook.state === `error`') q-card-section.bg-negative.text-white(v-if='props.hookId && state.hook.state === `error`')
.flex.items-center .flex.items-center
q-icon.q-mr-sm( q-icon.q-mr-sm(
color='white' color='white'
size='xs' size='xs'
name='las la-exclamation-triangle' name='las la-exclamation-triangle'
) )
.text-caption {{$t('admin.webhooks.stateErrorExplain')}} .text-caption {{t('admin.webhooks.stateErrorExplain')}}
.text-caption.q-pl-lg.q-ml-xs.text-red-2 {{hook.lastErrorMessage}} .text-caption.q-pl-lg.q-ml-xs.text-red-2 {{state.hook.lastErrorMessage}}
//- FORM //- FORM
q-form.q-py-sm(ref='editWebhookForm') q-form.q-py-sm(ref='editWebhookForm')
q-item q-item
@ -37,15 +37,12 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='hook.name' v-model='state.hook.name'
dense dense
:rules=`[ :rules='hookNameValidation'
val => val.length > 0 || $t('admin.webhooks.nameMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.webhooks.nameInvalidChars')
]`
hide-bottom-space hide-bottom-space
:label='$t(`common.field.name`)' :label='t(`common.field.name`)'
:aria-label='$t(`common.field.name`)' :aria-label='t(`common.field.name`)'
lazy-rules='ondemand' lazy-rules='ondemand'
autofocus autofocus
) )
@ -55,7 +52,7 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-select( q-select(
outlined outlined
:options='events' :options='events'
v-model='hook.events' v-model='state.hook.events'
multiple multiple
map-options map-options
emit-value emit-value
@ -63,21 +60,18 @@ q-dialog(ref='dialog', @hide='onDialogHide')
option-label='name' option-label='name'
options-dense options-dense
dense dense
:rules=`[ :rules='hookEventsValidation'
val => val.length > 0 || $t('admin.webhooks.eventsMissing')
]`
hide-bottom-space hide-bottom-space
:label='$t(`admin.webhooks.events`)' :label='t(`admin.webhooks.events`)'
:aria-label='$t(`admin.webhooks.events`)' :aria-label='t(`admin.webhooks.events`)'
lazy-rules='ondemand' lazy-rules='ondemand'
) )
template(v-slot:selected) template(v-slot:selected)
.text-caption(v-if='hook.events.length > 0') {{$tc(`admin.webhooks.eventsSelected`, hook.events.length, { count: hook.events.length })}} .text-caption(v-if='state.hook.events.length > 0') {{t(`admin.webhooks.eventsSelected`, state.hook.events.length, { count: state.hook.events.length })}}
span(v-else) span(v-else) &nbsp;
template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }') template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
q-item( q-item(
v-bind='itemProps' v-bind='itemProps'
v-on='itemEvents'
) )
q-item-section(side) q-item-section(side)
q-checkbox( q-checkbox(
@ -97,19 +91,16 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item q-item
blueprint-icon.self-start(icon='unknown-status') blueprint-icon.self-start(icon='unknown-status')
q-item-section q-item-section
q-item-label {{$t(`admin.webhooks.url`)}} q-item-label {{t(`admin.webhooks.url`)}}
q-item-label(caption) {{$t(`admin.webhooks.urlHint`)}} q-item-label(caption) {{t(`admin.webhooks.urlHint`)}}
q-input.q-mt-sm( q-input.q-mt-sm(
outlined outlined
v-model='hook.url' v-model='state.hook.url'
dense dense
:rules=`[ :rules='hookUrlValidation'
val => (val.length > 0 && val.startsWith('http')) || $t('admin.webhooks.urlMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.webhooks.urlInvalidChars')
]`
hide-bottom-space hide-bottom-space
placeholder='https://' placeholder='https://'
:aria-label='$t(`admin.webhooks.url`)' :aria-label='t(`admin.webhooks.url`)'
lazy-rules='ondemand' lazy-rules='ondemand'
) )
template(v-slot:prepend) template(v-slot:prepend)
@ -122,301 +113,328 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='rescan-document') blueprint-icon(icon='rescan-document')
q-item-section q-item-section
q-item-label {{$t(`admin.webhooks.includeMetadata`)}} q-item-label {{t(`admin.webhooks.includeMetadata`)}}
q-item-label(caption) {{$t(`admin.webhooks.includeMetadataHint`)}} q-item-label(caption) {{t(`admin.webhooks.includeMetadataHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='hook.includeMetadata' v-model='state.hook.includeMetadata'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.webhooks.includeMetadata`)' :aria-label='t(`admin.webhooks.includeMetadata`)'
) )
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='select-all') blueprint-icon(icon='select-all')
q-item-section q-item-section
q-item-label {{$t(`admin.webhooks.includeContent`)}} q-item-label {{t(`admin.webhooks.includeContent`)}}
q-item-label(caption) {{$t(`admin.webhooks.includeContentHint`)}} q-item-label(caption) {{t(`admin.webhooks.includeContentHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='hook.includeContent' v-model='state.hook.includeContent'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.webhooks.includeContent`)' :aria-label='t(`admin.webhooks.includeContent`)'
) )
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='security-ssl') blueprint-icon(icon='security-ssl')
q-item-section q-item-section
q-item-label {{$t(`admin.webhooks.acceptUntrusted`)}} q-item-label {{t(`admin.webhooks.acceptUntrusted`)}}
q-item-label(caption) {{$t(`admin.webhooks.acceptUntrustedHint`)}} q-item-label(caption) {{t(`admin.webhooks.acceptUntrustedHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='hook.acceptUntrusted' v-model='state.hook.acceptUntrusted'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.webhooks.acceptUntrusted`)' :aria-label='t(`admin.webhooks.acceptUntrusted`)'
) )
q-item q-item
blueprint-icon.self-start(icon='fingerprint-scan') blueprint-icon.self-start(icon='fingerprint-scan')
q-item-section q-item-section
q-item-label {{$t(`admin.webhooks.authHeader`)}} q-item-label {{t(`admin.webhooks.authHeader`)}}
q-item-label(caption) {{$t(`admin.webhooks.authHeaderHint`)}} q-item-label(caption) {{t(`admin.webhooks.authHeaderHint`)}}
q-input.q-mt-sm( q-input.q-mt-sm(
outlined outlined
v-model='hook.authHeader' v-model='state.hook.authHeader'
dense dense
:aria-label='$t(`admin.webhooks.authHeader`)' :aria-label='t(`admin.webhooks.authHeader`)'
) )
q-card-actions.card-actions q-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
v-if='hookId' v-if='props.hookId'
unelevated unelevated
:label='$t(`common.actions.save`)' :label='t(`common.actions.save`)'
color='primary' color='primary'
padding='xs md' padding='xs md'
@click='save' @click='save'
:loading='loading' :loading='state.isLoading'
) )
q-btn( q-btn(
v-else v-else
unelevated unelevated
:label='$t(`common.actions.create`)' :label='t(`common.actions.create`)'
color='primary' color='primary'
padding='xs md' padding='xs md'
@click='create' @click='create'
:loading='loading' :loading='state.isLoading'
) )
q-inner-loading(:showing='loading') q-inner-loading(:showing='state.isLoading')
q-spinner(color='accent', size='lg') q-spinner(color='accent', size='lg')
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { computed, onMounted, reactive, ref } from 'vue'
import { QSpinnerClock, QSpinnerInfinity } from 'quasar' // PROPS
export default { const props = defineProps({
components: { hookId: {
QSpinnerClock, type: String,
QSpinnerInfinity default: null
}, }
props: { })
hookId: {
type: String, // EMITS
default: null
} defineEmits([
}, ...useDialogPluginComponent.emits
emits: ['ok', 'hide'], ])
data () {
return { // QUASAR
hook: {
name: '', const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
events: [], const $q = useQuasar()
url: '',
acceptUntrusted: false, // I18N
authHeader: '',
includeMetadata: true, const { t } = useI18n()
includeContent: false,
state: 'pending', // DATA
lastErrorMessage: ''
}, const state = reactive({
loading: false isLoading: false,
hook: {
name: '',
events: [],
url: '',
acceptUntrusted: false,
authHeader: '',
includeMetadata: true,
includeContent: false,
state: 'pending',
lastErrorMessage: ''
}
})
// COMPUTED
const events = computed(() => ([
{ key: 'page:create', name: t('admin.webhooks.eventCreatePage'), type: t('admin.webhooks.typePage') },
{ key: 'page:edit', name: t('admin.webhooks.eventEditPage'), type: t('admin.webhooks.typePage') },
{ key: 'page:rename', name: t('admin.webhooks.eventRenamePage'), type: t('admin.webhooks.typePage') },
{ key: 'page:delete', name: t('admin.webhooks.eventDeletePage'), type: t('admin.webhooks.typePage') },
{ key: 'asset:upload', name: t('admin.webhooks.eventUploadAsset'), type: t('admin.webhooks.typeAsset') },
{ key: 'asset:edit', name: t('admin.webhooks.eventEditAsset'), type: t('admin.webhooks.typeAsset') },
{ key: 'asset:rename', name: t('admin.webhooks.eventRenameAsset'), type: t('admin.webhooks.typeAsset') },
{ key: 'asset:delete', name: t('admin.webhooks.eventDeleteAsset'), type: t('admin.webhooks.typeAsset') },
{ key: 'comment:new', name: t('admin.webhooks.eventNewComment'), type: t('admin.webhooks.typeComment') },
{ key: 'comment:edit', name: t('admin.webhooks.eventEditComment'), type: t('admin.webhooks.typeComment') },
{ key: 'comment:delete', name: t('admin.webhooks.eventDeleteComment'), type: t('admin.webhooks.typeComment') },
{ key: 'user:join', name: t('admin.webhooks.eventUserJoin'), type: t('admin.webhooks.typeUser') },
{ key: 'user:login', name: t('admin.webhooks.eventUserLogin'), type: t('admin.webhooks.typeUser') },
{ key: 'user:logout', name: t('admin.webhooks.eventUserLogout'), type: t('admin.webhooks.typeUser') }
]))
// REFS
const editWebhookForm = ref(null)
// VALIDATION RULES
const hookNameValidation = [
val => val.length > 0 || t('admin.webhooks.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.webhooks.nameInvalidChars')
]
const hookEventsValidation = [
val => val.length > 0 || t('admin.webhooks.eventsMissing')
]
const hookUrlValidation = [
val => (val.length > 0 && val.startsWith('http')) || t('admin.webhooks.urlMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.webhooks.urlInvalidChars')
]
// METHODS
async function fetchHook (id) {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query getHook (
$id: UUID!
) {
hookById (
id: $id
) {
name
events
url
includeMetadata
includeContent
acceptUntrusted
authHeader
state
lastErrorMessage
}
}
`,
fetchPolicy: 'no-cache',
variables: { id }
})
if (resp?.data?.hookById) {
state.hook = cloneDeep(resp.data.hookById)
} else {
throw new Error('Failed to fetch webhook configuration.')
} }
}, } catch (err) {
computed: { $q.notify({
events () { type: 'negative',
return [ message: err.message
{ key: 'page:create', name: this.$t('admin.webhooks.eventCreatePage'), type: this.$t('admin.webhooks.typePage') }, })
{ key: 'page:edit', name: this.$t('admin.webhooks.eventEditPage'), type: this.$t('admin.webhooks.typePage') }, onDialogHide()
{ key: 'page:rename', name: this.$t('admin.webhooks.eventRenamePage'), type: this.$t('admin.webhooks.typePage') }, }
{ key: 'page:delete', name: this.$t('admin.webhooks.eventDeletePage'), type: this.$t('admin.webhooks.typePage') }, state.isLoading = false
{ key: 'asset:upload', name: this.$t('admin.webhooks.eventUploadAsset'), type: this.$t('admin.webhooks.typeAsset') }, }
{ key: 'asset:edit', name: this.$t('admin.webhooks.eventEditAsset'), type: this.$t('admin.webhooks.typeAsset') },
{ key: 'asset:rename', name: this.$t('admin.webhooks.eventRenameAsset'), type: this.$t('admin.webhooks.typeAsset') }, async function create () {
{ key: 'asset:delete', name: this.$t('admin.webhooks.eventDeleteAsset'), type: this.$t('admin.webhooks.typeAsset') }, state.isLoading = true
{ key: 'comment:new', name: this.$t('admin.webhooks.eventNewComment'), type: this.$t('admin.webhooks.typeComment') }, try {
{ key: 'comment:edit', name: this.$t('admin.webhooks.eventEditComment'), type: this.$t('admin.webhooks.typeComment') }, const isFormValid = await editWebhookForm.value.validate(true)
{ key: 'comment:delete', name: this.$t('admin.webhooks.eventDeleteComment'), type: this.$t('admin.webhooks.typeComment') }, if (!isFormValid) {
{ key: 'user:join', name: this.$t('admin.webhooks.eventUserJoin'), type: this.$t('admin.webhooks.typeUser') }, throw new Error(t('admin.webhooks.createInvalidData'))
{ key: 'user:login', name: this.$t('admin.webhooks.eventUserLogin'), type: this.$t('admin.webhooks.typeUser') },
{ key: 'user:logout', name: this.$t('admin.webhooks.eventUserLogout'), type: this.$t('admin.webhooks.typeUser') }
]
} }
}, const resp = await APOLLO_CLIENT.mutate({
methods: { mutation: gql`
show () { mutation createHook (
this.$refs.dialog.show() $name: String!
if (this.hookId) { $events: [String]!
this.fetchHook(this.hookId) $url: String!
} $includeMetadata: Boolean!
}, $includeContent: Boolean!
hide () { $acceptUntrusted: Boolean!
this.$refs.dialog.hide() $authHeader: String
}, ) {
onDialogHide () { createHook (
this.$emit('hide') name: $name
}, events: $events
async fetchHook (id) { url: $url
this.loading = true includeMetadata: $includeMetadata
try { includeContent: $includeContent
const resp = await this.$apollo.query({ acceptUntrusted: $acceptUntrusted
query: gql` authHeader: $authHeader
query getHook ( ) {
$id: UUID! operation {
) { succeeded
hookById ( message
id: $id
) {
name
events
url
includeMetadata
includeContent
acceptUntrusted
authHeader
state
lastErrorMessage
}
} }
`, }
fetchPolicy: 'no-cache',
variables: { id }
})
if (resp?.data?.hookById) {
this.hook = cloneDeep(resp.data.hookById)
} else {
throw new Error('Failed to fetch webhook configuration.')
}
} catch (err) {
this.$q.notify({
type: 'negative',
message: err.message
})
this.hide()
}
this.loading = false
},
async create () {
this.loading = true
try {
const isFormValid = await this.$refs.editWebhookForm.validate(true)
if (!isFormValid) {
throw new Error(this.$t('admin.webhooks.createInvalidData'))
}
const resp = await this.$apollo.mutate({
mutation: gql`
mutation createHook (
$name: String!
$events: [String]!
$url: String!
$includeMetadata: Boolean!
$includeContent: Boolean!
$acceptUntrusted: Boolean!
$authHeader: String
) {
createHook (
name: $name
events: $events
url: $url
includeMetadata: $includeMetadata
includeContent: $includeContent
acceptUntrusted: $acceptUntrusted
authHeader: $authHeader
) {
status {
succeeded
message
}
}
}
`,
variables: this.hook
})
if (resp?.data?.createHook?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.webhooks.createSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.createHook?.status?.message || 'An unexpected error occured.')
}
} catch (err) {
this.$q.notify({
type: 'negative',
message: err.message
})
}
this.loading = false
},
async save () {
this.loading = true
try {
const isFormValid = await this.$refs.editWebhookForm.validate(true)
if (!isFormValid) {
throw new Error(this.$t('admin.webhooks.createInvalidData'))
} }
const resp = await this.$apollo.mutate({ `,
mutation: gql` variables: state.hook
mutation saveHook ( })
$id: UUID! if (resp?.data?.createHook?.operation?.succeeded) {
$patch: HookUpdateInput! $q.notify({
) { type: 'positive',
updateHook ( message: t('admin.webhooks.createSuccess')
id: $id })
patch: $patch onDialogOK()
) { } else {
status { throw new Error(resp?.data?.createHook?.operation?.message || 'An unexpected error occured.')
succeeded }
message } catch (err) {
} $q.notify({
} type: 'negative',
} message: err.message
`, })
variables: { }
id: this.hookId, state.isLoading = false
patch: { }
name: this.hook.name,
events: this.hook.events, async function save () {
url: this.hook.url, state.isLoading = true
acceptUntrusted: this.hook.acceptUntrusted, try {
authHeader: this.hook.authHeader, const isFormValid = await editWebhookForm.value.validate(true)
includeMetadata: this.hook.includeMetadata, if (!isFormValid) {
includeContent: this.hook.includeContent throw new Error(t('admin.webhooks.createInvalidData'))
}
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation saveHook (
$id: UUID!
$patch: HookUpdateInput!
) {
updateHook (
id: $id
patch: $patch
) {
operation {
succeeded
message
} }
} }
})
if (resp?.data?.updateHook?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.webhooks.updateSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.updateHook?.status?.message || 'An unexpected error occured.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', id: props.hookId,
message: err.message patch: {
}) name: state.hook.name,
events: state.hook.events,
url: state.hook.url,
acceptUntrusted: state.hook.acceptUntrusted,
authHeader: state.hook.authHeader,
includeMetadata: state.hook.includeMetadata,
includeContent: state.hook.includeContent
}
} }
this.loading = false })
if (resp?.data?.updateHook?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.webhooks.updateSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.updateHook?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
state.isLoading = false
} }
// MOUNTED
onMounted(() => {
if (props.hookId) {
fetchHook(props.hookId)
}
})
</script> </script>

@ -41,7 +41,7 @@ q-page.admin-locale
) )
q-separator(inset) q-separator(inset)
.row.q-pa-md.q-col-gutter-md .row.q-pa-md.q-col-gutter-md
.col-7 .col-12.col-lg-7
//- ----------------------- //- -----------------------
//- Locale Options //- Locale Options
//- ----------------------- //- -----------------------
@ -89,7 +89,7 @@ q-page.admin-locale
span {{ t('admin.locale.namespacingPrefixWarning.title', { langCode: state.selectedLocale }) }} span {{ t('admin.locale.namespacingPrefixWarning.title', { langCode: state.selectedLocale }) }}
.text-caption.text-yellow-1 {{ t('admin.locale.namespacingPrefixWarning.subtitle') }} .text-caption.text-yellow-1 {{ t('admin.locale.namespacingPrefixWarning.subtitle') }}
.col-5 .col-12.col-lg-5
//- ----------------------- //- -----------------------
//- Namespacing //- Namespacing
//- ----------------------- //- -----------------------

@ -4,14 +4,9 @@ q-page.admin-webhooks
.col-auto .col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-lightning-bolt.svg') img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-lightning-bolt.svg')
.col.q-pl-md .col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ $t('admin.webhooks.title') }} .text-h5.text-primary.animated.fadeInLeft {{ t('admin.webhooks.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.webhooks.subtitle') }} .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.webhooks.subtitle') }}
.col-auto .col-auto
q-spinner-tail.q-mr-md(
v-show='loading'
color='accent'
size='sm'
)
q-btn.q-mr-sm.acrylic-btn( q-btn.q-mr-sm.acrylic-btn(
icon='las la-question-circle' icon='las la-question-circle'
flat flat
@ -24,19 +19,19 @@ q-page.admin-webhooks
icon='las la-redo-alt' icon='las la-redo-alt'
flat flat
color='secondary' color='secondary'
:loading='loading > 0' :loading='state.loading > 0'
@click='load' @click='load'
) )
q-btn( q-btn(
unelevated unelevated
icon='las la-plus' icon='las la-plus'
:label='$t(`admin.webhooks.new`)' :label='t(`admin.webhooks.new`)'
color='primary' color='primary'
@click='createHook' @click='createHook'
) )
q-separator(inset) q-separator(inset)
.row.q-pa-md.q-col-gutter-md .row.q-pa-md.q-col-gutter-md
.col-12(v-if='hooks.length < 1') .col-12(v-if='state.hooks.length < 1')
q-card.rounded-borders( q-card.rounded-borders(
flat flat
:class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`' :class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`'
@ -44,11 +39,11 @@ q-page.admin-webhooks
q-card-section.items-center(horizontal) q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm') q-icon(name='las la-info-circle', size='sm')
q-card-section.text-caption {{ $t('admin.webhooks.none') }} q-card-section.text-caption {{ t('admin.webhooks.none') }}
.col-12(v-else) .col-12(v-else)
q-card q-card
q-list(separator) q-list(separator)
q-item(v-for='hook of hooks', :key='hook.id') q-item(v-for='hook of state.hooks', :key='hook.id')
q-item-section(side) q-item-section(side)
q-icon(name='las la-bolt', color='primary') q-icon(name='las la-bolt', color='primary')
q-item-section q-item-section
@ -60,23 +55,23 @@ q-page.admin-webhooks
color='indigo' color='indigo'
size='xs' size='xs'
) )
.text-caption.text-indigo {{$t('admin.webhooks.statePending')}} .text-caption.text-indigo {{t('admin.webhooks.statePending')}}
q-tooltip(anchor='center left', self='center right') {{$t('admin.webhooks.statePendingHint')}} q-tooltip(anchor='center left', self='center right') {{t('admin.webhooks.statePendingHint')}}
template(v-else-if='hook.state === `success`') template(v-else-if='hook.state === `success`')
q-spinner-infinity.q-mr-sm( q-spinner-infinity.q-mr-sm(
color='positive' color='positive'
size='xs' size='xs'
) )
.text-caption.text-positive {{$t('admin.webhooks.stateSuccess')}} .text-caption.text-positive {{t('admin.webhooks.stateSuccess')}}
q-tooltip(anchor='center left', self='center right') {{$t('admin.webhooks.stateSuccessHint')}} q-tooltip(anchor='center left', self='center right') {{t('admin.webhooks.stateSuccessHint')}}
template(v-else-if='hook.state === `error`') template(v-else-if='hook.state === `error`')
q-icon.q-mr-sm( q-icon.q-mr-sm(
color='negative' color='negative'
size='xs' size='xs'
name='las la-exclamation-triangle' name='las la-exclamation-triangle'
) )
.text-caption.text-negative {{$t('admin.webhooks.stateError')}} .text-caption.text-negative {{t('admin.webhooks.stateError')}}
q-tooltip(anchor='center left', self='center right') {{$t('admin.webhooks.stateErrorHint')}} q-tooltip(anchor='center left', self='center right') {{t('admin.webhooks.stateErrorHint')}}
q-separator.q-ml-md(vertical) q-separator.q-ml-md(vertical)
q-item-section(side, style='flex-direction: row; align-items: center;') q-item-section(side, style='flex-direction: row; align-items: center;')
q-btn.acrylic-btn.q-mr-sm( q-btn.acrylic-btn.q-mr-sm(
@ -96,88 +91,100 @@ q-page.admin-webhooks
</template> </template>
<script> <script setup>
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { createMetaMixin, QSpinnerClock, QSpinnerInfinity } from 'quasar' import { useI18n } from 'vue-i18n'
import WebhookDeleteDialog from '../components/WebhookDeleteDialog.vue' import { useMeta, useQuasar } from 'quasar'
import WebhookEditDialog from '../components/WebhookEditDialog.vue' import { onMounted, reactive } from 'vue'
export default { import WebhookEditDialog from 'src/components/WebhookEditDialog.vue'
components: { import WebhookDeleteDialog from 'src/components/WebhookDeleteDialog.vue'
QSpinnerClock,
QSpinnerInfinity // QUASAR
},
mixins: [ const $q = useQuasar()
createMetaMixin(function () {
return { // I18N
title: this.$t('admin.webhooks.title')
const { t } = useI18n()
// META
useMeta({
title: t('admin.webhooks.title')
})
// DATA
const state = reactive({
hooks: [],
loading: 0
})
// METHODS
async function load () {
state.loading++
$q.loading.show()
const resp = await APOLLO_CLIENT.query({
query: gql`
query getHooks {
hooks {
id
name
url
state
}
} }
}) `,
], fetchPolicy: 'network-only'
data () { })
return { state.hooks = cloneDeep(resp?.data?.hooks) ?? []
hooks: [], $q.loading.hide()
loading: 0 state.loading--
}
function createHook () {
$q.dialog({
component: WebhookEditDialog,
componentProps: {
hookId: null
} }
}, }).onOk(() => {
mounted () { load()
this.load() })
}, }
methods: {
async load () { function editHook (id) {
this.loading++ $q.dialog({
this.$q.loading.show() component: WebhookEditDialog,
const resp = await this.$apollo.query({ componentProps: {
query: gql` hookId: id
query getHooks {
hooks {
id
name
url
state
}
}
`,
fetchPolicy: 'network-only'
})
this.config = cloneDeep(resp?.data?.hooks) ?? []
this.$q.loading.hide()
this.loading--
},
createHook () {
this.$q.dialog({
component: WebhookEditDialog,
componentProps: {
hookId: null
}
}).onOk(() => {
this.load()
})
},
editHook (id) {
this.$q.dialog({
component: WebhookEditDialog,
componentProps: {
hookId: id
}
}).onOk(() => {
this.load()
})
},
deleteHook (hook) {
this.$q.dialog({
component: WebhookDeleteDialog,
componentProps: {
hook
}
}).onOk(() => {
this.load()
})
} }
} }).onOk(() => {
load()
})
} }
function deleteHook (hook) {
$q.dialog({
component: WebhookDeleteDialog,
componentProps: {
hook
}
}).onOk(() => {
load()
})
}
// MOUNTED
onMounted(() => {
load()
})
</script> </script>
<style lang='scss'> <style lang='scss'>

@ -50,7 +50,7 @@ const routes = [
{ path: 'security', component: () => import('../pages/AdminSecurity.vue') }, { path: 'security', component: () => import('../pages/AdminSecurity.vue') },
{ path: 'system', component: () => import('../pages/AdminSystem.vue') }, { path: 'system', component: () => import('../pages/AdminSystem.vue') },
// { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') }, // { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
// { path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') }, { path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') },
{ path: 'flags', component: () => import('../pages/AdminFlags.vue') } { path: 'flags', component: () => import('../pages/AdminFlags.vue') }
] ]
}, },

Loading…
Cancel
Save