feat(admin): migrate webhooks to vue 3 composable

parent cc506a08
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)
}
}
}
}
# ===============================================
# 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
}
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)
}
}
<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: {
const props = defineProps({
hook: { hook: {
type: Object type: Object,
} required: true
},
emits: ['ok', 'hide'],
data () {
return {
} }
}, })
methods: {
show () { // EMITS
this.$refs.dialog.show()
}, defineEmits([
hide () { ...useDialogPluginComponent.emits
this.$refs.dialog.hide() ])
},
onDialogHide () { // QUASAR
this.$emit('hide')
}, const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
async confirm () { const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// METHODS
async function confirm () {
state.isLoading = true
try { try {
const resp = await this.$apollo.mutate({ const resp = await APOLLO_CLIENT.mutate({
mutation: gql` mutation: gql`
mutation deleteHook ($id: UUID!) { mutation deleteHook ($id: UUID!) {
deleteHook(id: $id) { deleteHook(id: $id) {
status { operation {
succeeded succeeded
message message
} }
...@@ -67,26 +83,24 @@ export default { ...@@ -67,26 +83,24 @@ export default {
} }
`, `,
variables: { variables: {
id: this.hook.id id: props.hook.id
} }
}) })
if (resp?.data?.deleteHook?.status?.succeeded) { if (resp?.data?.deleteHook?.operation?.succeeded) {
this.$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: this.$t('admin.webhooks.deleteSuccess') message: t('admin.webhooks.deleteSuccess')
}) })
this.$emit('ok') onDialogOK()
this.hide()
} else { } else {
throw new Error(resp?.data?.deleteHook?.status?.message || 'An unexpected error occured.') throw new Error(resp?.data?.deleteHook?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) { } catch (err) {
this.$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: err.message message: err.message
}) })
} }
} state.isLoading = false
}
} }
</script> </script>
...@@ -41,7 +41,7 @@ q-page.admin-locale ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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,40 +91,44 @@ q-page.admin-webhooks ...@@ -96,40 +91,44 @@ 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
data () {
return { useMeta({
title: t('admin.webhooks.title')
})
// DATA
const state = reactive({
hooks: [], hooks: [],
loading: 0 loading: 0
} })
},
mounted () { // METHODS
this.load()
}, async function load () {
methods: { state.loading++
async load () { $q.loading.show()
this.loading++ const resp = await APOLLO_CLIENT.query({
this.$q.loading.show()
const resp = await this.$apollo.query({
query: gql` query: gql`
query getHooks { query getHooks {
hooks { hooks {
...@@ -142,42 +141,50 @@ export default { ...@@ -142,42 +141,50 @@ export default {
`, `,
fetchPolicy: 'network-only' fetchPolicy: 'network-only'
}) })
this.config = cloneDeep(resp?.data?.hooks) ?? [] state.hooks = cloneDeep(resp?.data?.hooks) ?? []
this.$q.loading.hide() $q.loading.hide()
this.loading-- state.loading--
}, }
createHook () {
this.$q.dialog({ function createHook () {
$q.dialog({
component: WebhookEditDialog, component: WebhookEditDialog,
componentProps: { componentProps: {
hookId: null hookId: null
} }
}).onOk(() => { }).onOk(() => {
this.load() load()
}) })
}, }
editHook (id) {
this.$q.dialog({ function editHook (id) {
$q.dialog({
component: WebhookEditDialog, component: WebhookEditDialog,
componentProps: { componentProps: {
hookId: id hookId: id
} }
}).onOk(() => { }).onOk(() => {
this.load() load()
}) })
}, }
deleteHook (hook) {
this.$q.dialog({ function deleteHook (hook) {
$q.dialog({
component: WebhookDeleteDialog, component: WebhookDeleteDialog,
componentProps: { componentProps: {
hook hook
} }
}).onOk(() => { }).onOk(() => {
this.load() load()
}) })
}
}
} }
// MOUNTED
onMounted(() => {
load()
})
</script> </script>
<style lang='scss'> <style lang='scss'>
......
...@@ -50,7 +50,7 @@ const routes = [ ...@@ -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') }
] ]
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment