WebhookEditDialog.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. <template lang="pug">
  2. q-dialog(ref='dialogRef', @hide='onDialogHide')
  3. q-card(style='min-width: 850px;')
  4. q-card-section.card-header
  5. template(v-if='props.hookId')
  6. q-icon(name='img:/_assets/icons/fluent-pencil-drawing.svg', left, size='sm')
  7. span {{t(`admin.webhooks.edit`)}}
  8. template(v-else)
  9. q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
  10. span {{t(`admin.webhooks.new`)}}
  11. //- STATE INFO BAR
  12. q-card-section.flex.items-center.bg-indigo.text-white(v-if='props.hookId && state.hook.state === `pending`')
  13. q-spinner-clock.q-mr-sm(
  14. color='white'
  15. size='xs'
  16. )
  17. .text-caption {{t('admin.webhooks.statePendingHint')}}
  18. q-card-section.flex.items-center.bg-positive.text-white(v-if='props.hookId && state.hook.state === `success`')
  19. q-spinner-infinity.q-mr-sm(
  20. color='white'
  21. size='xs'
  22. )
  23. .text-caption {{t('admin.webhooks.stateSuccessHint')}}
  24. q-card-section.bg-negative.text-white(v-if='props.hookId && state.hook.state === `error`')
  25. .flex.items-center
  26. q-icon.q-mr-sm(
  27. color='white'
  28. size='xs'
  29. name='las la-exclamation-triangle'
  30. )
  31. .text-caption {{t('admin.webhooks.stateErrorExplain')}}
  32. .text-caption.q-pl-lg.q-ml-xs.text-red-2 {{state.hook.lastErrorMessage}}
  33. //- FORM
  34. q-form.q-py-sm(ref='editWebhookForm')
  35. q-item
  36. blueprint-icon(icon='info-popup')
  37. q-item-section
  38. q-input(
  39. outlined
  40. v-model='state.hook.name'
  41. dense
  42. :rules='hookNameValidation'
  43. hide-bottom-space
  44. :label='t(`common.field.name`)'
  45. :aria-label='t(`common.field.name`)'
  46. lazy-rules='ondemand'
  47. autofocus
  48. )
  49. q-item
  50. blueprint-icon(icon='lightning-bolt')
  51. q-item-section
  52. q-select(
  53. outlined
  54. :options='events'
  55. v-model='state.hook.events'
  56. multiple
  57. map-options
  58. emit-value
  59. option-value='key'
  60. option-label='name'
  61. options-dense
  62. dense
  63. :rules='hookEventsValidation'
  64. hide-bottom-space
  65. :label='t(`admin.webhooks.events`)'
  66. :aria-label='t(`admin.webhooks.events`)'
  67. lazy-rules='ondemand'
  68. )
  69. template(v-slot:selected)
  70. .text-caption(v-if='state.hook.events.length > 0') {{t(`admin.webhooks.eventsSelected`, state.hook.events.length, { count: state.hook.events.length })}}
  71. span(v-else) &nbsp;
  72. template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
  73. q-item(
  74. v-bind='itemProps'
  75. )
  76. q-item-section(side)
  77. q-checkbox(
  78. :model-value='selected'
  79. @update:model-value='toggleOption(opt)'
  80. size='sm'
  81. )
  82. q-item-section(side)
  83. q-chip.q-mx-none(
  84. size='sm'
  85. color='positive'
  86. text-color='white'
  87. square
  88. ) {{opt.type}}
  89. q-item-section
  90. q-item-label {{opt.name}}
  91. q-item
  92. blueprint-icon.self-start(icon='unknown-status')
  93. q-item-section
  94. q-item-label {{t(`admin.webhooks.url`)}}
  95. q-item-label(caption) {{t(`admin.webhooks.urlHint`)}}
  96. q-input.q-mt-sm(
  97. outlined
  98. v-model='state.hook.url'
  99. dense
  100. :rules='hookUrlValidation'
  101. hide-bottom-space
  102. placeholder='https://'
  103. :aria-label='t(`admin.webhooks.url`)'
  104. lazy-rules='ondemand'
  105. )
  106. template(v-slot:prepend)
  107. q-chip.q-mx-none(
  108. color='positive'
  109. text-color='white'
  110. square
  111. size='sm'
  112. ) POST
  113. q-item(tag='label', v-ripple)
  114. blueprint-icon(icon='rescan-document')
  115. q-item-section
  116. q-item-label {{t(`admin.webhooks.includeMetadata`)}}
  117. q-item-label(caption) {{t(`admin.webhooks.includeMetadataHint`)}}
  118. q-item-section(avatar)
  119. q-toggle(
  120. v-model='state.hook.includeMetadata'
  121. color='primary'
  122. checked-icon='las la-check'
  123. unchecked-icon='las la-times'
  124. :aria-label='t(`admin.webhooks.includeMetadata`)'
  125. )
  126. q-item(tag='label', v-ripple)
  127. blueprint-icon(icon='select-all')
  128. q-item-section
  129. q-item-label {{t(`admin.webhooks.includeContent`)}}
  130. q-item-label(caption) {{t(`admin.webhooks.includeContentHint`)}}
  131. q-item-section(avatar)
  132. q-toggle(
  133. v-model='state.hook.includeContent'
  134. color='primary'
  135. checked-icon='las la-check'
  136. unchecked-icon='las la-times'
  137. :aria-label='t(`admin.webhooks.includeContent`)'
  138. )
  139. q-item(tag='label', v-ripple)
  140. blueprint-icon(icon='security-ssl')
  141. q-item-section
  142. q-item-label {{t(`admin.webhooks.acceptUntrusted`)}}
  143. q-item-label(caption) {{t(`admin.webhooks.acceptUntrustedHint`)}}
  144. q-item-section(avatar)
  145. q-toggle(
  146. v-model='state.hook.acceptUntrusted'
  147. color='primary'
  148. checked-icon='las la-check'
  149. unchecked-icon='las la-times'
  150. :aria-label='t(`admin.webhooks.acceptUntrusted`)'
  151. )
  152. q-item
  153. blueprint-icon.self-start(icon='fingerprint-scan')
  154. q-item-section
  155. q-item-label {{t(`admin.webhooks.authHeader`)}}
  156. q-item-label(caption) {{t(`admin.webhooks.authHeaderHint`)}}
  157. q-input.q-mt-sm(
  158. outlined
  159. v-model='state.hook.authHeader'
  160. dense
  161. :aria-label='t(`admin.webhooks.authHeader`)'
  162. )
  163. q-card-actions.card-actions
  164. q-space
  165. q-btn.acrylic-btn(
  166. flat
  167. :label='t(`common.actions.cancel`)'
  168. color='grey'
  169. padding='xs md'
  170. @click='onDialogCancel'
  171. )
  172. q-btn(
  173. v-if='props.hookId'
  174. unelevated
  175. :label='t(`common.actions.save`)'
  176. color='primary'
  177. padding='xs md'
  178. @click='save'
  179. :loading='state.isLoading'
  180. )
  181. q-btn(
  182. v-else
  183. unelevated
  184. :label='t(`common.actions.create`)'
  185. color='primary'
  186. padding='xs md'
  187. @click='create'
  188. :loading='state.isLoading'
  189. )
  190. q-inner-loading(:showing='state.isLoading')
  191. q-spinner(color='accent', size='lg')
  192. </template>
  193. <script setup>
  194. import gql from 'graphql-tag'
  195. import { cloneDeep } from 'lodash-es'
  196. import { useI18n } from 'vue-i18n'
  197. import { useDialogPluginComponent, useQuasar } from 'quasar'
  198. import { computed, onMounted, reactive, ref } from 'vue'
  199. // PROPS
  200. const props = defineProps({
  201. hookId: {
  202. type: String,
  203. default: null
  204. }
  205. })
  206. // EMITS
  207. defineEmits([
  208. ...useDialogPluginComponent.emits
  209. ])
  210. // QUASAR
  211. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
  212. const $q = useQuasar()
  213. // I18N
  214. const { t } = useI18n()
  215. // DATA
  216. const state = reactive({
  217. isLoading: false,
  218. hook: {
  219. name: '',
  220. events: [],
  221. url: '',
  222. acceptUntrusted: false,
  223. authHeader: '',
  224. includeMetadata: true,
  225. includeContent: false,
  226. state: 'pending',
  227. lastErrorMessage: ''
  228. }
  229. })
  230. // COMPUTED
  231. const events = computed(() => ([
  232. { key: 'page:create', name: t('admin.webhooks.eventCreatePage'), type: t('admin.webhooks.typePage') },
  233. { key: 'page:edit', name: t('admin.webhooks.eventEditPage'), type: t('admin.webhooks.typePage') },
  234. { key: 'page:rename', name: t('admin.webhooks.eventRenamePage'), type: t('admin.webhooks.typePage') },
  235. { key: 'page:delete', name: t('admin.webhooks.eventDeletePage'), type: t('admin.webhooks.typePage') },
  236. { key: 'asset:upload', name: t('admin.webhooks.eventUploadAsset'), type: t('admin.webhooks.typeAsset') },
  237. { key: 'asset:edit', name: t('admin.webhooks.eventEditAsset'), type: t('admin.webhooks.typeAsset') },
  238. { key: 'asset:rename', name: t('admin.webhooks.eventRenameAsset'), type: t('admin.webhooks.typeAsset') },
  239. { key: 'asset:delete', name: t('admin.webhooks.eventDeleteAsset'), type: t('admin.webhooks.typeAsset') },
  240. { key: 'comment:new', name: t('admin.webhooks.eventNewComment'), type: t('admin.webhooks.typeComment') },
  241. { key: 'comment:edit', name: t('admin.webhooks.eventEditComment'), type: t('admin.webhooks.typeComment') },
  242. { key: 'comment:delete', name: t('admin.webhooks.eventDeleteComment'), type: t('admin.webhooks.typeComment') },
  243. { key: 'user:join', name: t('admin.webhooks.eventUserJoin'), type: t('admin.webhooks.typeUser') },
  244. { key: 'user:login', name: t('admin.webhooks.eventUserLogin'), type: t('admin.webhooks.typeUser') },
  245. { key: 'user:logout', name: t('admin.webhooks.eventUserLogout'), type: t('admin.webhooks.typeUser') }
  246. ]))
  247. // REFS
  248. const editWebhookForm = ref(null)
  249. // VALIDATION RULES
  250. const hookNameValidation = [
  251. val => val.length > 0 || t('admin.webhooks.nameMissing'),
  252. val => /^[^<>"]+$/.test(val) || t('admin.webhooks.nameInvalidChars')
  253. ]
  254. const hookEventsValidation = [
  255. val => val.length > 0 || t('admin.webhooks.eventsMissing')
  256. ]
  257. const hookUrlValidation = [
  258. val => (val.length > 0 && val.startsWith('http')) || t('admin.webhooks.urlMissing'),
  259. val => /^[^<>"]+$/.test(val) || t('admin.webhooks.urlInvalidChars')
  260. ]
  261. // METHODS
  262. async function fetchHook (id) {
  263. state.isLoading = true
  264. try {
  265. const resp = await APOLLO_CLIENT.query({
  266. query: gql`
  267. query getHook (
  268. $id: UUID!
  269. ) {
  270. hookById (
  271. id: $id
  272. ) {
  273. name
  274. events
  275. url
  276. includeMetadata
  277. includeContent
  278. acceptUntrusted
  279. authHeader
  280. state
  281. lastErrorMessage
  282. }
  283. }
  284. `,
  285. fetchPolicy: 'no-cache',
  286. variables: { id }
  287. })
  288. if (resp?.data?.hookById) {
  289. state.hook = cloneDeep(resp.data.hookById)
  290. } else {
  291. throw new Error('Failed to fetch webhook configuration.')
  292. }
  293. } catch (err) {
  294. $q.notify({
  295. type: 'negative',
  296. message: err.message
  297. })
  298. onDialogHide()
  299. }
  300. state.isLoading = false
  301. }
  302. async function create () {
  303. state.isLoading = true
  304. try {
  305. const isFormValid = await editWebhookForm.value.validate(true)
  306. if (!isFormValid) {
  307. throw new Error(t('admin.webhooks.createInvalidData'))
  308. }
  309. const resp = await APOLLO_CLIENT.mutate({
  310. mutation: gql`
  311. mutation createHook (
  312. $name: String!
  313. $events: [String]!
  314. $url: String!
  315. $includeMetadata: Boolean!
  316. $includeContent: Boolean!
  317. $acceptUntrusted: Boolean!
  318. $authHeader: String
  319. ) {
  320. createHook (
  321. name: $name
  322. events: $events
  323. url: $url
  324. includeMetadata: $includeMetadata
  325. includeContent: $includeContent
  326. acceptUntrusted: $acceptUntrusted
  327. authHeader: $authHeader
  328. ) {
  329. operation {
  330. succeeded
  331. message
  332. }
  333. }
  334. }
  335. `,
  336. variables: state.hook
  337. })
  338. if (resp?.data?.createHook?.operation?.succeeded) {
  339. $q.notify({
  340. type: 'positive',
  341. message: t('admin.webhooks.createSuccess')
  342. })
  343. onDialogOK()
  344. } else {
  345. throw new Error(resp?.data?.createHook?.operation?.message || 'An unexpected error occured.')
  346. }
  347. } catch (err) {
  348. $q.notify({
  349. type: 'negative',
  350. message: err.message
  351. })
  352. }
  353. state.isLoading = false
  354. }
  355. async function save () {
  356. state.isLoading = true
  357. try {
  358. const isFormValid = await editWebhookForm.value.validate(true)
  359. if (!isFormValid) {
  360. throw new Error(t('admin.webhooks.createInvalidData'))
  361. }
  362. const resp = await APOLLO_CLIENT.mutate({
  363. mutation: gql`
  364. mutation saveHook (
  365. $id: UUID!
  366. $patch: HookUpdateInput!
  367. ) {
  368. updateHook (
  369. id: $id
  370. patch: $patch
  371. ) {
  372. operation {
  373. succeeded
  374. message
  375. }
  376. }
  377. }
  378. `,
  379. variables: {
  380. id: props.hookId,
  381. patch: {
  382. name: state.hook.name,
  383. events: state.hook.events,
  384. url: state.hook.url,
  385. acceptUntrusted: state.hook.acceptUntrusted,
  386. authHeader: state.hook.authHeader,
  387. includeMetadata: state.hook.includeMetadata,
  388. includeContent: state.hook.includeContent
  389. }
  390. }
  391. })
  392. if (resp?.data?.updateHook?.operation?.succeeded) {
  393. $q.notify({
  394. type: 'positive',
  395. message: t('admin.webhooks.updateSuccess')
  396. })
  397. onDialogOK()
  398. } else {
  399. throw new Error(resp?.data?.updateHook?.operation?.message || 'An unexpected error occured.')
  400. }
  401. } catch (err) {
  402. $q.notify({
  403. type: 'negative',
  404. message: err.message
  405. })
  406. }
  407. state.isLoading = false
  408. }
  409. // MOUNTED
  410. onMounted(() => {
  411. if (props.hookId) {
  412. fetchHook(props.hookId)
  413. }
  414. })
  415. </script>