110 lines
5.4 KiB
Vue
110 lines
5.4 KiB
Vue
<template>
|
|
<div class="space-y-3">
|
|
<p class="text-[11px] text-muted leading-relaxed">
|
|
aggregations · eager loading · chunked batch · transactions · cross-model joins
|
|
</p>
|
|
<div class="flex items-center gap-3 text-[10px] text-muted/60">
|
|
<span class="font-mono">app/Temporal/EloquentQuery/</span>
|
|
<a href="https://laravel.com/docs/eloquent" target="_blank" class="text-accent/50 hover:text-accent transition-colors">Eloquent ORM</a>
|
|
<a href="https://docs.temporal.io/develop/php/message-passing#queries" target="_blank" class="text-accent/50 hover:text-accent transition-colors">queries</a>
|
|
<a href="https://docs.temporal.io/develop/php/failure-detection#activity-heartbeats" target="_blank" class="text-accent/50 hover:text-accent transition-colors">heartbeat</a>
|
|
</div>
|
|
|
|
<SimulationControls v-model="simulation" />
|
|
|
|
<div class="flex items-center gap-2">
|
|
<button @click="startPipeline"
|
|
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">
|
|
Run Pipeline
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="workflowId" class="bg-deep/50 border border-border rounded-sm p-3 space-y-3">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-1.5 h-1.5 rounded-full"
|
|
:class="isTerminal ? 'bg-accent' : 'bg-accent animate-pulse-dot'"></span>
|
|
<span class="text-[11px] text-value">{{ status.status || 'starting' }}</span>
|
|
<span v-if="status.currentStep" class="text-[10px] text-muted">· {{ status.currentStep }}</span>
|
|
</div>
|
|
|
|
<!-- Step progress -->
|
|
<div class="flex items-center gap-1">
|
|
<div v-for="i in (status.totalSteps || 6)" :key="i"
|
|
class="h-1 flex-1 rounded-full transition-all duration-300"
|
|
:class="i <= (status.completedSteps || 0) ? 'bg-accent' : i === (status.completedSteps || 0) + 1 && !isTerminal ? 'bg-accent/40 animate-pulse' : 'bg-border'">
|
|
</div>
|
|
</div>
|
|
<div class="text-[10px] text-muted">
|
|
step {{ status.completedSteps || 0 }} / {{ status.totalSteps || 6 }}
|
|
</div>
|
|
|
|
<!-- Totals bar -->
|
|
<div class="flex items-center gap-4 text-[10px] text-muted">
|
|
<span>queries <span class="text-value">{{ status.totalQueriesRun || 0 }}</span></span>
|
|
<span>rows affected <span class="text-value">{{ status.totalRowsAffected || 0 }}</span></span>
|
|
<span>time <span class="text-value">{{ (status.totalExecutionTimeMs || 0).toFixed(1) }}ms</span></span>
|
|
</div>
|
|
|
|
<!-- Per-step results -->
|
|
<div v-if="Object.keys(status.stepResults || {}).length" class="space-y-1 pt-1 border-t border-border/30">
|
|
<div v-for="(result, key) in status.stepResults" :key="key"
|
|
class="border border-border/20 rounded-sm">
|
|
<button @click="toggleStep(key)"
|
|
class="w-full flex items-center gap-2 px-2 py-1 text-left cursor-pointer hover:bg-deep/30 transition-colors">
|
|
<span class="text-[10px] text-accent">{{ expandedSteps[key] ? '▾' : '▸' }}</span>
|
|
<span class="text-[10px] text-label flex-1">{{ result.label }}</span>
|
|
<span class="text-[9px] text-muted">{{ result.queriesRun }}q · {{ result.rowsAffected }}r · {{ result.executionTimeMs?.toFixed(1) }}ms</span>
|
|
<span v-if="result.attempt > 1" class="text-[9px] text-warn">attempt {{ result.attempt }}</span>
|
|
</button>
|
|
<div v-if="expandedSteps[key]" class="px-2 pb-2">
|
|
<pre class="text-[9px] text-muted/80 overflow-x-auto max-h-48 whitespace-pre-wrap">{{ JSON.stringify(result.data, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, 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 expandedSteps = reactive({});
|
|
|
|
const isTerminal = computed(() =>
|
|
['completed', 'failed'].includes(status.value.status) || !!status.value.error
|
|
);
|
|
|
|
function toggleStep(key) {
|
|
expandedSteps[key] = !expandedSteps[key];
|
|
}
|
|
|
|
const { start: startPolling } = usePolling(async () => {
|
|
const { data } = await axios.get(`/temporal/eloquent-query/${workflowId.value}/status`);
|
|
if (data.error && !data.status) {
|
|
status.value = { ...status.value, status: 'completed' };
|
|
return true;
|
|
}
|
|
status.value = data;
|
|
return isTerminal.value;
|
|
});
|
|
|
|
async function startPipeline() {
|
|
const { data } = await axios.post('/temporal/eloquent-query/start', { simulation: simulation.value });
|
|
workflowId.value = data.workflow_id;
|
|
status.value = { status: 'starting' };
|
|
Object.keys(expandedSteps).forEach(k => delete expandedSteps[k]);
|
|
startPolling();
|
|
}
|
|
</script>
|