mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-22 17:31:03 +01:00
@@ -1,3 +1,16 @@
|
||||
# 0.4.6 (2020-02-13)
|
||||
### 功能 / 优化
|
||||
- **Node.js SDK **. 用户可以将 SDK 应用到他们的 Node.js 爬虫中.
|
||||
- **日志管理优化**. 日志搜索,错误高亮,自动滚动.
|
||||
- **任务执行流程优化**. 允许用户在触发任务后跳转到该任务详情页.
|
||||
- **任务展示优化**. 在爬虫详情页的最近任务表格中加入了“参数”列. [#295](https://github.com/crawlab-team/crawlab/issues/295)
|
||||
- **爬虫列表优化**. 在爬虫列表页加入"更新时间"和"创建时间". [#505](https://github.com/crawlab-team/crawlab/issues/505)
|
||||
- **页面加载展位器**.
|
||||
|
||||
### Bug 修复
|
||||
- **定时任务配置失去焦点**. [#519](https://github.com/crawlab-team/crawlab/issues/519)
|
||||
- **无法用 CLI 工具上传爬虫**. [#524](https://github.com/crawlab-team/crawlab/issues/524)
|
||||
|
||||
# 0.4.5 (2020-02-03)
|
||||
### 功能 / 优化
|
||||
- **交互式教程**. 引导用户了解 Crawlab 的主要功能.
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
# 0.4.6 (2020-02-13)
|
||||
### Features / Enhancement
|
||||
- **SDK for Node.js**. Users can apply SDK in their Node.js spiders.
|
||||
- **Log Management Optimization**. Log search, error highlight, auto-scrolling.
|
||||
- **Task Execution Process Optimization**. Allow users to be redirected to task detail page after triggering a task.
|
||||
- **Task Display Optimization**. Added "Param" in the Latest Tasks table in the spider detail page. [#295](https://github.com/crawlab-team/crawlab/issues/295)
|
||||
- **Spider List Optimization**. Added "Update Time" and "Create Time" in spider list page.
|
||||
- **Page Loading Placeholder**.
|
||||
|
||||
### Bug Fixes
|
||||
- **Lost Focus in Schedule Configuration**. [#519](https://github.com/crawlab-team/crawlab/issues/519)
|
||||
- **Unable to Upload Spider using CLI**. [#524](https://github.com/crawlab-team/crawlab/issues/524)
|
||||
|
||||
# 0.4.5 (2020-02-03)
|
||||
### Features / Enhancement
|
||||
- **Interactive Tutorial**. Guide users through the main functionalities of Crawlab.
|
||||
|
||||
@@ -54,6 +54,9 @@ COPY --from=frontend-build /app/conf/crawlab.conf /etc/nginx/conf.d
|
||||
# working directory
|
||||
WORKDIR /app/backend
|
||||
|
||||
# timezone environment
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
# frontend port
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ COPY --from=frontend-build /app/conf/crawlab.conf /etc/nginx/conf.d
|
||||
# working directory
|
||||
WORKDIR /app/backend
|
||||
|
||||
# timezone environment
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
# frontend port
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ task:
|
||||
workers: 4
|
||||
other:
|
||||
tmppath: "/tmp"
|
||||
version: 0.4.5
|
||||
version: 0.4.6
|
||||
setting:
|
||||
allowRegister: "N"
|
||||
notification:
|
||||
|
||||
@@ -100,6 +100,9 @@ func PutTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 任务ID
|
||||
var taskIds []string
|
||||
|
||||
if reqBody.RunType == constants.RunTypeAllNodes {
|
||||
// 所有节点
|
||||
nodes, err := model.GetNodeList(nil)
|
||||
@@ -115,10 +118,13 @@ func PutTask(c *gin.Context) {
|
||||
UserId: services.GetCurrentUser(c).Id,
|
||||
}
|
||||
|
||||
if err := services.AddTask(t); err != nil {
|
||||
id, err := services.AddTask(t);
|
||||
if err != nil {
|
||||
HandleError(http.StatusInternalServerError, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
taskIds = append(taskIds, id)
|
||||
}
|
||||
} else if reqBody.RunType == constants.RunTypeRandom {
|
||||
// 随机
|
||||
@@ -127,10 +133,12 @@ func PutTask(c *gin.Context) {
|
||||
Param: reqBody.Param,
|
||||
UserId: services.GetCurrentUser(c).Id,
|
||||
}
|
||||
if err := services.AddTask(t); err != nil {
|
||||
id, err := services.AddTask(t);
|
||||
if err != nil {
|
||||
HandleError(http.StatusInternalServerError, c, err)
|
||||
return
|
||||
}
|
||||
taskIds = append(taskIds, id)
|
||||
} else if reqBody.RunType == constants.RunTypeSelectedNodes {
|
||||
// 指定节点
|
||||
for _, nodeId := range reqBody.NodeIds {
|
||||
@@ -141,16 +149,19 @@ func PutTask(c *gin.Context) {
|
||||
UserId: services.GetCurrentUser(c).Id,
|
||||
}
|
||||
|
||||
if err := services.AddTask(t); err != nil {
|
||||
id, err := services.AddTask(t);
|
||||
if err != nil {
|
||||
HandleError(http.StatusInternalServerError, c, err)
|
||||
return
|
||||
}
|
||||
taskIds = append(taskIds, id)
|
||||
}
|
||||
} else {
|
||||
HandleErrorF(http.StatusInternalServerError, c, "invalid run_type")
|
||||
return
|
||||
}
|
||||
HandleSuccess(c)
|
||||
|
||||
HandleSuccessData(c, taskIds)
|
||||
}
|
||||
|
||||
func DeleteTaskByStatus(c *gin.Context) {
|
||||
|
||||
@@ -37,7 +37,7 @@ func AddScheduleTask(s model.Schedule) func() {
|
||||
UserId: s.UserId,
|
||||
}
|
||||
|
||||
if err := AddTask(t); err != nil {
|
||||
if _, err := AddTask(t); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func AddScheduleTask(s model.Schedule) func() {
|
||||
Param: s.Param,
|
||||
UserId: s.UserId,
|
||||
}
|
||||
if err := AddTask(t); err != nil {
|
||||
if _, err := AddTask(t); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
debug.PrintStack()
|
||||
return
|
||||
@@ -65,7 +65,7 @@ func AddScheduleTask(s model.Schedule) func() {
|
||||
UserId: s.UserId,
|
||||
}
|
||||
|
||||
if err := AddTask(t); err != nil {
|
||||
if _, err := AddTask(t); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,7 +666,7 @@ func CancelTask(id string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddTask(t model.Task) error {
|
||||
func AddTask(t model.Task) (string, error) {
|
||||
// 生成任务ID
|
||||
id := uuid.NewV4()
|
||||
t.Id = id.String()
|
||||
@@ -683,17 +683,17 @@ func AddTask(t model.Task) error {
|
||||
if err := model.AddTask(t); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
debug.PrintStack()
|
||||
return err
|
||||
return t.Id, err
|
||||
}
|
||||
|
||||
// 加入任务队列
|
||||
if err := AssignTask(t); err != nil {
|
||||
log.Errorf(err.Error())
|
||||
debug.PrintStack()
|
||||
return err
|
||||
return t.Id, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return t.Id, nil
|
||||
}
|
||||
|
||||
func GetTaskEmailMarkdownContent(t model.Task, s model.Spider) string {
|
||||
|
||||
@@ -35,6 +35,8 @@ services:
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
# volumes:
|
||||
# - "/var/crawlab/log:/var/logs/crawlab" # log persistent 日志持久化
|
||||
worker:
|
||||
image: tikazyq/crawlab:latest
|
||||
container_name: worker
|
||||
@@ -45,6 +47,8 @@ services:
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
# volumes:
|
||||
# - "/var/crawlab/log:/var/logs/crawlab" # log persistent 日志持久化
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
restart: always
|
||||
@@ -55,7 +59,7 @@ services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
restart: always
|
||||
# command: redis --requirepass "password" # set redis password 设置 Redis 密码
|
||||
# command: redis-server --requirepass "password" # set redis password 设置 Redis 密码
|
||||
# volumes:
|
||||
# - "/opt/crawlab/redis/data:/data" # make data persistent 持久化
|
||||
# ports:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
|
||||
<title>Crawlab</title>
|
||||
</head>
|
||||
<body>
|
||||
<!--<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>-->
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "crawlab",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --ip=0.0.0.0 --mode=development",
|
||||
|
||||
@@ -9,11 +9,140 @@
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<style>
|
||||
#app {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#loading-placeholder {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#loading-placeholder .title-wrapper {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
#loading-placeholder .title {
|
||||
font-family: "Verdana", serif;
|
||||
font-weight: 600;
|
||||
font-size: 48px;
|
||||
color: #409EFF;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
letter-spacing: -5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span {
|
||||
display: inline-block;
|
||||
animation: change-shape 1s infinite;
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(1) {
|
||||
animation-delay: calc(1s / 7 * 0 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(2) {
|
||||
animation-delay: calc(1s / 7 * 1 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(3) {
|
||||
animation-delay: calc(1s / 7 * 2 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(4) {
|
||||
animation-delay: calc(1s / 7 * 3 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(5) {
|
||||
animation-delay: calc(1s / 7 * 4 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(6) {
|
||||
animation-delay: calc(1s / 7 * 5 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .title > span:nth-child(7) {
|
||||
animation-delay: calc(1s / 7 * 6 / 2);
|
||||
}
|
||||
|
||||
#loading-placeholder .loading-text {
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
font-family: "Verdana", serif;
|
||||
font-style: italic;
|
||||
color: #889aa4;
|
||||
font-size: 18px;
|
||||
animation: blink-loading 2s ease-in infinite;
|
||||
}
|
||||
|
||||
@keyframes blink-loading {
|
||||
0% {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes change-shape {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>Crawlab</title>
|
||||
</head>
|
||||
<body>
|
||||
<!--<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>-->
|
||||
<div id="loading-placeholder">
|
||||
<div style="margin-bottom: 150px">
|
||||
<div class="title-wrapper">
|
||||
<h3 class="title">
|
||||
<span>C</span>
|
||||
<span>R</span>
|
||||
<span>A</span>
|
||||
<span>W</span>
|
||||
<span>L</span>
|
||||
<span>A</span>
|
||||
<span>B</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="loading-text">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
@@ -30,13 +30,22 @@
|
||||
<el-input v-model="form.param" :placeholder="$t('Parameters')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="disclaimer-wrapper">
|
||||
<el-checkbox v-model="isAllowDisclaimer"/>
|
||||
<span style="margin-left: 5px">我已阅读并同意 <a href="javascript:" @click="onClickDisclaimer">《免责声明》</a> 所有内容</span>
|
||||
<div>
|
||||
<el-checkbox v-model="isAllowDisclaimer"/>
|
||||
<span style="margin-left: 5px">我已阅读并同意 <a href="javascript:" @click="onClickDisclaimer">《免责声明》</a> 所有内容</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-checkbox v-model="isRedirect"/>
|
||||
<span style="margin-left: 5px">跳转到任务详情页</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template slot="footer">
|
||||
<el-button type="plain" size="small" @click="$emit('close')">{{$t('Cancel')}}</el-button>
|
||||
<el-button type="primary" size="small" @click="onConfirm" :disabled="!isAllowDisclaimer">{{$t('Confirm')}}</el-button>
|
||||
<el-button type="primary" size="small" @click="onConfirm" :disabled="!isAllowDisclaimer">{{$t('Confirm')}}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -64,7 +73,8 @@ export default {
|
||||
param: '',
|
||||
nodeList: []
|
||||
},
|
||||
isAllowDisclaimer: true
|
||||
isAllowDisclaimer: true,
|
||||
isRedirect: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -72,20 +82,27 @@ export default {
|
||||
this.$emit('close')
|
||||
},
|
||||
onConfirm () {
|
||||
this.$refs['form'].validate(res => {
|
||||
if (!res) return
|
||||
this.$refs['form'].validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.$store.dispatch('spider/crawlSpider', {
|
||||
const res = await this.$store.dispatch('spider/crawlSpider', {
|
||||
spiderId: this.spiderId,
|
||||
nodeIds: this.form.nodeIds,
|
||||
param: this.form.param,
|
||||
runType: this.form.runType
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('A task has been scheduled successfully'))
|
||||
})
|
||||
|
||||
const id = res.data.data[0]
|
||||
|
||||
this.$message.success(this.$t('A task has been scheduled successfully'))
|
||||
|
||||
this.$emit('close')
|
||||
this.$st.sendEv('爬虫确认', '确认运行', this.form.runType)
|
||||
|
||||
if (this.isRedirect) {
|
||||
this.$router.push('/tasks/' + id)
|
||||
this.$st.sendEv('爬虫确认', '跳转到任务详情')
|
||||
}
|
||||
})
|
||||
},
|
||||
onClickDisclaimer () {
|
||||
|
||||
@@ -131,12 +131,16 @@
|
||||
|
||||
<div class="button-group-container">
|
||||
<div class="button-group">
|
||||
<el-button id="btn-run" type="danger" @click="onCrawl">{{$t('Run')}}</el-button>
|
||||
<el-button id="btn-run" size="small" type="danger" @click="onCrawl">
|
||||
{{$t('Run')}}
|
||||
</el-button>
|
||||
<!-- <el-button type="primary" @click="onExtractFields" v-loading="extractFieldsLoading">-->
|
||||
<!-- {{$t('ExtractFields')}}-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button type="warning" @click="onPreview" v-loading="previewLoading">{{$t('Preview')}}</el-button>-->
|
||||
<el-button id="btn-save" type="success" @click="onSave" v-loading="saveLoading">{{$t('Save')}}</el-button>
|
||||
<el-button id="btn-save" size="small" type="success" @click="onSave" v-loading="saveLoading">
|
||||
{{$t('Save')}}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
v-model="spiderForm.project_id"
|
||||
:placeholder="$t('Project')"
|
||||
filterable
|
||||
:disabled="isView"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in projectList"
|
||||
@@ -60,7 +61,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('Remark')">
|
||||
<el-input type="textarea" v-model="spiderForm.remark" :placeholder="$t('Remark')"/>
|
||||
<el-input type="textarea" v-model="spiderForm.remark" :placeholder="$t('Remark')" :disabled="isView"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
|
||||
@@ -11,6 +11,24 @@
|
||||
</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">
|
||||
<i class="el-icon-warning"></i>
|
||||
{{$t('Log with errors')}}
|
||||
</el-tag>
|
||||
</el-badge>
|
||||
<el-tag
|
||||
v-if="taskForm.status === 'finished' && taskForm.result_count === 0"
|
||||
type="danger"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
<i class="el-icon-warning"></i>
|
||||
{{$t('Empty results')}}
|
||||
</el-tag>
|
||||
</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 +46,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 +55,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>-->
|
||||
@@ -51,7 +69,9 @@
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row class="button-container">
|
||||
<el-button v-if="isRunning" type="danger" @click="onStop">{{$t('Stop')}}</el-button>
|
||||
<el-button v-if="isRunning" size="small" type="danger" @click="onStop" icon="el-icon-video-pause">
|
||||
{{$t('Stop')}}
|
||||
</el-button>
|
||||
<!--<el-button type="danger" @click="onRestart">Restart</el-button>-->
|
||||
</el-row>
|
||||
</div>
|
||||
@@ -59,7 +79,8 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
mapState,
|
||||
mapGetters
|
||||
} from 'vuex'
|
||||
import StatusTag from '../Status/StatusTag'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -69,7 +90,11 @@ export default {
|
||||
components: { StatusTag },
|
||||
computed: {
|
||||
...mapState('task', [
|
||||
'taskForm'
|
||||
'taskForm',
|
||||
'taskLog'
|
||||
]),
|
||||
...mapGetters('task', [
|
||||
'errorLogData'
|
||||
]),
|
||||
isRunning () {
|
||||
return ['pending', 'running'].includes(this.taskForm.status)
|
||||
@@ -99,6 +124,10 @@ 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')
|
||||
this.$st.sendEv('任务详情', '概览', '点击日志错误')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +1,14 @@
|
||||
<template>
|
||||
<div class="log-item">
|
||||
<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="isAnsi" v-html="data">{{data}}</span><span v-else="">{{data}}</span></div>
|
||||
<div class="line-content">
|
||||
<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>
|
||||
<span v-else v-html="dataHtml"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,6 +27,39 @@ export default {
|
||||
isAnsi: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchString: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
errorRegex: this.$utils.log.errorRegex
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataHtml () {
|
||||
let html = this.data.replace(this.errorRegex, ' <span style="font-weight: bolder; text-decoration: underline">$1</span> ')
|
||||
if (!this.searchString) return html
|
||||
html = html.replace(new RegExp(`(${this.searchString})`, 'gi'), '<mark>$1</mark>')
|
||||
return html
|
||||
},
|
||||
style () {
|
||||
let color = ''
|
||||
if (this.data.match(this.errorRegex)) {
|
||||
color = '#F56C6C'
|
||||
}
|
||||
return {
|
||||
color
|
||||
}
|
||||
},
|
||||
isLogEnd () {
|
||||
return this.data === '###LOG_END###'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,28 +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 {
|
||||
margin-right: 5px;
|
||||
animation: blink 2s ease-in infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,97 @@
|
||||
<template>
|
||||
<virtual-list
|
||||
class="log-view"
|
||||
:size="6"
|
||||
:remain="100"
|
||||
:item="item"
|
||||
:itemcount="logData.length"
|
||||
:itemprops="getItemProps"
|
||||
>
|
||||
</virtual-list>
|
||||
<div class="log-view-container">
|
||||
<div class="filter-wrapper">
|
||||
<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="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 LogItem from './LogItem'
|
||||
import {
|
||||
mapState,
|
||||
mapGetters
|
||||
} from 'vuex'
|
||||
import VirtualList from 'vue-virtual-scroll-list'
|
||||
import Convert from 'ansi-to-html'
|
||||
import hasAnsi from 'has-ansi'
|
||||
|
||||
import LogItem from './LogItem'
|
||||
|
||||
const convert = new Convert()
|
||||
export default {
|
||||
name: 'LogView',
|
||||
@@ -29,23 +106,52 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
item: LogItem
|
||||
item: LogItem,
|
||||
searchString: '',
|
||||
isToBottom: false,
|
||||
isScrolling: false,
|
||||
isScrolling2nd: false,
|
||||
errorRegex: this.$utils.log.errorRegex,
|
||||
currentOffset: 0,
|
||||
isErrorsCollapsed: true,
|
||||
isErrorCollapsing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
logData () {
|
||||
return this.data.split('\n')
|
||||
.map((d, i) => {
|
||||
return {
|
||||
index: i + 1,
|
||||
data: d
|
||||
}
|
||||
})
|
||||
...mapState('task', [
|
||||
'taskForm'
|
||||
]),
|
||||
...mapGetters('task', [
|
||||
'logData',
|
||||
'errorLogData'
|
||||
]),
|
||||
currentLogIndex: {
|
||||
get () {
|
||||
return this.$store.state.task.currentLogIndex
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
|
||||
}
|
||||
},
|
||||
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: {
|
||||
searchString () {
|
||||
this.$st.sendEv('任务详情', '日志', '搜索日志')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getItemProps (index) {
|
||||
const logItem = this.logData[index]
|
||||
const logItem = this.filteredLogData[index]
|
||||
const isAnsi = hasAnsi(logItem.data)
|
||||
return {
|
||||
// <item/> will render with itemProps.
|
||||
@@ -53,24 +159,162 @@ export default {
|
||||
props: {
|
||||
index: logItem.index,
|
||||
data: isAnsi ? convert.toHtml(logItem.data) : logItem.data,
|
||||
searchString: this.searchString,
|
||||
active: logItem.active,
|
||||
isAnsi
|
||||
}
|
||||
}
|
||||
},
|
||||
onToBottom () {
|
||||
if (this.isScrolling) return
|
||||
this.isToBottom = true
|
||||
},
|
||||
onScroll () {
|
||||
if (this.isScrolling2nd) {
|
||||
this.isToBottom = false
|
||||
}
|
||||
this.isScrolling = true
|
||||
setTimeout(() => {
|
||||
this.isScrolling2nd = true
|
||||
setTimeout(() => {
|
||||
this.isScrolling2nd = false
|
||||
}, 50)
|
||||
}, 50)
|
||||
setTimeout(() => {
|
||||
this.isScrolling = false
|
||||
}, 100)
|
||||
},
|
||||
toBottom () {
|
||||
this.$el.querySelector('.log-view').scrollTo({ top: 99999999 })
|
||||
setTimeout(() => {
|
||||
this.isToBottom = true
|
||||
}, 50)
|
||||
},
|
||||
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 () {
|
||||
this.currentLogIndex = 0
|
||||
this.handle = setInterval(() => {
|
||||
if (this.isToBottom) {
|
||||
this.toBottom()
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
destroyed () {
|
||||
clearInterval(this.handle)
|
||||
}
|
||||
}
|
||||
</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;
|
||||
min-height: 100%;
|
||||
overflow-y: scroll!important;
|
||||
margin-top: 0 !important;
|
||||
overflow-y: scroll !important;
|
||||
list-style: none;
|
||||
color: #A9B7C6;
|
||||
background: #2B2B2B;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-tag :type="type">
|
||||
<el-tag :type="type" class="status-tag">
|
||||
<i :class="icon"></i>
|
||||
{{$t(label)}}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -38,6 +39,16 @@ export default {
|
||||
return s.label
|
||||
}
|
||||
return 'NA'
|
||||
},
|
||||
icon () {
|
||||
if (this.status === 'finished') {
|
||||
return 'el-icon-check'
|
||||
} else if (this.status === 'running') {
|
||||
return 'el-icon-loading'
|
||||
} else if (this.status === 'error') {
|
||||
return 'el-icon-error'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
<el-table-column :key="col" :label="col" :property="col" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-popover trigger="hover" :content="getString(scope.row[col])" popper-class="cell-popover">
|
||||
<div slot="reference" class="wrapper">
|
||||
<div v-if="isUrl(scope.row[col])" slot="reference" class="wrapper">
|
||||
<a :href="getString(scope.row[col])" target="_blank" style="color: #409eff">
|
||||
{{getString(scope.row[col])}}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else slot="reference" class="wrapper">
|
||||
{{getString(scope.row[col])}}
|
||||
</div>
|
||||
</el-popover>
|
||||
@@ -68,6 +73,11 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isUrl (value) {
|
||||
if (!value) return false
|
||||
if (!value.match) return false
|
||||
return !!value.match(/^https?:\/\//)
|
||||
},
|
||||
onPageChange () {
|
||||
this.$emit('page-change', { pageNum: this.pageNum, pageSize: this.pageSize })
|
||||
},
|
||||
|
||||
@@ -208,6 +208,12 @@ export default {
|
||||
'Run Type': '运行类型',
|
||||
'Random': '随机',
|
||||
'Selected Nodes': '指定节点',
|
||||
'Search Log': '搜索日志',
|
||||
'Auto-Scroll': '自动滚动',
|
||||
'Updating log...': '正在更新日志...',
|
||||
'Error Count': '错误数',
|
||||
'Log with errors': '日志错误',
|
||||
'Empty results': '空结果',
|
||||
|
||||
// 任务列表
|
||||
'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
|
||||
},
|
||||
@@ -110,7 +141,6 @@ const actions = {
|
||||
})
|
||||
},
|
||||
getTaskLog ({ state, commit }, id) {
|
||||
commit('SET_TASK_LOG', '')
|
||||
return request.get(`/tasks/${id}/log`)
|
||||
.then(response => {
|
||||
commit('SET_TASK_LOG', response.data.data)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export default {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: white;
|
||||
|
||||
&.mobile.openSidebar {
|
||||
position: fixed;
|
||||
|
||||
@@ -362,7 +362,7 @@ const initCanvas = () => {
|
||||
</style>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
$bg: transparent;
|
||||
$bg: white;
|
||||
$dark_gray: #889aa4;
|
||||
$light_gray: #aaa;
|
||||
.login-container {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<!--tabs-->
|
||||
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="card">
|
||||
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="border-card">
|
||||
<el-tab-pane :label="$t('Overview')" name="overview">
|
||||
<node-overview></node-overview>
|
||||
</el-tab-pane>
|
||||
@@ -136,8 +136,8 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
margin-top: -7px;
|
||||
right: 30px;
|
||||
margin-top: 4px;
|
||||
/*float: right;*/
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -147,6 +147,8 @@ export default {
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #909399;
|
||||
font-weight: 100;
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!--add popup-->
|
||||
<el-dialog
|
||||
:title="isEditMode ? 'Edit Spider' : 'Add Spider'"
|
||||
:visible.sync="dialogVisible"
|
||||
width="60%"
|
||||
:before-close="onDialogClose">
|
||||
<el-form label-width="150px"
|
||||
:model="spiderForm"
|
||||
:rules="spiderFormRules"
|
||||
ref="spiderForm"
|
||||
label-position="right">
|
||||
<el-form-item label="Spider Name">
|
||||
<el-input v-model="spiderForm.name" placeholder="Spider Name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Source Folder">
|
||||
<el-input v-model="spiderForm.src" placeholder="Source Folder"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Execute Command">
|
||||
<el-input v-model="spiderForm.cmd" placeholder="Execute Command"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Spider Type">
|
||||
<el-select v-model="spiderForm.type" placeholder="Select Spider Type">
|
||||
<el-option :value="1" label="Scrapy"></el-option>
|
||||
<el-option :value="2" label="PySpider"></el-option>
|
||||
<el-option :value="3" label="WebMagic"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Language">
|
||||
<el-select v-model="spiderForm.lang" placeholder="Select Language">
|
||||
<el-option :value="1" label="Python"></el-option>
|
||||
<el-option :value="2" label="Nodejs"></el-option>
|
||||
<el-option :value="3" label="Java"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="onCancel">Cancel</el-button>
|
||||
<el-button type="primary" @click="onSubmit">Submit</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<!--filter-->
|
||||
<div class="filter">
|
||||
<el-input prefix-icon="el-icon-search"
|
||||
placeholder="Search"
|
||||
class="filter-search"
|
||||
v-model="filter.keyword"
|
||||
@change="onSearch">
|
||||
</el-input>
|
||||
<div class="right">
|
||||
<el-button type="success"
|
||||
icon="el-icon-refresh"
|
||||
class="refresh"
|
||||
@click="onRefresh">
|
||||
Refresh
|
||||
</el-button>
|
||||
<el-button type="primary"
|
||||
v-if="false"
|
||||
icon="el-icon-plus"
|
||||
class="add"
|
||||
@click="onAdd">
|
||||
Add Spider
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--table list-->
|
||||
<el-table :data="filteredTableData"
|
||||
class="table"
|
||||
height="500"
|
||||
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
|
||||
border>
|
||||
<template v-for="col in columns">
|
||||
<el-table-column v-if="col.name === 'type'"
|
||||
:key="col.name"
|
||||
:label="col.label"
|
||||
:sortable="col.sortable"
|
||||
align="center"
|
||||
:width="col.width">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.type === 'scrapy'">Scrapy</el-tag>
|
||||
<el-tag type="warning" v-else-if="scope.row.type === 'pyspider'">PySpider</el-tag>
|
||||
<el-tag type="info" v-else-if="scope.row.type === 'webmagic'">WebMagic</el-tag>
|
||||
<el-tag type="success" v-else>Other</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else-if="col.name === 'lang'"
|
||||
:key="col.name"
|
||||
:label="col.label"
|
||||
:sortable="col.sortable"
|
||||
align="center"
|
||||
:width="col.width">
|
||||
<template slot-scope="scope">
|
||||
<el-tag type="warning" v-if="scope.row.lang === 'python'">Python</el-tag>
|
||||
<el-tag type="warning" v-else-if="scope.row.lang === 'javascript'">JavaScript</el-tag>
|
||||
<el-tag type="info" v-else-if="scope.row.lang === 'java'">Java</el-tag>
|
||||
<el-tag type="danger" v-else-if="scope.row.lang === 'go'">Go</el-tag>
|
||||
<el-tag type="success" v-else>Other</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else
|
||||
:key="col.name"
|
||||
:property="col.name"
|
||||
:label="col.label"
|
||||
:sortable="col.sortable"
|
||||
align="center"
|
||||
:width="col.width">
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column label="Action" align="center" width="250">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip content="View" placement="top">
|
||||
<el-button type="info" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="Edit" placement="top">
|
||||
<el-button type="warning" icon="el-icon-edit" size="mini" @click="onView(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="Remove" placement="top">
|
||||
<el-button type="danger" icon="el-icon-delete" size="mini" @click="onRemove(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="Deploy" placement="top">
|
||||
<el-button type="primary" icon="fa fa-cloud" size="mini" @click="onDeploy(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="Run" placement="top">
|
||||
<el-button type="success" icon="fa fa-bug" size="mini" @click="onCrawl(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'SpiderList',
|
||||
data () {
|
||||
// let tableData = []
|
||||
// for (let i = 0; i < 50; i++) {
|
||||
// tableData.push({
|
||||
// spider_name: `Spider ${Math.floor(Math.random() * 100)}`,
|
||||
// spider_ip: '127.0.0.1:8888',
|
||||
// 'spider_description': `The ID of the spider is ${Math.random().toString().replace('0.', '')}`,
|
||||
// status: Math.floor(Math.random() * 100) % 2
|
||||
// })
|
||||
// }
|
||||
return {
|
||||
isEditMode: false,
|
||||
dialogVisible: false,
|
||||
filter: {
|
||||
keyword: ''
|
||||
},
|
||||
// tableData,
|
||||
columns: [
|
||||
{ name: 'name', label: 'Name', width: 'auto' },
|
||||
{ name: 'type', label: 'Spider Type', width: '160', sortable: true },
|
||||
{ name: 'lang', label: 'Language', width: '160', sortable: true },
|
||||
{ name: 'status', label: 'Status', width: '160' }
|
||||
],
|
||||
spiderFormRules: {
|
||||
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('spider', [
|
||||
'spiderList',
|
||||
'spiderForm'
|
||||
]),
|
||||
filteredTableData () {
|
||||
return this.spiderList.filter(d => {
|
||||
if (!this.filter.keyword) return true
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const colName = this.columns[i].name
|
||||
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSearch (value) {
|
||||
console.log(value)
|
||||
},
|
||||
onAdd () {
|
||||
this.$store.commit('spider/SET_SPIDER_FORM', {})
|
||||
this.isEditMode = false
|
||||
this.dialogVisible = true
|
||||
},
|
||||
onRefresh () {
|
||||
// this.$store.dispatch('spider/getSpiderList')
|
||||
},
|
||||
onSubmit () {
|
||||
const vm = this
|
||||
const formName = 'spiderForm'
|
||||
this.$refs[formName].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.isEditMode) {
|
||||
vm.$store.dispatch('spider/editSpider')
|
||||
} else {
|
||||
vm.$store.dispatch('spider/addSpider')
|
||||
}
|
||||
vm.dialogVisible = false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
onCancel () {
|
||||
this.$store.commit('spider/SET_SPIDER_FORM', {})
|
||||
this.dialogVisible = false
|
||||
},
|
||||
onDialogClose () {
|
||||
this.$store.commit('spider/SET_SPIDER_FORM', {})
|
||||
this.dialogVisible = false
|
||||
},
|
||||
onEdit (row) {
|
||||
console.log(row)
|
||||
this.isEditMode = true
|
||||
this.$store.commit('spider/SET_SPIDER_FORM', row)
|
||||
this.dialogVisible = true
|
||||
},
|
||||
onRemove (row) {
|
||||
this.$confirm(this.$t('Are you sure to delete this spider?)', this.$t('Notification'), {
|
||||
confirmButtonText: this.$t('Confirm'),
|
||||
cancelButtonText: this.$t('Cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$store.dispatch('spider/deleteSpider', row._id)
|
||||
.then(() => {
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'Deleted successfully'
|
||||
})
|
||||
})
|
||||
}))
|
||||
},
|
||||
onDeploy (row) {
|
||||
this.$store.dispatch('spider/getSpiderData', row._id)
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
|
||||
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderDeploy')
|
||||
},
|
||||
onCrawl (row) {
|
||||
this.$store.dispatch('spider/getSpiderData', row._id)
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
|
||||
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderRun')
|
||||
},
|
||||
onView (row) {
|
||||
this.$router.push(`/spiders/${row._id}`)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// this.$store.dispatch('spider/getSpiderList')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-dialog {
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.filter-search {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.add {
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 20px;
|
||||
border-radius: 5px;
|
||||
|
||||
.el-button {
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-confirm {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -31,7 +31,7 @@
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
<el-tabs v-model="activeName" @tab-click="tabActiveHandle">
|
||||
<el-tabs v-model="activeName" @tab-click="tabActiveHandle" type="border-card">
|
||||
<el-tab-pane :label="$t('General')" name="general">
|
||||
<el-form :model="userInfo" class="setting-form" ref="setting-form" label-width="200px" :rules="rules"
|
||||
inline-message>
|
||||
|
||||
@@ -399,7 +399,8 @@ export default {
|
||||
{ name: 'type', label: 'Spider Type', width: '120', sortable: true },
|
||||
{ name: 'last_status', label: 'Last Status', width: '120' },
|
||||
{ name: 'last_run_ts', label: 'Last Run', width: '140' },
|
||||
// { name: 'update_ts', label: 'Update Time', width: '140' },
|
||||
{ name: 'update_ts', label: 'Update Time', width: '140' },
|
||||
{ name: 'create_ts', label: 'Create Time', width: '140' },
|
||||
{ name: 'remark', label: 'Remark', width: '140' }
|
||||
],
|
||||
spiderFormRules: {
|
||||
|
||||
@@ -10,18 +10,16 @@
|
||||
<!--./tour-->
|
||||
|
||||
<!--tabs-->
|
||||
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="card">
|
||||
<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">
|
||||
<el-card>
|
||||
<log-view :data="taskLog"/>
|
||||
</el-card>
|
||||
<log-view/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('Results')" name="results">
|
||||
<div class="button-group">
|
||||
<el-button class="btn-download" type="primary" icon="el-icon-download" @click="downloadCSV">
|
||||
<el-button size="small" class="btn-download" type="primary" icon="el-icon-download" @click="downloadCSV">
|
||||
{{$t('Download CSV')}}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -44,7 +42,6 @@ import {
|
||||
import TaskOverview from '../../components/Overview/TaskOverview'
|
||||
import GeneralTableView from '../../components/TableView/GeneralTableView'
|
||||
import LogView from '../../components/ScrollView/LogView'
|
||||
import request from '../../api/request'
|
||||
|
||||
export default {
|
||||
name: 'TaskDetail',
|
||||
@@ -57,7 +54,6 @@ export default {
|
||||
return {
|
||||
activeTabName: 'overview',
|
||||
handle: undefined,
|
||||
taskLog: '',
|
||||
|
||||
// tutorial
|
||||
tourSteps: [
|
||||
@@ -139,7 +135,8 @@ export default {
|
||||
...mapState('task', [
|
||||
'taskForm',
|
||||
'taskResultsData',
|
||||
'taskResultsTotalCount'
|
||||
'taskResultsTotalCount',
|
||||
'taskLog'
|
||||
]),
|
||||
...mapGetters('task', [
|
||||
'taskResultsColumns'
|
||||
@@ -188,11 +185,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 () {
|
||||
|
||||
@@ -18,6 +18,8 @@ services:
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
volumes:
|
||||
- "/opt/crawlab/log:/var/logs/crawlab" # log persistent 日志持久化
|
||||
worker:
|
||||
image: "tikazyq/crawlab:master"
|
||||
environment:
|
||||
@@ -30,6 +32,8 @@ services:
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
volumes:
|
||||
- "/opt/crawlab/log:/var/logs/crawlab" # log persistent 日志持久化
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
restart: always
|
||||
|
||||
Reference in New Issue
Block a user