| | |
| | <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> |
| |
|
| | |
| | <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"> |
| | |
| | <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> |
| | |
| | |
| | <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> |
| | |
| | |
| | <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> |
| | |
| | |
| | <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> |
| | |
| | |
| | <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(); |
| | |
| | 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 (previousCount > 0 && this.operations.length === 0) { |
| | |
| | setTimeout(() => { |
| | window.location.reload(); |
| | }, 1000); |
| | } |
| | |
| | |
| | if (this.operations.length > 5 && !this.collapsed) { |
| | |
| | } |
| | } catch (error) { |
| | console.error('Error fetching operations:', error); |
| | |
| | } |
| | }, |
| | |
| | async cancelOperation(jobID, operationID) { |
| | |
| | const operation = this.operations.find(op => op.jobID === jobID); |
| | if (operation && operation.isCancelled) { |
| | |
| | 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'; |
| | |
| | |
| | if (errorMessage.includes('already cancelled')) { |
| | if (operation) { |
| | operation.isCancelled = true; |
| | operation.cancellable = false; |
| | } |
| | this.fetchOperations(); |
| | return; |
| | } |
| | |
| | throw new Error(errorMessage); |
| | } |
| | |
| | |
| | if (operation) { |
| | operation.isCancelled = true; |
| | operation.cancellable = false; |
| | operation.message = 'Cancelling...'; |
| | } |
| | |
| | |
| | this.fetchOperations(); |
| | } catch (error) { |
| | console.error('Error cancelling operation:', error); |
| | |
| | if (!error.message.includes('already cancelled')) { |
| | alert('Failed to cancel operation: ' + error.message); |
| | } |
| | } |
| | }, |
| | |
| | destroy() { |
| | if (this.pollInterval) { |
| | clearInterval(this.pollInterval); |
| | } |
| | } |
| | } |
| | } |
| | </script> |
| |
|