|
|
@@ -5,7 +5,7 @@
|
|
|
<h1>{{ t('pages.execution.title') }}</h1>
|
|
|
<p class="subtitle">{{ t('pages.execution.subtitle') }}</p>
|
|
|
</div>
|
|
|
- <el-button type="primary" @click="refresh">
|
|
|
+ <el-button type="primary" :loading="statsLoading || tableLoading" @click="refresh">
|
|
|
<el-icon><RefreshRight /></el-icon>
|
|
|
{{ t('common.refresh') }}
|
|
|
</el-button>
|
|
|
@@ -17,8 +17,8 @@
|
|
|
<SvgIcon name="play" size="20" />
|
|
|
</div>
|
|
|
<div>
|
|
|
- <div class="stat-value">186</div>
|
|
|
- <div class="stat-label">{{ t('pages.execution.stats.todayExecutions') }}</div>
|
|
|
+ <div class="stat-value">{{ stats.runTotal }}</div>
|
|
|
+ <div class="stat-label">{{ t('pages.execution.stats.recentExecutions') }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-card">
|
|
|
@@ -26,7 +26,7 @@
|
|
|
<SvgIcon name="check-circle" size="20" />
|
|
|
</div>
|
|
|
<div>
|
|
|
- <div class="stat-value">96.8%</div>
|
|
|
+ <div class="stat-value">{{ successRateText }}</div>
|
|
|
<div class="stat-label">{{ t('pages.execution.stats.successRate') }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -35,7 +35,7 @@
|
|
|
<SvgIcon name="clock" size="20" />
|
|
|
</div>
|
|
|
<div>
|
|
|
- <div class="stat-value">2.4s</div>
|
|
|
+ <div class="stat-value">{{ stats.avgElapsedSec }}s</div>
|
|
|
<div class="stat-label">{{ t('pages.execution.stats.avgDuration') }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -44,7 +44,7 @@
|
|
|
<SvgIcon name="x-circle" size="20" />
|
|
|
</div>
|
|
|
<div>
|
|
|
- <div class="stat-value">6</div>
|
|
|
+ <div class="stat-value">{{ stats.errorTotal }}</div>
|
|
|
<div class="stat-label">{{ t('pages.execution.stats.failedCount') }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -53,9 +53,11 @@
|
|
|
<div class="filters">
|
|
|
<el-input
|
|
|
v-model="keyword"
|
|
|
- :placeholder="t('pages.execution.filters.keyword')"
|
|
|
+ placeholder="关键词搜索"
|
|
|
clearable
|
|
|
class="filter-item"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ @clear="handleSearch"
|
|
|
>
|
|
|
<template #prefix>
|
|
|
<el-icon><Search /></el-icon>
|
|
|
@@ -66,30 +68,59 @@
|
|
|
:placeholder="t('pages.execution.filters.status')"
|
|
|
class="filter-item"
|
|
|
clearable
|
|
|
+ @change="handleSearch"
|
|
|
>
|
|
|
<el-option :label="t('common.all')" value="" />
|
|
|
- <el-option :label="t('common.status.success')" value="success" />
|
|
|
- <el-option :label="t('common.status.failed')" value="failed" />
|
|
|
<el-option :label="t('common.status.running')" value="running" />
|
|
|
+ <el-option :label="t('common.status.success')" value="finish" />
|
|
|
+ <el-option :label="t('common.status.failed')" value="error" />
|
|
|
</el-select>
|
|
|
- <el-date-picker
|
|
|
- v-model="dateRange"
|
|
|
- type="daterange"
|
|
|
- :range-separator="t('common.date.rangeSeparator')"
|
|
|
- :start-placeholder="t('common.date.start')"
|
|
|
- :end-placeholder="t('common.date.end')"
|
|
|
+ <el-select
|
|
|
+ v-model="source"
|
|
|
+ :placeholder="t('pages.execution.filters.source')"
|
|
|
class="filter-item"
|
|
|
- />
|
|
|
+ clearable
|
|
|
+ @change="handleSearch"
|
|
|
+ >
|
|
|
+ <el-option :label="t('common.all')" value="" />
|
|
|
+ <el-option :label="t('pages.execution.sources.manual')" value="manual" />
|
|
|
+ <el-option :label="t('pages.execution.sources.schedule')" value="schedule" />
|
|
|
+ <el-option :label="t('pages.execution.sources.webhook')" value="webhook" />
|
|
|
+ <el-option :label="t('pages.execution.sources.api')" value="api" />
|
|
|
+ </el-select>
|
|
|
<el-button @click="resetFilters">{{ t('common.reset') }}</el-button>
|
|
|
</div>
|
|
|
|
|
|
<div class="panel">
|
|
|
- <el-table :data="filteredExecutions" stripe style="width: 100%">
|
|
|
- <el-table-column prop="workflow" :label="t('pages.execution.table.workflow')" />
|
|
|
- <el-table-column prop="executionId" :label="t('pages.execution.table.executionId')" />
|
|
|
- <el-table-column prop="startedAt" :label="t('pages.execution.table.startedAt')" />
|
|
|
- <el-table-column prop="duration" :label="t('pages.execution.table.duration')" />
|
|
|
- <el-table-column prop="trigger" :label="t('pages.execution.table.trigger')" />
|
|
|
+ <el-table v-loading="tableLoading" :data="executions" stripe style="width: 100%">
|
|
|
+ <el-table-column
|
|
|
+ prop="agentName"
|
|
|
+ :label="t('pages.execution.table.agentName')"
|
|
|
+ min-width="220"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="executionId"
|
|
|
+ :label="t('pages.execution.table.executionId')"
|
|
|
+ min-width="260"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="startedAt"
|
|
|
+ :label="t('pages.execution.table.startedAt')"
|
|
|
+ min-width="180"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="endedAt"
|
|
|
+ :label="t('pages.execution.table.endedAt')"
|
|
|
+ min-width="180"
|
|
|
+ />
|
|
|
+ <el-table-column
|
|
|
+ prop="duration"
|
|
|
+ :label="t('pages.execution.table.duration')"
|
|
|
+ min-width="120"
|
|
|
+ />
|
|
|
+ <el-table-column prop="source" :label="t('pages.execution.filters.source')" />
|
|
|
<el-table-column :label="t('pages.execution.table.status')">
|
|
|
<template #default="scope">
|
|
|
<el-tag :type="statusType(scope.row.status)">
|
|
|
@@ -97,13 +128,19 @@
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column :label="t('pages.execution.table.actions')">
|
|
|
- <template #default>
|
|
|
- <el-button text size="small">{{ t('common.details') }}</el-button>
|
|
|
- <el-button text size="small">{{ t('common.retry') }}</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
</el-table>
|
|
|
+ <div v-if="pagination.totalCount" class="pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.pageIndex"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ background
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="pagination.totalCount"
|
|
|
+ @current-change="handlePageChange"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<div class="side-panels">
|
|
|
@@ -125,66 +162,190 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
import { RefreshRight, Search } from '@element-plus/icons-vue'
|
|
|
import { useI18n } from '@/composables/useI18n'
|
|
|
+import { agentLog } from '@repo/api-service'
|
|
|
+
|
|
|
+type AgentRunnerPageResponse = Awaited<ReturnType<typeof agentLog.postAgentGetAgentRunnerPageList>>
|
|
|
+type AgentRunnerPageItem = NonNullable<AgentRunnerPageResponse['result']>['model'][number]
|
|
|
+
|
|
|
+interface ExecutionRow {
|
|
|
+ agentName: string
|
|
|
+ executionId: string
|
|
|
+ startedAt: string
|
|
|
+ endedAt: string
|
|
|
+ duration: string
|
|
|
+ status: string
|
|
|
+}
|
|
|
|
|
|
-const { t, tm } = useI18n()
|
|
|
+const { t } = useI18n()
|
|
|
const keyword = ref('')
|
|
|
const status = ref('')
|
|
|
-const dateRange = ref<[Date, Date] | null>(null)
|
|
|
-
|
|
|
-const executions = computed(
|
|
|
- () =>
|
|
|
- (tm('pages.execution.executions') as Array<{
|
|
|
- workflow: string
|
|
|
- executionId: string
|
|
|
- startedAt: string
|
|
|
- duration: string
|
|
|
- trigger: string
|
|
|
- status: string
|
|
|
- }>) || []
|
|
|
-)
|
|
|
-
|
|
|
-const summary = computed(
|
|
|
- () => (tm('pages.execution.summary') as Array<{ label: string; value: string }>) || []
|
|
|
-)
|
|
|
-
|
|
|
-const tips = computed(() => (tm('pages.execution.tips') as string[]) || [])
|
|
|
-
|
|
|
-const filteredExecutions = computed(() => {
|
|
|
- const q = keyword.value.trim().toLowerCase()
|
|
|
- return executions.value.filter((item) => {
|
|
|
- const matchKeyword =
|
|
|
- !q || item.workflow.toLowerCase().includes(q) || item.executionId.toLowerCase().includes(q)
|
|
|
- const matchStatus = !status.value || item.status === status.value
|
|
|
- return matchKeyword && matchStatus
|
|
|
- })
|
|
|
+const source = ref('')
|
|
|
+const tableLoading = ref(false)
|
|
|
+const statsLoading = ref(false)
|
|
|
+const executions = ref<ExecutionRow[]>([])
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ pageIndex: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ totalCount: 0,
|
|
|
+ totalPages: 0
|
|
|
+})
|
|
|
+
|
|
|
+const stats = reactive({
|
|
|
+ runTotal: 0,
|
|
|
+ successTotal: 0,
|
|
|
+ errorTotal: 0,
|
|
|
+ processTotal: 0,
|
|
|
+ avgElapsedSec: 0
|
|
|
+})
|
|
|
+
|
|
|
+const sourceMap = {
|
|
|
+ manual: t('pages.execution.sources.manual'),
|
|
|
+ schedule: t('pages.execution.sources.schedule'),
|
|
|
+ webhook: t('pages.execution.sources.webhook'),
|
|
|
+ api: t('pages.execution.sources.api')
|
|
|
+}
|
|
|
+
|
|
|
+const summary = computed(() => [
|
|
|
+ { label: t('pages.execution.summaryLabels.processing'), value: `${stats.processTotal}` },
|
|
|
+ { label: t('pages.execution.summaryLabels.success'), value: `${stats.successTotal}` },
|
|
|
+ { label: t('pages.execution.summaryLabels.failed'), value: `${stats.errorTotal}` },
|
|
|
+ { label: t('pages.execution.summaryLabels.currentPage'), value: `${executions.value.length}` }
|
|
|
+])
|
|
|
+
|
|
|
+const tips = computed(() => [
|
|
|
+ t('pages.execution.tips.0'),
|
|
|
+ t('pages.execution.tips.1'),
|
|
|
+ t('pages.execution.tips.2')
|
|
|
+])
|
|
|
+
|
|
|
+const successRateText = computed(() => {
|
|
|
+ if (!stats.runTotal) return '0%'
|
|
|
+ return `${((stats.successTotal / stats.runTotal) * 100).toFixed(1)}%`
|
|
|
+})
|
|
|
+
|
|
|
+const allStatuses = 'running,finish,error'
|
|
|
+const allSources = 'manual,schedule,webhook,api'
|
|
|
+
|
|
|
+const formatDuration = (value?: number | string | null) => {
|
|
|
+ const duration = Number(value || 0)
|
|
|
+ if (!Number.isFinite(duration) || duration <= 0) return '-'
|
|
|
+ if (duration < 1000) return `${Math.round(duration)}ms`
|
|
|
+ if (duration < 60_000) {
|
|
|
+ const seconds = duration / 1000
|
|
|
+ return `${Number.isInteger(seconds) ? seconds.toFixed(0) : seconds.toFixed(1)}s`
|
|
|
+ }
|
|
|
+ const minutes = Math.floor(duration / 60_000)
|
|
|
+ const seconds = Math.round((duration % 60_000) / 1000)
|
|
|
+ return `${minutes}m ${seconds}s`
|
|
|
+}
|
|
|
+
|
|
|
+const normalizeStatus = (value?: string) => `${value || ''}`.trim().toLowerCase()
|
|
|
+const getStatusQuery = () => status.value || allStatuses
|
|
|
+const getSourceQuery = () => source.value || allSources
|
|
|
+
|
|
|
+const mapExecutionRow = (item: AgentRunnerPageItem): ExecutionRow => ({
|
|
|
+ agentName: item.agentName || '-',
|
|
|
+ executionId: item.runnerId || '-',
|
|
|
+ startedAt: item.beginDate || '-',
|
|
|
+ endedAt: item.endDate || '-',
|
|
|
+ duration: formatDuration(item.useTime),
|
|
|
+ status: normalizeStatus(item.status),
|
|
|
+ source: sourceMap[item.source]
|
|
|
})
|
|
|
|
|
|
const statusType = (value: string) => {
|
|
|
- if (value === 'success') return 'success'
|
|
|
- if (value === 'failed') return 'danger'
|
|
|
+ if (value === 'finish') return 'success'
|
|
|
+ if (value === 'error') return 'danger'
|
|
|
if (value === 'running') return 'warning'
|
|
|
return 'info'
|
|
|
}
|
|
|
|
|
|
const statusText = (value: string) => {
|
|
|
- if (value === 'success') return t('common.status.success')
|
|
|
- if (value === 'failed') return t('common.status.failed')
|
|
|
+ if (value === 'finish') return t('common.status.success')
|
|
|
+ if (value === 'error') return t('common.status.failed')
|
|
|
if (value === 'running') return t('common.status.running')
|
|
|
return t('common.status.unknown')
|
|
|
}
|
|
|
|
|
|
-const refresh = () => {
|
|
|
- console.log('refresh executions')
|
|
|
+const loadStats = async () => {
|
|
|
+ statsLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await agentLog.postAgentGetBefore7DayAgentRunnerStatic({})
|
|
|
+ if (!res.isSuccess || !res.result) throw new Error('load stats failed')
|
|
|
+ const avgElapsedValue = (res.result as typeof res.result & { avg_elapsed_time?: string })
|
|
|
+ .avg_elapsed_time
|
|
|
+ stats.runTotal = res.result.run_total || 0
|
|
|
+ stats.successTotal = res.result.success_total || 0
|
|
|
+ stats.errorTotal = res.result.error_total || 0
|
|
|
+ stats.processTotal = res.result.process_total || 0
|
|
|
+ stats.avgElapsedSec = Number(res.result.avg_elapsed_sec || avgElapsedValue || 0)
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error)
|
|
|
+ ElMessage.error(t('pages.execution.messages.loadStatsFailed'))
|
|
|
+ } finally {
|
|
|
+ statsLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadExecutions = async (pageIndex = pagination.pageIndex) => {
|
|
|
+ tableLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await agentLog.postAgentGetAgentRunnerPageList({
|
|
|
+ keyword: keyword.value.trim(),
|
|
|
+ status: getStatusQuery(),
|
|
|
+ source: getSourceQuery(),
|
|
|
+ pageIndex,
|
|
|
+ pageSize: pagination.pageSize
|
|
|
+ })
|
|
|
+ if (!res.isSuccess || !res.result) throw new Error('load executions failed')
|
|
|
+ executions.value = (res.result.model || []).map(mapExecutionRow)
|
|
|
+ pagination.pageIndex = res.result.currentPage || pageIndex
|
|
|
+ pagination.pageSize = res.result.pageSize || pagination.pageSize
|
|
|
+ pagination.totalCount = res.result.totalCount || 0
|
|
|
+ pagination.totalPages = res.result.totalPages || 0
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error)
|
|
|
+ ElMessage.error(t('pages.execution.messages.loadListFailed'))
|
|
|
+ } finally {
|
|
|
+ tableLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const refresh = async () => {
|
|
|
+ await Promise.all([loadStats(), loadExecutions(pagination.pageIndex)])
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = async () => {
|
|
|
+ pagination.pageIndex = 1
|
|
|
+ await loadExecutions(1)
|
|
|
+}
|
|
|
+
|
|
|
+const handlePageChange = async (page: number) => {
|
|
|
+ pagination.pageIndex = page
|
|
|
+ await loadExecutions(page)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = async (size: number) => {
|
|
|
+ pagination.pageSize = size
|
|
|
+ pagination.pageIndex = 1
|
|
|
+ await loadExecutions(1)
|
|
|
}
|
|
|
|
|
|
const resetFilters = () => {
|
|
|
keyword.value = ''
|
|
|
status.value = ''
|
|
|
- dateRange.value = null
|
|
|
+ source.value = ''
|
|
|
+ handleSearch()
|
|
|
}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ refresh()
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
@@ -285,6 +446,12 @@ const resetFilters = () => {
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
}
|
|
|
|
|
|
+ .pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
.side-panels {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|