b07935964b
* Fix: Minor styling/copy updates * Fix linting
243 lines
7.0 KiB
Vue
243 lines
7.0 KiB
Vue
<template>
|
|
<ProjectPageSettingsBlock title="Webhooks">
|
|
<template #introduction>
|
|
<p class="text-body-xs text-foreground">
|
|
Subscribe to events and get notified in real time. Use to trigger CI apps,
|
|
automation workflows, and more.
|
|
</p>
|
|
</template>
|
|
<template #top-buttons>
|
|
<FormButton
|
|
color="outline"
|
|
:icon-left="BookOpenIcon"
|
|
to="https://speckle.guide/dev/server-webhooks.html"
|
|
external
|
|
target="_blank"
|
|
>
|
|
Docs
|
|
</FormButton>
|
|
<FormButton @click="openCreateWebhookDialog">New</FormButton>
|
|
</template>
|
|
<template v-if="webhooks.length !== 0">
|
|
<LayoutTable
|
|
class="mt-6"
|
|
:columns="[
|
|
{ id: 'enabled', header: 'State', classes: 'col-span-1' },
|
|
{ id: 'data', header: 'Data', classes: 'col-span-5' },
|
|
{
|
|
id: 'triggers',
|
|
header: 'Trigger events',
|
|
classes: 'col-span-6 whitespace-break-spaces text-xs'
|
|
}
|
|
]"
|
|
:items="webhooks"
|
|
:buttons="[
|
|
{
|
|
icon: PencilIcon,
|
|
label: 'Edit',
|
|
action: openEditWebhookDialog,
|
|
class: '!text-primary'
|
|
},
|
|
{
|
|
icon: TrashIcon,
|
|
label: 'Delete',
|
|
action: openDeleteWebhookDialog,
|
|
class: '!text-danger'
|
|
}
|
|
]"
|
|
>
|
|
<template #enabled="{ item }">
|
|
<FormSwitch
|
|
:model-value="!!item.enabled"
|
|
icons
|
|
:name="'switch-' + item.id"
|
|
:show-label="false"
|
|
class="scale-90"
|
|
@update:model-value="(newValue) => onEnabledChange(item, newValue)"
|
|
/>
|
|
</template>
|
|
<template #data="{ item }">
|
|
<div class="flex flex-col">
|
|
<h3
|
|
class="font-medium text-sm truncate"
|
|
:class="{ 'opacity-60': !item.enabled }"
|
|
>
|
|
{{ item.description }}
|
|
</h3>
|
|
|
|
<div class="flex gap-1 items-center">
|
|
<div class="h-4 w-4" :class="{ grayscale: !item.enabled }">
|
|
<InformationCircleIcon
|
|
v-if="getHistoryStatus(item) === HistoryStatus.NoEvents"
|
|
class="opacity-40"
|
|
/>
|
|
<CheckCircleIcon
|
|
v-if="getHistoryStatus(item) === HistoryStatus.Called"
|
|
class="text-success"
|
|
/>
|
|
<XCircleIcon
|
|
v-if="
|
|
[HistoryStatus.Alert, HistoryStatus.Error].includes(
|
|
getHistoryStatus(item)
|
|
)
|
|
"
|
|
class="text-danger"
|
|
/>
|
|
</div>
|
|
<span class="text-foreground opacity-50 text-xs truncate">
|
|
{{ getHistoryStatusInfo(item) }}
|
|
</span>
|
|
</div>
|
|
<span class="text-foreground opacity-50 text-xs truncate">
|
|
{{ item.url }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<template #triggers="{ item }">
|
|
<div :class="{ 'opacity-60': !item.enabled }">
|
|
{{ formatTriggers(item) }}
|
|
</div>
|
|
</template>
|
|
</LayoutTable>
|
|
</template>
|
|
<template v-else>
|
|
<div class="mt-6">
|
|
<ProjectPageSettingsWebhooksEmptyState />
|
|
</div>
|
|
</template>
|
|
<ProjectPageSettingsWebhooksDeleteDialog
|
|
v-model:open="showDeleteWebhookDialog"
|
|
:webhook="webhookToModify"
|
|
/>
|
|
|
|
<ProjectPageSettingsWebhooksCreateOrEditDialog
|
|
v-model:open="showEditWebhookDialog"
|
|
:webhook="webhookToModify"
|
|
:stream-id="projectId"
|
|
@webhook-created="handleWebhookCreated"
|
|
/>
|
|
</ProjectPageSettingsBlock>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { useMutation, useQuery } from '@vue/apollo-composable'
|
|
import {
|
|
InformationCircleIcon,
|
|
CheckCircleIcon,
|
|
XCircleIcon
|
|
} from '@heroicons/vue/20/solid'
|
|
import { TrashIcon, PencilIcon, BookOpenIcon } from '@heroicons/vue/24/outline'
|
|
import { FormSwitch, LayoutTable } from '@speckle/ui-components'
|
|
import { projectWebhooksQuery } from '~~/lib/projects/graphql/queries'
|
|
import { updateWebhookMutation } from '~~/lib/projects/graphql/mutations'
|
|
import { useGlobalToast } from '~~/lib/common/composables/toast'
|
|
import type { WebhookItem } from '~~/lib/projects/helpers/types'
|
|
import {
|
|
convertThrowIntoFetchResult,
|
|
getCacheId,
|
|
getFirstErrorMessage
|
|
} from '~~/lib/common/helpers/graphql'
|
|
import type { Optional } from '@speckle/shared'
|
|
|
|
const projectId = computed(() => route.params.id as string)
|
|
const route = useRoute()
|
|
|
|
const { result: pageResult, refetch: refetchWebhooks } = useQuery(
|
|
projectWebhooksQuery,
|
|
() => ({
|
|
projectId: projectId.value
|
|
})
|
|
)
|
|
|
|
const { triggerNotification } = useGlobalToast()
|
|
const { mutate: updateMutation } = useMutation(updateWebhookMutation)
|
|
|
|
const webhookToModify = ref<WebhookItem | null>(null)
|
|
const showDeleteWebhookDialog = ref(false)
|
|
const showEditWebhookDialog = ref(false)
|
|
|
|
const webhooks = computed<WebhookItem[]>(() => {
|
|
return pageResult.value?.project?.webhooks?.items || []
|
|
})
|
|
|
|
enum HistoryStatus {
|
|
NoEvents = 0,
|
|
Alert = 1,
|
|
Called = 2,
|
|
Error = 3
|
|
}
|
|
|
|
const getHistoryStatus = (item: WebhookItem) => {
|
|
const recentHistory = item.history?.items?.[0]
|
|
const status = recentHistory?.status as Optional<HistoryStatus>
|
|
return status || HistoryStatus.NoEvents
|
|
}
|
|
|
|
const getHistoryStatusInfo = (item: WebhookItem) => {
|
|
const recentHistory = item.history?.items?.[0]
|
|
return recentHistory ? recentHistory.statusInfo : 'No events yet'
|
|
}
|
|
|
|
const formatTriggers = (item: WebhookItem): string => {
|
|
return item.triggers
|
|
.map((event, index, array) => `"${event}"${index < array.length - 1 ? ',' : ''}`)
|
|
.join(' ')
|
|
}
|
|
|
|
const openDeleteWebhookDialog = (item: WebhookItem) => {
|
|
webhookToModify.value = item
|
|
showDeleteWebhookDialog.value = true
|
|
}
|
|
|
|
const openEditWebhookDialog = (item: WebhookItem) => {
|
|
webhookToModify.value = item
|
|
showEditWebhookDialog.value = true
|
|
}
|
|
|
|
const openCreateWebhookDialog = () => {
|
|
webhookToModify.value = null
|
|
showEditWebhookDialog.value = true
|
|
}
|
|
|
|
const handleWebhookCreated = () => {
|
|
refetchWebhooks()
|
|
}
|
|
|
|
const onEnabledChange = async (item: WebhookItem, newValue: boolean) => {
|
|
const result = await updateMutation(
|
|
{
|
|
webhook: {
|
|
streamId: projectId.value,
|
|
id: item.id,
|
|
enabled: newValue
|
|
}
|
|
},
|
|
{
|
|
update: (cache, { data }) => {
|
|
if (data?.webhookUpdate) {
|
|
cache.modify({
|
|
id: getCacheId('Webhook', item.id),
|
|
fields: {
|
|
enabled: () => newValue
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
).catch(convertThrowIntoFetchResult)
|
|
|
|
if (result?.data?.webhookUpdate) {
|
|
triggerNotification({
|
|
type: ToastNotificationType.Success,
|
|
title: newValue ? 'Webhook Enabled' : 'Webhook Disabled'
|
|
})
|
|
} else {
|
|
triggerNotification({
|
|
type: ToastNotificationType.Danger,
|
|
title: 'Failed to update webhook',
|
|
description: getFirstErrorMessage(result?.errors)
|
|
})
|
|
}
|
|
}
|
|
</script>
|