File size: 11,266 Bytes
0f07ba7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
<!-- Global Operations Status Bar -->
<div x-data="operationsStatus()" x-init="init()" x-show="operations.length > 0"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="sticky top-0 left-0 right-0 z-40 bg-[#1E293B]/95 backdrop-blur-sm border-b border-[#38BDF8]/50">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-2">
<div class="relative">
<i class="fas fa-spinner fa-spin text-[#38BDF8] text-lg"></i>
</div>
<h3 class="text-[#E5E7EB] font-semibold text-sm">
Operations in Progress
<span class="ml-2 bg-[#38BDF8]/20 px-2 py-1 rounded-full text-xs border border-[#38BDF8]/30" x-text="operations.length"></span>
</h3>
</div>
</div>
<button @click="collapsed = !collapsed"
class="text-[#94A3B8] hover:text-[#E5E7EB] transition-colors">
<i class="fas" :class="collapsed ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
</button>
</div>
<!-- Operations List -->
<div x-show="!collapsed"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 max-h-0"
x-transition:enter-end="opacity-100 max-h-96"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 max-h-96"
x-transition:leave-end="opacity-0 max-h-0"
class="space-y-2 overflow-y-auto max-h-96">
<template x-for="operation in operations" :key="operation.id">
<div class="bg-[#101827]/80 rounded-lg p-3 border border-[#1E293B] hover:border-[#38BDF8]/50 transition-colors">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-3 flex-1 min-w-0">
<!-- Icon based on type -->
<div class="flex-shrink-0">
<i class="text-lg"
:class="{
'fas fa-cube text-[#38BDF8]': !operation.isBackend && !operation.isDeletion,
'fas fa-cubes text-[#8B5CF6]': operation.isBackend && !operation.isDeletion,
'fas fa-trash text-red-400': operation.isDeletion
}"></i>
</div>
<!-- Operation details -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2">
<span class="text-[#E5E7EB] font-medium text-sm truncate" x-text="operation.name"></span>
<span class="flex-shrink-0 text-xs px-2 py-0.5 rounded border"
:class="{
'bg-[#38BDF8]/10 text-[#38BDF8]': !operation.isDeletion && !operation.isBackend,
'bg-[#8B5CF6]/10 text-[#8B5CF6]': !operation.isDeletion && operation.isBackend,
'bg-red-500/10 text-red-300': operation.isDeletion
}"
x-text="operation.isBackend ? 'Backend' : 'Model'"></span>
</div>
<!-- Status message -->
<div class="flex items-center space-x-2 mt-1">
<template x-if="operation.isQueued">
<span class="text-xs text-[#38BDF8] flex items-center">
<i class="fas fa-clock mr-1"></i>
Queued
</span>
</template>
<template x-if="operation.isCancelled">
<span class="text-xs text-red-400 flex items-center">
<i class="fas fa-ban mr-1"></i>
Cancelling...
</span>
</template>
<template x-if="!operation.isQueued && !operation.isCancelled && operation.message">
<span class="text-xs text-[#94A3B8] truncate" x-text="operation.message"></span>
</template>
</div>
</div>
<!-- Progress percentage and cancel button -->
<div class="flex-shrink-0 text-right flex items-center space-x-2">
<span class="text-[#E5E7EB] font-bold text-lg" x-text="operation.progress + '%'"></span>
<template x-if="operation.cancellable && !operation.isCancelled">
<button @click="cancelOperation(operation.jobID, operation.id)"
class="text-red-400 hover:text-red-300 transition-colors p-1 rounded hover:bg-red-500/20"
title="Cancel operation">
<i class="fas fa-times"></i>
</button>
</template>
<template x-if="operation.isCancelled">
<span class="text-red-400 text-xs flex items-center">
<i class="fas fa-ban mr-1"></i>
Cancelled
</span>
</template>
</div>
</div>
</div>
<!-- Progress bar -->
<div class="w-full bg-[#101827] rounded-full h-2 overflow-hidden border border-[#1E293B]">
<div class="h-full rounded-full transition-all duration-300"
:class="{
'bg-[#38BDF8]': !operation.isDeletion && !operation.isCancelled,
'bg-red-500': operation.isDeletion || operation.isCancelled
}"
:style="'width: ' + operation.progress + '%'">
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<script>
function operationsStatus() {
return {
operations: [],
collapsed: false,
pollInterval: null,
init() {
this.fetchOperations();
// Poll every 1s for smooth updates
this.pollInterval = setInterval(() => this.fetchOperations(), 1000);
},
async fetchOperations() {
try {
const response = await fetch('/api/operations');
if (!response.ok) {
throw new Error('Failed to fetch operations');
}
const data = await response.json();
const previousCount = this.operations.length;
this.operations = data.operations || [];
// If we had operations before and now we don't, refresh the page
if (previousCount > 0 && this.operations.length === 0) {
// Small delay to ensure the user sees the completion
setTimeout(() => {
window.location.reload();
}, 1000);
}
// Auto-collapse if there are many operations
if (this.operations.length > 5 && !this.collapsed) {
// Don't auto-collapse, let user control it
}
} catch (error) {
console.error('Error fetching operations:', error);
// Don't clear operations on error, just keep showing last known state
}
},
async cancelOperation(jobID, operationID) {
// Check if operation is already cancelled
const operation = this.operations.find(op => op.jobID === jobID);
if (operation && operation.isCancelled) {
// Already cancelled, no need to do anything
return;
}
try {
const response = await fetch(`/api/operations/${jobID}/cancel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json();
const errorMessage = error.error || 'Failed to cancel operation';
// Don't show alert for "already cancelled" - just update UI silently
if (errorMessage.includes('already cancelled')) {
if (operation) {
operation.isCancelled = true;
operation.cancellable = false;
}
this.fetchOperations();
return;
}
throw new Error(errorMessage);
}
// Update the operation status immediately
if (operation) {
operation.isCancelled = true;
operation.cancellable = false;
operation.message = 'Cancelling...';
}
// Refresh operations to get updated status
this.fetchOperations();
} catch (error) {
console.error('Error cancelling operation:', error);
// Only show alert if it's not an "already cancelled" error
if (!error.message.includes('already cancelled')) {
alert('Failed to cancel operation: ' + error.message);
}
}
},
destroy() {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
}
}
</script>
|