Files
speckle-server/packages/frontend-2/components/project/page/settings/webhooks/Webhooks.vue
T
Mike b07935964b Fix: Minor styling/copy updates (#2755)
* Fix: Minor styling/copy updates

* Fix linting
2024-08-26 11:43:51 +02:00

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>