rapid interations of server side app and firefox extension
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Filters -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="filterStatus"
|
||||
:items="statusOptions"
|
||||
label="Status"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="filterSourceId"
|
||||
:items="sourceOptions"
|
||||
label="Source"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field
|
||||
v-model="filterFromDate"
|
||||
label="From Date"
|
||||
type="date"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field
|
||||
v-model="filterToDate"
|
||||
label="To Date"
|
||||
type="date"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Downloads Table -->
|
||||
<v-card>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="downloadsStore.downloads"
|
||||
:loading="downloadsStore.loading"
|
||||
:items-per-page="20"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="small">{{ getStatusIcon(item.status) }}</v-icon>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.url="{ item }">
|
||||
<span :title="item.url">{{ truncateUrl(item.url) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.error_type="{ item }">
|
||||
<v-chip
|
||||
v-if="item.error_type"
|
||||
color="error"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ formatErrorType(item.error_type) }}
|
||||
</v-chip>
|
||||
<span v-else class="text-grey">-</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.file_count="{ item }">
|
||||
<v-chip v-if="item.file_count > 0" color="success" size="small">
|
||||
{{ item.file_count }} files
|
||||
</v-chip>
|
||||
<span v-else class="text-grey">0</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.created_at="{ item }">
|
||||
{{ formatDate(item.created_at) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.duration="{ item }">
|
||||
{{ formatDuration(item) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
v-if="item.status === 'failed'"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="retryDownload(item)"
|
||||
:loading="retryingIds.includes(item.id)"
|
||||
>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
<v-tooltip activator="parent">Retry</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="showDetails(item)"
|
||||
>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
<v-tooltip activator="parent">Details</v-tooltip>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- Details Dialog -->
|
||||
<v-dialog v-model="detailsDialog" max-width="700">
|
||||
<v-card v-if="selectedDownload">
|
||||
<v-card-title>
|
||||
Download Details
|
||||
<v-chip
|
||||
:color="getStatusColor(selectedDownload.status)"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ selectedDownload.status }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-table density="compact">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-weight-bold" width="150">URL</td>
|
||||
<td style="word-break: break-all">{{ selectedDownload.url }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-weight-bold">Source ID</td>
|
||||
<td>{{ selectedDownload.source_id || 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-weight-bold">Files Downloaded</td>
|
||||
<td>{{ selectedDownload.file_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-weight-bold">Started</td>
|
||||
<td>{{ formatDate(selectedDownload.started_at) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-weight-bold">Completed</td>
|
||||
<td>{{ formatDate(selectedDownload.completed_at) }}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedDownload.error_type">
|
||||
<td class="font-weight-bold">Error Type</td>
|
||||
<td class="text-error">{{ formatErrorType(selectedDownload.error_type) }}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedDownload.error_message">
|
||||
<td class="font-weight-bold">Error Message</td>
|
||||
<td class="text-error">{{ selectedDownload.error_message }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<v-expansion-panels class="mt-4" v-if="selectedDownload.metadata">
|
||||
<v-expansion-panel v-if="selectedDownload.metadata.stdout">
|
||||
<v-expansion-panel-title>Output Log</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<pre class="text-caption" style="white-space: pre-wrap; max-height: 300px; overflow: auto">{{ selectedDownload.metadata.stdout }}</pre>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel v-if="selectedDownload.metadata.stderr">
|
||||
<v-expansion-panel-title>Error Log</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<pre class="text-caption text-error" style="white-space: pre-wrap; max-height: 300px; overflow: auto">{{ selectedDownload.metadata.stderr }}</pre>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="selectedDownload.status === 'failed'"
|
||||
color="primary"
|
||||
@click="retryDownload(selectedDownload); detailsDialog = false"
|
||||
>
|
||||
Retry Download
|
||||
</v-btn>
|
||||
<v-btn variant="text" @click="detailsDialog = false">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useDownloadsStore } from '../stores/downloads'
|
||||
import { useSourcesStore } from '../stores/sources'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
|
||||
const downloadsStore = useDownloadsStore()
|
||||
const sourcesStore = useSourcesStore()
|
||||
const notifications = useNotificationStore()
|
||||
|
||||
const filterStatus = ref(null)
|
||||
const filterSourceId = ref(null)
|
||||
const filterFromDate = ref(null)
|
||||
const filterToDate = ref(null)
|
||||
const detailsDialog = ref(false)
|
||||
const selectedDownload = ref(null)
|
||||
const retryingIds = ref([])
|
||||
|
||||
const statusOptions = [
|
||||
{ title: 'Pending', value: 'pending' },
|
||||
{ title: 'Running', value: 'running' },
|
||||
{ title: 'Completed', value: 'completed' },
|
||||
{ title: 'Failed', value: 'failed' },
|
||||
{ title: 'Skipped', value: 'skipped' },
|
||||
]
|
||||
|
||||
const sourceOptions = computed(() =>
|
||||
sourcesStore.sources.map(s => ({ title: s.name, value: s.id }))
|
||||
)
|
||||
|
||||
const headers = [
|
||||
{ title: 'Status', key: 'status', width: 120 },
|
||||
{ title: 'URL', key: 'url' },
|
||||
{ title: 'Error', key: 'error_type', width: 150 },
|
||||
{ title: 'Files', key: 'file_count', width: 100 },
|
||||
{ title: 'Date', key: 'created_at', width: 180 },
|
||||
{ title: 'Duration', key: 'duration', width: 100 },
|
||||
{ title: 'Actions', key: 'actions', width: 100, sortable: false },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadDownloads(),
|
||||
sourcesStore.fetchSources(),
|
||||
])
|
||||
})
|
||||
|
||||
watch([filterStatus, filterSourceId, filterFromDate, filterToDate], () => {
|
||||
loadDownloads()
|
||||
})
|
||||
|
||||
async function loadDownloads() {
|
||||
const params = {}
|
||||
if (filterStatus.value) params.status = filterStatus.value
|
||||
if (filterSourceId.value) params.source_id = filterSourceId.value
|
||||
if (filterFromDate.value) params.from_date = filterFromDate.value
|
||||
if (filterToDate.value) params.to_date = filterToDate.value
|
||||
await downloadsStore.fetchDownloads(params)
|
||||
}
|
||||
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
completed: 'mdi-check-circle',
|
||||
failed: 'mdi-alert-circle',
|
||||
running: 'mdi-loading',
|
||||
pending: 'mdi-clock-outline',
|
||||
skipped: 'mdi-skip-next',
|
||||
}
|
||||
return icons[status] || 'mdi-help-circle'
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
completed: 'success',
|
||||
failed: 'error',
|
||||
running: 'info',
|
||||
pending: 'warning',
|
||||
skipped: 'grey',
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-'
|
||||
return new Date(dateStr).toLocaleString()
|
||||
}
|
||||
|
||||
function formatDuration(download) {
|
||||
if (!download.started_at || !download.completed_at) return '-'
|
||||
const start = new Date(download.started_at)
|
||||
const end = new Date(download.completed_at)
|
||||
const seconds = Math.round((end - start) / 1000)
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
return `${minutes}m ${seconds % 60}s`
|
||||
}
|
||||
|
||||
function formatErrorType(errorType) {
|
||||
if (!errorType) return ''
|
||||
return errorType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||
}
|
||||
|
||||
function truncateUrl(url) {
|
||||
if (!url) return ''
|
||||
if (url.length <= 50) return url
|
||||
return url.substring(0, 47) + '...'
|
||||
}
|
||||
|
||||
function showDetails(download) {
|
||||
selectedDownload.value = download
|
||||
detailsDialog.value = true
|
||||
}
|
||||
|
||||
async function retryDownload(download) {
|
||||
retryingIds.value.push(download.id)
|
||||
try {
|
||||
await downloadsStore.retryDownload(download.id)
|
||||
notifications.success('Download queued for retry')
|
||||
await loadDownloads()
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to retry: ${error.message}`)
|
||||
} finally {
|
||||
retryingIds.value = retryingIds.value.filter(id => id !== download.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user