Files
keystone/resources/js/components/operations/OperationTimeline.vue
Harry Bayliss 5b977c1f41
Some checks failed
CI / Lint (push) Failing after 22s
CI / Tests (push) Failing after 33s
wowowowowo
2026-05-28 15:15:41 +01:00

150 lines
6.3 KiB
Vue

<script setup lang="ts">
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Link } from "@inertiajs/vue3";
import { GitCommitIcon } from "lucide-vue-next";
import { ref } from "vue";
defineProps<{
operations: Record<string, any>[];
showTarget?: boolean;
}>();
const selectedStep = ref<Record<string, any> | null>(null);
const label = (value?: string | null): string => value?.replaceAll("_", " ").replaceAll("-", " ") ?? "";
const targetLabel = (target?: Record<string, any> | null): string => {
if (!target) {
return "Unknown target";
}
return target.name ?? target.hostname ?? `#${target.id}`;
};
</script>
<template>
<div class="grid gap-3">
<div
v-for="operation in operations"
:key="operation.id"
class="rounded-md border p-3 text-sm"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<GitCommitIcon class="size-4 text-muted-foreground" />
<Link
:href="
route('operations.show', {
organisation: $page.props.organisation.id,
operation: operation.id,
})
"
class="font-medium hover:underline"
>
{{ label(operation.kind) }}
</Link>
<Badge variant="outline">{{ operation.hash }}</Badge>
<Badge
:variant="operation.status === 'completed' ? 'success' : 'secondary'"
>
{{ label(operation.status) }}
</Badge>
</div>
<p v-if="showTarget" class="mt-1 text-muted-foreground">
Target: {{ targetLabel(operation.target) }}
</p>
</div>
<div class="text-xs text-muted-foreground">
{{ operation.steps_count ?? operation.steps?.length ?? 0 }} steps
<span v-if="operation.children_count ?? operation.children?.length">
· {{ operation.children_count ?? operation.children?.length }} child ops
</span>
</div>
</div>
<div v-if="operation.steps?.length" class="mt-3 grid gap-2 border-l pl-3">
<div
v-for="step in operation.steps"
:key="step.id"
class="flex items-start justify-between gap-3"
>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<div class="font-medium">{{ step.name ?? "Unnamed step" }}</div>
<Badge
:variant="step.status === 'completed' ? 'success' : 'secondary'"
>
{{ label(step.status) }}
</Badge>
</div>
<pre
v-if="step.error_logs_excerpt || step.logs_excerpt"
class="mt-1 max-h-20 overflow-hidden whitespace-pre-wrap text-xs text-muted-foreground"
>{{ step.error_logs_excerpt ?? step.logs_excerpt }}</pre
>
</div>
<Button
v-if="step.logs || step.error_logs"
size="xs"
variant="link"
@click="selectedStep = step"
>
Logs
</Button>
</div>
</div>
<div v-if="operation.children?.length" class="mt-3 grid gap-2 border-l pl-3">
<div
v-for="child in operation.children"
:key="child.id"
class="rounded-md bg-muted/40 p-2"
>
<div class="flex flex-wrap items-center gap-2">
<Link
:href="
route('operations.show', {
organisation: $page.props.organisation.id,
operation: child.id,
})
"
class="font-medium hover:underline"
>
{{ label(child.kind) }}
</Link>
<Badge
:variant="child.status === 'completed' ? 'success' : 'secondary'"
>
{{ label(child.status) }}
</Badge>
<span class="text-muted-foreground">{{ targetLabel(child.target) }}</span>
</div>
</div>
</div>
</div>
<div v-if="operations.length === 0" class="rounded-md border border-dashed p-6 text-sm text-muted-foreground">
No operations recorded yet.
</div>
</div>
<Dialog :open="!!selectedStep" @update:open="($event) => (!$event ? (selectedStep = null) : null)">
<DialogContent class="md:max-w-3xl">
<DialogHeader>
<DialogTitle>Logs for {{ selectedStep?.name ?? "step" }}</DialogTitle>
</DialogHeader>
<section v-if="selectedStep?.logs">
<h3 class="text-sm font-medium">Logs</h3>
<pre class="max-h-80 overflow-auto whitespace-pre-wrap text-xs text-muted-foreground">{{ selectedStep.logs }}</pre>
</section>
<section v-if="selectedStep?.error_logs">
<h3 class="text-sm font-medium">Error Logs</h3>
<pre class="max-h-80 overflow-auto whitespace-pre-wrap text-xs text-muted-foreground">{{ selectedStep.error_logs }}</pre>
</section>
</DialogContent>
</Dialog>
</template>