89 lines
4.2 KiB
Vue
89 lines
4.2 KiB
Vue
<template>
|
|
<div class="space-y-3">
|
|
<p class="text-[11px] text-muted leading-relaxed">
|
|
fan-out parallel activities · per-endpoint retries · dead-letter queue
|
|
</p>
|
|
<div class="flex items-center gap-3 text-[10px] text-muted/60">
|
|
<span class="font-mono">app/Temporal/WebhookDelivery/</span>
|
|
<a href="https://docs.temporal.io/develop/php/asynchronous-activity-completion" target="_blank" class="text-accent/50 hover:text-accent transition-colors">Workflow::async()</a>
|
|
<a href="https://php.temporal.io/classes/Temporal-Common-RetryOptions.html" target="_blank" class="text-accent/50 hover:text-accent transition-colors">RetryOptions</a>
|
|
<a href="https://docs.temporal.io/develop/php/failure-detection#activity-timeouts" target="_blank" class="text-accent/50 hover:text-accent transition-colors">failure detection</a>
|
|
</div>
|
|
|
|
<SimulationControls v-model="simulation" />
|
|
|
|
<div class="flex items-center gap-2">
|
|
<button @click="deliver"
|
|
class="px-3 py-1 text-[11px] font-medium uppercase tracking-wider bg-accent/15 text-accent border border-accent/30 hover:bg-accent/25 hover:border-accent/50 transition-all rounded-sm cursor-pointer">
|
|
Deliver Webhooks
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="workflowId" class="bg-deep/50 border border-border rounded-sm p-3 space-y-2">
|
|
<div class="flex items-center gap-4 text-[10px] text-muted">
|
|
<span>delivered <span class="text-accent">{{ status.totalDelivered || 0 }}</span></span>
|
|
<span>failed <span class="text-danger">{{ status.totalFailed || 0 }}</span></span>
|
|
<span>dead-lettered <span class="text-warn">{{ status.totalDeadLettered || 0 }}</span></span>
|
|
<span v-if="status.retryCount">retries <span class="text-warn">{{ status.retryCount }}</span></span>
|
|
</div>
|
|
|
|
<div v-if="status.endpoints" class="space-y-1">
|
|
<div v-for="(info, endpoint) in status.endpoints" :key="endpoint"
|
|
class="flex items-center gap-2 text-[10px]">
|
|
<span class="w-2 h-2 rounded-full shrink-0"
|
|
:class="{
|
|
'bg-accent': info.status === 'delivered',
|
|
'bg-danger': info.status === 'failed',
|
|
'bg-warn': info.status === 'dead_lettered',
|
|
'bg-muted animate-pulse-dot': info.status === 'delivering',
|
|
'bg-border': info.status === 'pending',
|
|
}"></span>
|
|
<span class="text-muted truncate flex-1">{{ endpoint }}</span>
|
|
<span class="text-value shrink-0">{{ info.status }}</span>
|
|
<span v-if="info.responseTime" class="text-muted shrink-0">{{ info.responseTime }}ms</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue';
|
|
import axios from 'axios';
|
|
import { usePolling } from '../composables/usePolling.js';
|
|
import SimulationControls from './SimulationControls.vue';
|
|
|
|
const simulation = ref({
|
|
failureRate: 0,
|
|
latencyMs: 0,
|
|
rateLimiting: { enabled: false, hitChance: 25, retryAfterMs: 2000 },
|
|
});
|
|
|
|
const workflowId = ref(null);
|
|
const status = ref({});
|
|
|
|
const isTerminal = computed(() => {
|
|
if (status.value.error) return true;
|
|
if (!status.value.endpoints) return false;
|
|
const statuses = Object.values(status.value.endpoints).map(e => e.status);
|
|
return statuses.length > 0 && statuses.every(s => ['delivered', 'failed', 'dead_lettered'].includes(s));
|
|
});
|
|
|
|
const { start: startPolling } = usePolling(async () => {
|
|
const { data } = await axios.get(`/temporal/webhooks/${workflowId.value}/status`);
|
|
if (data.error && !data.endpoints) {
|
|
status.value = { ...status.value, error: data.error };
|
|
return true;
|
|
}
|
|
status.value = data;
|
|
return isTerminal.value;
|
|
});
|
|
|
|
async function deliver() {
|
|
const { data } = await axios.post('/temporal/webhooks/deliver', { simulation: simulation.value });
|
|
workflowId.value = data.workflow_id;
|
|
status.value = {};
|
|
startPolling();
|
|
}
|
|
</script>
|