mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-22 17:31:03 +01:00
优化日志,错误标记
This commit is contained in:
@@ -11,6 +11,15 @@
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Status')">
|
||||
<status-tag :status="taskForm.status"/>
|
||||
<el-badge
|
||||
v-if="errorLogData.length > 0"
|
||||
:value="errorLogData.length"
|
||||
style="margin-left:10px; cursor:pointer;"
|
||||
>
|
||||
<el-tag type="danger" @click="onClickLogWithErrors">
|
||||
{{$t('Log with errors')}}
|
||||
</el-tag>
|
||||
</el-badge>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Log File Path')">
|
||||
<el-input v-model="taskForm.log_path" placeholder="Log File Path" disabled></el-input>
|
||||
@@ -28,7 +37,7 @@
|
||||
<el-input :value="getTime(taskForm.finish_ts)" placeholder="Finish Time" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Wait Duration (sec)')">
|
||||
<el-input :value="getWaitDuration(taskForm)" placeholder="Wait Duration" disabled></el-input>
|
||||
<el-input :value="getWaitDuration(taskForm)" placeholder="Wait Duration" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Runtime Duration (sec)')">
|
||||
<el-input :value="getRuntimeDuration(taskForm)" placeholder="Runtime Duration" disabled></el-input>
|
||||
@@ -37,7 +46,7 @@
|
||||
<el-input :value="getTotalDuration(taskForm)" placeholder="Runtime Duration" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Results Count')">
|
||||
<el-input v-model="taskForm.result_count" placeholder="Results Count" disabled></el-input>
|
||||
<el-input v-model="taskForm.result_count" placeholder="Results Count" disabled></el-input>
|
||||
</el-form-item>
|
||||
<!--<el-form-item :label="$t('Average Results Count per Second')">-->
|
||||
<!--<el-input v-model="taskForm.avg_num_results" placeholder="Average Results Count per Second" disabled>-->
|
||||
@@ -59,7 +68,8 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
mapState,
|
||||
mapGetters
|
||||
} from 'vuex'
|
||||
import StatusTag from '../Status/StatusTag'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -69,7 +79,11 @@ export default {
|
||||
components: { StatusTag },
|
||||
computed: {
|
||||
...mapState('task', [
|
||||
'taskForm'
|
||||
'taskForm',
|
||||
'taskLog'
|
||||
]),
|
||||
...mapGetters('task', [
|
||||
'errorLogData'
|
||||
]),
|
||||
isRunning () {
|
||||
return ['pending', 'running'].includes(this.taskForm.status)
|
||||
@@ -99,6 +113,9 @@ export default {
|
||||
getTotalDuration (row) {
|
||||
if (!row.finish_ts || row.finish_ts.match('^0001')) return 'NA'
|
||||
return dayjs(row.finish_ts).diff(row.create_ts, 'second')
|
||||
},
|
||||
onClickLogWithErrors () {
|
||||
this.$emit('click-log')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-col :span="12" style="padding-right: 20px;">
|
||||
<el-row class="task-info-overview-wrapper wrapper">
|
||||
<h4 class="title">{{$t('Task Info')}}</h4>
|
||||
<task-info-view/>
|
||||
<task-info-view @click-log="() => $emit('click-log')"/>
|
||||
</el-row>
|
||||
<el-row style="border-bottom:1px solid #e4e7ed;margin:0 0 20px 0;padding-bottom:20px;"/>
|
||||
</el-col>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="log-item" :style="style">
|
||||
<div class="log-item" :style="style" :class="`log-item-${index} ${active ? 'active' : ''}`">
|
||||
<div class="line-no">{{index}}</div>
|
||||
<div class="line-content">
|
||||
<span v-if="isLogEnd" style="color: #E6A23C" class="loading-text">
|
||||
{{$t('Updating log...')}}
|
||||
<span v-if="isLogEnd" style="color: #E6A23C">
|
||||
<span class="loading-text">{{$t('Updating log...')}}</span>
|
||||
<i class="el-icon-loading"></i>
|
||||
</span>
|
||||
<span v-else-if="isAnsi" v-html="dataHtml"></span>
|
||||
@@ -31,13 +31,15 @@ export default {
|
||||
searchString: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const token = ' :,.'
|
||||
return {
|
||||
errorRegex: new RegExp(`(?:[${token}]|^)((?:error|exception|traceback)s?)(?:[${token}]|$)`, 'gi')
|
||||
// errorRegex: new RegExp('(error|exception|traceback)', 'gi')
|
||||
errorRegex: this.$utils.log.errorRegex
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -65,42 +67,56 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.log-item {
|
||||
display: table;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.log-item:hover {
|
||||
background: rgba(55, 57, 59, 0.5);
|
||||
}
|
||||
|
||||
.log-item:first-child .line-no {
|
||||
padding-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.log-item .line-no {
|
||||
display: table-cell;
|
||||
display: inline-block;
|
||||
width: 70px;
|
||||
color: #A9B7C6;
|
||||
background: #313335;
|
||||
padding-right: 10px;
|
||||
text-align: right;
|
||||
flex-basis: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.log-item.active .line-no {
|
||||
background: #E6A23C;
|
||||
color: white;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.log-item .line-content {
|
||||
padding-left: 10px;
|
||||
display: table-cell;
|
||||
/*display: inline-block;*/
|
||||
word-break: break-word;
|
||||
flex-basis: calc(100% - 50px);
|
||||
display: inline-block;
|
||||
width: calc(100% - 70px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
animation: blink;
|
||||
margin-right: 5px;
|
||||
animation: blink 2s ease-in infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +1,90 @@
|
||||
<template>
|
||||
<div class="log-view-wrapper">
|
||||
<div class="log-view-container">
|
||||
<div class="filter-wrapper">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon="el-icon-download"
|
||||
style="margin-right: 10px"
|
||||
:disabled="isToBottom"
|
||||
@click="onAutoScroll"
|
||||
>
|
||||
{{$t('Auto-Scroll')}}
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="searchString"
|
||||
size="small"
|
||||
suffix-icon="el-icon-search"
|
||||
:placeholder="$t('Search Log')"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<div class="left">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon="el-icon-download"
|
||||
style="margin-right: 10px"
|
||||
:disabled="isToBottom"
|
||||
@click="onAutoScroll"
|
||||
>
|
||||
{{$t('Auto-Scroll')}}
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="searchString"
|
||||
size="small"
|
||||
suffix-icon="el-icon-search"
|
||||
:placeholder="$t('Search Log')"
|
||||
style="width: 240px; margin-right: 10px"
|
||||
/>
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-badge
|
||||
v-if="errorLogData.length > 0"
|
||||
:value="errorLogData.length"
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="el-icon-warning-outline"
|
||||
@click="toggleErrors"
|
||||
>
|
||||
{{$t('Error Count')}}
|
||||
</el-button>
|
||||
</el-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-view-wrapper" ref="log-view-wrapper">
|
||||
<virtual-list
|
||||
class="log-view"
|
||||
ref="log-view"
|
||||
:size="6"
|
||||
:remain="100"
|
||||
:item="item"
|
||||
:itemcount="filteredLogData.length"
|
||||
:itemprops="getItemProps"
|
||||
:tobottom="onToBottom"
|
||||
:onscroll="onScroll"
|
||||
/>
|
||||
<div class="content">
|
||||
<div
|
||||
class="log-view-wrapper"
|
||||
:class="isErrorsCollapsed ? 'errors-collapsed' : ''"
|
||||
>
|
||||
<virtual-list
|
||||
class="log-view"
|
||||
ref="log-view"
|
||||
:start="currentLogIndex - 1"
|
||||
:offset="0"
|
||||
:size="18"
|
||||
:remain="remainSize"
|
||||
:item="item"
|
||||
:itemcount="filteredLogData.length"
|
||||
:itemprops="getItemProps"
|
||||
:tobottom="onToBottom"
|
||||
:onscroll="onScroll"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="!isErrorsCollapsed && !isErrorCollapsing"
|
||||
class="errors-wrapper"
|
||||
:class="isErrorsCollapsed ? 'collapsed' : ''"
|
||||
>
|
||||
<ul class="error-list">
|
||||
<li
|
||||
v-for="item in errorLogData"
|
||||
:key="item.index"
|
||||
class="error-item"
|
||||
:class="currentLogIndex === item.index ? 'active' : ''"
|
||||
@click="onClickError(item)"
|
||||
>
|
||||
<span class="line-no">
|
||||
{{item.index}}
|
||||
</span>
|
||||
<span class="line-content">
|
||||
{{item.data}}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
mapState,
|
||||
mapGetters
|
||||
} from 'vuex'
|
||||
import VirtualList from 'vue-virtual-scroll-list'
|
||||
import Convert from 'ansi-to-html'
|
||||
@@ -63,38 +110,38 @@ export default {
|
||||
searchString: '',
|
||||
isToBottom: false,
|
||||
isScrolling: false,
|
||||
isScrolling2nd: false
|
||||
isScrolling2nd: false,
|
||||
errorRegex: this.$utils.log.errorRegex,
|
||||
currentOffset: 0,
|
||||
isErrorsCollapsed: true,
|
||||
isErrorCollapsing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('task', [
|
||||
'taskForm'
|
||||
]),
|
||||
logData () {
|
||||
const data = this.data.split('\n')
|
||||
.map((d, i) => {
|
||||
return {
|
||||
index: i + 1,
|
||||
data: d
|
||||
}
|
||||
})
|
||||
if (this.taskForm && this.taskForm.status === 'running') {
|
||||
data.push({
|
||||
index: data.length + 1,
|
||||
data: '###LOG_END###'
|
||||
})
|
||||
data.push({
|
||||
index: data.length + 2,
|
||||
data: ''
|
||||
})
|
||||
...mapGetters('task', [
|
||||
'logData',
|
||||
'errorLogData'
|
||||
]),
|
||||
currentLogIndex: {
|
||||
get () {
|
||||
return this.$store.state.task.currentLogIndex
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
|
||||
}
|
||||
return data
|
||||
},
|
||||
filteredLogData () {
|
||||
return this.logData.filter(d => {
|
||||
if (!this.searchString) return true
|
||||
return !!d.data.toLowerCase().match(this.searchString.toLowerCase())
|
||||
})
|
||||
},
|
||||
remainSize () {
|
||||
const height = document.querySelector('body').clientHeight
|
||||
return (height - 240) / 18
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -113,6 +160,7 @@ export default {
|
||||
index: logItem.index,
|
||||
data: isAnsi ? convert.toHtml(logItem.data) : logItem.data,
|
||||
searchString: this.searchString,
|
||||
active: logItem.active,
|
||||
isAnsi
|
||||
}
|
||||
}
|
||||
@@ -144,6 +192,23 @@ export default {
|
||||
},
|
||||
onAutoScroll () {
|
||||
this.toBottom()
|
||||
},
|
||||
toggleErrors () {
|
||||
this.isErrorsCollapsed = !this.isErrorsCollapsed
|
||||
this.isErrorCollapsing = true
|
||||
setTimeout(() => {
|
||||
this.isErrorCollapsing = false
|
||||
}, 300)
|
||||
},
|
||||
onClickError (item) {
|
||||
this.currentLogIndex = item.index
|
||||
this.isToBottom = false
|
||||
const handle = setInterval(() => {
|
||||
this.isToBottom = false
|
||||
}, 10)
|
||||
setTimeout(() => {
|
||||
clearInterval(handle)
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
@@ -151,7 +216,7 @@ export default {
|
||||
if (this.isToBottom) {
|
||||
this.toBottom()
|
||||
}
|
||||
}, 100)
|
||||
}, 500)
|
||||
},
|
||||
destroyed () {
|
||||
clearInterval(this.handle)
|
||||
@@ -160,15 +225,94 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.log-view-wrapper {
|
||||
float: left;
|
||||
flex-basis: calc(100% - 240px);
|
||||
width: calc(100% - 300px);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.log-view-wrapper.errors-collapsed {
|
||||
flex-basis: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.log-view {
|
||||
margin-top: 0 !important;
|
||||
overflow-y: scroll !important;
|
||||
list-style: none;
|
||||
color: #A9B7C6;
|
||||
background: #2B2B2B;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.filter-wrapper {
|
||||
margin-bottom: 10px;
|
||||
.errors-wrapper {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-basis: 240px;
|
||||
width: 300px;
|
||||
transition: opacity 0.3s;
|
||||
border-top: 1px solid #DCDFE6;
|
||||
border-right: 1px solid #DCDFE6;
|
||||
border-bottom: 1px solid #DCDFE6;
|
||||
height: calc(100vh - 240px);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.errors-wrapper.collapsed {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.errors-wrapper .error-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.errors-wrapper .error-list .error-item {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
/*height: 18px;*/
|
||||
border-bottom: 1px solid white;
|
||||
padding: 5px 0;
|
||||
background: #F56C6C;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.errors-wrapper .error-list .error-item.active {
|
||||
background: #E6A23C;
|
||||
font-weight: bolder;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.errors-wrapper .error-list .error-item:hover {
|
||||
font-weight: bolder;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.errors-wrapper .error-list .error-item .line-no {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.errors-wrapper .error-list .error-item .line-content {
|
||||
display: inline;
|
||||
width: calc(100% - 70px);
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -211,6 +211,8 @@ export default {
|
||||
'Search Log': '搜索日志',
|
||||
'Auto-Scroll': '自动滚动',
|
||||
'Updating log...': '正在更新日志...',
|
||||
'Error Count': '错误数',
|
||||
'Log with errors': '日志错误',
|
||||
|
||||
// 任务列表
|
||||
'Node': '节点',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '../../api/request'
|
||||
import utils from '../../utils'
|
||||
|
||||
const state = {
|
||||
// TaskList
|
||||
@@ -6,6 +7,7 @@ const state = {
|
||||
taskListTotalCount: 0,
|
||||
taskForm: {},
|
||||
taskLog: '',
|
||||
currentLogIndex: 0,
|
||||
taskResultsData: [],
|
||||
taskResultsColumns: [],
|
||||
taskResultsTotalCount: 0,
|
||||
@@ -36,6 +38,32 @@ const getters = {
|
||||
}
|
||||
}
|
||||
return keys
|
||||
},
|
||||
logData (state) {
|
||||
const data = state.taskLog.split('\n')
|
||||
.map((d, i) => {
|
||||
return {
|
||||
index: i + 1,
|
||||
data: d,
|
||||
active: state.currentLogIndex === i + 1
|
||||
}
|
||||
})
|
||||
if (state.taskForm && state.taskForm.status === 'running') {
|
||||
data.push({
|
||||
index: data.length + 1,
|
||||
data: '###LOG_END###'
|
||||
})
|
||||
data.push({
|
||||
index: data.length + 1,
|
||||
data: ''
|
||||
})
|
||||
}
|
||||
return data
|
||||
},
|
||||
errorLogData (state, getters) {
|
||||
return getters.logData.filter(d => {
|
||||
return d.data.match(utils.log.errorRegex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +77,9 @@ const mutations = {
|
||||
SET_TASK_LOG (state, value) {
|
||||
state.taskLog = value
|
||||
},
|
||||
SET_CURRENT_LOG_INDEX (state, value) {
|
||||
state.currentLogIndex = value
|
||||
},
|
||||
SET_TASK_RESULTS_DATA (state, value) {
|
||||
state.taskResultsData = value
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import stats from './stats'
|
||||
import encrypt from './encrypt'
|
||||
import tour from './tour'
|
||||
import log from './log'
|
||||
|
||||
export default {
|
||||
stats,
|
||||
encrypt,
|
||||
tour
|
||||
tour,
|
||||
log
|
||||
}
|
||||
|
||||
5
frontend/src/utils/log.js
Normal file
5
frontend/src/utils/log.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const regexToken = ' :,.'
|
||||
|
||||
export default {
|
||||
errorRegex: new RegExp(`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`, 'gi')
|
||||
}
|
||||
@@ -12,10 +12,10 @@
|
||||
<!--tabs-->
|
||||
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="border-card">
|
||||
<el-tab-pane :label="$t('Overview')" name="overview">
|
||||
<task-overview/>
|
||||
<task-overview @click-log="activeTabName = 'log'"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('Log')" name="log">
|
||||
<log-view :data="taskLog"/>
|
||||
<log-view/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('Results')" name="results">
|
||||
<div class="button-group">
|
||||
@@ -55,7 +55,6 @@ export default {
|
||||
return {
|
||||
activeTabName: 'overview',
|
||||
handle: undefined,
|
||||
taskLog: '',
|
||||
|
||||
// tutorial
|
||||
tourSteps: [
|
||||
@@ -137,7 +136,8 @@ export default {
|
||||
...mapState('task', [
|
||||
'taskForm',
|
||||
'taskResultsData',
|
||||
'taskResultsTotalCount'
|
||||
'taskResultsTotalCount',
|
||||
'taskLog'
|
||||
]),
|
||||
...mapGetters('task', [
|
||||
'taskResultsColumns'
|
||||
@@ -186,11 +186,7 @@ export default {
|
||||
this.$st.sendEv('任务详情', '结果', '下载CSV')
|
||||
},
|
||||
getTaskLog () {
|
||||
if (this.$route.params.id) {
|
||||
request.get(`/tasks/${this.$route.params.id}/log`).then(response => {
|
||||
this.taskLog = response.data.data
|
||||
})
|
||||
}
|
||||
this.$store.dispatch('task/getTaskLog', this.$route.params.id)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
||||
Reference in New Issue
Block a user