wowowowowo
This commit is contained in:
149
resources/js/components/operations/OperationTimeline.vue
Normal file
149
resources/js/components/operations/OperationTimeline.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user