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">
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>
......@@ -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') }
]
},
......
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