Merge pull request #605 from crawlab-team/release

Release
This commit is contained in:
Marvin Zhang
2020-03-02 10:43:48 +08:00
committed by GitHub
26 changed files with 406 additions and 19 deletions

View File

@@ -38,6 +38,7 @@ other:
version: 0.4.7
setting:
allowRegister: "N"
enableTutorial: "N"
notification:
mail:
server: ''

8
backend/entity/doc.go Normal file
View File

@@ -0,0 +1,8 @@
package entity
type DocItem struct {
Title string `json:"title"`
Url string `json:"url"`
Path string `json:"path"`
Children []DocItem `json:"children"`
}

View File

@@ -135,6 +135,8 @@ func main() {
// release版本
anonymousGroup.GET("/version", routes.GetVersion) // 获取发布的版本
anonymousGroup.GET("/releases/latest", routes.GetLatestRelease) // 获取最近发布的版本
// 文档
anonymousGroup.GET("/docs", routes.GetDocs) // 获取文档数据
}
authGroup := app.Group("/", middlewares.AuthorizationMiddleware())
{
@@ -259,7 +261,6 @@ func main() {
authGroup.GET("/git/branches", routes.GetGitBranches) // 获取 Git 分支
authGroup.GET("/git/public-key", routes.GetGitSshPublicKey) // 获取 SSH 公钥
}
}
// 路由ping

25
backend/routes/doc.go Normal file
View File

@@ -0,0 +1,25 @@
package routes
import (
"crawlab/services"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"net/http"
"runtime/debug"
)
func GetDocs(c *gin.Context) {
type ResData struct {
String string `json:"string"`
}
data, err := services.GetDocs()
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
Data: ResData{String:data},
})
}

View File

@@ -7,7 +7,8 @@ import (
)
type SettingBody struct {
AllowRegister string `json:"allow_register"`
AllowRegister string `json:"allow_register"`
EnableTutorial string `json:"enable_tutorial"`
}
func GetVersion(c *gin.Context) {
@@ -21,9 +22,10 @@ func GetVersion(c *gin.Context) {
}
func GetSetting(c *gin.Context) {
allowRegister := viper.GetString("setting.allowRegister")
body := SettingBody{AllowRegister: allowRegister}
body := SettingBody{
AllowRegister: viper.GetString("setting.allowRegister"),
EnableTutorial: viper.GetString("setting.enableTutorial"),
}
c.JSON(http.StatusOK, Response{
Status: "ok",

27
backend/services/doc.go Normal file
View File

@@ -0,0 +1,27 @@
package services
import (
"github.com/apex/log"
"github.com/imroc/req"
"runtime/debug"
)
func GetDocs() (data string, err error) {
// 获取远端数据
res, err := req.Get("https://docs.crawlab.cn/search_plus_index.json")
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return data, err
}
// 反序列化
data, err = res.ToString()
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return data, err
}
return data, nil
}

View File

@@ -24,6 +24,7 @@ services:
# CRAWLAB_TASK_WORKERS: 4 # number of task executors 任务执行器个数并行执行任务数
# CRAWLAB_SERVER_LANG_NODE: "Y" # whether to pre-install Node.js 预安装 Node.js 语言环境
# CRAWLAB_SETTING_ALLOWREGISTER: "N" # whether to allow user registration 是否允许用户注册
# CRAWLAB_SETTING_ENABLETUTORIAL: "N" # whether to enable tutorial 是否启用教程
# CRAWLAB_NOTIFICATION_MAIL_SERVER: smtp.exmaple.com # STMP server address STMP 服务器地址
# CRAWLAB_NOTIFICATION_MAIL_PORT: 465 # STMP server port STMP 服务器端口
# CRAWLAB_NOTIFICATION_MAIL_SENDEREMAIL: admin@exmaple.com # sender email 发送者邮箱

View File

@@ -1,3 +1,4 @@
NODE_ENV='development'
VUE_APP_BASE_URL=http://localhost:8000
VUE_APP_CRAWLAB_BASE_URL=https://api.crawlab.cn
VUE_APP_CRAWLAB_BASE_URL=https://api.crawlab.cn
VUE_APP_DOC_URL=http://docs.crawlab.cn

View File

@@ -1,3 +1,4 @@
NODE_ENV='production'
VUE_APP_BASE_URL=/api
VUE_APP_CRAWLAB_BASE_URL=https://api.crawlab.cn
VUE_APP_DOC_URL=http://docs.crawlab.cn

View File

@@ -1,3 +1,4 @@
NODE_ENV='test'
VUE_APP_BASE_URL='http://localhost:8000'
VUE_APP_CRAWLAB_BASE_URL=https://api.crawlab.cn
VUE_APP_DOC_URL=http://docs.crawlab.cn

View File

@@ -0,0 +1,114 @@
<template>
<el-tree
:data="docData"
ref="documentation-tree"
node-key="fullUrl"
>
<span class="custom-tree-node" :class="[data.active ? 'active' : '', `level-${data.level}`]"
slot-scope="{ node, data }">
<template v-if="data.level === 1 && data.children && data.children.length">
<span>{{node.label}}</span>
</template>
<template v-else>
<span>
<a :href="data.fullUrl" target="_blank" style="display: block" @click="onClickDocumentationLink">
{{node.label}}
</a>
</span>
</template>
</span>
</el-tree>
</template>
<script>
import {
mapState
} from 'vuex'
export default {
name: 'Documentation',
data () {
return {
data: []
}
},
computed: {
...mapState('doc', [
'docData'
]),
pathLv1 () {
if (this.$route.path === '/') return '/'
const m = this.$route.path.match(/(^\/\w+)/)
return m[1]
},
currentDoc () {
// find current doc
let currentDoc
for (let i = 0; i < this.$utils.doc.docs.length; i++) {
const doc = this.$utils.doc.docs[i]
if (this.pathLv1 === doc.path) {
currentDoc = doc
break
}
}
return currentDoc
}
},
watch: {
pathLv1 () {
this.update()
}
},
methods: {
isActiveNode (d) {
// check match
if (!this.currentDoc) return false
return !!d.url.match(this.currentDoc.pattern)
},
update () {
// expand related documentation list
setTimeout(() => {
this.docData.forEach(d => {
// parent node
const isActive = this.isActiveNode(d)
const node = this.$refs['documentation-tree'].getNode(d)
node.expanded = isActive
this.$set(d, 'active', isActive)
// child nodes
d.children.forEach(c => {
const node = this.$refs['documentation-tree'].getNode(c)
const isActive = this.isActiveNode(c)
if (!node.parent.expanded && isActive) {
node.parent.expanded = true
}
this.$set(c, 'active', isActive)
})
})
}, 100)
},
async getDocumentationData () {
// fetch api data
await this.$store.dispatch('doc/getDocData')
},
onClickDocumentationLink () {
this.$st.sendEv('全局', '点击右侧文档链接')
}
},
async created () {
},
mounted () {
this.update()
}
}
</script>
<style scoped>
.el-tree >>> .custom-tree-node.active {
color: #409eff;
/*text-decoration: underline;*/
}
.el-tree >>> .custom-tree-node.level-1 {
font-weight: bolder;
}
</style>

View File

@@ -448,6 +448,11 @@ export default {
'Key': '设置',
'Allow Sending Statistics': '允许发送统计信息',
'General': '通用',
'Enable Tutorial': '启用教程',
// 全局
'Related Documentation': '相关文档',
'Click to view related Documentation': '点击查看相关文档',
// 其他
tagsView: {

View File

@@ -17,6 +17,7 @@ import setting from './modules/setting'
import version from './modules/version'
import tour from './modules/tour'
import project from './modules/project'
import doc from './modules/doc'
import getters from './getters'
Vue.use(Vuex)
@@ -39,6 +40,7 @@ const store = new Vuex.Store({
version,
tour,
project,
doc,
// 统计
stats
},

View File

@@ -0,0 +1,61 @@
import request from '../../api/request'
const state = {
docData: []
}
const getters = {}
const mutations = {
SET_DOC_DATA (state, value) {
state.docData = value
}
}
const actions = {
async getDocData ({ commit }) {
const res = await request.get('/docs')
const data = JSON.parse(res.data.data.string)
// init cache
const cache = {}
// iterate paths
for (let path in data) {
if (data.hasOwnProperty(path)) {
const d = data[path]
if (path.match(/\/$/)) {
cache[path] = d
cache[path].children = []
} else if (path.match(/\.html$/)) {
const parentPath = path.split('/')[0] + '/'
cache[parentPath].children.push(d)
}
}
}
commit('SET_DOC_DATA', Object.values(cache).map(d => {
d.level = 1
d.label = d.title
d.fullUrl = process.env.VUE_APP_DOC_URL + '/' + d.url
if (d.children) {
d.children = d.children.map(c => {
c.level = 2
c.label = c.title
c.fullUrl = process.env.VUE_APP_DOC_URL + '/' + c.url
return c
})
}
return d
}))
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}

View File

@@ -16,6 +16,12 @@ const actions = {
async getSetting ({ commit }) {
const res = await request.get('/setting')
commit('SET_SETTING', res.data.data)
// set default enable_tutorial
const enableTutorial = res.data.data.enable_tutorial
if (!localStorage.getItem('enableTutorial')) {
localStorage.setItem('enableTutorial', enableTutorial === 'Y' ? '1' : '0')
}
}
}

28
frontend/src/utils/doc.js Normal file
View File

@@ -0,0 +1,28 @@
export default {
docs: [
{
path: '/projects',
pattern: '^Project'
},
{
path: '/spiders',
pattern: '^Spider|^SDK|^Integration|^CI/Git'
},
{
path: '/tasks',
pattern: '^Task|^Architecture/Task'
},
{
path: '/schedules',
pattern: '^Schedule'
},
{
path: '/nodes',
pattern: '^Node|^Architecture/Node'
},
{
path: '/setting',
pattern: '^Notification'
}
]
}

View File

@@ -3,11 +3,13 @@ import encrypt from './encrypt'
import tour from './tour'
import log from './log'
import scrapy from './scrapy'
import doc from './doc'
export default {
stats,
encrypt,
tour,
log,
scrapy
scrapy,
doc
}

View File

@@ -18,6 +18,10 @@ export default {
}
return !!data[tourName]
},
startTour: (vm, tourName) => {
if (localStorage.getItem('enableTutorial') === '0') return
vm.$tours[tourName].start()
},
finishTour: (tourName) => {
let data
try {

View File

@@ -1,12 +1,36 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"></div>
<!--sidebar-->
<sidebar class="sidebar-container"/>
<!--./sidebar-->
<!--main container-->
<div class="main-container">
<navbar/>
<tags-view/>
<app-main/>
</div>
<!--./main container-->
<!--documentation-->
<div class="documentation">
<el-tooltip
:content="$t('Click to view related Documentation')"
>
<i class="el-icon-question" @click="onClickDocumentation"></i>
</el-tooltip>
<el-drawer
:title="$t('Related Documentation')"
:visible.sync="isShowDocumentation"
:before-close="onCloseDocumentation"
size="300px"
>
<documentation/>
</el-drawer>
</div>
<!--./documentation-->
</div>
</template>
@@ -18,16 +42,23 @@ import {
TagsView
} from './components'
import ResizeMixin from './mixin/ResizeHandler'
import Documentation from '../../components/Documentation/Documentation'
export default {
name: 'Layout',
components: {
Documentation,
Navbar,
Sidebar,
TagsView,
AppMain
},
mixins: [ResizeMixin],
data () {
return {
isShowDocumentation: false
}
},
computed: {
sidebar () {
return this.$store.state.app.sidebar
@@ -47,7 +78,18 @@ export default {
methods: {
handleClickOutside () {
this.$store.dispatch('CloseSideBar', { withoutAnimation: false })
},
onClickDocumentation () {
this.isShowDocumentation = true
this.$st.sendEv('全局', '打开右侧文档')
},
onCloseDocumentation () {
this.isShowDocumentation = false
this.$st.sendEv('全局', '关闭右侧文档')
}
},
async created () {
await this.$store.dispatch('doc/getDocData')
}
}
</script>
@@ -77,4 +119,45 @@ export default {
position: absolute;
z-index: 999;
}
.documentation {
z-index: 9999;
position: fixed;
right: 25px;
bottom: 20px;
font-size: 32px;
cursor: pointer;
color: #909399;
}
</style>
<style scoped>
.documentation >>> .el-drawer__body {
overflow: auto;
}
.documentation >>> span[role="heading"]:focus {
outline: none;
}
.documentation >>> .el-tree-node__content {
height: 40px;
line-height: 40px;
}
.documentation >>> .custom-tree-node {
display: block;
width: 100%;
height: 40px;
line-height: 40px;
font-size: 14px;
}
.documentation >>> .custom-tree-node a {
display: block;
}
.documentation >>> .custom-tree-node:hover a {
text-decoration: underline;
}
</style>

View File

@@ -124,7 +124,7 @@ export default {
},
mounted () {
if (!this.$utils.tour.isFinishedTour('node-detail')) {
this.$tours['node-detail'].start()
this.$utils.tour.startTour(this, 'node-detail')
this.$st.sendEv('教程', '开始', 'node-detail')
}
}

View File

@@ -467,7 +467,7 @@ export default {
if (!this.$utils.tour.isFinishedTour('schedule-list-add')) {
setTimeout(() => {
this.$tours['schedule-list-add'].start()
this.$utils.tour.startTour(this, 'schedule-list-add')
this.$st.sendEv('教程', '开始', 'schedule-list-add')
}, 500)
}
@@ -617,7 +617,7 @@ export default {
mounted () {
if (!this.isDisabledSpiderSchedule) {
if (!this.$utils.tour.isFinishedTour('schedule-list')) {
this.$tours['schedule-list'].start()
this.$utils.tour.startTour(this, 'schedule-list')
this.$st.sendEv('教程', '开始', 'schedule-list')
}
}

View File

@@ -9,7 +9,7 @@
/>
<!--./tour-->
<!-- 新增全局变量 -->
<!--新增全局变量-->
<el-dialog :title="$t('Add Global Variable')"
:visible.sync="addDialogVisible">
<el-form label-width="80px" ref="globalVariableForm">
@@ -30,6 +30,7 @@
</el-form-item>
</el-form>
</el-dialog>
<!--./新增全局变量-->
<el-tabs v-model="activeName" @tab-click="tabActiveHandle" type="border-card">
<el-tab-pane :label="$t('General')" name="general">
@@ -49,6 +50,14 @@
inactive-color="#909399"
/>
</el-form-item>
<el-form-item :label="$t('Enable Tutorial')">
<el-switch
v-model="isEnableTutorial"
@change="onEnableTutorialChange"
active-color="#67C23A"
inactive-color="#909399"
/>
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button type="success" size="small" @click="saveUserInfo">
@@ -213,7 +222,8 @@ export default {
this.$utils.tour.nextStep('setting', currentStep)
}
},
isAllowSendingStatistics: localStorage.getItem('useStats') === '1'
isAllowSendingStatistics: localStorage.getItem('useStats') === '1',
isEnableTutorial: localStorage.getItem('enableTutorial') === '1'
}
},
computed: {
@@ -291,6 +301,10 @@ export default {
}
this.$message.success(this.$t('Saved successfully'))
localStorage.setItem('useStats', value ? '1' : '0')
},
onEnableTutorialChange (value) {
this.$message.success(this.$t('Saved successfully'))
localStorage.setItem('enableTutorial', value ? '1' : '0')
}
},
async created () {
@@ -300,7 +314,7 @@ export default {
},
mounted () {
if (!this.$utils.tour.isFinishedTour('setting')) {
this.$tours['setting'].start()
this.$utils.tour.startTour(this, 'setting')
this.$st.sendEv('教程', '开始', 'setting')
}
}

View File

@@ -208,7 +208,7 @@ export default {
if (!this.$utils.tour.isFinishedTour('spider-detail-config')) {
setTimeout(() => {
this.$tours['spider-detail-config'].start()
this.$utils.tour.startTour(this, 'spider-detail-config')
this.$st.sendEv('教程', '开始', 'spider-detail-config')
}, 100)
}
@@ -260,7 +260,7 @@ export default {
},
mounted () {
if (!this.$utils.tour.isFinishedTour('spider-detail')) {
this.$tours['spider-detail'].start()
this.$utils.tour.startTour(this, 'spider-detail')
this.$st.sendEv('教程', '开始', 'spider-detail')
}
}

View File

@@ -887,7 +887,7 @@ export default {
setTimeout(() => {
if (!this.$utils.tour.isFinishedTour('spider-list-add')) {
this.$tours['spider-list-add'].start()
this.$utils.tour.startTour(this, 'spider-list-add')
this.$st.sendEv('教程', '开始', 'spider-list-add')
}
}, 300)
@@ -1241,7 +1241,7 @@ export default {
})
if (!this.$utils.tour.isFinishedTour('spider-list')) {
this.$tours['spider-list'].start()
this.$utils.tour.startTour(this, 'spider-list')
this.$st.sendEv('教程', '开始', 'spider-list')
}
},

View File

@@ -202,7 +202,7 @@ export default {
},
mounted () {
if (!this.$utils.tour.isFinishedTour('task-detail')) {
this.$tours['task-detail'].start()
this.$utils.tour.startTour(this, 'task-detail')
this.$st.sendEv('教程', '开始', 'task-detail')
}
},

View File

@@ -412,7 +412,7 @@ export default {
}, 5000)
if (!this.$utils.tour.isFinishedTour('task-list')) {
this.$tours['task-list'].start()
this.$utils.tour.startTour(this, 'task-list')
this.$st.sendEv('教程', '开始', 'task-list')
}
},