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">
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header
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
.text-body2
i18n-t(keypath='admin.webhooks.deleteConfirm')
template(v-slot:name)
strong {{hook.name}}
.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-space
q-btn.acrylic-btn(
flat
:label='$t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='hide'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='$t(`common.actions.delete`)'
:label='t(`common.actions.delete`)'
color='negative'
padding='xs md'
@click='confirm'
:loading='state.isLoading'
)
</template>
<script>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
export default {
props: {
hook: {
type: Object
}
},
emits: ['ok', 'hide'],
data () {
return {
}
},
methods: {
show () {
this.$refs.dialog.show()
},
hide () {
this.$refs.dialog.hide()
},
onDialogHide () {
this.$emit('hide')
},
async confirm () {
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation deleteHook ($id: UUID!) {
deleteHook(id: $id) {
status {
succeeded
message
}
}
// PROPS
const props = defineProps({
hook: {
type: Object,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// 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({
type: 'negative',
message: err.message
})
`,
variables: {
id: props.hook.id
}
})
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>

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

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

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

@ -50,7 +50,7 @@ const routes = [
{ path: 'security', component: () => import('../pages/AdminSecurity.vue') },
{ path: 'system', component: () => import('../pages/AdminSystem.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') }
]
},

Loading…
Cancel
Save