feat(webhooks): add webhook management form
This commit is contained in:
parent
df09bca010
commit
3d2fe4cf65
@ -8,6 +8,7 @@ import {
|
||||
faArrowUpFromBracket,
|
||||
faBars,
|
||||
faBell,
|
||||
faBolt,
|
||||
faCalendar,
|
||||
faCheck,
|
||||
faCheckDouble,
|
||||
@ -144,6 +145,7 @@ library.add(faUsers)
|
||||
library.add(faArrowUpFromBracket)
|
||||
library.add(faX)
|
||||
library.add(faAnglesUp)
|
||||
library.add(faBolt)
|
||||
|
||||
// overwriting the wrong types
|
||||
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
@ -72,6 +72,12 @@
|
||||
@update:model-value="setSubscriptionInStore"
|
||||
type="dropdown"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'project.settings.webhooks', params: { projectId: project.id } }"
|
||||
icon="bolt"
|
||||
>
|
||||
{{ $t('project.webhooks.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="level < 2"
|
||||
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
|
||||
|
@ -359,6 +359,20 @@
|
||||
"favorites": {
|
||||
"title": "Favorites"
|
||||
}
|
||||
},
|
||||
"webhooks": {
|
||||
"title": "Webhooks",
|
||||
"targetUrl": "Target URL",
|
||||
"targetUrlInvalid": "Please provide a valid URL.",
|
||||
"events": "Events",
|
||||
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
|
||||
"delete": "Delete this webhook",
|
||||
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
|
||||
"deleteSuccess": "The webhook was successfully deleted.",
|
||||
"create": "Create webhook",
|
||||
"secret": "Secret",
|
||||
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
|
||||
"secretDocs": "Check out the docs for more details about how to use secrets."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
@ -480,6 +494,7 @@
|
||||
"custom": "Custom",
|
||||
"id": "ID",
|
||||
"created": "Created at",
|
||||
"createdBy": "Created by {0}",
|
||||
"actions": "Actions",
|
||||
"cannotBeUndone": "This cannot be undone!"
|
||||
},
|
||||
|
14
src/modelTypes/IWebhook.ts
Normal file
14
src/modelTypes/IWebhook.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
|
||||
export interface IWebhook extends IAbstract {
|
||||
id: number
|
||||
projectId: number
|
||||
secret: string
|
||||
targetUrl: string
|
||||
events: string[]
|
||||
createdBy: IUser
|
||||
|
||||
created: Date
|
||||
updated: Date
|
||||
}
|
25
src/models/webhook.ts
Normal file
25
src/models/webhook.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import AbstractModel from '@/models/abstractModel'
|
||||
import type {IWebhook} from '@/modelTypes/IWebhook'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
export default class WebhookModel extends AbstractModel<IWebhook> implements IWebhook {
|
||||
id = 0
|
||||
projectId = 0
|
||||
secret = ''
|
||||
targetUrl = ''
|
||||
events = []
|
||||
createdBy = null
|
||||
|
||||
created: Date
|
||||
updated: Date
|
||||
|
||||
constructor(data: Partial<IWebhook> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
|
||||
this.createdBy = new UserModel(this.createdBy)
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@ const ProjectSettingEdit = () => import('@/views/project/settings/edit.vue')
|
||||
const ProjectSettingBackground = () => import('@/views/project/settings/background.vue')
|
||||
const ProjectSettingDuplicate = () => import('@/views/project/settings/duplicate.vue')
|
||||
const ProjectSettingShare = () => import('@/views/project/settings/share.vue')
|
||||
const ProjectSettingWebhooks = () => import('@/views/project/settings/webhooks.vue')
|
||||
const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
|
||||
const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
|
||||
|
||||
@ -286,6 +287,14 @@ const router = createRouter({
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/webhooks',
|
||||
name: 'project.settings.webhooks',
|
||||
component: ProjectSettingWebhooks,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/delete',
|
||||
name: 'project.settings.delete',
|
||||
|
29
src/services/webhook.ts
Normal file
29
src/services/webhook.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import type {IWebhook} from '@/modelTypes/IWebhook'
|
||||
import WebhookModel from '@/models/webhook'
|
||||
|
||||
export default class WebhookService extends AbstractService<IWebhook> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/projects/{projectId}/webhooks',
|
||||
create: '/projects/{projectId}/webhooks',
|
||||
update: '/projects/{projectId}/webhooks/{id}',
|
||||
delete: '/projects/{projectId}/webhooks/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new WebhookModel(data)
|
||||
}
|
||||
|
||||
async getAvailableEvents(): Promise<string[]> {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.get('/webhooks/events')
|
||||
return response.data
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
215
src/views/project/settings/webhooks.vue
Normal file
215
src/views/project/settings/webhooks.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<script lang="ts">
|
||||
export default {name: 'project-setting-webhooks'}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed, watchEffect} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useTitle} from '@vueuse/core'
|
||||
|
||||
import ProjectService from '@/services/project'
|
||||
import ProjectModel from '@/models/project'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import type {IWebhook} from '@/modelTypes/IWebhook'
|
||||
import WebhookService from '@/services/webhook'
|
||||
import {formatDateShort} from '@/helpers/time/formatDate'
|
||||
import User from '@/components/misc/user.vue'
|
||||
import WebhookModel from '@/models/webhook'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import {success} from '@/message'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const project = ref<IProject>()
|
||||
useTitle(t('project.webhooks.title'))
|
||||
|
||||
const showNewForm = ref(false)
|
||||
|
||||
async function loadProject(projectId: number) {
|
||||
const projectService = new ProjectService()
|
||||
const newProject = await projectService.get(new ProjectModel({id: projectId}))
|
||||
await useBaseStore().handleSetCurrentProject({project: newProject})
|
||||
project.value = newProject
|
||||
await loadWebhooks()
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.projectId !== undefined
|
||||
? parseInt(route.params.projectId as string)
|
||||
: undefined,
|
||||
)
|
||||
|
||||
watchEffect(() => projectId.value !== undefined && loadProject(projectId.value))
|
||||
|
||||
const webhooks = ref<IWebhook[]>()
|
||||
const webhookService = new WebhookService()
|
||||
const availableEvents = ref<string[]>()
|
||||
|
||||
async function loadWebhooks() {
|
||||
webhooks.value = await webhookService.getAll({projectId: project.value.id})
|
||||
availableEvents.value = await webhookService.getAvailableEvents()
|
||||
}
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const webhookIdToDelete = ref<number>()
|
||||
|
||||
async function deleteWebhook() {
|
||||
await webhookService.delete({
|
||||
id: webhookIdToDelete.value,
|
||||
projectId: project.value.id,
|
||||
})
|
||||
showDeleteModal.value = false
|
||||
success({message: t('project.webhooks.deleteSuccess')})
|
||||
await loadWebhooks()
|
||||
}
|
||||
|
||||
const newWebhook = ref(new WebhookModel())
|
||||
const newWebhookEvents = ref({})
|
||||
|
||||
async function create() {
|
||||
const selectedEvents = Object.entries(newWebhookEvents.value)
|
||||
.filter(([event, use]) => use)
|
||||
.map(([event]) => event)
|
||||
newWebhook.value.events = selectedEvents
|
||||
newWebhook.value.projectId = project.value.id
|
||||
const created = await webhookService.create(newWebhook.value)
|
||||
webhooks.value.push(created)
|
||||
newWebhook.value = new WebhookModel()
|
||||
showNewForm.value = false
|
||||
}
|
||||
|
||||
const webhookTargetUrlValid = ref(true)
|
||||
function validateTargetUrl() {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<create-edit
|
||||
:title="$t('project.webhooks.title')"
|
||||
:has-primary-action="false"
|
||||
>
|
||||
<x-button
|
||||
v-if="!(webhooks?.length === 0 || showNewForm)"
|
||||
@click="showNewForm = true"
|
||||
icon="plus"
|
||||
class="mb-4">
|
||||
{{ $t('project.webhooks.create') }}
|
||||
</x-button>
|
||||
|
||||
<div class="p-4" v-if="webhooks?.length === 0 || showNewForm">
|
||||
<div class="field">
|
||||
<label class="label" for="targetUrl">
|
||||
{{ $t('project.webhooks.targetUrl') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
required
|
||||
id="targetUrl"
|
||||
class="input"
|
||||
:placeholder="$t('project.webhooks.targetUrl')"
|
||||
v-model="newWebhook.targetUrl"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!webhookTargetUrlValid">
|
||||
{{ $t('project.webhooks.targetUrlInvalid') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="secret">
|
||||
{{ $t('project.webhooks.secret') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="secret"
|
||||
class="input"
|
||||
v-model="newWebhook.secret"
|
||||
/>
|
||||
</div>
|
||||
<p class="help">
|
||||
{{ $t('project.webhooks.secretHint') }}
|
||||
<BaseButton href="https://vikunja.io/docs/webhooks/">
|
||||
{{ $t('project.webhooks.secretDocs') }}
|
||||
</BaseButton>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="secret">
|
||||
{{ $t('project.webhooks.events') }}
|
||||
</label>
|
||||
<p class="help">
|
||||
{{ $t('project.webhooks.eventsHint') }}
|
||||
</p>
|
||||
<div class="control">
|
||||
<fancycheckbox
|
||||
v-for="event in availableEvents"
|
||||
:key="event"
|
||||
class="mr-2"
|
||||
v-model="newWebhookEvents[event]"
|
||||
>
|
||||
{{ event }}
|
||||
</fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="create" icon="plus">
|
||||
{{ $t('project.webhooks.create') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<table
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth"
|
||||
v-if="webhooks"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('project.webhooks.targetUrl') }}</th>
|
||||
<th>{{ $t('project.webhooks.events') }}</th>
|
||||
<th>{{ $t('misc.created') }}</th>
|
||||
<th>{{ $t('misc.createdBy') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :key="w.id" v-for="w in webhooks">
|
||||
<td>{{ w.targetUrl }}</td>
|
||||
<td>{{ w.events.join(', ') }}</td>
|
||||
<td>{{ formatDateShort(w.created) }}</td>
|
||||
<td>
|
||||
<User
|
||||
:avatar-size="25"
|
||||
:user="w.createdBy"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="actions">
|
||||
<x-button
|
||||
@click="() => {showDeleteModal = true;webhookIdToDelete = w.id}"
|
||||
class="is-danger"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteWebhook()"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{ $t('project.webhooks.delete') }}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('project.webhooks.deleteText') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</create-edit>
|
||||
</template>
|
Reference in New Issue
Block a user