push front-end code
5
orion-ops-vue/.editorconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
3
orion-ops-vue/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
VUE_APP_BASE_API='/orion/api'
|
||||
VUE_APP_WATERMARK=true
|
||||
VUE_APP_DEMO_MODE=false
|
||||
3
orion-ops-vue/.env.dev
Normal file
@@ -0,0 +1,3 @@
|
||||
VUE_APP_BASE_API='/orion/api'
|
||||
VUE_APP_WATERMARK=true
|
||||
VUE_APP_DEMO_MODE=false
|
||||
3
orion-ops-vue/.env.production
Normal file
@@ -0,0 +1,3 @@
|
||||
VUE_APP_BASE_API='/orion/api'
|
||||
VUE_APP_WATERMARK=true
|
||||
VUE_APP_DEMO_MODE=false
|
||||
23
orion-ops-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
5
orion-ops-vue/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19
orion-ops-vue/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
60
orion-ops-vue/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "orion-ops-vue",
|
||||
"version": "1.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --mode dev",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"build:demo": "set VUE_APP_DEMO_MODE=true&& npm run build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/g2": "^4.1.48",
|
||||
"ant-design-vue": "^1.7.8",
|
||||
"asciinema-player": "^3.0.1",
|
||||
"axios": "^0.23.0",
|
||||
"core-js": "^3.8.3",
|
||||
"js-md5": "^0.7.3",
|
||||
"lodash": "^4.17.21",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.1",
|
||||
"vue2-ace-editor": "^0.0.15",
|
||||
"xterm": "^4.14.1",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-search": "^0.8.1",
|
||||
"xterm-addon-web-links": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"less": "^4.0.0",
|
||||
"less-loader": "^8.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"eslintIgnore": ["*"],
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
BIN
orion-ops-vue/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
13
orion-ops-vue/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>orion-ops</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
25
orion-ops-vue/src/App.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '../src/css/common.less'
|
||||
import '../src/css/layout.less'
|
||||
import '../src/css/table.less'
|
||||
import '../src/css/component.less'
|
||||
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data: function() {
|
||||
return {
|
||||
locale: zhCN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
BIN
orion-ops-vue/src/assets/left-top-bg.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
orion-ops-vue/src/assets/login-btn.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
20
orion-ops-vue/src/assets/logo.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.cls-1{fill:url(#SVGID_1_);}
|
||||
.cls-2{fill:url(#SVGID_2_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" x1="0.32" y1="15.03" x2="20.16" y2="15.03" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#23b6b6"/>
|
||||
<stop offset="1" stop-color="#189c98"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="SVGID_2_" x1="11.84" y1="16.97" x2="31.68" y2="16.97" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#08589b"/>
|
||||
<stop offset="1" stop-color="#2167b2"/>
|
||||
</linearGradient>
|
||||
<path class="cls-1"
|
||||
d="M20,17.37a1.56,1.56,0,0,0-2.13-.65l-7.56,4.07A4.65,4.65,0,0,1,4,18.91H4a4.65,4.65,0,0,1,1.88-6.3L13.6,8.44a1.56,1.56,0,0,0,.64-2.1h0a1.56,1.56,0,0,0-2.13-.65l-8,4.3a7.24,7.24,0,0,0-2.94,9.82l.51.94a7.24,7.24,0,0,0,9.82,2.94l7.81-4.22a1.56,1.56,0,0,0,.65-2.1Z"/>
|
||||
<path class="cls-2"
|
||||
d="M12,14.63a1.56,1.56,0,0,0,2.13.65l7.56-4.07A4.65,4.65,0,0,1,28,13.09h0a4.65,4.65,0,0,1-1.88,6.3L18.4,23.56a1.56,1.56,0,0,0-.64,2.1h0a1.56,1.56,0,0,0,2.13.65l8-4.3a7.24,7.24,0,0,0,2.94-9.82l-.51-.94a7.24,7.24,0,0,0-9.82-2.94l-7.81,4.22a1.56,1.56,0,0,0-.65,2.1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
orion-ops-vue/src/assets/logo_100.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
orion-ops-vue/src/assets/logo_horizontal.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
orion-ops-vue/src/assets/right-bottom-bg.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
154
orion-ops-vue/src/components/app/AddAppEnvModal.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="500"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="key">
|
||||
<a-input v-decorator="decorators.key" :disabled="id != null" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="value" style="margin-bottom: 12px;">
|
||||
<a-textarea v-decorator="decorators.value" :autoSize="{minRows: 4}" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" style="margin-bottom: 0;">
|
||||
<a-textarea v-decorator="decorators.description" allowClear/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pick } from 'lodash'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 20 }
|
||||
}
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
key: ['key', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入key'
|
||||
}, {
|
||||
max: 128,
|
||||
message: 'key长度不能大于128位'
|
||||
}]
|
||||
}],
|
||||
value: ['value', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入value'
|
||||
}, {
|
||||
max: 2048,
|
||||
message: 'value长度不能大于2048位'
|
||||
}]
|
||||
}],
|
||||
description: ['description', {
|
||||
rules: [{
|
||||
max: 64,
|
||||
message: '描述长度不能大于64位'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddAppEnvModal',
|
||||
data: function() {
|
||||
return {
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
appId: null,
|
||||
profileId: null,
|
||||
layout,
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add(appId, profileId) {
|
||||
this.title = '新增变量'
|
||||
this.appId = appId
|
||||
this.profileId = profileId
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改变量'
|
||||
this.$api.getAppEnvDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.visible = true
|
||||
this.id = row.id
|
||||
this.record = pick(Object.assign({}, row), 'key', 'value', 'description')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addAppEnv({
|
||||
...values,
|
||||
appId: this.appId,
|
||||
profileId: this.profileId
|
||||
})
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateAppEnv({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.appId = null
|
||||
this.profileId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
176
orion-ops-vue/src/components/app/AddAppForm.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="名称" hasFeedback>
|
||||
<a-input v-decorator="decorators.name" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="唯一标识" hasFeedback>
|
||||
<a-input v-decorator="decorators.tag" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="版本仓库">
|
||||
<a-select placeholder="请选择" v-decorator="decorators.repoId" style="width: calc(100% - 45px)" allowClear>
|
||||
<a-select-option v-for="repo in repoList" :key="repo.id" :value="repo.id">
|
||||
<span>{{ repo.name }}</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a class="reload-repo" title="刷新" @click="getRepositoryList">
|
||||
<a-icon type="reload"/>
|
||||
</a>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" style="margin-bottom: 0;">
|
||||
<a-textarea v-decorator="decorators.description" allowClear/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pick } from 'lodash'
|
||||
import { REPOSITORY_STATUS } from '@/lib/enum'
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
name: ['name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入名称'
|
||||
}, {
|
||||
max: 32,
|
||||
message: '名称长度不能大于32位'
|
||||
}]
|
||||
}],
|
||||
tag: ['tag', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入唯一标识'
|
||||
}, {
|
||||
max: 32,
|
||||
message: '唯一标识长度不能大于32位'
|
||||
}]
|
||||
}],
|
||||
repoId: ['repoId'],
|
||||
description: ['description', {
|
||||
rules: [{
|
||||
max: 64,
|
||||
message: '描述长度不能大于64位'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddAppForm',
|
||||
props: {
|
||||
layout: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 17 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
id: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
repoList: [],
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.$api.getAppDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.id = row.id
|
||||
if (!row.repoId) {
|
||||
row.repoId = undefined
|
||||
}
|
||||
this.record = pick(Object.assign({}, row), 'name', 'tag', 'repoId', 'description')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
},
|
||||
async getRepositoryList() {
|
||||
this.$api.getRepositoryList({
|
||||
limit: 10000,
|
||||
status: REPOSITORY_STATUS.OK.value
|
||||
}).then(({ data }) => {
|
||||
if (data && data.rows && data.rows.length) {
|
||||
this.repoList = data.rows.map(s => {
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.$emit('loading', true)
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
this.$emit('loading', false)
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addApp({ ...values })
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateApp({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
this.$emit('loading', false)
|
||||
},
|
||||
close() {
|
||||
this.loading = false
|
||||
this.$emit('loading', false)
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getRepositoryList()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reload-repo {
|
||||
margin-left: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
60
orion-ops-vue/src/components/app/AddAppModal.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="660"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<!-- 表单 -->
|
||||
<AddAppForm ref="from"
|
||||
@close="close"
|
||||
@loading="l => loading = l"
|
||||
@added="() => $emit('added')"
|
||||
@updated="() => $emit('updated')"/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddAppForm from './AddAppForm'
|
||||
|
||||
export default {
|
||||
name: 'AddAppModal',
|
||||
components: {
|
||||
AddAppForm
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
title: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.title = '添加应用'
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.from.add()
|
||||
})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改应用'
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.from.update(id)
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.$refs.from.check()
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
162
orion-ops-vue/src/components/app/AddAppProfileModal.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="650"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="环境名称" hasFeedback>
|
||||
<a-input v-decorator="decorators.name" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="唯一标识" hasFeedback>
|
||||
<a-input v-decorator="decorators.tag" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否需要审核">
|
||||
<a-radio-group v-decorator="decorators.releaseAudit" buttonStyle="solid">
|
||||
<a-radio-button v-for="type in PROFILE_AUDIT_STATUS" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-decorator="decorators.description" allowClear/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pick } from 'lodash'
|
||||
import { PROFILE_AUDIT_STATUS } from '@/lib/enum'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 17 }
|
||||
}
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
name: ['name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入环境名称'
|
||||
}, {
|
||||
max: 32,
|
||||
message: '环境名称长度不能大于32位'
|
||||
}]
|
||||
}],
|
||||
tag: ['tag', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入唯一标识'
|
||||
}, {
|
||||
max: 32,
|
||||
message: '唯一标识长度不能大于32位'
|
||||
}]
|
||||
}],
|
||||
releaseAudit: ['releaseAudit', {
|
||||
initialValue: 2,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择是否需要审核'
|
||||
}]
|
||||
}],
|
||||
description: ['description', {
|
||||
rules: [{
|
||||
max: 64,
|
||||
message: '描述长度不能大于64位'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddAppProfileModal',
|
||||
data: function() {
|
||||
return {
|
||||
PROFILE_AUDIT_STATUS,
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
layout,
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.title = '新增环境'
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改环境'
|
||||
this.$api.getProfileDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.visible = true
|
||||
this.id = row.id
|
||||
this.record = pick(Object.assign({}, row), 'name', 'tag', 'releaseAudit', 'description')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addProfile({
|
||||
...values
|
||||
})
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateProfile({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
269
orion-ops-vue/src/components/app/AddPipelineModal.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="578"
|
||||
:maskStyle="{opacity: 0.8, animation: 'none'}"
|
||||
:dialogStyle="{top: '64px', padding: 0}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
:footer="false"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form-model v-bind="layout">
|
||||
<!-- 流水线名称 -->
|
||||
<a-form-model-item label="名称" class="name-form-item" required>
|
||||
<a-input class="name-input" v-model="record.name" :maxLength="32" allowClear/>
|
||||
</a-form-model-item>
|
||||
<!-- 流水线描述 -->
|
||||
<a-form-model-item label="描述" class="description-form-item">
|
||||
<a-textarea class="description-input" v-model="record.description" :maxLength="64" allowClear/>
|
||||
</a-form-model-item>
|
||||
<!-- 流水线操作 -->
|
||||
<a-form-model-item label="操作" class="detail-form-item" required>
|
||||
<div class="pipeline-details-wrapper">
|
||||
<template v-for="(detail, index) of record.details">
|
||||
<div class="pipeline-detail" :key="detail.id" v-if="detail.visible">
|
||||
<!-- 操作 -->
|
||||
<a-input-group compact>
|
||||
<!-- 操作类型 -->
|
||||
<a-select style="width: 80px" v-model="detail.stageType" placeholder="请选择">
|
||||
<a-select-option v-for="stage of STAGE_TYPE"
|
||||
:key="stage.value"
|
||||
:value="stage.value">
|
||||
{{ stage.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- 操作应用 -->
|
||||
<a-select style="width: 271px" v-model="detail.appId" placeholder="请选择应用">
|
||||
<a-select-option v-for="app of appList"
|
||||
:key="app.id"
|
||||
:value="app.id">
|
||||
{{ app.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
<!-- 操作 -->
|
||||
<div class="pipeline-detail-handler">
|
||||
<a-button-group v-if="record.details.length > 1">
|
||||
<a-button title="移除" @click="removeOption(index)" icon="minus-circle"/>
|
||||
<a-button title="上移" v-if="index !== 0" @click="swapOption(index, index - 1)" icon="arrow-up"/>
|
||||
<a-button title="下移" v-if="index !== record.details.length - 1" @click="swapOption(index + 1, index )" icon="arrow-down"/>
|
||||
</a-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 添加 -->
|
||||
<a-button type="dashed" class="add-option-button" @click="addOption">
|
||||
添加应用操作
|
||||
</a-button>
|
||||
<!-- 保存 -->
|
||||
<a-button type="primary" class="save-button" @click="check">
|
||||
保存
|
||||
</a-button>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CONFIG_STATUS, STAGE_TYPE } from '@/lib/enum'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 21 }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddPipelineModal',
|
||||
data: function() {
|
||||
return {
|
||||
STAGE_TYPE,
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
profileId: null,
|
||||
record: {},
|
||||
appList: [],
|
||||
layout
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.title = '新增流水线'
|
||||
this.initRecord({
|
||||
details: []
|
||||
})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '配置流水线'
|
||||
this.$api.getAppPipelineDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.id = row.id
|
||||
this.record = row
|
||||
// 设置明细显示
|
||||
this.record.details.forEach(detail => {
|
||||
detail.visible = true
|
||||
})
|
||||
this.visible = true
|
||||
// 读取当前环境
|
||||
const activeProfile = this.$storage.get(this.$storage.keys.ACTIVE_PROFILE)
|
||||
if (!activeProfile) {
|
||||
this.$message.warning('请先维护应用环境')
|
||||
return
|
||||
}
|
||||
this.profileId = JSON.parse(activeProfile).id
|
||||
// 加载应用列表
|
||||
this.loadAppList()
|
||||
},
|
||||
loadAppList() {
|
||||
this.loading = true
|
||||
this.app = null
|
||||
this.appList = []
|
||||
this.$api.getAppList({
|
||||
profileId: this.profileId,
|
||||
limit: 10000
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
if (data.rows && data.rows.length) {
|
||||
this.appList = data.rows.filter(s => s.isConfig === CONFIG_STATUS.CONFIGURED.value)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
addOption() {
|
||||
this.record.details.push({
|
||||
visible: true,
|
||||
id: Date.now(),
|
||||
appId: undefined,
|
||||
stageType: STAGE_TYPE.BUILD.value
|
||||
})
|
||||
},
|
||||
removeOption(index) {
|
||||
this.record.details.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.record.details.splice(index, 1)
|
||||
})
|
||||
},
|
||||
swapOption(index, target) {
|
||||
const temp = this.record.details[target]
|
||||
this.$set(this.record.details, target, this.record.details[index])
|
||||
this.$set(this.record.details, index, temp)
|
||||
},
|
||||
check() {
|
||||
if (!this.record.name || !this.record.name.trim().length) {
|
||||
this.$message.warning('请输入流水线名称')
|
||||
return
|
||||
}
|
||||
if (!this.record.details.length) {
|
||||
this.$message.warning('请设置流水线操作')
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < this.record.details.length; i++) {
|
||||
const detail = this.record.details[i]
|
||||
if (!detail.stageType) {
|
||||
this.$message.warning(`请选择操作类型 [${i + 1}]`)
|
||||
return
|
||||
}
|
||||
if (!detail.appId) {
|
||||
this.$message.warning(`请选择操作应用 [${i + 1}]`)
|
||||
return
|
||||
}
|
||||
}
|
||||
this.loading = true
|
||||
// 设置数据
|
||||
const req = {
|
||||
id: this.id,
|
||||
profileId: this.profileId,
|
||||
name: this.record.name,
|
||||
description: this.record.description,
|
||||
details: []
|
||||
}
|
||||
// 设置详情
|
||||
this.record.details.forEach(({
|
||||
appId,
|
||||
stageType
|
||||
}) => {
|
||||
req.details.push({
|
||||
appId,
|
||||
stageType
|
||||
})
|
||||
})
|
||||
this.submit(req)
|
||||
},
|
||||
async submit(req) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addAppPipeline(req)
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateAppPipeline(req)
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.name-form-item {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.name-input {
|
||||
width: 350px
|
||||
}
|
||||
}
|
||||
|
||||
.description-form-item {
|
||||
margin-bottom: 4px;
|
||||
|
||||
.description-input {
|
||||
width: 350px
|
||||
}
|
||||
}
|
||||
|
||||
.detail-form-item {
|
||||
margin-bottom: 4px;
|
||||
|
||||
.pipeline-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin: 2px 0 6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.add-option-button {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
</style>
|
||||
261
orion-ops-vue/src/components/app/AddRepositoryModal.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="560"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="名称" hasFeedback>
|
||||
<a-input v-decorator="decorators.name" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="url" hasFeedback>
|
||||
<a-input v-decorator="decorators.url" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="认证方式">
|
||||
<a-radio-group v-decorator="decorators.authType">
|
||||
<a-radio :value="type.value" v-for="type in REPOSITORY_AUTH_TYPE" :key="type.value">
|
||||
{{ type.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="资源用户" v-if="visibleUsername()" hasFeedback>
|
||||
<a-input v-decorator="decorators.username" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="认证令牌" v-if="!visiblePassword()" style="margin-bottom: 0">
|
||||
<a-form-item style="display: inline-block; width: 30%">
|
||||
<a-select v-decorator="decorators.tokenType">
|
||||
<a-select-option :value="type.value" v-for="type in REPOSITORY_TOKEN_TYPE" :key="type.value">
|
||||
{{ type.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item style="display: inline-block; width: 70%">
|
||||
<a-input v-decorator="decorators.privateToken"
|
||||
:placeholder="getPrivateTokenPlaceholder()"
|
||||
allowClear/>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item label="资源密码" v-if="visiblePassword()">
|
||||
<a-input-password v-decorator="decorators.password" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-decorator="decorators.description" allowClear/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumValueOf, REPOSITORY_AUTH_TYPE, REPOSITORY_TOKEN_TYPE } from '@/lib/enum'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 16 }
|
||||
}
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
name: ['name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入名称'
|
||||
}, {
|
||||
max: 32,
|
||||
message: '名称长度不能大于32位'
|
||||
}]
|
||||
}],
|
||||
url: ['url', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入url'
|
||||
}, {
|
||||
max: 1024,
|
||||
message: 'url长度不能大于1024位'
|
||||
}]
|
||||
}],
|
||||
authType: ['authType', {
|
||||
initialValue: REPOSITORY_AUTH_TYPE.PASSWORD.value
|
||||
}],
|
||||
tokenType: ['tokenType', {
|
||||
initialValue: REPOSITORY_TOKEN_TYPE.GITHUB.value
|
||||
}],
|
||||
privateToken: ['privateToken', {
|
||||
rules: [{
|
||||
max: 128,
|
||||
message: '令牌长度不能大于256位'
|
||||
}, {
|
||||
validator: this.validatePrivateToken
|
||||
}]
|
||||
}],
|
||||
username: ['username', {
|
||||
rules: [{
|
||||
max: 128,
|
||||
message: '用户名长度不能大于128位'
|
||||
}, {
|
||||
validator: this.validateUsername
|
||||
}]
|
||||
}],
|
||||
password: ['password', {
|
||||
rules: [{
|
||||
max: 128,
|
||||
message: '密码长度不能大于128位'
|
||||
}, {
|
||||
validator: this.validatePassword
|
||||
}]
|
||||
}],
|
||||
description: ['description', {
|
||||
rules: [{
|
||||
max: 64,
|
||||
message: '描述长度不能大于64位'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddRepositoryModal',
|
||||
data: function() {
|
||||
return {
|
||||
REPOSITORY_AUTH_TYPE,
|
||||
REPOSITORY_TOKEN_TYPE,
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
layout,
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.title = '新增仓库'
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改仓库'
|
||||
this.$api.getRepositoryDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.visible = true
|
||||
this.id = row.id
|
||||
const username = row.username
|
||||
const tokenType = row.tokenType
|
||||
this.record = pick(Object.assign({}, row), 'name', 'url', 'authType', 'description')
|
||||
// 设置数据
|
||||
new Promise((resolve) => {
|
||||
// 加载数据
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
resolve()
|
||||
}).then(() => {
|
||||
// 加载令牌类型
|
||||
if (this.record.authType === REPOSITORY_AUTH_TYPE.TOKEN.value) {
|
||||
this.$nextTick(() => {
|
||||
this.record.tokenType = tokenType
|
||||
this.form.setFieldsValue({ tokenType })
|
||||
})
|
||||
}
|
||||
}).then(() => {
|
||||
// 加载用户名
|
||||
if (this.visibleUsername(this.record.authType, tokenType)) {
|
||||
this.$nextTick(() => {
|
||||
this.record.username = username
|
||||
this.form.setFieldsValue({ username })
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
validatePrivateToken(rule, value, callback) {
|
||||
if (!this.id && !value) {
|
||||
callback(new Error('请输入私人令牌'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
validateUsername(rule, value, callback) {
|
||||
if (this.form.getFieldValue('password') && !value) {
|
||||
callback(new Error('用户名和密码须同时存在'))
|
||||
} else if (this.form.getFieldValue('tokenType') === REPOSITORY_TOKEN_TYPE.GITEE.value && !value) {
|
||||
callback(new Error('gitee 令牌认证用户名必填'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
validatePassword(rule, value, callback) {
|
||||
if (this.form.getFieldValue('username') && !value && !this.id) {
|
||||
callback(new Error('新增时用户名和密码须同时存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
visibleUsername(authType = this.form.getFieldValue('authType'), tokenType = this.form.getFieldValue('tokenType')) {
|
||||
return authType !== REPOSITORY_AUTH_TYPE.TOKEN.value ||
|
||||
(authType === REPOSITORY_AUTH_TYPE.TOKEN.value && tokenType === REPOSITORY_AUTH_TYPE.TOKEN.value)
|
||||
},
|
||||
visiblePassword() {
|
||||
return this.form.getFieldValue('authType') === REPOSITORY_AUTH_TYPE.PASSWORD.value
|
||||
},
|
||||
getPrivateTokenPlaceholder() {
|
||||
return enumValueOf(REPOSITORY_TOKEN_TYPE, this.form.getFieldValue('tokenType')).description ||
|
||||
REPOSITORY_TOKEN_TYPE.GITHUB.description
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addRepository({ ...values })
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateRepository({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
90
orion-ops-vue/src/components/app/AppAutoComplete.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-auto-complete v-model="value"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@change="change"
|
||||
@search="search"
|
||||
allowClear>
|
||||
<template #dataSource>
|
||||
<a-select-option v-for="app in visibleApp"
|
||||
:key="app.id"
|
||||
:value="JSON.stringify(app)"
|
||||
@click="choose">
|
||||
{{ app.name }}
|
||||
</a-select-option>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppAutoComplete',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '全部'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appList: [],
|
||||
visibleApp: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change(value) {
|
||||
let id
|
||||
let val = value
|
||||
try {
|
||||
const v = JSON.parse(value)
|
||||
if (typeof v === 'object') {
|
||||
id = v.id
|
||||
val = v.name
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
this.$emit('change', id, val)
|
||||
this.value = val
|
||||
},
|
||||
choose() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('choose')
|
||||
})
|
||||
},
|
||||
search(value) {
|
||||
if (!value) {
|
||||
this.visibleApp = this.appList
|
||||
return
|
||||
}
|
||||
this.visibleApp = this.appList.filter(s => s.name.toLowerCase().includes(value.toLowerCase()))
|
||||
},
|
||||
reset() {
|
||||
this.value = undefined
|
||||
this.visibleApp = this.appList
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const { data } = await this.$api.getAppList({
|
||||
limit: 10000
|
||||
})
|
||||
if (data && data.rows && data.rows.length) {
|
||||
for (const row of data.rows) {
|
||||
this.appList.push({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
})
|
||||
}
|
||||
this.visibleApp = this.appList
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
328
orion-ops-vue/src/components/app/AppBuildConfigForm.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div id="app-build-conf-container">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 产物路径 -->
|
||||
<div id="app-bundle-container">
|
||||
<div id="app-bundle-wrapper">
|
||||
<span class="label normal-label required-label">构建产物路径</span>
|
||||
<a-textarea class="bundle-input"
|
||||
v-model="bundlePath"
|
||||
:maxLength="1024"
|
||||
:autoSize="{minRows: 1}"
|
||||
:placeholder="'基于版本仓库的相对路径 或 绝对路径, 路径不能包含 \\\ 应该用 / 替换'"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 构建操作 -->
|
||||
<div id="app-action-container">
|
||||
<template v-for="(action, index) in actions">
|
||||
<div class="app-action-block" :key="index" v-if="action.visible">
|
||||
<!-- 分隔符 -->
|
||||
<a-divider class="action-divider">构建操作{{ index + 1 }}</a-divider>
|
||||
<div class="app-action-wrapper">
|
||||
<!-- 操作 -->
|
||||
<div class="app-action">
|
||||
<div class="action-name-wrapper">
|
||||
<span class="label normal-label required-label action-label">操作名称{{ index + 1 }}</span>
|
||||
<a-input class="action-name-input" v-model="action.name" :maxLength="32" placeholder="操作名称"/>
|
||||
</div>
|
||||
<!-- 代码块 -->
|
||||
<div class="action-editor-wrapper" v-if="action.type === BUILD_ACTION_TYPE.COMMAND.value">
|
||||
<span class="label normal-label required-label action-label">主机命令{{ index + 1 }}</span>
|
||||
<div class="app-action-editor">
|
||||
<Editor :config="editorConfig" :value="action.command" @change="(v) => action.command = v"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-type-wrapper" v-else>
|
||||
<span class="label normal-label action-label">操作类型</span>
|
||||
<a-button class="action-type-name" ghost disabled>
|
||||
{{ action.type | formatActionType('label') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<div class="app-action-handler">
|
||||
<a-button-group v-if="actions.length > 1">
|
||||
<a-button title="移除" @click="removeAction(index)" icon="minus-circle"/>
|
||||
<a-button title="上移" v-if="index !== 0" @click="swapAction(index, index - 1)" icon="arrow-up"/>
|
||||
<a-button title="下移" v-if="index !== actions.length - 1" @click="swapAction(index + 1, index )" icon="arrow-down"/>
|
||||
</a-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 底部按钮 -->
|
||||
<div id="app-action-footer">
|
||||
<a-button class="app-action-footer-button" type="dashed"
|
||||
@click="addAction(BUILD_ACTION_TYPE.COMMAND.value)">
|
||||
添加命令操作 (宿主机执行)
|
||||
</a-button>
|
||||
<a-button class="app-action-footer-button" type="dashed"
|
||||
v-if="visibleAddCheckout"
|
||||
@click="addAction(BUILD_ACTION_TYPE.CHECKOUT.value)">
|
||||
添加检出操作 (宿主机执行)
|
||||
</a-button>
|
||||
<a-button class="app-action-footer-button" type="primary" @click="save">保存</a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BUILD_ACTION_TYPE, enumValueOf } from '@/lib/enum'
|
||||
import Editor from '@/components/editor/Editor'
|
||||
|
||||
const editorConfig = {
|
||||
enableLiveAutocompletion: true,
|
||||
fontSize: 14
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppBuildConfigForm',
|
||||
props: {
|
||||
appId: Number,
|
||||
dataLoading: Boolean,
|
||||
detail: Object
|
||||
},
|
||||
components: {
|
||||
Editor
|
||||
},
|
||||
computed: {
|
||||
visibleAddCheckout() {
|
||||
return this.repoId &&
|
||||
this.repoId !== null &&
|
||||
this.actions.map(s => s.type).filter(t => t === BUILD_ACTION_TYPE.CHECKOUT.value).length < 1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detail(e) {
|
||||
this.initData(e)
|
||||
},
|
||||
dataLoading(e) {
|
||||
this.loading = e
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
BUILD_ACTION_TYPE,
|
||||
loading: false,
|
||||
profileId: null,
|
||||
repoId: null,
|
||||
bundlePath: undefined,
|
||||
actions: [],
|
||||
editorConfig
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initData(detail) {
|
||||
this.profileId = detail.profileId
|
||||
this.repoId = detail.repoId
|
||||
this.bundlePath = detail.env && detail.env.bundlePath
|
||||
if (detail.buildActions && detail.buildActions.length) {
|
||||
this.actions = detail.buildActions.map(s => {
|
||||
return {
|
||||
visible: true,
|
||||
name: s.name,
|
||||
type: s.type,
|
||||
command: s.command
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.actions = []
|
||||
}
|
||||
},
|
||||
addAction(type) {
|
||||
this.actions.push({
|
||||
type,
|
||||
command: '',
|
||||
name: undefined,
|
||||
visible: true
|
||||
})
|
||||
},
|
||||
removeAction(index) {
|
||||
this.actions[index].visible = false
|
||||
this.$nextTick(() => {
|
||||
this.actions.splice(index, 1)
|
||||
})
|
||||
},
|
||||
swapAction(index, target) {
|
||||
const temp = this.actions[target]
|
||||
this.$set(this.actions, target, this.actions[index])
|
||||
this.$set(this.actions, index, temp)
|
||||
},
|
||||
save() {
|
||||
if (!this.bundlePath || !this.bundlePath.trim().length) {
|
||||
this.$message.warning('请输入构建产物路径')
|
||||
return
|
||||
}
|
||||
if (this.bundlePath.includes('\\')) {
|
||||
this.$message.warning('构建产物路径不能包含 \\ 应该用 / 替换')
|
||||
return
|
||||
}
|
||||
if (!this.actions.length) {
|
||||
this.$message.warning('请设置构建操作')
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < this.actions.length; i++) {
|
||||
const action = this.actions[i]
|
||||
if (!action.name) {
|
||||
this.$message.warning(`请输入操作名称 [构建操作${i + 1}]`)
|
||||
return
|
||||
}
|
||||
if (BUILD_ACTION_TYPE.COMMAND.value === action.type) {
|
||||
if (!action.command) {
|
||||
this.$message.warning(`请输入操作命令 [构建操作${i + 1}]`)
|
||||
return
|
||||
} else if (action.command.length > 2048) {
|
||||
this.$message.warning(`操作命令长度不能大于2048位 [构建操作${i + 1}] 当前: ${action.command.length}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.configApp({
|
||||
appId: this.appId,
|
||||
profileId: this.profileId,
|
||||
stageType: 10,
|
||||
env: {
|
||||
bundlePath: this.bundlePath
|
||||
},
|
||||
buildActions: this.actions
|
||||
}).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.$emit('updated')
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatActionType(type, f) {
|
||||
return enumValueOf(BUILD_ACTION_TYPE, type)[f]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initData(this.detail)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@label-width: 160px;
|
||||
@action-handler-width: 120px;
|
||||
@bundle-container-width: 994px;
|
||||
@bundle-input-width: 700px;
|
||||
@app-action-container-width: 994px;
|
||||
@app-action-width: 876px;
|
||||
@action-name-input-width: 700px;
|
||||
@app-action-editor-width: 700px;
|
||||
@app-action-editor-height: 250px;
|
||||
@action-type-name-width: 700px;
|
||||
@action-divider-min-width: 830px;
|
||||
@action-divider-width: 990px;
|
||||
@app-action-footer-width: 700px;
|
||||
@footer-margin-left: 168px;
|
||||
@desc-margin-left: 168px;
|
||||
|
||||
#app-build-conf-container {
|
||||
padding: 18px 8px 0 8px;
|
||||
overflow: auto;
|
||||
|
||||
.label {
|
||||
width: @label-width;
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
#app-bundle-wrapper {
|
||||
width: @bundle-container-width;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
|
||||
.bundle-input {
|
||||
width: @bundle-input-width;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
#app-action-container {
|
||||
width: @app-action-container-width;
|
||||
margin-top: 16px;
|
||||
|
||||
.app-action-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.action-label {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.app-action {
|
||||
width: @app-action-width;
|
||||
padding: 0 8px 8px 8px;
|
||||
|
||||
.action-name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-name-input {
|
||||
width: @action-name-input-width;
|
||||
}
|
||||
}
|
||||
|
||||
.action-editor-wrapper {
|
||||
display: flex;
|
||||
|
||||
.app-action-editor {
|
||||
width: @app-action-editor-width;
|
||||
height: @app-action-editor-height;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-type-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-type-name {
|
||||
width: @action-type-name-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-action-handler {
|
||||
width: @action-handler-width;
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-divider {
|
||||
min-width: @action-divider-min-width;
|
||||
width: @action-divider-width;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#app-action-footer {
|
||||
margin: 16px 0 8px @footer-margin-left;
|
||||
width: @app-action-footer-width;
|
||||
|
||||
.app-action-footer-button {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.config-description {
|
||||
margin: 4px 0 0 @desc-margin-left;
|
||||
display: block;
|
||||
color: rgba(0, 0, 0, .45);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
256
orion-ops-vue/src/components/app/AppBuildDetailDrawer.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<a-drawer title="构建详情"
|
||||
placement="right"
|
||||
:visible="visible"
|
||||
:maskStyle="{opacity: 0, animation: 'none'}"
|
||||
:width="430"
|
||||
@close="onClose">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading">
|
||||
<a-skeleton active :paragraph="{rows: 12}"/>
|
||||
</div>
|
||||
<!-- 加载完成 -->
|
||||
<div v-else>
|
||||
<!-- 构建信息 -->
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="应用名称" :span="3">
|
||||
{{ detail.appName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="环境名称" :span="3">
|
||||
{{ detail.profileName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建序列" :span="3">
|
||||
<span class="span-blue">#{{ detail.seq }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建仓库" :span="3" v-if="detail.repoName != null">
|
||||
{{ detail.repoName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建分支" :span="3" v-if="detail.branchName != null">
|
||||
<a-icon type="branches"/>
|
||||
{{ detail.branchName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="commitId" :span="3" v-if="detail.commitId != null">
|
||||
{{ detail.commitId }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建描述" :span="3" v-if="detail.description != null">
|
||||
{{ detail.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建状态" :span="3">
|
||||
<a-tag :color="detail.status | formatBuildStatus('color')">
|
||||
{{ detail.status | formatBuildStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建用户" :span="3">
|
||||
{{ detail.createUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="修改时间" :span="3">
|
||||
{{ detail.updateTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="detail.startTime !== null">
|
||||
{{ detail.startTime | formatDate }} ({{ detail.startTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="detail.endTime !== null">
|
||||
{{ detail.endTime | formatDate }} ({{ detail.endTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="detail.used !== null">
|
||||
{{ `${detail.keepTime} (${detail.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="日志" :span="3" v-if="detail.status !== BUILD_STATUS.WAIT.value">
|
||||
<a v-if="detail.logUrl" @click="clearDownloadUrl(detail,'logUrl')" target="_blank" :href="detail.logUrl">下载</a>
|
||||
<a v-else @click="loadDownloadUrl(detail, FILE_DOWNLOAD_TYPE.APP_BUILD_LOG.value,'logUrl')">获取日志文件</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="产物" :span="3" v-if="detail.status === BUILD_STATUS.FINISH.value">
|
||||
<a v-if="detail.downloadUrl" @click="clearDownloadUrl(detail)" target="_blank" :href="detail.downloadUrl">下载</a>
|
||||
<a v-else @click="loadDownloadUrl(detail, FILE_DOWNLOAD_TYPE.APP_BUILD_BUNDLE.value)">获取产物文件</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<!-- 构建操作 -->
|
||||
<a-divider>构建操作</a-divider>
|
||||
<a-list :dataSource="detail.actions">
|
||||
<template #renderItem="item">
|
||||
<a-list-item>
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="操作名称" :span="3">
|
||||
{{ item.actionName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作类型" :span="3">
|
||||
<a-tag>{{ item.actionType | formatActionType('label') }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作状态" :span="3">
|
||||
<a-tag :color="item.status | formatActionStatus('color')">
|
||||
{{ item.status | formatActionStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="item.startTime !== null">
|
||||
{{ item.startTime | formatDate }} ({{ item.startTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="item.endTime !== null">
|
||||
{{ item.endTime | formatDate }} ({{ item.endTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="item.used !== null">
|
||||
{{ `${item.keepTime} (${item.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="退出码" :span="3" v-if="item.exitCode !== null">
|
||||
<span :style="{'color': item.exitCode === 0 ? '#4263EB' : '#E03131'}">
|
||||
{{ item.exitCode }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="命令" :span="3" v-if="item.actionType === BUILD_ACTION_TYPE.COMMAND.value">
|
||||
<a @click="preview(item.actionCommand)">预览</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="日志" :span="3" v-if="statusHolder.visibleActionLog(item.status)">
|
||||
<a v-if="item.downloadUrl" @click="clearDownloadUrl(item)" target="_blank" :href="item.downloadUrl">下载</a>
|
||||
<a v-else @click="loadDownloadUrl(item, FILE_DOWNLOAD_TYPE.APP_ACTION_LOG.value)">获取日志文件</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
<!-- 事件 -->
|
||||
<div class="detail-event">
|
||||
<EditorPreview ref="preview"/>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineArrayKey } from '@/lib/utils'
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { ACTION_STATUS, BUILD_ACTION_TYPE, BUILD_STATUS, enumValueOf, FILE_DOWNLOAD_TYPE } from '@/lib/enum'
|
||||
import EditorPreview from '@/components/preview/EditorPreview'
|
||||
|
||||
const statusHolder = {
|
||||
visibleActionLog: (status) => {
|
||||
return status === ACTION_STATUS.RUNNABLE.value ||
|
||||
status === ACTION_STATUS.FINISH.value ||
|
||||
status === ACTION_STATUS.FAILURE.value ||
|
||||
status === ACTION_STATUS.TERMINATED.value
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppBuildDetailDrawer',
|
||||
components: {
|
||||
EditorPreview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
ACTION_STATUS,
|
||||
BUILD_STATUS,
|
||||
BUILD_ACTION_TYPE,
|
||||
visible: false,
|
||||
loading: true,
|
||||
pollId: null,
|
||||
detail: {},
|
||||
statusHolder
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
this.detail = {}
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
this.$api.getAppBuildDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
data.logUrl = null
|
||||
data.downloadUrl = null
|
||||
defineArrayKey(data.actions, 'downloadUrl')
|
||||
this.detail = data
|
||||
// 轮询状态
|
||||
if (data.status === BUILD_STATUS.WAIT.value || data.status === BUILD_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 5000)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
pollStatus() {
|
||||
if (!this.detail || !this.detail.status) {
|
||||
return
|
||||
}
|
||||
if (this.detail.status !== BUILD_STATUS.WAIT.value && this.detail.status !== BUILD_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
return
|
||||
}
|
||||
this.$api.getAppBuildStatus({
|
||||
id: this.detail.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
this.detail.used = data.used
|
||||
this.detail.keepTime = data.keepTime
|
||||
this.detail.startTime = data.startTime
|
||||
this.detail.startTimeAgo = data.startTimeAgo
|
||||
this.detail.endTime = data.endTime
|
||||
this.detail.endTimeAgo = data.endTimeAgo
|
||||
if (data.actions && data.actions.length) {
|
||||
for (const action of data.actions) {
|
||||
this.detail.actions.filter(s => s.id === action.id).forEach(s => {
|
||||
s.status = action.status
|
||||
s.keepTime = action.keepTime
|
||||
s.used = action.used
|
||||
s.startTime = action.startTime
|
||||
s.startTimeAgo = action.startTimeAgo
|
||||
s.endTime = action.endTime
|
||||
s.endTimeAgo = action.endTimeAgo
|
||||
s.exitCode = action.exitCode
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadDownloadUrl(record, type, field = 'downloadUrl') {
|
||||
try {
|
||||
const downloadUrl = await this.$api.getFileDownloadToken({
|
||||
type,
|
||||
id: record.id
|
||||
})
|
||||
record[field] = this.$api.fileDownloadExec({ token: downloadUrl.data })
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
clearDownloadUrl(record, field = 'downloadUrl') {
|
||||
setTimeout(() => {
|
||||
record[field] = null
|
||||
})
|
||||
},
|
||||
preview(command) {
|
||||
this.$refs.preview.preview(command)
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatActionStatus(status, f) {
|
||||
return enumValueOf(ACTION_STATUS, status)[f]
|
||||
},
|
||||
formatBuildStatus(status, f) {
|
||||
return enumValueOf(BUILD_STATUS, status)[f]
|
||||
},
|
||||
formatActionType(type, f) {
|
||||
return enumValueOf(BUILD_ACTION_TYPE, type)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
393
orion-ops-vue/src/components/app/AppBuildModal.vue
Normal file
@@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:width="550"
|
||||
:maskStyle="{opacity: 0.8, animation: 'none'}"
|
||||
:dialogStyle="{top: '64px', padding: 0}"
|
||||
:bodyStyle="{padding: '8px'}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true">
|
||||
<!-- 页头 -->
|
||||
<template #title>
|
||||
<span v-if="selectAppPage">选择应用</span>
|
||||
<span v-if="!selectAppPage">
|
||||
<a-icon class="mx4 pointer span-blue"
|
||||
title="重新选择"
|
||||
v-if="visibleReselect && appId"
|
||||
@click="reselectAppList"
|
||||
type="arrow-left"/>
|
||||
应用构建
|
||||
</span>
|
||||
</template>
|
||||
<!-- 初始化骨架 -->
|
||||
<a-skeleton v-if="initiating" active :paragraph="{rows: 4}"/>
|
||||
<!-- 主体 -->
|
||||
<a-spin v-else :spinning="loading || appLoading">
|
||||
<!-- 应用选择 -->
|
||||
<div class="app-list-container" v-if="selectAppPage">
|
||||
<!-- 无应用数据 -->
|
||||
<a-empty v-if="!appList.length" description="请先配置应用"/>
|
||||
<!-- 应用列表 -->
|
||||
<div v-else class="app-list">
|
||||
<div class="app-item" v-for="app of appList" :key="app.id" @click="chooseApp(app.id)">
|
||||
<div class="app-name">
|
||||
<a-icon class="mx8" type="code-sandbox"/>
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<a-tag color="#5C7CFA">
|
||||
{{ app.tag }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 构建配置 -->
|
||||
<div class="build-container" v-else>
|
||||
<div class="build-form">
|
||||
<!-- 分支 -->
|
||||
<div class="build-form-item" v-if="app && app.repoId">
|
||||
<span class="build-form-item-label normal-label required-label">分支</span>
|
||||
<a-select class="build-form-item-input"
|
||||
v-model="submit.branchName"
|
||||
placeholder="分支"
|
||||
@change="reloadCommit"
|
||||
allowClear>
|
||||
<a-select-option v-for="branch of branchList" :key="branch.name" :value="branch.name">
|
||||
{{ branch.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="reload" class="reload" title="刷新" @click="reloadBranch"/>
|
||||
</div>
|
||||
<!-- commit -->
|
||||
<div class="build-form-item" v-if="app && app.repoId">
|
||||
<span class="build-form-item-label normal-label required-label">commit</span>
|
||||
<a-select class="build-form-item-input commit-selector"
|
||||
v-model="submit.commitId"
|
||||
placeholder="提交记录"
|
||||
allowClear>
|
||||
<a-select-option v-for="commit of commitList" :key="commit.id" :value="commit.id">
|
||||
<div class="commit-item">
|
||||
<div class="commit-item-left">
|
||||
<span class="commit-item-id">{{ commit.id.substring(0, 7) }}</span>
|
||||
<span class="commit-item-message">{{ commit.message }}</span>
|
||||
</div>
|
||||
<span class="commit-item-date">
|
||||
{{ commit.time | formatDate('MM-dd HH:mm' ) }}
|
||||
</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="reload" class="reload" title="刷新" @click="reloadCommit"/>
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div class="build-form-item" style="margin: 8px 0;">
|
||||
<span class="build-form-item-label normal-label">构建描述</span>
|
||||
<a-textarea class="build-form-item-input"
|
||||
v-model="submit.description"
|
||||
style="height: 50px; width: 430px"
|
||||
:maxLength="64"
|
||||
allowClear/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 页脚 -->
|
||||
<template #footer>
|
||||
<!-- 关闭 -->
|
||||
<a-button @click="close">关闭</a-button>
|
||||
<!-- 构建 -->
|
||||
<a-button type="primary"
|
||||
:loading="loading"
|
||||
:disabled="selectAppPage || loading || appLoading || initiating"
|
||||
@click="build">
|
||||
构建
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { CONFIG_STATUS } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppBuildModal',
|
||||
props: {
|
||||
visibleReselect: Boolean
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
selectAppPage: true,
|
||||
appId: null,
|
||||
profileId: null,
|
||||
app: null,
|
||||
appList: [],
|
||||
branchList: [],
|
||||
commitList: [],
|
||||
submit: {
|
||||
branchName: null,
|
||||
commitId: null,
|
||||
description: null
|
||||
},
|
||||
visible: false,
|
||||
loading: false,
|
||||
appLoading: false,
|
||||
initiating: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async openBuild(profileId, id) {
|
||||
if (!profileId) {
|
||||
this.$message.warning('请先维护应用环境')
|
||||
return
|
||||
}
|
||||
this.profileId = profileId
|
||||
this.appList = []
|
||||
this.cleanData()
|
||||
this.selectAppPage = !id
|
||||
this.loading = false
|
||||
this.appLoading = false
|
||||
this.initiating = true
|
||||
this.visible = true
|
||||
await this.loadAppList()
|
||||
if (id) {
|
||||
this.chooseApp(id)
|
||||
}
|
||||
this.initiating = false
|
||||
},
|
||||
async chooseApp(id) {
|
||||
this.cleanData()
|
||||
this.appId = id
|
||||
this.selectAppPage = false
|
||||
const filter = this.appList.filter(s => s.id === id)
|
||||
if (!filter.length) {
|
||||
this.$message.warning('未找到该应用')
|
||||
}
|
||||
this.app = filter[0]
|
||||
if (this.app.repoId) {
|
||||
this.appLoading = true
|
||||
await this.loadRepository()
|
||||
this.appLoading = false
|
||||
}
|
||||
},
|
||||
async loadAppList() {
|
||||
const { data: { rows } } = await this.$api.getAppList({
|
||||
profileId: this.profileId,
|
||||
limit: 10000
|
||||
})
|
||||
this.appList = rows.filter(s => s.isConfig === CONFIG_STATUS.CONFIGURED.value)
|
||||
},
|
||||
async reselectAppList() {
|
||||
this.selectAppPage = true
|
||||
if (this.appList.length) {
|
||||
return
|
||||
}
|
||||
this.initiating = true
|
||||
await this.loadAppList()
|
||||
this.initiating = false
|
||||
},
|
||||
cleanData() {
|
||||
this.app = {}
|
||||
this.appId = null
|
||||
this.branchList = []
|
||||
this.commitList = []
|
||||
this.submit.branchName = null
|
||||
this.submit.commitId = null
|
||||
this.submit.description = null
|
||||
},
|
||||
async loadRepository() {
|
||||
await this.$api.getRepositoryInfo({
|
||||
id: this.app.repoId,
|
||||
appId: this.appId,
|
||||
profileId: this.profileId
|
||||
}).then(({ data }) => {
|
||||
this.branchList = data.branches
|
||||
// 分支列表
|
||||
const defaultBranch = this.branchList.filter(s => s.isDefault === 1)
|
||||
if (defaultBranch && defaultBranch.length) {
|
||||
this.submit.branchName = defaultBranch[0].name
|
||||
} else {
|
||||
this.submit.branchName = null
|
||||
}
|
||||
// 提交列表
|
||||
this.commitList = data.commits
|
||||
if (data.commits && data.commits.length) {
|
||||
this.submit.commitId = this.commitList[0].id
|
||||
} else {
|
||||
this.submit.commitId = null
|
||||
}
|
||||
})
|
||||
},
|
||||
reloadBranch() {
|
||||
this.appLoading = true
|
||||
this.$api.getRepositoryBranchList({
|
||||
id: this.app.repoId
|
||||
}).then(({ data }) => {
|
||||
this.appLoading = false
|
||||
this.branchList = data
|
||||
}).catch(() => {
|
||||
this.appLoading = false
|
||||
})
|
||||
},
|
||||
reloadCommit() {
|
||||
if (!this.submit.branchName) {
|
||||
this.commitList = []
|
||||
this.submit.commitId = undefined
|
||||
return
|
||||
}
|
||||
if (!this.submit.branchName) {
|
||||
this.$message.warning('请先选择分支')
|
||||
return
|
||||
}
|
||||
this.appLoading = true
|
||||
this.$api.getRepositoryCommitList({
|
||||
id: this.app.repoId,
|
||||
branchName: this.submit.branchName
|
||||
}).then(({ data }) => {
|
||||
this.appLoading = false
|
||||
this.commitList = data
|
||||
if (data && data.length) {
|
||||
this.submit.commitId = this.commitList[0].id
|
||||
} else {
|
||||
this.submit.commitId = null
|
||||
}
|
||||
}).catch(() => {
|
||||
this.appLoading = false
|
||||
})
|
||||
},
|
||||
async build() {
|
||||
if (!this.app) {
|
||||
this.$message.warning('请选择构建应用')
|
||||
return
|
||||
}
|
||||
if (this.app.repoId) {
|
||||
if (!this.submit.branchName) {
|
||||
this.$message.warning('请选择分支')
|
||||
return
|
||||
}
|
||||
if (!this.submit.commitId) {
|
||||
this.$message.warning('请选择commit')
|
||||
return
|
||||
}
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.submitAppBuild({
|
||||
appId: this.appId,
|
||||
profileId: this.profileId,
|
||||
...this.submit
|
||||
}).then(() => {
|
||||
this.$message.success('已开始构建')
|
||||
this.$emit('submit')
|
||||
this.visible = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-list {
|
||||
margin: 0 4px 0 8px;
|
||||
height: 355px;
|
||||
overflow-y: auto;
|
||||
|
||||
.app-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: #F8F9FA;
|
||||
border-radius: 4px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
.app-name {
|
||||
width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.app-item:hover {
|
||||
background: #E7F5FF;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.build-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.build-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.build-form-item-label {
|
||||
width: 70px;
|
||||
margin: 16px 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.build-form-item-input {
|
||||
width: 390px;
|
||||
}
|
||||
|
||||
.reload {
|
||||
font-size: 19px;
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
color: #339AF0;
|
||||
}
|
||||
|
||||
.reload:hover {
|
||||
color: #228BE6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commit-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.commit-item-left {
|
||||
display: flex;
|
||||
width: 285px;
|
||||
}
|
||||
|
||||
.commit-item-id {
|
||||
width: 50px;
|
||||
margin-right: 8px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.commit-item-message {
|
||||
width: 227px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.commit-item-date {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .commit-selector .ant-select-selection-selected-value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<!-- 统计折线图 -->
|
||||
<div class="statistic-chart-container">
|
||||
<p class="statistics-description">近七天构建统计</p>
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 折线图 -->
|
||||
<div class="statistic-chart-wrapper">
|
||||
<div id="statistic-chart"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Chart } from '@antv/g2'
|
||||
|
||||
export default {
|
||||
name: 'AppBuildStatisticsCharts',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(appId, profileId) {
|
||||
this.loading = true
|
||||
const { data } = await this.$api.getAppBuildStatisticsChart({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.loading = false
|
||||
this.renderChart(data)
|
||||
},
|
||||
clean() {
|
||||
this.loading = false
|
||||
this.chart && this.chart.destroy()
|
||||
this.chart = null
|
||||
},
|
||||
renderChart(data) {
|
||||
// 处理数据
|
||||
const chartsData = []
|
||||
for (const d of data) {
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '构建次数',
|
||||
value: d.buildCount
|
||||
})
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '成功次数',
|
||||
value: d.successCount
|
||||
})
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '失败次数',
|
||||
value: d.failureCount
|
||||
})
|
||||
}
|
||||
this.clean()
|
||||
// 渲染图表
|
||||
this.chart = new Chart({
|
||||
container: 'statistic-chart',
|
||||
autoFit: true
|
||||
})
|
||||
this.chart.data(chartsData)
|
||||
|
||||
this.chart.tooltip({
|
||||
showCrosshairs: true,
|
||||
shared: true
|
||||
})
|
||||
|
||||
this.chart.line()
|
||||
.position('date*value')
|
||||
.color('type')
|
||||
.shape('circle')
|
||||
this.chart.render()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.statistic-chart-wrapper {
|
||||
margin: 0 24px 16px 24px;
|
||||
|
||||
#statistic-chart {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
124
orion-ops-vue/src/components/app/AppBuildStatisticsMetrics.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 全部构建指标 -->
|
||||
<p class="statistics-description">全部构建指标</p>
|
||||
<div class="app-build-statistic-metrics">
|
||||
<div class="clean"/>
|
||||
<!-- 统计指标 -->
|
||||
<div class="app-build-statistic-header">
|
||||
<a-statistic class="statistic-metrics-item" title="构建次数" :value="allMetrics.buildCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item green" title="成功次数" :value="allMetrics.successCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item red" title="失败次数" :value="allMetrics.failureCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item" title="平均耗时" :value="allMetrics.avgUsedInterval"/>
|
||||
</div>
|
||||
<div class="clean"/>
|
||||
</div>
|
||||
<!-- 近七天构建指标 -->
|
||||
<p class="statistics-description" style="margin-top: 28px">近七天构建指标</p>
|
||||
<div class="app-build-statistic-metrics">
|
||||
<div class="clean"/>
|
||||
<!-- 统计指标 -->
|
||||
<div class="app-build-statistic-header">
|
||||
<a-statistic class="statistic-metrics-item" title="构建次数" :value="latelyMetrics.buildCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item green" title="成功次数" :value="latelyMetrics.successCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item red" title="失败次数" :value="latelyMetrics.failureCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item" title="平均耗时" :value="latelyMetrics.avgUsedInterval"/>
|
||||
</div>
|
||||
<div class="clean"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'AppBuildStatisticsMetrics',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
allMetrics: {
|
||||
buildCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
},
|
||||
latelyMetrics: {
|
||||
buildCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(appId, profileId) {
|
||||
this.loading = true
|
||||
const { data } = await this.$api.getAppBuildStatisticsMetrics({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.loading = false
|
||||
this.allMetrics = data.all
|
||||
this.latelyMetrics = data.lately
|
||||
},
|
||||
clean() {
|
||||
this.loading = false
|
||||
this.allMetrics = {
|
||||
buildCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
}
|
||||
this.latelyMetrics = {
|
||||
buildCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-build-statistic-metrics {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 24px 0 12px 16px;
|
||||
|
||||
.app-build-statistic-header {
|
||||
display: flex;
|
||||
|
||||
.statistic-metrics-item {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
::v-deep .statistic-metrics-item.green .ant-statistic-content {
|
||||
color: #58C612;
|
||||
}
|
||||
|
||||
::v-deep .statistic-metrics-item.red .ant-statistic-content {
|
||||
color: #DD2C00;
|
||||
}
|
||||
|
||||
.statistic-metrics-divider {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
::v-deep .ant-statistic-content {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
338
orion-ops-vue/src/components/app/AppBuildStatisticsViews.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div class="app-build-statistic-record-view-container">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 构建视图 -->
|
||||
<div class="app-build-statistic-record-view-wrapper" v-if="initialized && view">
|
||||
<div class="app-build-statistic-record-view">
|
||||
<p class="statistics-description">近十次构建视图</p>
|
||||
<!-- 构建视图主体 -->
|
||||
<div class="app-build-statistic-main">
|
||||
<!-- 构建操作 -->
|
||||
<div class="app-build-actions-wrapper">
|
||||
<!-- 平均时间 -->
|
||||
<div class="app-build-actions-legend">
|
||||
<span class="avg-used-legend-wrapper">
|
||||
平均构建时间: <span class="avg-used-legend">{{ view.avgUsedInterval }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 构建操作 -->
|
||||
<div class="app-build-actions" v-for="(action, index) of view.actions" :key="action.id">
|
||||
<div :class="['app-build-actions-name', index % 2 === 0 ? 'app-build-actions-name-theme1' : 'app-build-actions-name-theme2']">
|
||||
{{ action.name }}
|
||||
</div>
|
||||
<div :class="['app-build-actions-avg', index % 2 === 0 ? 'app-build-actions-avg-theme1' : 'app-build-actions-avg-theme2']">
|
||||
<div class="app-build-actions-avg">
|
||||
{{ action.avgUsedInterval }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 构建日志 -->
|
||||
<div class="app-build-action-logs-container">
|
||||
<!-- 日志 -->
|
||||
<div class="app-build-action-logs-wrapper"
|
||||
v-for="buildRecord of view.buildRecordList"
|
||||
:key="buildRecord.buildId">
|
||||
<!-- 构建信息头 -->
|
||||
<a target="_blank"
|
||||
title="点击查看构建日志"
|
||||
:href="`#/app/build/log/view/${buildRecord.buildId}`"
|
||||
@click="openLogView($event,'build', buildRecord.buildId)">
|
||||
<div class="app-build-record-legend">
|
||||
<!-- 构建状态 -->
|
||||
<div class="app-build-record-status">
|
||||
<!-- 构建序列 -->
|
||||
<div class="build-seq">
|
||||
<span class="span-blue">#{{ buildRecord.seq }}</span>
|
||||
</div>
|
||||
<!-- 构建状态 -->
|
||||
<a-tag class="m0" :color="buildRecord.status | formatBuildStatus('color')">
|
||||
{{ buildRecord.status | formatBuildStatus('label') }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<!-- 构建时间 -->
|
||||
<div class="app-build-record-info">
|
||||
<span>{{ buildRecord.buildDate | formatDate('MM-dd HH:mm') }}</span>
|
||||
<span v-if="buildRecord.usedInterval" class="ml4"> (used: {{ buildRecord.usedInterval }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- 构建操作 -->
|
||||
<div class="app-build-action-log-actions-wrapper">
|
||||
<div class="app-build-action-log-action-wrapper"
|
||||
v-for="(actionLog, index) of buildRecord.actionLogs" :key="index">
|
||||
<!-- 构建操作值 -->
|
||||
<div class="app-build-action-log-action"
|
||||
v-if="!getCanOpenLog(actionLog)"
|
||||
:style="getActionLogStyle(actionLog)"
|
||||
v-text="getActionLogValue(actionLog)">
|
||||
</div>
|
||||
<!-- 可打开日志 -->
|
||||
<a v-else target="_blank"
|
||||
title="点击查看操作日志"
|
||||
:href="`#/app/action/log/view/${actionLog.id}`"
|
||||
@click="openLogView($event,'action', actionLog.id)">
|
||||
<div class="app-build-action-log-action"
|
||||
:style="getActionLogStyle(actionLog)"
|
||||
v-text="getActionLogValue(actionLog)">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无数据 -->
|
||||
<div style="padding: 0 16px" v-else-if="initialized && !view">
|
||||
无构建记录
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 事件 -->
|
||||
<div class="app-build-statistic-event-container">
|
||||
<!-- 构建日志模态框 -->
|
||||
<AppBuildLogAppenderModal ref="buildLogView"/>
|
||||
<!-- 操作日志模态框 -->
|
||||
<AppActionLogAppenderModal ref="actionLogView"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, ACTION_STATUS, BUILD_STATUS } from '@/lib/enum'
|
||||
import AppBuildLogAppenderModal from '@/components/log/AppBuildLogAppenderModal'
|
||||
import AppActionLogAppenderModal from '@/components/log/AppActionLogAppenderModal'
|
||||
|
||||
export default {
|
||||
name: 'AppBuildStatisticsViews',
|
||||
components: {
|
||||
AppBuildLogAppenderModal,
|
||||
AppActionLogAppenderModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
initialized: false,
|
||||
view: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(appId, profileId) {
|
||||
this.loading = true
|
||||
this.initialized = false
|
||||
const { data } = await this.$api.getAppBuildStatisticsView({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.view = data
|
||||
this.initialized = true
|
||||
this.loading = false
|
||||
},
|
||||
clean() {
|
||||
this.initialized = false
|
||||
this.loading = false
|
||||
this.view = {}
|
||||
},
|
||||
async refresh(appId, profileId) {
|
||||
const { data } = await this.$api.getAppBuildStatisticsView({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.view = data
|
||||
},
|
||||
openLogView(e, type, id) {
|
||||
if (!e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
// 打开模态框
|
||||
this.$refs[`${type}LogView`].open(id)
|
||||
return false
|
||||
} else {
|
||||
// 跳转页面
|
||||
return true
|
||||
}
|
||||
},
|
||||
getCanOpenLog(actionLog) {
|
||||
if (actionLog) {
|
||||
return enumValueOf(ACTION_STATUS, actionLog.status).log
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
getActionLogStyle(actionLog) {
|
||||
if (actionLog) {
|
||||
return enumValueOf(ACTION_STATUS, actionLog.status).actionStyle
|
||||
} else {
|
||||
return {
|
||||
background: '#FFD43B'
|
||||
}
|
||||
}
|
||||
},
|
||||
getActionLogValue(actionLog) {
|
||||
if (actionLog) {
|
||||
return enumValueOf(ACTION_STATUS, actionLog.status).actionValue(actionLog)
|
||||
} else {
|
||||
return '未执行'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatBuildStatus(status, f) {
|
||||
return enumValueOf(BUILD_STATUS, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-build-statistic-record-view-wrapper {
|
||||
margin: 0 24px 24px 24px;
|
||||
overflow: auto;
|
||||
|
||||
.app-build-statistic-record-view {
|
||||
margin: 0 16px 16px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.app-build-statistic-main {
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 4px 1px #DEE2E6;
|
||||
}
|
||||
|
||||
.app-build-actions-wrapper {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
|
||||
.app-build-actions-legend {
|
||||
width: 180px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 8px 4px 0;
|
||||
font-weight: 600;
|
||||
|
||||
.avg-used-legend-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avg-used-legend {
|
||||
margin-left: 4px;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-build-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 148px;
|
||||
|
||||
.app-build-actions-name {
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
color: #181E33;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 16px;
|
||||
border-bottom: 1px solid #DEE2E6;
|
||||
}
|
||||
|
||||
.app-build-actions-name-theme1 {
|
||||
background: #F1F3F5;
|
||||
}
|
||||
|
||||
.app-build-actions-name-theme2 {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
.app-build-actions-avg {
|
||||
text-align: center;
|
||||
height: 28px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.app-build-actions-avg-theme1 {
|
||||
background: #E9ECEF;
|
||||
}
|
||||
|
||||
.app-build-actions-avg-theme2 {
|
||||
background: #F1F4F7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-build-action-logs-wrapper {
|
||||
display: flex;
|
||||
height: 74px;
|
||||
|
||||
.app-build-record-legend {
|
||||
width: 180px;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border-top: 2px solid #F8F9FA;
|
||||
|
||||
.app-build-record-status {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-build-record-info {
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-build-record-legend:hover {
|
||||
transition: .3s;
|
||||
background: #D0EBFF;
|
||||
}
|
||||
|
||||
.app-build-action-log-actions-wrapper {
|
||||
display: flex;
|
||||
|
||||
.app-build-action-log-action-wrapper {
|
||||
width: 148px;
|
||||
border-radius: 4px;
|
||||
|
||||
.app-build-action-log-action {
|
||||
margin: 2px 1px;
|
||||
height: calc(100% - 2px);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0.8;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.app-build-action-log-action:hover {
|
||||
opacity: 1;
|
||||
transition: .3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
title="流水线详情"
|
||||
placement="right"
|
||||
:maskStyle="{opacity: 0, animation: 'none'}"
|
||||
:closable="false"
|
||||
:visible="visible"
|
||||
:width="350"
|
||||
@close="onClose">
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="loading" active :paragraph="{rows: 6}"/>
|
||||
<!-- 描述 -->
|
||||
<a-descriptions v-else size="middle">
|
||||
<a-descriptions-item label="流水线名称" :span="3">
|
||||
{{ record.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="流水线描述" :span="3">
|
||||
{{ record.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间" :span="3">
|
||||
{{ record.createTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="修改时间" :span="3">
|
||||
{{ record.updateTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<!-- 操作流水线 -->
|
||||
<a-descriptions-item :span="3">
|
||||
<a-timeline style="margin-top: 16px">
|
||||
<a-timeline-item v-for="detail of record.details" :key="detail.id">
|
||||
<!-- 操作 -->
|
||||
<span class="mr4 span-blue">{{ detail.stageType | formatStageType('label') }}</span>
|
||||
<!-- 应用名称 -->
|
||||
<span>{{ detail.appName }}</span>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, STAGE_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineDetailViewDrawer',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
record: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.record = {}
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
this.$api.getAppPipelineDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.record = data
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatStageType(status, f) {
|
||||
return enumValueOf(STAGE_TYPE, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="执行审核"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
:width="550">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="audit-description">
|
||||
<span class="description-label normal-label mr8">审核描述</span>
|
||||
<a-textarea class="description-area" v-model="description" :maxLength="64"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 操作 -->
|
||||
<template #footer>
|
||||
<a-button @click="audit(false)" :disabled="loading">驳回</a-button>
|
||||
<a-button type="primary" @click="audit(true)" :disabled="loading">通过</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AUDIT_STATUS } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineExecAuditModal',
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
visible: false,
|
||||
loading: false,
|
||||
description: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
this.loading = false
|
||||
this.id = id
|
||||
},
|
||||
audit(res) {
|
||||
if (!res && !this.description) {
|
||||
this.$message.warning('请输入驳回描述')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.auditAppPipelineTask({
|
||||
id: this.id,
|
||||
auditStatus: res ? AUDIT_STATUS.RESOLVE.value : AUDIT_STATUS.REJECT.value,
|
||||
auditReason: this.description
|
||||
}).then(() => {
|
||||
this.$message.success('审核完成')
|
||||
this.$emit('audit', this.id, res)
|
||||
this.close()
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.description = null
|
||||
this.id = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.audit-description {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.description-label {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.description-area {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
239
orion-ops-vue/src/components/app/AppPipelineExecBuildModal.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="560"
|
||||
:maskStyle="{opacity: 0.4, animation: 'none'}"
|
||||
:bodyStyle="{padding: '24px 24px 0 24px'}"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form-model v-bind="layout">
|
||||
<!-- 分支 -->
|
||||
<a-form-model-item label="分支" v-if="detail.repoId" required>
|
||||
<a-select class="build-form-item-input"
|
||||
v-model="submit.branchName"
|
||||
placeholder="分支"
|
||||
@change="reloadCommit"
|
||||
allowClear>
|
||||
<a-select-option v-for="branch of branchList" :key="branch.name" :value="branch.name">
|
||||
{{ branch.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="reload" class="reload" title="刷新" @click="reloadBranch"/>
|
||||
</a-form-model-item>
|
||||
<!-- commit -->
|
||||
<a-form-model-item label="commit" v-if="detail.repoId" required>
|
||||
<a-select class="build-form-item-input commit-selector"
|
||||
v-model="submit.commitId"
|
||||
placeholder="提交记录"
|
||||
allowClear>
|
||||
<a-select-option v-for="commit of commitList" :key="commit.id" :value="commit.id">
|
||||
<div class="commit-item">
|
||||
<div class="commit-item-left">
|
||||
<span class="commit-item-id">{{ commit.id.substring(0, 7) }}</span>
|
||||
<span class="commit-item-message">{{ commit.message }}</span>
|
||||
</div>
|
||||
<span class="commit-item-date">
|
||||
{{ commit.time | formatDate('MM-dd HH:mm' ) }}
|
||||
</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="reload" class="reload" title="刷新" @click="reloadCommit"/>
|
||||
</a-form-model-item>
|
||||
<!-- 构建描述 -->
|
||||
<a-form-model-item label="构建描述">
|
||||
<a-textarea v-model="submit.description" :maxLength="64" allowClear/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 18 }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineExecBuildModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
title: null,
|
||||
detail: {},
|
||||
branchList: [],
|
||||
commitList: [],
|
||||
submit: {
|
||||
branchName: null,
|
||||
commitId: null,
|
||||
description: null
|
||||
},
|
||||
layout
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(detail) {
|
||||
this.detail = { ...detail }
|
||||
this.title = `${detail.appName} 构建配置`
|
||||
this.branchList = []
|
||||
this.commitList = []
|
||||
this.submit.branchName = detail.branchName
|
||||
this.submit.commitId = detail.commitId
|
||||
this.submit.description = detail.description
|
||||
this.visible = true
|
||||
this.loadRepository()
|
||||
},
|
||||
loadRepository() {
|
||||
if (!this.detail.repoId) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.getRepositoryInfo({
|
||||
id: this.detail.repoId,
|
||||
appId: this.detail.appId,
|
||||
profileId: this.detail.profileId
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.branchList = data.branches
|
||||
// 分支列表
|
||||
const defaultBranch = this.branchList.filter(s => s.isDefault === 1)
|
||||
if (this.submit.branchName) {
|
||||
// nothing
|
||||
} else if (defaultBranch && defaultBranch.length) {
|
||||
this.submit.branchName = defaultBranch[0].name
|
||||
} else {
|
||||
this.submit.branchName = null
|
||||
}
|
||||
// 提交列表
|
||||
this.commitList = data.commits
|
||||
if (this.submit.commitId) {
|
||||
// nothing
|
||||
} else if (data.commits && data.commits.length) {
|
||||
this.submit.commitId = this.commitList[0].id
|
||||
} else {
|
||||
this.submit.commitId = null
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
reloadBranch() {
|
||||
this.loading = true
|
||||
this.$api.getRepositoryBranchList({
|
||||
id: this.detail.repoId
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.branchList = data
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
reloadCommit() {
|
||||
if (!this.submit.branchName) {
|
||||
this.commitList = []
|
||||
this.submit.commitId = undefined
|
||||
return
|
||||
}
|
||||
if (!this.submit.branchName) {
|
||||
this.$message.warning('请先选择分支')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.getRepositoryCommitList({
|
||||
id: this.detail.repoId,
|
||||
branchName: this.submit.branchName
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.commitList = data
|
||||
if (data && data.length) {
|
||||
this.submit.commitId = this.commitList[0].id
|
||||
} else {
|
||||
this.submit.commitId = null
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
check() {
|
||||
if (this.detail.repoId) {
|
||||
if (!this.submit.branchName) {
|
||||
this.$message.warning('请选择分支')
|
||||
return
|
||||
}
|
||||
if (!this.submit.commitId) {
|
||||
this.$message.warning('请选择commit')
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$emit('ok', this.detail.id, this.submit)
|
||||
this.close()
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.build-form-item-input {
|
||||
width: 346px;
|
||||
}
|
||||
|
||||
.reload {
|
||||
font-size: 19px;
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
color: #339AF0;
|
||||
}
|
||||
|
||||
.reload:hover {
|
||||
color: #228BE6;
|
||||
}
|
||||
|
||||
.commit-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.commit-item-left {
|
||||
display: flex;
|
||||
width: 285px;
|
||||
}
|
||||
|
||||
.commit-item-id {
|
||||
width: 50px;
|
||||
margin-right: 8px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.commit-item-message {
|
||||
width: 174px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.commit-item-date {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .commit-selector .ant-select-selection-selected-value {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
415
orion-ops-vue/src/components/app/AppPipelineExecModal.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:width="550"
|
||||
:maskStyle="{opacity: 0.8, animation: 'none'}"
|
||||
:dialogStyle="{top: '64px', padding: 0}"
|
||||
:bodyStyle="{padding: '8px'}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true">
|
||||
<!-- 页头 -->
|
||||
<template #title>
|
||||
<span v-if="selectPipelinePage">选择流水线</span>
|
||||
<span v-if="!selectPipelinePage">
|
||||
<a-icon class="mx4 pointer span-blue"
|
||||
title="重新选择"
|
||||
v-if="visibleReselect && id"
|
||||
@click="reselectPipelineList"
|
||||
type="arrow-left"/>
|
||||
执行流水线
|
||||
</span>
|
||||
</template>
|
||||
<!-- 初始化骨架 -->
|
||||
<a-skeleton v-if="initiating" active :paragraph="{rows: 8}"/>
|
||||
<!-- 主体 -->
|
||||
<a-spin v-else :spinning="loading">
|
||||
<!-- 流水线选择 -->
|
||||
<div class="pipeline-list-container" v-if="selectPipelinePage">
|
||||
<!-- 无流水线数据 -->
|
||||
<a-empty v-if="!pipelineList.length" description="请先配置应用流水线"/>
|
||||
<!-- 流水线列表 -->
|
||||
<div class="pipeline-list">
|
||||
<div class="pipeline-item" v-for="pipeline of pipelineList"
|
||||
:key="pipeline.id"
|
||||
@click="choosePipeline(pipeline.id)">
|
||||
<div class="pipeline-name">
|
||||
<a-icon class="mx8" type="code-sandbox"/>
|
||||
{{ pipeline.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 执行配置 -->
|
||||
<div class="pipeline-container" v-else>
|
||||
<!-- 执行参数 -->
|
||||
<a-form-model v-bind="layout">
|
||||
<!-- 执行标题 -->
|
||||
<a-form-model-item class="exec-form-item" label="执行标题" required>
|
||||
<a-input class="name-input" v-model="submit.title" :maxLength="32" allowClear/>
|
||||
</a-form-model-item>
|
||||
<!-- 执行类型 -->
|
||||
<a-form-model-item class="exec-form-item" label="执行类型" required>
|
||||
<a-radio-group v-model="submit.timedExec" buttonStyle="solid">
|
||||
<a-radio-button :value="type.value" v-for="type in TIMED_TYPE" :key="type.value">
|
||||
{{ type.execLabel }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-model-item>
|
||||
<!-- 调度时间 -->
|
||||
<a-form-model-item class="exec-form-item" label="调度时间"
|
||||
v-if="submit.timedExec === TIMED_TYPE.TIMED.value" required>
|
||||
<a-date-picker v-model="submit.timedExecTime" :showTime="true" format="YYYY-MM-DD HH:mm:ss"/>
|
||||
</a-form-model-item>
|
||||
<!-- 执行描述 -->
|
||||
<a-form-model-item class="exec-form-item" label="执行描述">
|
||||
<a-textarea class="description-input" v-model="submit.description" :maxLength="64" allowClear/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
<a-divider class="detail-divider">流水线操作</a-divider>
|
||||
<!-- 执行操作 -->
|
||||
<div class="pipeline-wrapper">
|
||||
<a-timeline>
|
||||
<a-timeline-item v-for="detail of details" :key="detail.id">
|
||||
<div class="pipeline-detail-wrapper">
|
||||
<!-- 操作名称 -->
|
||||
<div class="pipeline-stage-type span-blue">
|
||||
{{ detail.stageType | formatStageType('label') }}
|
||||
</div>
|
||||
<!-- 应用名称 -->
|
||||
<div class="pipeline-app-name">
|
||||
{{ detail.appName }}
|
||||
</div>
|
||||
<!-- 应用操作 -->
|
||||
<div class="pipeline-handler">
|
||||
<span class="pipeline-config-message auto-ellipsis-item"
|
||||
:title="getConfigMessage(detail)"
|
||||
v-text="getConfigMessage(detail)"/>
|
||||
<!-- 设置 -->
|
||||
<a-badge class="pipeline-set-badge" :dot="visibleConfigDot(detail)">
|
||||
<span class="span-blue pointer" title="设置" @click="openSetting(detail)">设置</span>
|
||||
</a-badge>
|
||||
</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 页脚 -->
|
||||
<template #footer>
|
||||
<!-- 关闭 -->
|
||||
<a-button @click="close">关闭</a-button>
|
||||
<!-- 执行 -->
|
||||
<a-button type="primary"
|
||||
:loading="loading"
|
||||
:disabled="selectPipelinePage || loading || initiating"
|
||||
@click="execPipeline">
|
||||
执行
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 事件 -->
|
||||
<div class="pipeline-event-container">
|
||||
<!-- 构建配置 -->
|
||||
<AppPipelineExecBuildModal ref="buildSetting" @ok="pipelineConfigured"/>
|
||||
<!-- 发布配置 -->
|
||||
<AppPipelineExecReleaseModal ref="releaseSetting" @ok="pipelineConfigured"/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumValueOf, STAGE_TYPE, TIMED_TYPE } from '@/lib/enum'
|
||||
import AppPipelineExecBuildModal from '@/components/app/AppPipelineExecBuildModal'
|
||||
import AppPipelineExecReleaseModal from '@/components/app/AppPipelineExecReleaseModal'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 18 }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineExecModal',
|
||||
components: {
|
||||
AppPipelineExecReleaseModal,
|
||||
AppPipelineExecBuildModal
|
||||
},
|
||||
props: {
|
||||
visibleReselect: Boolean
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
TIMED_TYPE,
|
||||
selectPipelinePage: false,
|
||||
id: null,
|
||||
visible: false,
|
||||
initiating: false,
|
||||
loading: false,
|
||||
profileId: null,
|
||||
pipelineList: [],
|
||||
record: {},
|
||||
submit: {
|
||||
title: null,
|
||||
description: null,
|
||||
timedExec: null,
|
||||
timedExecTime: null
|
||||
},
|
||||
details: [],
|
||||
layout
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async openPipelineList(profileId) {
|
||||
this.profileId = profileId
|
||||
this.pipelineList = []
|
||||
this.selectPipelinePage = true
|
||||
this.loading = false
|
||||
this.initiating = true
|
||||
this.visible = true
|
||||
await this.loadPipelineList()
|
||||
this.initiating = false
|
||||
},
|
||||
async openPipeline(profileId, id) {
|
||||
this.profileId = profileId
|
||||
this.pipelineList = []
|
||||
this.cleanPipelineData()
|
||||
this.selectPipelinePage = false
|
||||
this.loading = false
|
||||
this.initiating = true
|
||||
this.visible = true
|
||||
await this.selectPipeline(id)
|
||||
this.initiating = false
|
||||
},
|
||||
async choosePipeline(id) {
|
||||
this.cleanPipelineData()
|
||||
this.selectPipelinePage = false
|
||||
this.loading = true
|
||||
await this.selectPipeline(id)
|
||||
this.loading = false
|
||||
},
|
||||
async loadPipelineList() {
|
||||
const { data: { rows } } = await this.$api.getAppPipelineList({
|
||||
profileId: this.profileId,
|
||||
limit: 10000
|
||||
})
|
||||
this.pipelineList = rows
|
||||
},
|
||||
async selectPipeline(id) {
|
||||
this.id = id
|
||||
await this.$api.getAppPipelineDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.record = data
|
||||
this.details = data.details
|
||||
this.submit.title = `执行${data.name}`
|
||||
})
|
||||
},
|
||||
async reselectPipelineList() {
|
||||
this.selectPipelinePage = true
|
||||
if (this.pipelineList.length) {
|
||||
return
|
||||
}
|
||||
this.initiating = true
|
||||
await this.loadPipelineList()
|
||||
this.initiating = false
|
||||
},
|
||||
cleanPipelineData() {
|
||||
this.record = {}
|
||||
this.details = []
|
||||
this.submit.title = null
|
||||
this.submit.description = null
|
||||
this.submit.timedExec = TIMED_TYPE.NORMAL.value
|
||||
this.submit.timedExecTime = null
|
||||
},
|
||||
visibleConfigDot(detail) {
|
||||
if (detail.stageType === STAGE_TYPE.BUILD.value) {
|
||||
if (detail.repoId) {
|
||||
return !detail.branchName
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
getConfigMessage(detail) {
|
||||
if (detail.stageType === STAGE_TYPE.BUILD.value) {
|
||||
// 构建
|
||||
if (detail.repoId) {
|
||||
if (detail.branchName) {
|
||||
return `${detail.branchName} ${detail.commitId.substring(0, 7)}`
|
||||
} else {
|
||||
return '请选择构建版本'
|
||||
}
|
||||
} else {
|
||||
return '无需配置'
|
||||
}
|
||||
} else {
|
||||
// 发布
|
||||
if (detail.buildSeq) {
|
||||
return `发布版本: #${detail.buildSeq}`
|
||||
} else {
|
||||
return '发布版本: 最新版本'
|
||||
}
|
||||
}
|
||||
},
|
||||
openSetting(detail) {
|
||||
if (detail.stageType === STAGE_TYPE.BUILD.value) {
|
||||
this.$refs.buildSetting.open(detail)
|
||||
} else {
|
||||
this.$refs.releaseSetting.open(detail)
|
||||
}
|
||||
},
|
||||
pipelineConfigured(detailId, config) {
|
||||
this.details.filter(s => s.id === detailId).forEach((detail) => {
|
||||
for (const configKey in config) {
|
||||
detail[configKey] = config[configKey]
|
||||
}
|
||||
})
|
||||
this.$set(this.details, 0, this.details[0])
|
||||
},
|
||||
execPipeline() {
|
||||
// 检查参数
|
||||
if (!this.submit.title) {
|
||||
this.$message.warning('请输入执行标题')
|
||||
return
|
||||
}
|
||||
if (this.submit.timedExec === TIMED_TYPE.TIMED.value) {
|
||||
if (!this.submit.timedExecTime) {
|
||||
this.$message.warning('请选择调度时间')
|
||||
return
|
||||
}
|
||||
if (this.submit.timedExecTime.unix() * 1000 < Date.now()) {
|
||||
this.$message.warning('调度时间需要大于当前时间')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this.submit.timedExecTime = undefined
|
||||
}
|
||||
for (const detail of this.details) {
|
||||
if (detail.stageType === STAGE_TYPE.BUILD.value && detail.repoId && !detail.branchName) {
|
||||
this.$message.warning(`请选择 ${detail.appName} 构建版本`)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 封装数据
|
||||
const request = {
|
||||
pipelineId: this.id,
|
||||
...this.submit,
|
||||
details: []
|
||||
}
|
||||
for (const detail of this.details) {
|
||||
request.details.push({
|
||||
id: detail.id,
|
||||
branchName: detail.branchName,
|
||||
commitId: detail.commitId,
|
||||
buildId: detail.buildId,
|
||||
title: detail.title,
|
||||
description: detail.description,
|
||||
machineIdList: detail.machineIdList
|
||||
})
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.submitAppPipelineTask(request).then(() => {
|
||||
this.$message.success('已创建流水线任务')
|
||||
this.$emit('submit')
|
||||
this.visible = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatStageType(type, f) {
|
||||
return enumValueOf(STAGE_TYPE, type)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.pipeline-container {
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.pipeline-list {
|
||||
margin: 0 4px 0 8px;
|
||||
height: 355px;
|
||||
overflow-y: auto;
|
||||
|
||||
.pipeline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: #F8F9FA;
|
||||
border-radius: 4px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
.pipeline-name {
|
||||
width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-item:hover {
|
||||
background: #E7F5FF;
|
||||
}
|
||||
}
|
||||
|
||||
.exec-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-divider {
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.pipeline-detail-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.pipeline-stage-type {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.pipeline-app-name {
|
||||
width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pipeline-handler {
|
||||
width: 180px;
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.pipeline-config-message {
|
||||
margin-right: 8px;
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
width: 144px;
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .pipeline-set-badge .ant-badge-dot {
|
||||
margin: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .ant-timeline-item-last {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
226
orion-ops-vue/src/components/app/AppPipelineExecReleaseModal.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="560"
|
||||
:maskStyle="{opacity: 0.4, animation: 'none'}"
|
||||
:bodyStyle="{padding: '24px 24px 0 24px'}"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<!-- 发布配置 -->
|
||||
<a-spin :spinning="loading">
|
||||
<a-form-model v-bind="layout">
|
||||
<!-- 发布标题 -->
|
||||
<a-form-model-item label="发布标题" required>
|
||||
<a-input class="release-form-item-input"
|
||||
v-model="submit.title"
|
||||
placeholder="标题"
|
||||
:maxLength="32"
|
||||
allowClear/>
|
||||
</a-form-model-item>
|
||||
<!-- 发布版本 -->
|
||||
<a-form-model-item label="发布版本">
|
||||
<a-select class="release-form-item-input build-selector"
|
||||
v-model="submit.buildId"
|
||||
placeholder="最新版本"
|
||||
allowClear>
|
||||
<a-select-option v-for="build of buildList" :key="build.id" :value="build.id">
|
||||
<div class="build-item">
|
||||
<div class="build-item-left">
|
||||
<span class="span-blue">#{{ build.seq }}</span>
|
||||
<span class="build-item-message">{{ build.description }}</span>
|
||||
</div>
|
||||
<span class="build-item-date">
|
||||
{{ build.createTime | formatDate('MM-dd HH:mm') }}
|
||||
</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="reload" class="reload" title="刷新" @click="loadBuildList"/>
|
||||
</a-form-model-item>
|
||||
<!-- 发布机器 -->
|
||||
<a-form-model-item label="发布机器">
|
||||
<MachineChecker ref="machineChecker"
|
||||
placement="bottomLeft"
|
||||
:defaultValue="submit.machineIdList"
|
||||
:query="{idList: appMachineIdList}">
|
||||
<template #trigger>
|
||||
<span class="span-blue pointer">已选择 {{ submit.machineIdList.length }} 台机器</span>
|
||||
</template>
|
||||
<template #footer>
|
||||
<a-button type="primary" size="small" @click="chooseMachines">确定</a-button>
|
||||
</template>
|
||||
</MachineChecker>
|
||||
</a-form-model-item>
|
||||
<!-- 发布描述 -->
|
||||
<a-form-model-item label="发布描述" required>
|
||||
<a-textarea class="release-form-item-input"
|
||||
v-model="submit.description"
|
||||
style="height: 50px; width: 430px"
|
||||
:maxLength="64"
|
||||
allowClear/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import MachineChecker from '@/components/machine/MachineChecker'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 18 }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineExecReleaseModal',
|
||||
components: {
|
||||
MachineChecker
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
title: null,
|
||||
buildList: [],
|
||||
appMachineIdList: [],
|
||||
submit: {
|
||||
title: null,
|
||||
buildId: null,
|
||||
buildSeq: null,
|
||||
description: null,
|
||||
machineIdList: []
|
||||
},
|
||||
detail: {},
|
||||
visible: false,
|
||||
loading: false,
|
||||
layout
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open(detail) {
|
||||
this.detail = { ...detail }
|
||||
this.title = `${detail.appName} 发布配置`
|
||||
this.buildList = []
|
||||
this.appMachineIdList = []
|
||||
this.submit.title = detail.title || `发布${detail.appName}`
|
||||
this.submit.buildId = detail.buildId
|
||||
this.submit.buildSeq = detail.buildSeq
|
||||
this.submit.description = detail.description
|
||||
this.submit.machineIdList = detail.machineIdList || []
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
// 加载构建列表
|
||||
await this.loadBuildList()
|
||||
// 加载发布机器
|
||||
await this.loadReleaseMachine()
|
||||
this.loading = false
|
||||
},
|
||||
async loadBuildList() {
|
||||
await this.$api.getBuildReleaseList({
|
||||
appId: this.detail.appId,
|
||||
profileId: this.detail.profileId
|
||||
}).then(({ data }) => {
|
||||
this.buildList = data
|
||||
})
|
||||
},
|
||||
async loadReleaseMachine() {
|
||||
await this.$api.getAppMachineId({
|
||||
id: this.detail.appId,
|
||||
profileId: this.detail.profileId
|
||||
}).then(({ data }) => {
|
||||
if (data && data.length) {
|
||||
this.appMachineIdList = data
|
||||
if (!this.submit.machineIdList.length) {
|
||||
this.submit.machineIdList = data
|
||||
}
|
||||
} else {
|
||||
this.$message.warning('请先配置应用发布机器')
|
||||
}
|
||||
})
|
||||
},
|
||||
chooseMachines() {
|
||||
const ref = this.$refs.machineChecker
|
||||
if (!ref.checkedList.length) {
|
||||
this.$message.warning('请选择发布机器机器')
|
||||
return
|
||||
}
|
||||
this.submit.machineIdList = ref.checkedList
|
||||
ref.hide()
|
||||
},
|
||||
async check() {
|
||||
if (!this.submit.title) {
|
||||
this.$message.warning('请输入发布标题')
|
||||
return
|
||||
}
|
||||
if (!this.submit.machineIdList.length) {
|
||||
this.$message.warning('请选择发布机器')
|
||||
return
|
||||
}
|
||||
this.submit.buildSeq = this.buildList.filter(s => s.id === this.submit.buildId).map(s => s.seq)[0]
|
||||
this.$emit('ok', this.detail.id, this.submit)
|
||||
this.close()
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.release-form-item-input {
|
||||
width: 348px;
|
||||
}
|
||||
|
||||
.reload {
|
||||
font-size: 19px;
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
color: #339AF0;
|
||||
}
|
||||
|
||||
.reload:hover {
|
||||
color: #228BE6;
|
||||
}
|
||||
|
||||
.build-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.build-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 276px;
|
||||
}
|
||||
|
||||
.build-item-seq {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.build-item-message {
|
||||
width: 200px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.build-item-date {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .build-selector .ant-select-selection-selected-value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="设置定时执行"
|
||||
:width="330"
|
||||
:okButtonProps="{props: {disabled: !valid}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="submit"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="">
|
||||
<span class="normal-label mr8">调度时间 </span>
|
||||
<a-date-picker v-model="timedExecTime" :showTime="true" format="YYYY-MM-DD HH:mm:ss"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PIPELINE_STATUS, TIMED_TYPE } from '@/lib/enum'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineExecTimedModal',
|
||||
data() {
|
||||
return {
|
||||
record: null,
|
||||
visible: false,
|
||||
loading: false,
|
||||
timedExecTime: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valid() {
|
||||
if (!this.timedExecTime) {
|
||||
return false
|
||||
}
|
||||
return Date.now() < this.timedExecTime
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(record) {
|
||||
this.record = record
|
||||
this.timedExecTime = (record.timedExecTime && moment(record.timedExecTime)) || undefined
|
||||
this.visible = true
|
||||
},
|
||||
submit() {
|
||||
const time = this.timedExecTime.unix() * 1000
|
||||
this.loading = true
|
||||
this.$api.setAppPipelineTaskTimedExec({
|
||||
id: this.record.id,
|
||||
timedExecTime: time
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.record.timedExecTime = time
|
||||
this.record.status = PIPELINE_STATUS.WAIT_SCHEDULE.value
|
||||
this.record.timedExec = TIMED_TYPE.TIMED.value
|
||||
this.$emit('updated')
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
73
orion-ops-vue/src/components/app/AppPipelineSelector.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<a-select v-model="id"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@change="$emit('change', id)"
|
||||
allowClear>
|
||||
<a-select-option v-for="pipeline in pipelineList"
|
||||
:value="pipeline.id"
|
||||
:key="pipeline.id">
|
||||
{{ pipeline.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppPipelineSelector',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '全部'
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: undefined,
|
||||
pipelineList: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(e) {
|
||||
this.id = e
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.id = undefined
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// 读取当前环境
|
||||
const activeProfile = this.$storage.get(this.$storage.keys.ACTIVE_PROFILE)
|
||||
if (!activeProfile) {
|
||||
this.$message.warning('请先维护应用环境')
|
||||
return
|
||||
}
|
||||
const pipelineListRes = await this.$api.getAppPipelineList({
|
||||
profileId: JSON.parse(activeProfile).id,
|
||||
limit: 10000
|
||||
})
|
||||
if (pipelineListRes.data && pipelineListRes.data.rows && pipelineListRes.data.rows.length) {
|
||||
for (const row of pipelineListRes.data.rows) {
|
||||
this.pipelineList.push({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<!-- 统计折线图 -->
|
||||
<div class="statistic-chart-container">
|
||||
<p class="statistics-description">近七天执行统计</p>
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 折线图 -->
|
||||
<div class="statistic-chart-wrapper">
|
||||
<div id="statistic-chart"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Chart } from '@antv/g2'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineStatisticsCharts',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(pipelineId) {
|
||||
this.loading = true
|
||||
const { data } = await this.$api.getAppPipelineTaskStatisticsChart({
|
||||
pipelineId
|
||||
})
|
||||
this.loading = false
|
||||
this.renderChart(data)
|
||||
},
|
||||
clean() {
|
||||
this.loading = false
|
||||
this.chart && this.chart.destroy()
|
||||
this.chart = null
|
||||
},
|
||||
renderChart(data) {
|
||||
// 处理数据
|
||||
const chartsData = []
|
||||
for (const d of data) {
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '执行次数',
|
||||
value: d.execCount
|
||||
})
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '成功次数',
|
||||
value: d.successCount
|
||||
})
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '失败次数',
|
||||
value: d.failureCount
|
||||
})
|
||||
}
|
||||
this.clean()
|
||||
// 渲染图表
|
||||
this.chart = new Chart({
|
||||
container: 'statistic-chart',
|
||||
autoFit: true
|
||||
})
|
||||
this.chart.data(chartsData)
|
||||
|
||||
this.chart.tooltip({
|
||||
showCrosshairs: true,
|
||||
shared: true
|
||||
})
|
||||
|
||||
this.chart.line()
|
||||
.position('date*value')
|
||||
.color('type')
|
||||
.shape('circle')
|
||||
this.chart.render()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.statistic-chart-wrapper {
|
||||
margin: 0 24px 16px 24px;
|
||||
|
||||
#statistic-chart {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 全部执行指标 -->
|
||||
<p class="statistics-description">全部执行指标</p>
|
||||
<div class="app-pipeline-statistic-metrics">
|
||||
<div class="clean"/>
|
||||
<!-- 统计指标 -->
|
||||
<div class="app-pipeline-statistic-header">
|
||||
<a-statistic class="statistic-metrics-item" title="执行次数" :value="allMetrics.execCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item green" title="成功次数" :value="allMetrics.successCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item red" title="失败次数" :value="allMetrics.failureCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item" title="平均耗时" :value="allMetrics.avgUsedInterval"/>
|
||||
</div>
|
||||
<div class="clean"/>
|
||||
</div>
|
||||
<!-- 近七天执行指标 -->
|
||||
<p class="statistics-description" style="margin-top: 28px">近七天执行指标</p>
|
||||
<div class="app-pipeline-statistic-metrics">
|
||||
<div class="clean"/>
|
||||
<!-- 统计指标 -->
|
||||
<div class="app-pipeline-statistic-header">
|
||||
<a-statistic class="statistic-metrics-item" title="执行次数" :value="latelyMetrics.execCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item green" title="成功次数" :value="latelyMetrics.successCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item red" title="失败次数" :value="latelyMetrics.failureCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item" title="平均耗时" :value="latelyMetrics.avgUsedInterval"/>
|
||||
</div>
|
||||
<div class="clean"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineStatisticsMetrics',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
allMetrics: {
|
||||
execCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
},
|
||||
latelyMetrics: {
|
||||
execCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(pipelineId) {
|
||||
this.loading = true
|
||||
const { data } = await this.$api.getAppPipelineTaskStatisticsMetrics({
|
||||
pipelineId
|
||||
})
|
||||
this.loading = false
|
||||
this.allMetrics = data.all
|
||||
this.latelyMetrics = data.lately
|
||||
},
|
||||
clean() {
|
||||
this.loading = false
|
||||
this.allMetrics = {
|
||||
execCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
}
|
||||
this.latelyMetrics = {
|
||||
execCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-pipeline-statistic-metrics {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 24px 0 12px 16px;
|
||||
|
||||
.app-pipeline-statistic-header {
|
||||
display: flex;
|
||||
|
||||
.statistic-metrics-item {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
::v-deep .statistic-metrics-item.green .ant-statistic-content {
|
||||
color: #58C612;
|
||||
}
|
||||
|
||||
::v-deep .statistic-metrics-item.red .ant-statistic-content {
|
||||
color: #DD2C00;
|
||||
}
|
||||
|
||||
.statistic-metrics-divider {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
::v-deep .ant-statistic-content {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
347
orion-ops-vue/src/components/app/AppPipelineStatisticsViews.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div class="app-pipeline-statistic-record-view-container">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 流水线视图 -->
|
||||
<div class="app-pipeline-statistic-record-view-wrapper" v-if="initialized && view">
|
||||
<div class="app-pipeline-statistic-record-view">
|
||||
<p class="statistics-description">近十次执行视图</p>
|
||||
<!-- 流水线视图主体 -->
|
||||
<div class="app-pipeline-statistic-main">
|
||||
<!-- 流水线操作 -->
|
||||
<div class="app-pipeline-details-wrapper">
|
||||
<!-- 平均时间 -->
|
||||
<div class="app-pipeline-details-legend">
|
||||
<span class="avg-used-legend-wrapper">
|
||||
<span>平均执行时间: </span>
|
||||
<span class="avg-used-legend">{{ view.avgUsedInterval }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 流水线操作 -->
|
||||
<div class="app-pipeline-details" v-for="(detail, index) of view.details" :key="detail.id">
|
||||
<div :class="['app-pipeline-details-name', index % 2 === 0 ? 'app-pipeline-details-name-theme1' : 'app-pipeline-details-name-theme2']">
|
||||
<span class="span-blue mr4">{{ detail.stageType | formatStageType('label') }}</span>
|
||||
<span>{{ detail.appName }}</span>
|
||||
</div>
|
||||
<div :class="['app-pipeline-details-avg', index % 2 === 0 ? 'app-pipeline-details-avg-theme1' : 'app-pipeline-details-avg-theme2']">
|
||||
<div class="app-pipeline-details-avg">
|
||||
{{ detail.avgUsedInterval }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流水线日志 -->
|
||||
<div class="app-pipeline-detail-logs-container">
|
||||
<!-- 日志 -->
|
||||
<div class="app-pipeline-detail-logs-wrapper"
|
||||
v-for="pipelineTask of view.pipelineTaskList"
|
||||
:key="pipelineTask.id">
|
||||
<!-- 流水线信息头 -->
|
||||
<div class="app-pipeline-record-legend">
|
||||
<!-- 流水线状态 -->
|
||||
<div class="app-pipeline-record-status">
|
||||
<!-- 流水线序列 -->
|
||||
<div class="pipeline-title">
|
||||
<span class="span-blue">{{ pipelineTask.title }}</span>
|
||||
</div>
|
||||
<!-- 流水线状态 -->
|
||||
<a-tag class="m0" :color="pipelineTask.status | formatPipelineStatus('color')">
|
||||
{{ pipelineTask.status | formatPipelineStatus('label') }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<!-- 流水线时间 -->
|
||||
<div class="app-pipeline-record-info">
|
||||
<span>{{ pipelineTask.execDate | formatDate('MM-dd HH:mm') }}</span>
|
||||
<span v-if="pipelineTask.usedInterval" class="ml4"> (used: {{ pipelineTask.usedInterval }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 流水线操作 -->
|
||||
<div class="app-pipeline-detail-log-details-wrapper">
|
||||
<div class="app-pipeline-detail-log-detail-wrapper"
|
||||
v-for="(detailLog, index) of pipelineTask.details" :key="index">
|
||||
<!-- 流水线操作值 -->
|
||||
<div class="app-pipeline-detail-log-detail"
|
||||
v-if="!getCanOpenLog(detailLog)"
|
||||
:style="getDetailLogStyle(detailLog)"
|
||||
v-text="getDetailLogValue(detailLog)">
|
||||
</div>
|
||||
<!-- 可打开日志 -->
|
||||
<a v-else target="_blank"
|
||||
:title="detailLog.stageType | formatStageLogTitle"
|
||||
:href="detailLog.stageType | formatStageLogSrc(detailLog.relId)"
|
||||
@click="openLogView($event, detailLog.stageType, detailLog.relId)">
|
||||
<div class="app-pipeline-detail-log-detail"
|
||||
:style="getDetailLogStyle(detailLog)"
|
||||
v-text="getDetailLogValue(detailLog)">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无数据 -->
|
||||
<div style="padding: 0 16px" v-else-if="initialized && !view">
|
||||
无执行记录
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 事件 -->
|
||||
<div class="app-pipeline-statistic-event-container">
|
||||
<!-- 构建日志模态框 -->
|
||||
<AppBuildLogAppenderModal ref="buildLogView"/>
|
||||
<!-- 发布日志模态框 -->
|
||||
<AppReleaseLogAppenderModal ref="releaseLogView"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, PIPELINE_DETAIL_STATUS, PIPELINE_STATUS, STAGE_TYPE } from '@/lib/enum'
|
||||
import AppBuildLogAppenderModal from '@/components/log/AppBuildLogAppenderModal'
|
||||
import AppReleaseLogAppenderModal from '@/components/log/AppReleaseLogAppenderModal'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineStatisticsViews',
|
||||
components: {
|
||||
AppBuildLogAppenderModal,
|
||||
AppReleaseLogAppenderModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
STAGE_TYPE,
|
||||
loading: false,
|
||||
initialized: false,
|
||||
view: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(pipelineId) {
|
||||
this.loading = true
|
||||
this.initialized = false
|
||||
const { data } = await this.$api.getAppPipelineTaskStatisticsView({
|
||||
pipelineId
|
||||
})
|
||||
this.view = data
|
||||
this.initialized = true
|
||||
this.loading = false
|
||||
},
|
||||
clean() {
|
||||
this.initialized = false
|
||||
this.loading = false
|
||||
this.view = {}
|
||||
},
|
||||
async refresh(pipelineId) {
|
||||
const { data } = await this.$api.getAppPipelineTaskStatisticsView({
|
||||
pipelineId
|
||||
})
|
||||
this.view = data
|
||||
},
|
||||
openLogView(e, type, id) {
|
||||
const stageType = enumValueOf(STAGE_TYPE, type).symbol
|
||||
if (!e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
// 打开模态框
|
||||
this.$refs[`${stageType}LogView`].open(id)
|
||||
return false
|
||||
} else {
|
||||
// 跳转页面
|
||||
return true
|
||||
}
|
||||
},
|
||||
getCanOpenLog(detailLog) {
|
||||
if (detailLog) {
|
||||
return enumValueOf(PIPELINE_DETAIL_STATUS, detailLog.status).log
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
getDetailLogStyle(detailLog) {
|
||||
if (detailLog) {
|
||||
return enumValueOf(PIPELINE_DETAIL_STATUS, detailLog.status).actionStyle
|
||||
} else {
|
||||
return {
|
||||
background: '#FFD43B'
|
||||
}
|
||||
}
|
||||
},
|
||||
getDetailLogValue(detailLog) {
|
||||
if (detailLog) {
|
||||
return enumValueOf(PIPELINE_DETAIL_STATUS, detailLog.status).actionValue(detailLog)
|
||||
} else {
|
||||
return '未执行'
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatStageType(type, f) {
|
||||
return enumValueOf(STAGE_TYPE, type)[f]
|
||||
},
|
||||
formatStageLogTitle(type) {
|
||||
return `点击查看${enumValueOf(STAGE_TYPE, type).label}日志`
|
||||
},
|
||||
formatStageLogSrc(type, relId) {
|
||||
return `#/app/${enumValueOf(STAGE_TYPE, type).symbol}/log/view/${relId}`
|
||||
},
|
||||
formatPipelineStatus(status, f) {
|
||||
return enumValueOf(PIPELINE_STATUS, status)[f]
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-pipeline-statistic-record-view-wrapper {
|
||||
margin: 0 24px 24px 24px;
|
||||
overflow: auto;
|
||||
|
||||
.app-pipeline-statistic-record-view {
|
||||
margin: 0 16px 16px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.app-pipeline-statistic-main {
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 4px 1px #DEE2E6;
|
||||
}
|
||||
|
||||
.app-pipeline-details-wrapper {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
|
||||
.app-pipeline-details-legend {
|
||||
width: 180px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 8px 4px 0;
|
||||
font-weight: 600;
|
||||
|
||||
.avg-used-legend-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avg-used-legend {
|
||||
margin-left: 4px;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-pipeline-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 148px;
|
||||
|
||||
.app-pipeline-details-name {
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
color: #181E33;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 16px;
|
||||
border-bottom: 1px solid #DEE2E6;
|
||||
}
|
||||
|
||||
.app-pipeline-details-name-theme1 {
|
||||
background: #F1F3F5;
|
||||
}
|
||||
|
||||
.app-pipeline-details-name-theme2 {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
.app-pipeline-details-avg {
|
||||
text-align: center;
|
||||
height: 28px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.app-pipeline-details-avg-theme1 {
|
||||
background: #E9ECEF;
|
||||
}
|
||||
|
||||
.app-pipeline-details-avg-theme2 {
|
||||
background: #F1F4F7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-pipeline-detail-logs-wrapper {
|
||||
display: flex;
|
||||
height: 74px;
|
||||
|
||||
.app-pipeline-record-legend {
|
||||
width: 180px;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border-top: 2px solid #F8F9FA;
|
||||
|
||||
.app-pipeline-record-status {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pipeline-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 112px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.app-pipeline-record-info {
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-pipeline-detail-log-details-wrapper {
|
||||
display: flex;
|
||||
|
||||
.app-pipeline-detail-log-detail-wrapper {
|
||||
width: 148px;
|
||||
border-radius: 4px;
|
||||
|
||||
.app-pipeline-detail-log-detail {
|
||||
margin: 2px 1px;
|
||||
height: calc(100% - 2px);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0.8;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.app-pipeline-detail-log-detail:hover {
|
||||
opacity: 1;
|
||||
transition: .3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
226
orion-ops-vue/src/components/app/AppPipelineTaskDetailDrawer.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<a-drawer title="执行详情"
|
||||
placement="right"
|
||||
:visible="visible"
|
||||
:maskStyle="{opacity: 0, animation: 'none'}"
|
||||
:width="430"
|
||||
@close="onClose">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading">
|
||||
<a-skeleton active :paragraph="{rows: 12}"/>
|
||||
</div>
|
||||
<!-- 加载完成 -->
|
||||
<div v-else>
|
||||
<!-- 流水线信息 -->
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="流水线名称" :span="3">
|
||||
{{ detail.pipelineName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行标题" :span="3">
|
||||
{{ detail.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="环境名称" :span="3">
|
||||
{{ detail.profileName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行描述" :span="3" v-if="detail.description != null">
|
||||
{{ detail.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行状态" :span="3">
|
||||
<a-tag :color="detail.status | formatPipelineStatus('color')">
|
||||
{{ detail.status | formatPipelineStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="调度时间" :span="3" v-if="detail.timedExecTime !== null">
|
||||
{{ detail.timedExecTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建用户" :span="3">
|
||||
{{ detail.createUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间" :span="3" v-if="detail.createTime !== null">
|
||||
{{ detail.createTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核用户" :span="3" v-if="detail.auditUserName !== null">
|
||||
{{ detail.auditUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核时间" :span="3" v-if="detail.auditTime !== null">
|
||||
{{ detail.auditTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核批注" :span="3" v-if="detail.auditReason !== null">
|
||||
{{ detail.auditReason }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行用户" :span="3" v-if=" detail.execUserName !== null">
|
||||
{{ detail.execUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="detail.execStartTime !== null">
|
||||
{{ detail.execStartTime | formatDate }} ({{ detail.execStartTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="detail.execEndTime !== null">
|
||||
{{ detail.execEndTime | formatDate }} ({{ detail.execEndTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="detail.used !== null">
|
||||
{{ `${detail.keepTime} (${detail.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<!-- 流水线操作 -->
|
||||
<a-divider>流水线操作</a-divider>
|
||||
<a-list size="small" :dataSource="detail.details">
|
||||
<template #renderItem="item">
|
||||
<a-list-item>
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="执行操作" :span="3">
|
||||
<span class="span-blue">
|
||||
{{ item.stageType | formatStageType('label') }}
|
||||
</span>
|
||||
{{ item.appName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行状态" :span="3">
|
||||
<a-tag :color="item.status | formatPipelineDetailStatus('color')">
|
||||
{{ item.status | formatPipelineDetailStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建分支" :span="3" v-if="item.stageType === STAGE_TYPE.BUILD.value && item.config.branchName">
|
||||
<a-icon type="branches"/>
|
||||
{{ item.config.branchName }}
|
||||
<a-tooltip v-if="item.config.commitId">
|
||||
<template #title>
|
||||
{{ item.config.commitId }}
|
||||
</template>
|
||||
<span class="span-blue">
|
||||
#{{ item.config.commitId.substring(0, 7) }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布版本" :span="3" v-if="item.stageType === STAGE_TYPE.RELEASE.value">
|
||||
<span class="span-blue">
|
||||
{{ item.config.buildSeq ? `#${item.config.buildSeq}` : '最新版本' }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布机器" :span="3" v-if="item.stageType === STAGE_TYPE.RELEASE.value">
|
||||
<span v-if="item.config.machineIdList && item.config.machineIdList.length" class="span-blue">
|
||||
{{ item.config.machineList.map(s => s.name).join(', ') }}
|
||||
</span>
|
||||
<span v-else class="span-blue">全部机器</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="item.startTime !== null">
|
||||
{{ item.startTime | formatDate }} ({{ item.startTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="item.endTime !== null">
|
||||
{{ item.endTime | formatDate }} ({{ item.endTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="item.keepTime !== null">
|
||||
{{ `${item.keepTime} (${item.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, PIPELINE_DETAIL_STATUS, PIPELINE_STATUS, STAGE_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineTaskDetailDrawer',
|
||||
data() {
|
||||
return {
|
||||
STAGE_TYPE,
|
||||
visible: false,
|
||||
loading: true,
|
||||
pollId: null,
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
this.detail = {}
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
this.$api.getAppPipelineTaskDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.detail = data
|
||||
// 轮询状态
|
||||
if (data.status === PIPELINE_STATUS.WAIT_AUDIT.value ||
|
||||
data.status === PIPELINE_STATUS.AUDIT_REJECT.value ||
|
||||
data.status === PIPELINE_STATUS.WAIT_RUNNABLE.value ||
|
||||
data.status === PIPELINE_STATUS.WAIT_SCHEDULE.value ||
|
||||
data.status === PIPELINE_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 5000)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
},
|
||||
pollStatus() {
|
||||
if (!this.detail || !this.detail.status) {
|
||||
return
|
||||
}
|
||||
if (this.detail.status !== PIPELINE_STATUS.WAIT_AUDIT.value &&
|
||||
this.detail.status !== PIPELINE_STATUS.AUDIT_REJECT.value &&
|
||||
this.detail.status !== PIPELINE_STATUS.WAIT_RUNNABLE.value &&
|
||||
this.detail.status !== PIPELINE_STATUS.WAIT_SCHEDULE.value &&
|
||||
this.detail.status !== PIPELINE_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
return
|
||||
}
|
||||
this.$api.geAppPipelineTaskStatus({
|
||||
id: this.detail.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
this.detail.used = data.used
|
||||
this.detail.keepTime = data.keepTime
|
||||
this.detail.execStartTime = data.startTime
|
||||
this.detail.execStartTimeAgo = data.startTimeAgo
|
||||
this.detail.execEndTime = data.endTime
|
||||
this.detail.execEndTimeAgo = data.endTimeAgo
|
||||
if (data.details && data.details.length) {
|
||||
for (const detail of data.details) {
|
||||
this.detail.details.filter(s => s.id === detail.id).forEach(s => {
|
||||
s.status = detail.status
|
||||
s.keepTime = detail.keepTime
|
||||
s.used = detail.used
|
||||
s.startTime = detail.startTime
|
||||
s.startTimeAgo = detail.startTimeAgo
|
||||
s.endTime = detail.endTime
|
||||
s.endTimeAgo = detail.endTimeAgo
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatPipelineStatus(status, f) {
|
||||
return enumValueOf(PIPELINE_STATUS, status)[f]
|
||||
},
|
||||
formatPipelineDetailStatus(status, f) {
|
||||
return enumValueOf(PIPELINE_DETAIL_STATUS, status)[f]
|
||||
},
|
||||
formatStageType(type, f) {
|
||||
return enumValueOf(STAGE_TYPE, type)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
162
orion-ops-vue/src/components/app/AppProfileChecker.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<a-popover v-model="visible"
|
||||
:destroyTooltipOnHide="true"
|
||||
trigger="click"
|
||||
overlayClassName="profile-content-list-popover"
|
||||
placement="bottom">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<div class="profile-title">
|
||||
<a-checkbox v-if="profiles.length"
|
||||
:indeterminate="indeterminate"
|
||||
:checked="checkAll"
|
||||
@change="chooseAll">
|
||||
全选
|
||||
</a-checkbox>
|
||||
<div v-else/>
|
||||
<a @click="hide">关闭</a>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 内容 -->
|
||||
<template #content>
|
||||
<div class="profile-content">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 复选框 -->
|
||||
<div class="profile-list-wrapper" v-if="profiles.length">
|
||||
<div class="profile-list">
|
||||
<a-checkbox-group v-model="checkedList" @change="onChange">
|
||||
<a-row v-for="(option, index) of profiles" :key="index" style="margin: 4px 0">
|
||||
<a-checkbox :value="option.id" :disabled="checkDisabled(option.id)">
|
||||
{{ option.name }}
|
||||
</a-checkbox>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-list-empty" v-if="empty">
|
||||
<a-empty/>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<a-divider class="content-divider"/>
|
||||
<!-- 底部栏 -->
|
||||
<div class="profile-button-tools">
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 触发器 -->
|
||||
<slot name="trigger"/>
|
||||
</a-popover>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'AppProfileChecker',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
init: false,
|
||||
indeterminate: false,
|
||||
checkAll: false,
|
||||
loading: false,
|
||||
checkedList: [],
|
||||
profiles: [],
|
||||
empty: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(e) {
|
||||
if (e && !this.init) {
|
||||
this.initData()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initData() {
|
||||
this.loading = true
|
||||
this.init = true
|
||||
this.$api.fastGetProfileList()
|
||||
.then(({ data }) => {
|
||||
this.loading = false
|
||||
if (data && data.length) {
|
||||
this.profiles = data.map(s => {
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.empty = true
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
this.init = false
|
||||
})
|
||||
},
|
||||
hide() {
|
||||
this.visible = false
|
||||
},
|
||||
onChange(checkedList) {
|
||||
this.indeterminate = !!checkedList.length && checkedList.length < this.profiles.length - 1
|
||||
this.checkAll = checkedList.length === this.profiles.length - 1
|
||||
},
|
||||
chooseAll(e) {
|
||||
Object.assign(this, {
|
||||
checkedList: e.target.checked
|
||||
? this.profiles.map(d => d.id).filter(id => !this.checkDisabled(id))
|
||||
: [],
|
||||
indeterminate: false,
|
||||
checkAll: e.target.checked
|
||||
})
|
||||
},
|
||||
checkDisabled(id) {
|
||||
const activeProfile = this.$storage.get(this.$storage.keys.ACTIVE_PROFILE)
|
||||
if (activeProfile) {
|
||||
return JSON.parse(activeProfile).id === id
|
||||
}
|
||||
return false
|
||||
},
|
||||
clear() {
|
||||
this.checkedList = []
|
||||
this.checkAll = false
|
||||
this.indeterminate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.profile-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-list-wrapper {
|
||||
padding: 4px 2px 4px 16px;
|
||||
|
||||
.profile-list {
|
||||
max-height: 130px;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-list-empty {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.content-divider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-button-tools {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
</style>
|
||||
83
orion-ops-vue/src/components/app/AppReleaseAuditModal.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="发布审核"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
:width="550">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="audit-description">
|
||||
<span class="description-label normal-label mr8">审核描述</span>
|
||||
<a-textarea class="description-area" v-model="description" :maxLength="64"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 操作 -->
|
||||
<template #footer>
|
||||
<a-button @click="audit(false)" :disabled="loading">驳回</a-button>
|
||||
<a-button type="primary" @click="audit(true)" :disabled="loading">通过</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AUDIT_STATUS } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseAuditModal',
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
visible: false,
|
||||
loading: false,
|
||||
description: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
this.loading = false
|
||||
this.id = id
|
||||
},
|
||||
audit(res) {
|
||||
if (!res && !this.description) {
|
||||
this.$message.warning('请输入驳回描述')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.auditAppRelease({
|
||||
id: this.id,
|
||||
status: res ? AUDIT_STATUS.RESOLVE.value : AUDIT_STATUS.REJECT.value,
|
||||
reason: this.description
|
||||
}).then(() => {
|
||||
this.$message.success('审核完成')
|
||||
this.$emit('audit', this.id, res)
|
||||
this.close()
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.description = null
|
||||
this.id = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.audit-description {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.description-label {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.description-area {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
457
orion-ops-vue/src/components/app/AppReleaseConfigForm.vue
Normal file
@@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<div id="app-release-conf-container">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 发布机器 -->
|
||||
<div id="app-machine-wrapper">
|
||||
<span class="label normal-label required-label">发布机器</span>
|
||||
<MachineChecker style="margin-left: 8px"
|
||||
ref="machineChecker"
|
||||
:defaultValue="machines"
|
||||
:query="machineQuery">
|
||||
<template #trigger>
|
||||
<span class="span-blue pointer">已选择 {{ machines.length }} 台机器</span>
|
||||
</template>
|
||||
<template #footer>
|
||||
<a-button type="primary" size="small" @click="chooseMachines">确定</a-button>
|
||||
</template>
|
||||
</MachineChecker>
|
||||
</div>
|
||||
<!-- 发布序列 -->
|
||||
<div id="app-release-serial-wrapper">
|
||||
<span class="label normal-label required-label">发布序列</span>
|
||||
<a-radio-group class="ml8" v-model="releaseSerial">
|
||||
<a-radio v-for="type of SERIAL_TYPE" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 异常处理 -->
|
||||
<div id="app-release-exception-wrapper" v-show="releaseSerial === SERIAL_TYPE.SERIAL.value">
|
||||
<span class="label normal-label required-label">异常处理</span>
|
||||
<a-radio-group class="ml8" v-model="exceptionHandler">
|
||||
<a-tooltip v-for="type of EXCEPTION_HANDLER_TYPE" :key="type.value" :title="type.title">
|
||||
<a-radio :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-radio>
|
||||
</a-tooltip>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 发布操作 -->
|
||||
<div id="app-action-container">
|
||||
<template v-for="(action, index) in actions">
|
||||
<div class="app-action-block" :key="index" v-if="action.visible">
|
||||
<!-- 分隔符 -->
|
||||
<a-divider class="action-divider">发布操作{{ index + 1 }}</a-divider>
|
||||
<div class="app-action-wrapper">
|
||||
<!-- 操作 -->
|
||||
<div class="app-action">
|
||||
<!-- 名称 -->
|
||||
<div class="action-name-wrapper">
|
||||
<span class="label action-label normal-label required-label">操作名称</span>
|
||||
<a-input class="action-name-input" v-model="action.name" :maxLength="32" placeholder="操作名称"/>
|
||||
</div>
|
||||
<!-- 代码块 -->
|
||||
<div class="action-editor-wrapper" v-if="action.type === RELEASE_ACTION_TYPE.COMMAND.value">
|
||||
<span class="label action-label normal-label required-label">目标主机命令</span>
|
||||
<div class="app-action-editor">
|
||||
<Editor :config="editorConfig" :value="action.command" @change="(v) => action.command = v"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件传输方式 -->
|
||||
<div class="action-transfer-wrapper" v-if="action.type === RELEASE_ACTION_TYPE.TRANSFER.value">
|
||||
<span class="label action-label normal-label required-label">文件传输方式</span>
|
||||
<!-- 类型选择 -->
|
||||
<a-select class="transfer-input" v-model="transferMode">
|
||||
<a-select-option v-for="type of RELEASE_TRANSFER_MODE" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<!-- 文件传输路径 -->
|
||||
<div class="action-transfer-wrapper" v-if="action.type === RELEASE_ACTION_TYPE.TRANSFER.value">
|
||||
<span class="label action-label normal-label required-label">文件传输路径</span>
|
||||
<a-textarea class="transfer-input"
|
||||
v-model="transferPath"
|
||||
:autoSize="{minRows: 1}"
|
||||
:maxLength="1024"
|
||||
placeholder="目标机器产物传输绝对路径, 路径不能包含 \ 应该用 / 替换"/>
|
||||
</div>
|
||||
<!-- 文件传输类型 -->
|
||||
<div class="action-transfer-wrapper" v-if="action.type === RELEASE_ACTION_TYPE.TRANSFER.value">
|
||||
<span class="label action-label normal-label required-label">文件传输类型</span>
|
||||
<!-- 文件传输类型 -->
|
||||
<a-select class="transfer-input help-input" v-model="transferFileType">
|
||||
<a-select-option v-for="type of RELEASE_TRANSFER_FILE_TYPE" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- 描述 -->
|
||||
<a-popover placement="top">
|
||||
<template slot="content">
|
||||
如构建产物为普通文件选择 (文件 / 文件夹)<br/>
|
||||
如构建产物为文件夹且传输整个文件夹选择 (文件 / 文件夹)<br/>
|
||||
如构建产物为文件夹且传输文件夹zip选择 (文件夹zip)
|
||||
</template>
|
||||
<a-icon class="help-trigger" type="question-circle"/>
|
||||
</a-popover>
|
||||
</div>
|
||||
<!-- scp 传输命令 -->
|
||||
<div class="action-transfer-wrapper"
|
||||
v-if="action.type === RELEASE_ACTION_TYPE.TRANSFER.value && transferMode === RELEASE_TRANSFER_MODE.SCP.value">
|
||||
<span class="label action-label normal-label required-label"> scp 传输命令</span>
|
||||
<!-- scp 传输命令 -->
|
||||
<a-textarea class="transfer-input help-input"
|
||||
v-model="action.command"
|
||||
:autoSize="{minRows: 1}"
|
||||
:maxLength="1024"
|
||||
placeholder="目标机器和宿主机需要建立 ssh 免密登录"/>
|
||||
<!-- scp 传输命令描述 -->
|
||||
<a-popover placement="top">
|
||||
<template slot="content">
|
||||
bundle_path 构建产物路径<br/>
|
||||
target_username 目标机器用户<br/>
|
||||
target_host 目标机器主机<br/>
|
||||
transfer_path 传输路径
|
||||
</template>
|
||||
<a-icon class="help-trigger" type="question-circle"/>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<div class="app-action-handler">
|
||||
<a-button-group v-if="actions.length > 1">
|
||||
<a-button title="移除" @click="removeAction(index)" icon="minus-circle"/>
|
||||
<a-button title="上移" v-if="index !== 0" @click="swapAction(index, index - 1)" icon="arrow-up"/>
|
||||
<a-button title="下移" v-if="index !== actions.length - 1" @click="swapAction(index + 1, index )" icon="arrow-down"/>
|
||||
</a-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 底部按钮 -->
|
||||
<div id="app-action-footer">
|
||||
<a-button class="app-action-footer-button" type="dashed"
|
||||
@click="addAction(RELEASE_ACTION_TYPE.COMMAND.value)">
|
||||
添加命令操作 (发布机器执行)
|
||||
</a-button>
|
||||
<a-button class="app-action-footer-button" type="dashed"
|
||||
v-if="visibleAddTransfer"
|
||||
@click="addAction(RELEASE_ACTION_TYPE.TRANSFER.value)">
|
||||
添加传输操作 (构建产物传输至发布机器)
|
||||
</a-button>
|
||||
<a-button class="app-action-footer-button" type="primary" @click="save">保存</a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ENABLE_STATUS, EXCEPTION_HANDLER_TYPE, RELEASE_ACTION_TYPE, RELEASE_TRANSFER_FILE_TYPE, RELEASE_TRANSFER_MODE, SERIAL_TYPE } from '@/lib/enum'
|
||||
import Editor from '@/components/editor/Editor'
|
||||
import MachineChecker from '@/components/machine/MachineChecker'
|
||||
|
||||
const editorConfig = {
|
||||
enableLiveAutocompletion: true,
|
||||
fontSize: 14
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseConfigForm',
|
||||
props: {
|
||||
appId: Number,
|
||||
dataLoading: Boolean,
|
||||
detail: Object
|
||||
},
|
||||
components: {
|
||||
Editor,
|
||||
MachineChecker
|
||||
},
|
||||
computed: {
|
||||
visibleAddTransfer() {
|
||||
return this.actions.map(s => s.type).filter(t => t === RELEASE_ACTION_TYPE.TRANSFER.value).length < 1
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
detail(e) {
|
||||
this.initData(e)
|
||||
},
|
||||
dataLoading(e) {
|
||||
this.loading = e
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
RELEASE_ACTION_TYPE,
|
||||
RELEASE_TRANSFER_MODE,
|
||||
RELEASE_TRANSFER_FILE_TYPE,
|
||||
SERIAL_TYPE,
|
||||
EXCEPTION_HANDLER_TYPE,
|
||||
loading: false,
|
||||
profileId: null,
|
||||
transferPath: undefined,
|
||||
transferMode: RELEASE_TRANSFER_MODE.SCP.value,
|
||||
transferFileType: RELEASE_TRANSFER_FILE_TYPE.NORMAL.value,
|
||||
releaseSerial: SERIAL_TYPE.PARALLEL.value,
|
||||
exceptionHandler: EXCEPTION_HANDLER_TYPE.SKIP_ALL.value,
|
||||
machines: [],
|
||||
actions: [],
|
||||
editorConfig,
|
||||
machineQuery: { status: ENABLE_STATUS.ENABLE.value }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initData(detail) {
|
||||
this.profileId = detail.profileId
|
||||
this.transferPath = detail.env && detail.env.transferPath
|
||||
this.transferMode = detail.env && detail.env.transferMode
|
||||
this.transferFileType = detail.env && detail.env.transferFileType
|
||||
this.releaseSerial = detail.env && detail.env.releaseSerial
|
||||
this.exceptionHandler = detail.env && detail.env.exceptionHandler
|
||||
if (detail.releaseMachines && detail.releaseMachines.length) {
|
||||
this.machines = detail.releaseMachines.map(s => s.machineId)
|
||||
} else {
|
||||
this.machines = []
|
||||
}
|
||||
if (detail.releaseActions && detail.releaseActions.length) {
|
||||
this.actions = detail.releaseActions.map(s => {
|
||||
return {
|
||||
visible: true,
|
||||
name: s.name,
|
||||
type: s.type,
|
||||
command: s.command
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.actions = []
|
||||
}
|
||||
},
|
||||
chooseMachines() {
|
||||
const ref = this.$refs.machineChecker
|
||||
this.machines = ref.checkedList
|
||||
ref.hide()
|
||||
},
|
||||
addAction(type) {
|
||||
const action = {
|
||||
type,
|
||||
name: undefined,
|
||||
visible: true
|
||||
}
|
||||
if (RELEASE_ACTION_TYPE.TRANSFER.value === type) {
|
||||
action.command = 'scp "@{bundle_path}" @{target_username}@@{target_host}:"@{transfer_path}"'
|
||||
} else {
|
||||
action.command = ''
|
||||
}
|
||||
this.actions.push(action)
|
||||
},
|
||||
removeAction(index) {
|
||||
this.actions[index].visible = false
|
||||
this.$nextTick(() => {
|
||||
this.actions.splice(index, 1)
|
||||
})
|
||||
},
|
||||
swapAction(index, target) {
|
||||
const temp = this.actions[target]
|
||||
this.$set(this.actions, target, this.actions[index])
|
||||
this.$set(this.actions, index, temp)
|
||||
},
|
||||
save() {
|
||||
if (!this.machines.length) {
|
||||
this.$message.warning('请选择发布机器')
|
||||
return
|
||||
}
|
||||
if (!this.actions.length) {
|
||||
this.$message.warning('请设置发布操作')
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < this.actions.length; i++) {
|
||||
const action = this.actions[i]
|
||||
if (!action.name) {
|
||||
this.$message.warning(`请输入操作名称 [发布操作${i + 1}]`)
|
||||
return
|
||||
}
|
||||
if (RELEASE_ACTION_TYPE.COMMAND.value === action.type) {
|
||||
if (!action.command || !action.command.trim().length) {
|
||||
this.$message.warning(`请输入操作命令 [发布操作${i + 1}]`)
|
||||
return
|
||||
} else if (action.command.length > 2048) {
|
||||
this.$message.warning(`操作命令长度不能大于2048位 [发布操作${i + 1}] 当前: ${action.command.length}`)
|
||||
return
|
||||
}
|
||||
} else if (RELEASE_ACTION_TYPE.TRANSFER.value === action.type) {
|
||||
if (!this.transferPath || !this.transferPath.trim().length) {
|
||||
this.$message.warning('传输操作 传输路径不能为空')
|
||||
return
|
||||
}
|
||||
if (this.transferPath.includes('\\')) {
|
||||
this.$message.warning('产物传输路径不能包含 \\ 应该用 / 替换')
|
||||
return
|
||||
}
|
||||
if (RELEASE_TRANSFER_MODE.SCP.value === this.transferMode) {
|
||||
if (!action.command || !action.command.trim().length) {
|
||||
this.$message.warning('请输入 scp 传输命令')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.configApp({
|
||||
appId: this.appId,
|
||||
profileId: this.profileId,
|
||||
stageType: 20,
|
||||
env: {
|
||||
transferPath: this.transferPath,
|
||||
transferMode: this.transferMode,
|
||||
transferFileType: this.transferFileType,
|
||||
releaseSerial: this.releaseSerial,
|
||||
exceptionHandler: this.exceptionHandler
|
||||
},
|
||||
machineIdList: this.machines,
|
||||
releaseActions: this.actions
|
||||
}).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.$emit('updated')
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initData(this.detail)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@label-width: 160px;
|
||||
@action-handler-width: 120px;
|
||||
@app-top-container-width: 994px;
|
||||
@app-action-container-width: 994px;
|
||||
@app-action-width: 876px;
|
||||
@action-name-input-width: 700px;
|
||||
@app-action-editor-width: 700px;
|
||||
@app-action-editor-height: 250px;
|
||||
@transfer-input-width: 700px;
|
||||
@help-input-width: 670px;
|
||||
@action-divider-min-width: 830px;
|
||||
@action-divider-width: 990px;
|
||||
@app-action-footer-width: 700px;
|
||||
@footer-margin-left: 168px;
|
||||
@desc-margin-left: 160px;
|
||||
|
||||
#app-release-conf-container {
|
||||
padding: 18px 8px 0 8px;
|
||||
overflow: auto;
|
||||
|
||||
.label {
|
||||
width: @label-width;
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
#app-machine-wrapper {
|
||||
width: @app-top-container-width;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
#app-release-serial-wrapper, #app-release-exception-wrapper {
|
||||
width: @app-top-container-width;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#app-action-container {
|
||||
width: @app-action-container-width;
|
||||
margin-top: 16px;
|
||||
|
||||
.app-action-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.action-label {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.app-action {
|
||||
width: @app-action-width;
|
||||
padding: 0 8px 8px 8px;
|
||||
|
||||
.action-name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-name-input {
|
||||
width: @action-name-input-width;
|
||||
}
|
||||
}
|
||||
|
||||
.action-editor-wrapper {
|
||||
display: flex;
|
||||
|
||||
.app-action-editor {
|
||||
width: @app-action-editor-width;
|
||||
height: @app-action-editor-height;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-transfer-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.transfer-input {
|
||||
width: @transfer-input-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-action-handler {
|
||||
width: @action-handler-width;
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-divider {
|
||||
min-width: @action-divider-min-width;
|
||||
width: @action-divider-width;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#app-action-footer {
|
||||
margin: 16px 0 8px @footer-margin-left;
|
||||
width: @app-action-footer-width;
|
||||
|
||||
.app-action-footer-button {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.config-description {
|
||||
margin: 4px 0 0 @desc-margin-left;
|
||||
display: block;
|
||||
color: rgba(0, 0, 0, .45);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.help-input {
|
||||
width: @help-input-width !important;
|
||||
}
|
||||
|
||||
.help-trigger {
|
||||
cursor: pointer;
|
||||
color: #1890FF;
|
||||
font-size: 20px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
253
orion-ops-vue/src/components/app/AppReleaseDetailDrawer.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<a-drawer title="发布详情"
|
||||
placement="right"
|
||||
:visible="visible"
|
||||
:maskStyle="{opacity: 0, animation: 'none'}"
|
||||
:width="430"
|
||||
@close="onClose">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading">
|
||||
<a-skeleton active :paragraph="{rows: 12}"/>
|
||||
</div>
|
||||
<!-- 加载完成 -->
|
||||
<div v-else>
|
||||
<!-- 发布信息 -->
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="发布标题" :span="3">
|
||||
{{ detail.title }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="应用名称" :span="3">
|
||||
{{ detail.appName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="环境名称" :span="3">
|
||||
{{ detail.profileName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="构建序列" :span="3">
|
||||
<span class="span-blue">#{{ detail.buildSeq }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布描述" :span="3" v-if="detail.description != null">
|
||||
{{ detail.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布状态" :span="3">
|
||||
<a-tag :color="detail.status | formatReleaseStatus('color')">
|
||||
{{ detail.status | formatReleaseStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布类型" :span="3">
|
||||
{{ detail.type | formatReleaseType('label') }}
|
||||
-
|
||||
{{ detail.serializer | formatSerialType('label') }}
|
||||
<span v-if="detail.serializer === SERIAL_TYPE.SERIAL.value">
|
||||
({{ detail.exceptionHandler | formatExceptionHandler('label') }})
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="调度时间" :span="3" v-if="detail.timedReleaseTime !== null">
|
||||
{{ detail.timedReleaseTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建用户" :span="3">
|
||||
{{ detail.createUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间" :span="3" v-if="detail.createTime !== null">
|
||||
{{ detail.createTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核用户" :span="3" v-if="detail.auditUserName !== null">
|
||||
{{ detail.auditUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核时间" :span="3" v-if="detail.auditTime !== null">
|
||||
{{ detail.auditTime | formatDate }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核批注" :span="3" v-if="detail.auditReason !== null">
|
||||
{{ detail.auditReason }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布用户" :span="3" v-if=" detail.releaseUserName !== null">
|
||||
{{ detail.releaseUserName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="detail.startTime !== null">
|
||||
{{ detail.startTime | formatDate }} ({{ detail.startTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="detail.endTime !== null">
|
||||
{{ detail.endTime | formatDate }} ({{ detail.endTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="detail.used !== null">
|
||||
{{ `${detail.keepTime} (${detail.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<!-- 发布机器 -->
|
||||
<a-divider>发布机器</a-divider>
|
||||
<a-list class="machine-list-container" size="small" bordered :dataSource="detail.machines">
|
||||
<template #header>
|
||||
<span class="span-blue">共 {{ detail.machines.length }} 台机器</span>
|
||||
</template>
|
||||
<template #renderItem="item">
|
||||
<a-list-item>
|
||||
<span>{{ item.machineName }}</span>
|
||||
<div>
|
||||
<a @click="$copy(item.machineHost)">{{ item.machineHost }}</a>
|
||||
<a-tag :color="item.status | formatActionStatus('color')" style="margin: 0 0 0 8px">
|
||||
{{ item.status | formatActionStatus('label') }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<!-- 发布操作 -->
|
||||
<a-divider>发布操作</a-divider>
|
||||
<a-list size="small" :dataSource="detail.actions">
|
||||
<template #renderItem="item">
|
||||
<a-list-item>
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="操作名称" :span="3">
|
||||
{{ item.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作类型" :span="3">
|
||||
<a-tag>{{ item.type | formatReleaseActionType('label') }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="命令" :span="3" v-if="item.type === RELEASE_ACTION_TYPE.COMMAND.value">
|
||||
<a @click="preview(item.command)">预览</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
<!-- 事件 -->
|
||||
<div class="detail-event">
|
||||
<EditorPreview ref="preview"/>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, ACTION_STATUS, EXCEPTION_HANDLER_TYPE, RELEASE_ACTION_TYPE, RELEASE_STATUS, RELEASE_TYPE, SERIAL_TYPE } from '@/lib/enum'
|
||||
import EditorPreview from '@/components/preview/EditorPreview'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseDetailDrawer',
|
||||
components: {
|
||||
EditorPreview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
RELEASE_ACTION_TYPE,
|
||||
SERIAL_TYPE,
|
||||
visible: false,
|
||||
loading: true,
|
||||
pollId: null,
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
this.detail = {}
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
this.$api.getAppReleaseDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.detail = data
|
||||
// 轮询状态
|
||||
if (data.status === RELEASE_STATUS.WAIT_AUDIT.value ||
|
||||
data.status === RELEASE_STATUS.AUDIT_REJECT.value ||
|
||||
data.status === RELEASE_STATUS.WAIT_RUNNABLE.value ||
|
||||
data.status === RELEASE_STATUS.WAIT_SCHEDULE.value ||
|
||||
data.status === RELEASE_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 5000)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
pollStatus() {
|
||||
if (!this.detail || !this.detail.status) {
|
||||
return
|
||||
}
|
||||
if (this.detail.status !== RELEASE_STATUS.WAIT_AUDIT.value &&
|
||||
this.detail.status !== RELEASE_STATUS.AUDIT_REJECT.value &&
|
||||
this.detail.status !== RELEASE_STATUS.WAIT_RUNNABLE.value &&
|
||||
this.detail.status !== RELEASE_STATUS.WAIT_SCHEDULE.value &&
|
||||
this.detail.status !== RELEASE_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
return
|
||||
}
|
||||
this.$api.getAppReleaseStatus({
|
||||
id: this.detail.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
this.detail.used = data.used
|
||||
this.detail.keepTime = data.keepTime
|
||||
this.detail.startTime = data.startTime
|
||||
this.detail.startTimeAgo = data.startTimeAgo
|
||||
this.detail.endTime = data.endTime
|
||||
this.detail.endTimeAgo = data.endTimeAgo
|
||||
if (data.machines && data.machines.length) {
|
||||
for (const machine of data.machines) {
|
||||
this.detail.machines.filter(s => s.id === machine.id).forEach(s => {
|
||||
s.status = machine.status
|
||||
s.keepTime = machine.keepTime
|
||||
s.used = machine.used
|
||||
s.startTime = machine.startTime
|
||||
s.startTimeAgo = machine.startTimeAgo
|
||||
s.endTime = machine.endTime
|
||||
s.endTimeAgo = machine.endTimeAgo
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
preview(command) {
|
||||
this.$refs.preview.preview(command)
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatReleaseStatus(status, f) {
|
||||
return enumValueOf(RELEASE_STATUS, status)[f]
|
||||
},
|
||||
formatReleaseType(type, f) {
|
||||
return enumValueOf(RELEASE_TYPE, type)[f]
|
||||
},
|
||||
formatSerialType(type, f) {
|
||||
return enumValueOf(SERIAL_TYPE, type)[f]
|
||||
},
|
||||
formatExceptionHandler(type, f) {
|
||||
return enumValueOf(EXCEPTION_HANDLER_TYPE, type)[f]
|
||||
},
|
||||
formatReleaseActionType(status, f) {
|
||||
return enumValueOf(RELEASE_ACTION_TYPE, status)[f]
|
||||
},
|
||||
formatActionStatus(status, f) {
|
||||
return enumValueOf(ACTION_STATUS, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.machine-list-container {
|
||||
|
||||
::v-deep .ant-list-header {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
::v-deep .ant-list-item {
|
||||
padding-right: 8px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<a-drawer title="机器详情"
|
||||
placement="right"
|
||||
:visible="visible"
|
||||
:maskStyle="{opacity: 0, animation: 'none'}"
|
||||
:width="430"
|
||||
@close="onClose">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading">
|
||||
<a-skeleton active :paragraph="{rows: 12}"/>
|
||||
</div>
|
||||
<!-- 加载完成 -->
|
||||
<div v-else>
|
||||
<!-- 发布信息 -->
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="机器名称" :span="3">
|
||||
{{ detail.machineName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="机器主机" :span="3">
|
||||
{{ detail.machineHost }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发布状态" :span="3">
|
||||
<a-tag :color="detail.status | formatActionStatus('color')">
|
||||
{{ detail.status | formatActionStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="detail.startTime !== null">
|
||||
{{ detail.startTime | formatDate }} ({{ detail.startTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="detail.endTime !== null">
|
||||
{{ detail.endTime | formatDate }} ({{ detail.endTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="detail.used !== null">
|
||||
{{ `${detail.keepTime} (${detail.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="日志" :span="3" v-if="statusHolder.visibleActionLog(detail.status)">
|
||||
<a v-if="detail.downloadUrl" @click="clearDownloadUrl(detail)" target="_blank" :href="detail.downloadUrl">下载</a>
|
||||
<a v-else @click="loadDownloadUrl(detail, FILE_DOWNLOAD_TYPE.APP_RELEASE_MACHINE_LOG.value)">获取操作日志</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<!-- 发布操作 -->
|
||||
<a-divider>发布操作</a-divider>
|
||||
<a-list :dataSource="detail.actions">
|
||||
<template #renderItem="item">
|
||||
<a-list-item>
|
||||
<a-descriptions size="middle">
|
||||
<a-descriptions-item label="操作名称" :span="3">
|
||||
{{ item.actionName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作类型" :span="3">
|
||||
<a-tag>{{ item.actionType | formatActionType('label') }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作状态" :span="3">
|
||||
<a-tag :color="item.status | formatActionStatus('color')">
|
||||
{{ item.status | formatActionStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开始时间" :span="3" v-if="item.startTime !== null">
|
||||
{{ item.startTime | formatDate }} ({{ item.startTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="结束时间" :span="3" v-if="item.endTime !== null">
|
||||
{{ item.endTime | formatDate }} ({{ item.endTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="持续时间" :span="3" v-if="item.used !== null">
|
||||
{{ `${item.keepTime} (${item.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="退出码" :span="3" v-if="item.exitCode !== null">
|
||||
<span :style="{'color': item.exitCode === 0 ? '#4263EB' : '#E03131'}">
|
||||
{{ item.exitCode }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="命令" :span="3" v-if="item.actionType === RELEASE_ACTION_TYPE.COMMAND.value">
|
||||
<a @click="preview(item.actionCommand)">预览</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="日志" :span="3" v-if="statusHolder.visibleActionLog(item.status)">
|
||||
<a v-if="item.downloadUrl" @click="clearDownloadUrl(item)" target="_blank" :href="item.downloadUrl">下载</a>
|
||||
<a v-else @click="loadDownloadUrl(item, FILE_DOWNLOAD_TYPE.APP_ACTION_LOG.value)">获取操作日志</a>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
<!-- 事件 -->
|
||||
<div class="detail-event">
|
||||
<EditorPreview ref="preview"/>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineArrayKey } from '@/lib/utils'
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { ACTION_STATUS, enumValueOf, FILE_DOWNLOAD_TYPE, RELEASE_ACTION_TYPE } from '@/lib/enum'
|
||||
import EditorPreview from '@/components/preview/EditorPreview'
|
||||
|
||||
const statusHolder = {
|
||||
visibleActionLog: (status) => {
|
||||
return status === ACTION_STATUS.RUNNABLE.value ||
|
||||
status === ACTION_STATUS.FINISH.value ||
|
||||
status === ACTION_STATUS.FAILURE.value ||
|
||||
status === ACTION_STATUS.TERMINATED.value
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseMachineDetailDrawer',
|
||||
components: {
|
||||
EditorPreview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
RELEASE_ACTION_TYPE,
|
||||
visible: false,
|
||||
loading: true,
|
||||
pollId: null,
|
||||
detail: {},
|
||||
statusHolder
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
this.detail = {}
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
this.$api.getAppReleaseMachineDetail({
|
||||
releaseMachineId: id
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
data.downloadUrl = null
|
||||
defineArrayKey(data.actions, 'downloadUrl')
|
||||
this.detail = data
|
||||
// 轮询状态
|
||||
if (data.status === ACTION_STATUS.WAIT.value ||
|
||||
data.status === ACTION_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 5000)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
pollStatus() {
|
||||
if (!this.detail || !this.detail.status) {
|
||||
return
|
||||
}
|
||||
if (this.detail.status !== ACTION_STATUS.WAIT.value &&
|
||||
this.detail.status !== ACTION_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
return
|
||||
}
|
||||
this.$api.getAppReleaseMachineStatus({
|
||||
releaseMachineId: this.detail.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
this.detail.used = data.used
|
||||
this.detail.keepTime = data.keepTime
|
||||
this.detail.startTime = data.startTime
|
||||
this.detail.startTimeAgo = data.startTimeAgo
|
||||
this.detail.endTime = data.endTime
|
||||
this.detail.endTimeAgo = data.endTimeAgo
|
||||
if (data.actions && data.actions.length) {
|
||||
for (const action of data.actions) {
|
||||
this.detail.actions.filter(s => s.id === action.id).forEach(s => {
|
||||
s.status = action.status
|
||||
s.keepTime = action.keepTime
|
||||
s.used = action.used
|
||||
s.startTime = action.startTime
|
||||
s.startTimeAgo = action.startTimeAgo
|
||||
s.endTime = action.endTime
|
||||
s.endTimeAgo = action.endTimeAgo
|
||||
s.exitCode = action.exitCode
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadDownloadUrl(record, type) {
|
||||
try {
|
||||
const downloadUrl = await this.$api.getFileDownloadToken({
|
||||
type,
|
||||
id: record.id
|
||||
})
|
||||
record.downloadUrl = this.$api.fileDownloadExec({ token: downloadUrl.data })
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
clearDownloadUrl(record) {
|
||||
setTimeout(() => {
|
||||
record.downloadUrl = null
|
||||
})
|
||||
},
|
||||
preview(command) {
|
||||
this.$refs.preview.preview(command)
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
// 关闭轮询状态
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatActionStatus(status, f) {
|
||||
return enumValueOf(ACTION_STATUS, status)[f]
|
||||
},
|
||||
formatActionType(status, f) {
|
||||
return enumValueOf(RELEASE_ACTION_TYPE, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
414
orion-ops-vue/src/components/app/AppReleaseModal.vue
Normal file
@@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:width="550"
|
||||
:maskStyle="{opacity: 0.8, animation: 'none'}"
|
||||
:dialogStyle="{top: '64px', padding: 0}"
|
||||
:bodyStyle="{padding: '8px'}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span v-if="selectAppPage">选择应用</span>
|
||||
<span v-if="!selectAppPage">
|
||||
<a-icon class="mx4 pointer span-blue"
|
||||
title="重新选择"
|
||||
v-if="visibleReselect && appId"
|
||||
@click="reselectAppList"
|
||||
type="arrow-left"/>
|
||||
应用发布
|
||||
</span>
|
||||
</template>
|
||||
<!-- 初始化骨架 -->
|
||||
<a-skeleton v-if="initiating" active :paragraph="{rows: 5}"/>
|
||||
<!-- 主体 -->
|
||||
<a-spin v-else :spinning="loading || appLoading">
|
||||
<!-- 应用选择 -->
|
||||
<div class="app-list-container" v-if="selectAppPage">
|
||||
<!-- 无应用数据 -->
|
||||
<a-empty v-if="!appList.length" description="请先配置应用"/>
|
||||
<!-- 应用列表 -->
|
||||
<div v-else class="app-list">
|
||||
<div class="app-item" v-for="app of appList" :key="app.id" @click="chooseApp(app.id)">
|
||||
<div class="app-name">
|
||||
<a-icon class="mx8" type="code-sandbox"/>
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<a-tag color="#5C7CFA">
|
||||
{{ app.tag }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 发布配置 -->
|
||||
<div class="release-container" v-else>
|
||||
<div class="release-form">
|
||||
<!-- 发布标题 -->
|
||||
<div class="release-form-item">
|
||||
<span class="release-form-item-label normal-label required-label">发布标题</span>
|
||||
<a-input class="release-form-item-input"
|
||||
v-model="submit.title"
|
||||
placeholder="标题"
|
||||
:maxLength="32"
|
||||
allowClear/>
|
||||
</div>
|
||||
<!-- 发布版本 -->
|
||||
<div class="release-form-item">
|
||||
<span class="release-form-item-label normal-label required-label">发布版本</span>
|
||||
<a-select class="release-form-item-input build-selector"
|
||||
v-model="submit.buildId"
|
||||
placeholder="版本"
|
||||
allowClear>
|
||||
<a-select-option v-for="build of buildList" :key="build.id" :value="build.id">
|
||||
<div class="build-item">
|
||||
<div class="build-item-left">
|
||||
<span class="span-blue build-item-seq">#{{ build.seq }}</span>
|
||||
<span class="build-item-message">{{ build.description }}</span>
|
||||
</div>
|
||||
<span class="build-item-date">
|
||||
{{ build.createTime | formatDate('MM-dd HH:mm') }}
|
||||
</span>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-icon type="reload" class="reload" title="刷新" @click="loadBuildList"/>
|
||||
</div>
|
||||
<!-- 发布类型 -->
|
||||
<div class="release-form-item">
|
||||
<span class="release-form-item-label normal-label required-label">发布类型</span>
|
||||
<a-radio-group v-model="submit.timedRelease" buttonStyle="solid">
|
||||
<a-radio-button :value="type.value" v-for="type in TIMED_TYPE" :key="type.value">
|
||||
{{ type.releaseLabel }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 调度时间 -->
|
||||
<div class="release-form-item" v-if="submit.timedRelease === TIMED_TYPE.TIMED.value">
|
||||
<span class="release-form-item-label normal-label required-label">调度时间</span>
|
||||
<a-date-picker v-model="submit.timedReleaseTime" :showTime="true" format="YYYY-MM-DD HH:mm:ss"/>
|
||||
</div>
|
||||
<!-- 发布机器 -->
|
||||
<div class="release-form-item">
|
||||
<span class="release-form-item-label normal-label required-label">发布机器</span>
|
||||
<MachineChecker ref="machineChecker"
|
||||
class="release-form-item-input"
|
||||
placement="bottomLeft"
|
||||
:defaultValue="appMachineIdList"
|
||||
:query="{idList: appMachineIdList}">
|
||||
<template #trigger>
|
||||
<span class="span-blue pointer">已选择 {{ submit.machineIdList.length }} 台机器</span>
|
||||
</template>
|
||||
<template #footer>
|
||||
<a-button type="primary" size="small" @click="chooseMachines">确定</a-button>
|
||||
</template>
|
||||
</MachineChecker>
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div class="release-form-item" style="margin: 8px 0;">
|
||||
<span class="release-form-item-label normal-label">发布描述</span>
|
||||
<a-textarea class="release-form-item-input"
|
||||
v-model="submit.description"
|
||||
style="height: 50px; width: 430px"
|
||||
:maxLength="64"
|
||||
allowClear/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 页脚 -->
|
||||
<template #footer>
|
||||
<!-- 关闭 -->
|
||||
<a-button @click="close">关闭</a-button>
|
||||
<!-- 发布 -->
|
||||
<a-button type="primary"
|
||||
:loading="loading"
|
||||
:disabled="selectAppPage || loading || appLoading || initiating"
|
||||
@click="release">
|
||||
发布
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { CONFIG_STATUS, TIMED_TYPE } from '@/lib/enum'
|
||||
import MachineChecker from '@/components/machine/MachineChecker'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseModal',
|
||||
components: {
|
||||
MachineChecker
|
||||
},
|
||||
props: {
|
||||
visibleReselect: Boolean
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
TIMED_TYPE,
|
||||
selectAppPage: true,
|
||||
appId: null,
|
||||
profileId: null,
|
||||
app: null,
|
||||
appList: [],
|
||||
buildList: [],
|
||||
appMachineIdList: [-1],
|
||||
submit: {
|
||||
title: undefined,
|
||||
buildId: undefined,
|
||||
description: undefined,
|
||||
timedRelease: undefined,
|
||||
timedReleaseTime: undefined,
|
||||
machineIdList: []
|
||||
},
|
||||
visible: false,
|
||||
loading: false,
|
||||
appLoading: false,
|
||||
initiating: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async openRelease(profileId, id) {
|
||||
if (!profileId) {
|
||||
this.$message.warning('请先维护应用环境')
|
||||
return
|
||||
}
|
||||
this.cleanData()
|
||||
this.selectAppPage = !id
|
||||
this.profileId = profileId
|
||||
this.appId = null
|
||||
this.app = null
|
||||
this.appList = []
|
||||
this.loading = false
|
||||
this.appLoading = false
|
||||
this.initiating = true
|
||||
this.visible = true
|
||||
await this.loadAppList()
|
||||
if (id) {
|
||||
this.chooseApp(id)
|
||||
}
|
||||
this.initiating = false
|
||||
},
|
||||
async chooseApp(id) {
|
||||
this.cleanData()
|
||||
this.appId = id
|
||||
this.selectAppPage = false
|
||||
const filter = this.appList.filter(s => s.id === id)
|
||||
if (!filter.length) {
|
||||
this.$message.warning('未找到该应用')
|
||||
}
|
||||
this.app = filter[0]
|
||||
this.submit.title = `发布${this.app.name}`
|
||||
this.appLoading = true
|
||||
await this.loadReleaseMachine()
|
||||
await this.loadBuildList()
|
||||
this.appLoading = false
|
||||
},
|
||||
cleanData() {
|
||||
this.app = {}
|
||||
this.appId = null
|
||||
this.buildList = []
|
||||
this.appMachineIdList = [-1]
|
||||
this.submit.title = undefined
|
||||
this.submit.buildId = undefined
|
||||
this.submit.description = undefined
|
||||
this.submit.timedRelease = TIMED_TYPE.NORMAL.value
|
||||
this.submit.timedReleaseTime = undefined
|
||||
this.submit.machineIdList = []
|
||||
},
|
||||
async loadReleaseMachine() {
|
||||
const { data } = await this.$api.getAppMachineId({
|
||||
id: this.appId,
|
||||
profileId: this.profileId
|
||||
})
|
||||
if (data && data.length) {
|
||||
this.appMachineIdList = data
|
||||
this.submit.machineIdList = data
|
||||
} else {
|
||||
this.$message.warning('请先配置应用发布机器')
|
||||
}
|
||||
},
|
||||
async loadBuildList() {
|
||||
const { data } = await this.$api.getBuildReleaseList({
|
||||
appId: this.appId,
|
||||
profileId: this.profileId
|
||||
})
|
||||
this.buildList = data
|
||||
if (!this.submit.buildId && this.buildList && this.buildList.length) {
|
||||
this.submit.buildId = this.buildList[0].id
|
||||
}
|
||||
},
|
||||
async loadAppList() {
|
||||
const { data: { rows } } = await this.$api.getAppList({
|
||||
profileId: this.profileId,
|
||||
limit: 10000
|
||||
})
|
||||
this.appList = rows.filter(s => s.isConfig === CONFIG_STATUS.CONFIGURED.value)
|
||||
},
|
||||
async reselectAppList() {
|
||||
this.selectAppPage = true
|
||||
if (this.appList.length) {
|
||||
return
|
||||
}
|
||||
this.initiating = true
|
||||
await this.loadAppList()
|
||||
this.initiating = false
|
||||
},
|
||||
chooseMachines() {
|
||||
const ref = this.$refs.machineChecker
|
||||
if (!ref.checkedList.length) {
|
||||
this.$message.warning('请选择发布机器机器')
|
||||
return
|
||||
}
|
||||
this.submit.machineIdList = ref.checkedList
|
||||
ref.hide()
|
||||
},
|
||||
async release() {
|
||||
if (!this.app) {
|
||||
this.$message.warning('请选择发布应用')
|
||||
return
|
||||
}
|
||||
if (!this.submit.title) {
|
||||
this.$message.warning('请输入发布标题')
|
||||
return
|
||||
}
|
||||
if (!this.submit.buildId) {
|
||||
this.$message.warning('请选择发布版本')
|
||||
return
|
||||
}
|
||||
if (!this.submit.machineIdList.length) {
|
||||
this.$message.warning('请选择发布机器')
|
||||
return
|
||||
}
|
||||
if (this.submit.timedRelease === TIMED_TYPE.TIMED.value) {
|
||||
if (!this.submit.timedReleaseTime) {
|
||||
this.$message.warning('请选择调度时间')
|
||||
return
|
||||
}
|
||||
if (this.submit.timedReleaseTime.unix() * 1000 < Date.now()) {
|
||||
this.$message.warning('调度时间需要大于当前时间')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this.submit.timedReleaseTime = undefined
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.submitAppRelease({
|
||||
appId: this.appId,
|
||||
profileId: this.profileId,
|
||||
...this.submit
|
||||
}).then(() => {
|
||||
this.$message.success('已创建发布任务')
|
||||
this.$emit('submit')
|
||||
this.visible = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-list {
|
||||
margin: 0 4px 0 8px;
|
||||
height: 355px;
|
||||
overflow-y: auto;
|
||||
|
||||
.app-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: #F8F9FA;
|
||||
border-radius: 4px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
.app-name {
|
||||
width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.app-item:hover {
|
||||
background: #E7F5FF;
|
||||
}
|
||||
}
|
||||
|
||||
.release-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.release-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.release-form-item-label {
|
||||
width: 80px;
|
||||
margin: 16px 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.release-form-item-input {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
.reload {
|
||||
font-size: 19px;
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
color: #339AF0;
|
||||
}
|
||||
|
||||
.reload:hover {
|
||||
color: #228BE6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.build-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 276px;
|
||||
}
|
||||
|
||||
.build-item-seq {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.build-item-message {
|
||||
width: 225px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.build-item-date {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .build-selector .ant-select-selection-selected-value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<!-- 统计折线图 -->
|
||||
<div class="statistic-chart-container">
|
||||
<p class="statistics-description">近七天发布统计</p>
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 折线图 -->
|
||||
<div class="statistic-chart-wrapper">
|
||||
<div id="statistic-chart"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Chart } from '@antv/g2'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseStatisticsCharts',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(appId, profileId) {
|
||||
this.loading = true
|
||||
const { data } = await this.$api.getAppReleaseStatisticsChart({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.loading = false
|
||||
this.renderChart(data)
|
||||
},
|
||||
clean() {
|
||||
this.loading = false
|
||||
this.chart && this.chart.destroy()
|
||||
this.chart = null
|
||||
},
|
||||
renderChart(data) {
|
||||
// 处理数据
|
||||
const chartsData = []
|
||||
for (const d of data) {
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '发布次数',
|
||||
value: d.releaseCount
|
||||
})
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '成功次数',
|
||||
value: d.successCount
|
||||
})
|
||||
chartsData.push({
|
||||
date: d.date,
|
||||
type: '失败次数',
|
||||
value: d.failureCount
|
||||
})
|
||||
}
|
||||
this.clean()
|
||||
// 渲染图表
|
||||
this.chart = new Chart({
|
||||
container: 'statistic-chart',
|
||||
autoFit: true
|
||||
})
|
||||
this.chart.data(chartsData)
|
||||
|
||||
this.chart.tooltip({
|
||||
showCrosshairs: true,
|
||||
shared: true
|
||||
})
|
||||
|
||||
this.chart.line()
|
||||
.position('date*value')
|
||||
.color('type')
|
||||
.shape('circle')
|
||||
this.chart.render()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.statistic-chart-wrapper {
|
||||
margin: 0 24px 16px 24px;
|
||||
|
||||
#statistic-chart {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
124
orion-ops-vue/src/components/app/AppReleaseStatisticsMetrics.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 全部发布指标 -->
|
||||
<p class="statistics-description">全部发布指标</p>
|
||||
<div class="app-release-statistic-metrics">
|
||||
<div class="clean"/>
|
||||
<!-- 统计指标 -->
|
||||
<div class="app-release-statistic-header">
|
||||
<a-statistic class="statistic-metrics-item" title="发布次数" :value="allMetrics.releaseCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item green" title="成功次数" :value="allMetrics.successCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item red" title="失败次数" :value="allMetrics.failureCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item" title="平均耗时" :value="allMetrics.avgUsedInterval"/>
|
||||
</div>
|
||||
<div class="clean"/>
|
||||
</div>
|
||||
<!-- 近七天发布指标 -->
|
||||
<p class="statistics-description" style="margin-top: 28px">近七天发布指标</p>
|
||||
<div class="app-release-statistic-metrics">
|
||||
<div class="clean"/>
|
||||
<!-- 统计指标 -->
|
||||
<div class="app-release-statistic-header">
|
||||
<a-statistic class="statistic-metrics-item" title="发布次数" :value="latelyMetrics.releaseCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item green" title="成功次数" :value="latelyMetrics.successCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item red" title="失败次数" :value="latelyMetrics.failureCount"/>
|
||||
<a-divider class="statistic-metrics-divider" type="vertical"/>
|
||||
<a-statistic class="statistic-metrics-item" title="平均耗时" :value="latelyMetrics.avgUsedInterval"/>
|
||||
</div>
|
||||
<div class="clean"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseStatisticsMetrics',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
allMetrics: {
|
||||
releaseCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: '0s'
|
||||
},
|
||||
latelyMetrics: {
|
||||
releaseCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: '0s'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(appId, profileId) {
|
||||
this.loading = true
|
||||
const { data } = await this.$api.getAppReleaseStatisticsMetrics({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.loading = false
|
||||
this.allMetrics = data.all
|
||||
this.latelyMetrics = data.lately
|
||||
},
|
||||
clean() {
|
||||
this.loading = false
|
||||
this.allMetrics = {
|
||||
releaseCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: '0s'
|
||||
}
|
||||
this.latelyMetrics = {
|
||||
releaseCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
avgUsedInterval: '0s'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-release-statistic-metrics {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 24px 0 12px 16px;
|
||||
|
||||
.app-release-statistic-header {
|
||||
display: flex;
|
||||
|
||||
.statistic-metrics-item {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
::v-deep .statistic-metrics-item.green .ant-statistic-content {
|
||||
color: #58C612;
|
||||
}
|
||||
|
||||
::v-deep .statistic-metrics-item.red .ant-statistic-content {
|
||||
color: #DD2C00;
|
||||
}
|
||||
|
||||
.statistic-metrics-divider {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
::v-deep .ant-statistic-content {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
466
orion-ops-vue/src/components/app/AppReleaseStatisticsViews.vue
Normal file
@@ -0,0 +1,466 @@
|
||||
<template>
|
||||
<div class="app-release-statistic-view-container">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 发布视图 -->
|
||||
<div class="app-release-statistic-record-view-wrapper" v-if="initialized && view">
|
||||
<div class="app-release-statistic-record-view">
|
||||
<p class="statistics-description">近十次发布视图</p>
|
||||
<div class="app-release-statistic-main">
|
||||
<!-- 发布操作 -->
|
||||
<div class="app-release-actions-wrapper">
|
||||
<!-- 平均时间 -->
|
||||
<div class="app-release-actions-legend-wrapper">
|
||||
<div class="app-release-actions-legend">
|
||||
<span class="avg-used-legend-wrapper">
|
||||
平均发布时间: <span class="avg-used-legend">{{ view.avgUsedInterval }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="app-release-actions-machine-legend">
|
||||
发布机器
|
||||
</div>
|
||||
</div>
|
||||
<!-- 发布操作 -->
|
||||
<div class="app-release-actions" v-for="(action, index) of view.actions" :key="action.id">
|
||||
<div :class="['app-release-actions-name', index % 2 === 0 ? 'app-release-actions-name-theme1' : 'app-release-actions-name-theme2']">
|
||||
{{ action.name }}
|
||||
</div>
|
||||
<div :class="['app-release-actions-avg', index % 2 === 0 ? 'app-release-actions-avg-theme1' : 'app-release-actions-avg-theme2']">
|
||||
<div class="app-release-actions-avg">
|
||||
{{ action.avgUsedInterval }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 发布日志 -->
|
||||
<div class="app-release-machines-container">
|
||||
<!-- 发布机器 -->
|
||||
<div class="app-release-machines-wrapper" v-for="releaseRecord of view.releaseRecordList" :key="releaseRecord.releaseId">
|
||||
<!-- 发布信息头 -->
|
||||
<div class="app-release-machine-legend-wrapper">
|
||||
<!-- 发布信息 -->
|
||||
<a target="_blank"
|
||||
style="height: 100%"
|
||||
title="点击查看发布日志"
|
||||
:href="`#/app/release/log/view/${releaseRecord.releaseId}`"
|
||||
@click="openLogView($event,'release', releaseRecord.releaseId)">
|
||||
<div class="app-release-record-legend">
|
||||
<div class="app-release-record-status">
|
||||
<span class="app-release-title" :title="releaseRecord.releaseTitle">
|
||||
{{ releaseRecord.releaseTitle }}
|
||||
</span>
|
||||
<!-- 发布状态 -->
|
||||
<a-tag class="mx4" :color="releaseRecord.status | formatReleaseStatus('color')">
|
||||
{{ releaseRecord.status | formatReleaseStatus('label') }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="app-release-record-info">
|
||||
<span>{{ releaseRecord.releaseDate | formatDate('MM-dd HH:mm') }}</span>
|
||||
<span v-if="releaseRecord.usedInterval"> (used: {{ releaseRecord.usedInterval }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- 发布机器信息 -->
|
||||
<div class="app-release-machine-legend">
|
||||
<a target="_blank"
|
||||
style="height: 100%"
|
||||
title="点击查看机器日志"
|
||||
v-for="(machine, index) of releaseRecord.machines"
|
||||
:key="machine.id"
|
||||
:href="`#/app/release/machine/log/view/${machine.id}`"
|
||||
@click="openLogView($event,'machine', machine.id)">
|
||||
<!-- 发布机器 -->
|
||||
<div :class="['app-release-machine-info', index !== releaseRecord.machines.length - 1 ? 'app-release-machine-info-next' : '']">
|
||||
<span class="app-release-machine-info-name" :title=" machine.machineName">{{ machine.machineName }}</span>
|
||||
<div class="app-release-machine-info-status">
|
||||
<a-tag :color="machine.status | formatActionStatus('color')">
|
||||
{{ machine.status | formatActionStatus('label') }}
|
||||
</a-tag>
|
||||
<span class="app-release-machine-info-used" v-if="machine.usedInterval">{{ machine.usedInterval }}</span>
|
||||
<span v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 发布机器操作 -->
|
||||
<div class="app-release-machine-actions-wrapper">
|
||||
<div :class="['app-release-machine-actions', index !== releaseRecord.machines.length - 1 ? 'app-release-machine-actions-next' : '']"
|
||||
v-for="(machine, index) of releaseRecord.machines"
|
||||
:key="machine.id">
|
||||
<!-- 发布操作 -->
|
||||
<div class="app-release-action-log-actions-wrapper">
|
||||
<div class="app-release-action-log-action-wrapper"
|
||||
v-for="(actionLog, index) of machine.actionLogs" :key="index">
|
||||
<!-- 构建操作值 -->
|
||||
<div class="app-release-action-log-action"
|
||||
v-if="!getCanOpenLog(actionLog)"
|
||||
:style="getActionLogStyle(actionLog)"
|
||||
v-text="getActionLogValue(actionLog)">
|
||||
</div>
|
||||
<!-- 可打开日志 -->
|
||||
<a v-else target="_blank"
|
||||
title="点击查看操作日志"
|
||||
:href="`#/app/action/log/view/${actionLog.id}`"
|
||||
@click="openLogView($event,'action', actionLog.id)">
|
||||
<div class="app-release-action-log-action"
|
||||
:style="getActionLogStyle(actionLog)"
|
||||
v-text="getActionLogValue(actionLog)">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无数据 -->
|
||||
<div style="padding: 0 16px" v-else-if="initialized && !view">
|
||||
无发布记录
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- 事件 -->
|
||||
<div class="app-release-statistic-event-container">
|
||||
<!-- 发布日志模态框 -->
|
||||
<AppReleaseLogAppenderModal ref="releaseLogView"/>
|
||||
<!-- 发布日志模态框 -->
|
||||
<AppReleaseMachineLogAppenderModal ref="machineLogView"/>
|
||||
<!-- 操作日志模态框 -->
|
||||
<AppActionLogAppenderModal ref="actionLogView"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { ACTION_STATUS, enumValueOf, RELEASE_STATUS } from '@/lib/enum'
|
||||
import AppActionLogAppenderModal from '@/components/log/AppActionLogAppenderModal'
|
||||
import AppReleaseMachineLogAppenderModal from '@/components/log/AppReleaseMachineLogAppenderModal'
|
||||
import AppReleaseLogAppenderModal from '@/components/log/AppReleaseLogAppenderModal'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseStatisticsViews',
|
||||
components: {
|
||||
AppActionLogAppenderModal,
|
||||
AppReleaseLogAppenderModal,
|
||||
AppReleaseMachineLogAppenderModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
initialized: false,
|
||||
view: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init(appId, profileId) {
|
||||
this.loading = true
|
||||
this.initialized = false
|
||||
const { data } = await this.$api.getAppReleaseStatisticsView({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.view = data
|
||||
this.initialized = true
|
||||
this.loading = false
|
||||
},
|
||||
clean() {
|
||||
this.initialized = false
|
||||
this.loading = false
|
||||
this.view = {}
|
||||
},
|
||||
async refresh(appId, profileId) {
|
||||
const { data } = await this.$api.getAppReleaseStatisticsView({
|
||||
appId,
|
||||
profileId
|
||||
})
|
||||
this.view = data
|
||||
},
|
||||
openLogView(e, type, id) {
|
||||
if (!e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
// 打开模态框
|
||||
this.$refs[`${type}LogView`].open(id)
|
||||
return false
|
||||
} else {
|
||||
// 跳转页面
|
||||
return true
|
||||
}
|
||||
},
|
||||
getCanOpenLog(actionLog) {
|
||||
if (actionLog) {
|
||||
return enumValueOf(ACTION_STATUS, actionLog.status).log
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
getActionLogStyle(actionLog) {
|
||||
if (actionLog) {
|
||||
return enumValueOf(ACTION_STATUS, actionLog.status).actionStyle
|
||||
} else {
|
||||
return {
|
||||
background: '#FFD43B'
|
||||
}
|
||||
}
|
||||
},
|
||||
getActionLogValue(actionLog) {
|
||||
if (actionLog) {
|
||||
return enumValueOf(ACTION_STATUS, actionLog.status).actionValue(actionLog)
|
||||
} else {
|
||||
return '未执行'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clean()
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatReleaseStatus(status, f) {
|
||||
return enumValueOf(RELEASE_STATUS, status)[f]
|
||||
},
|
||||
formatActionStatus(status, f) {
|
||||
return enumValueOf(ACTION_STATUS, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-release-statistic-record-view-wrapper {
|
||||
margin: 0 24px 24px 24px;
|
||||
overflow: auto;
|
||||
|
||||
.app-release-statistic-record-view {
|
||||
margin: 0 16px 16px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.app-release-statistic-main {
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 4px 1px #DEE2E6;
|
||||
}
|
||||
|
||||
.app-release-actions-wrapper {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
|
||||
.app-release-actions-legend-wrapper {
|
||||
width: 320px;
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
|
||||
.avg-used-legend-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avg-used-legend {
|
||||
margin-left: 4px;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.app-release-actions-legend {
|
||||
width: 180px;
|
||||
height: 100%;
|
||||
padding: 4px 8px 4px 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
border-right: 2px solid #F8F9FA;
|
||||
}
|
||||
|
||||
.app-release-actions-machine-legend {
|
||||
width: 140px;
|
||||
height: 100%;
|
||||
padding: 4px 0 4px 8px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 148px;
|
||||
|
||||
.app-release-actions-name {
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
color: #181E33;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 16px;
|
||||
border-bottom: 1px solid #DEE2E6;
|
||||
}
|
||||
|
||||
.app-release-actions-name-theme1 {
|
||||
background: #F1F3F5;
|
||||
}
|
||||
|
||||
.app-release-actions-name-theme2 {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
.app-release-actions-avg {
|
||||
text-align: center;
|
||||
height: 28px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.app-release-actions-avg-theme1 {
|
||||
background: #E9ECEF;
|
||||
}
|
||||
|
||||
.app-release-actions-avg-theme2 {
|
||||
background: #F1F4F7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-machines-wrapper {
|
||||
display: flex;
|
||||
|
||||
.app-release-machine-legend-wrapper {
|
||||
width: 320px;
|
||||
border-top: 2px solid #F8F9FA;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
.app-release-record-legend {
|
||||
width: 180px;
|
||||
height: 100%;
|
||||
padding: 8px 0 8px 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
border-right: 2px solid #F8F9FA;
|
||||
|
||||
.app-release-record-status {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
|
||||
.app-release-title {
|
||||
width: 108px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-record-info {
|
||||
color: #000000;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-record-legend:hover {
|
||||
transition: .3s;
|
||||
background: #D0EBFF;
|
||||
}
|
||||
|
||||
.app-release-machine-legend {
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
border-right: 2px solid #F8F9FA;
|
||||
|
||||
.app-release-machine-info {
|
||||
height: 64px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.app-release-machine-info-name {
|
||||
color: #181E33;
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.app-release-machine-info-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 4px 4px 4px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.app-release-machine-info-used {
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-machine-info-next {
|
||||
border-bottom: 2px solid #F8F9FA;
|
||||
}
|
||||
|
||||
.app-release-machine-info:hover {
|
||||
transition: .3s;
|
||||
background: #dbe4ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-machine-actions-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.app-release-machine-actions {
|
||||
height: 64px;
|
||||
|
||||
.app-release-action-log-actions-wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.app-release-action-log-action-wrapper {
|
||||
width: 148px;
|
||||
border-radius: 4px;
|
||||
|
||||
.app-release-action-log-action {
|
||||
margin: 2px 1px;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0.8;
|
||||
color: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.app-release-action-log-action:hover {
|
||||
opacity: 1;
|
||||
transition: .3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-release-machine-actions-next {
|
||||
border-bottom: 2px solid #F8F9FA;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
74
orion-ops-vue/src/components/app/AppReleaseTimedModal.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="设置发布定时"
|
||||
:width="330"
|
||||
:okButtonProps="{props: {disabled: !valid}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="submit"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="timed-release-picker-wrapper">
|
||||
<span class="normal-label mr8">调度时间 </span>
|
||||
<a-date-picker v-model="timedReleaseTime" :showTime="true" format="YYYY-MM-DD HH:mm:ss"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RELEASE_STATUS, TIMED_TYPE } from '@/lib/enum'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseTimedModal',
|
||||
data() {
|
||||
return {
|
||||
record: null,
|
||||
visible: false,
|
||||
loading: false,
|
||||
timedReleaseTime: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valid() {
|
||||
if (!this.timedReleaseTime) {
|
||||
return false
|
||||
}
|
||||
return Date.now() < this.timedReleaseTime
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(record) {
|
||||
this.record = record
|
||||
this.timedReleaseTime = (record.timedReleaseTime && moment(record.timedReleaseTime)) || undefined
|
||||
this.visible = true
|
||||
},
|
||||
submit() {
|
||||
const time = this.timedReleaseTime.unix() * 1000
|
||||
this.loading = true
|
||||
this.$api.setAppTimedRelease({
|
||||
id: this.record.id,
|
||||
timedReleaseTime: time
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.record.timedReleaseTime = time
|
||||
this.record.status = RELEASE_STATUS.WAIT_SCHEDULE.value
|
||||
this.record.timedRelease = TIMED_TYPE.TIMED.value
|
||||
this.$emit('updated')
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
64
orion-ops-vue/src/components/app/AppSelector.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<a-select v-model="id"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@change="$emit('change', id)"
|
||||
allowClear>
|
||||
<a-select-option v-for="app in appList"
|
||||
:value="app.id"
|
||||
:key="app.id">
|
||||
{{ app.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppSelector',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '全部'
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: undefined,
|
||||
appList: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(e) {
|
||||
this.id = e
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.id = undefined
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const appListRes = await this.$api.getAppList({ limit: 10000 })
|
||||
if (appListRes.data && appListRes.data.rows && appListRes.data.rows.length) {
|
||||
for (const row of appListRes.data.rows) {
|
||||
this.appList.push({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
93
orion-ops-vue/src/components/app/PipelineAutoComplete.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<a-auto-complete v-model="value"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@change="change"
|
||||
@search="search"
|
||||
allowClear>
|
||||
<template #dataSource>
|
||||
<a-select-option v-for="pipeline in visiblePipeline"
|
||||
:key="pipeline.id"
|
||||
:value="JSON.stringify(pipeline)"
|
||||
@click="choose">
|
||||
{{ pipeline.name }}
|
||||
</a-select-option>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PipelineAutoComplete',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '全部'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pipelineList: [],
|
||||
visiblePipeline: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData(profileId) {
|
||||
this.pipelineList = []
|
||||
this.visiblePipeline = []
|
||||
const { data } = await this.$api.getAppPipelineList({
|
||||
profileId,
|
||||
limit: 10000
|
||||
})
|
||||
if (data && data.rows && data.rows.length) {
|
||||
for (const row of data.rows) {
|
||||
this.pipelineList.push({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
})
|
||||
}
|
||||
this.visiblePipeline = this.pipelineList
|
||||
}
|
||||
},
|
||||
change(value) {
|
||||
let id
|
||||
let val = value
|
||||
try {
|
||||
const v = JSON.parse(value)
|
||||
if (typeof v === 'object') {
|
||||
id = v.id
|
||||
val = v.name
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
this.$emit('change', id, val)
|
||||
this.value = val
|
||||
},
|
||||
choose() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('choose')
|
||||
})
|
||||
},
|
||||
search(value) {
|
||||
if (!value) {
|
||||
this.visiblePipeline = this.pipelineList
|
||||
return
|
||||
}
|
||||
this.visiblePipeline = this.pipelineList.filter(s => s.name.toLowerCase().includes(value.toLowerCase()))
|
||||
},
|
||||
reset() {
|
||||
this.value = undefined
|
||||
this.visiblePipeline = this.pipelineList
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
90
orion-ops-vue/src/components/app/ProfileAutoComplete.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<a-auto-complete v-model="value"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
@change="change"
|
||||
@search="search"
|
||||
allowClear>
|
||||
<template #dataSource>
|
||||
<a-select-option v-for="profile in visibleProfile"
|
||||
:key="profile.id"
|
||||
:value="JSON.stringify(profile)"
|
||||
@click="choose">
|
||||
{{ profile.name }}
|
||||
</a-select-option>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProfileAutoComplete',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '全部'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
profileList: [],
|
||||
visibleProfile: [],
|
||||
value: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change(value) {
|
||||
let id
|
||||
let val = value
|
||||
try {
|
||||
const v = JSON.parse(value)
|
||||
if (typeof v === 'object') {
|
||||
id = v.id
|
||||
val = v.name
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
this.$emit('change', id, val)
|
||||
this.value = val
|
||||
},
|
||||
choose() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('choose')
|
||||
})
|
||||
},
|
||||
search(value) {
|
||||
if (!value) {
|
||||
this.visibleProfile = this.profileList
|
||||
return
|
||||
}
|
||||
this.visibleProfile = this.profileList.filter(s => s.name.toLowerCase().includes(value.toLowerCase()))
|
||||
},
|
||||
reset() {
|
||||
this.value = undefined
|
||||
this.visibleProfile = this.profileList
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const { data } = await this.$api.getProfileList({
|
||||
limit: 10000
|
||||
})
|
||||
if (data && data.rows && data.rows.length) {
|
||||
for (const row of data.rows) {
|
||||
this.profileList.push({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
})
|
||||
}
|
||||
this.visibleProfile = this.profileList
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
181
orion-ops-vue/src/components/clear/AppBuildClearModal.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="应用构建 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.REL_ID.value">清理应用</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.REL_ID.value === submit.range">
|
||||
<span class="normal-label clear-label">清理应用</span>
|
||||
<AppSelector class="param-input"
|
||||
placeholder="请选择清理的应用"
|
||||
@change="(e) => submit.relIdList[0] = e"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 执行用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">执行用户</span>
|
||||
<a-checkbox class="param-input" v-model="iCreated">只清理我执行的</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
import AppSelector from '@/components/app/AppSelector'
|
||||
|
||||
export default {
|
||||
name: 'AppBuildClearModal',
|
||||
components: {
|
||||
AppSelector
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
iCreated: true,
|
||||
submit: {
|
||||
range: null,
|
||||
profileId: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null,
|
||||
relIdList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(profileId) {
|
||||
this.submit.profileId = profileId
|
||||
this.iCreated = true
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.relIdList = []
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (!this.submit.profileId) {
|
||||
this.$message.warning('请选择需要清理的环境')
|
||||
return
|
||||
}
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.REL_ID.value) {
|
||||
if (!this.submit.relIdList.length) {
|
||||
this.$message.warning('请选择需要清理的应用')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
iCreated: this.iCreated ? 1 : 2,
|
||||
clearType: DATA_CLEAR_TYPE.APP_BUILD.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.choose-param-wrapper {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
197
orion-ops-vue/src/components/clear/AppPipelineClearModal.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="应用流水线 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.REL_ID.value">清理流水线</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.REL_ID.value === submit.range">
|
||||
<span class="normal-label clear-label">流水线</span>
|
||||
<AppPipelineSelector class="param-input"
|
||||
placeholder="请选择清理的流水线"
|
||||
@change="(e) => submit.relIdList[0] = e"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 创建用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">创建用户</span>
|
||||
<a-checkbox class="param-input" v-model="iCreated">只清理我创建的</a-checkbox>
|
||||
</div>
|
||||
<!-- 执行用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">审核用户</span>
|
||||
<a-checkbox class="param-input" v-model="iAudited">只清理我审核的</a-checkbox>
|
||||
</div>
|
||||
<!-- 执行用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">执行用户</span>
|
||||
<a-checkbox class="param-input" v-model="iExecute">只清理我执行的</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
import AppPipelineSelector from '@/components/app/AppPipelineSelector'
|
||||
|
||||
export default {
|
||||
name: 'AppPipelineClearModal',
|
||||
components: {
|
||||
AppPipelineSelector
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
iCreated: true,
|
||||
iAudited: false,
|
||||
iExecute: false,
|
||||
submit: {
|
||||
range: null,
|
||||
profileId: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null,
|
||||
relIdList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(profileId) {
|
||||
this.submit.profileId = profileId
|
||||
this.iCreated = true
|
||||
this.iAudited = false
|
||||
this.iExecute = false
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.relIdList = []
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (!this.submit.profileId) {
|
||||
this.$message.warning('请选择需要清理的环境')
|
||||
return
|
||||
}
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.REL_ID.value) {
|
||||
if (!this.submit.relIdList.length) {
|
||||
this.$message.warning('请选择需要清理的应用')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
iCreated: this.iCreated ? 1 : 2,
|
||||
iAudited: this.iAudited ? 1 : 2,
|
||||
iExecute: this.iExecute ? 1 : 2,
|
||||
clearType: DATA_CLEAR_TYPE.APP_PIPELINE_EXEC.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 242px;
|
||||
}
|
||||
|
||||
.choose-param-wrapper {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
197
orion-ops-vue/src/components/clear/AppReleaseClearModal.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="应用发布 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.REL_ID.value">清理应用</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.REL_ID.value === submit.range">
|
||||
<span class="normal-label clear-label">清理应用</span>
|
||||
<AppSelector class="param-input"
|
||||
placeholder="请选择清理的应用"
|
||||
@change="(e) => submit.relIdList[0] = e"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 创建用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">创建用户</span>
|
||||
<a-checkbox class="param-input" v-model="iCreated">只清理我创建的</a-checkbox>
|
||||
</div>
|
||||
<!-- 执行用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">审核用户</span>
|
||||
<a-checkbox class="param-input" v-model="iAudited">只清理我审核的</a-checkbox>
|
||||
</div>
|
||||
<!-- 执行用户 -->
|
||||
<div class="choose-param-wrapper">
|
||||
<span class="normal-label clear-label">执行用户</span>
|
||||
<a-checkbox class="param-input" v-model="iExecute">只清理我执行的</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
import AppSelector from '@/components/app/AppSelector'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseClearModal',
|
||||
components: {
|
||||
AppSelector
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
iCreated: true,
|
||||
iAudited: false,
|
||||
iExecute: false,
|
||||
submit: {
|
||||
range: null,
|
||||
profileId: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null,
|
||||
relIdList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(profileId) {
|
||||
this.submit.profileId = profileId
|
||||
this.iCreated = true
|
||||
this.iAudited = false
|
||||
this.iExecute = false
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.relIdList = []
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (!this.submit.profileId) {
|
||||
this.$message.warning('请选择需要清理的环境')
|
||||
return
|
||||
}
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.REL_ID.value) {
|
||||
if (!this.submit.relIdList.length) {
|
||||
this.$message.warning('请选择需要清理的应用')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
iCreated: this.iCreated ? 1 : 2,
|
||||
iAudited: this.iAudited ? 1 : 2,
|
||||
iExecute: this.iExecute ? 1 : 2,
|
||||
clearType: DATA_CLEAR_TYPE.APP_RELEASE.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.choose-param-wrapper {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
173
orion-ops-vue/src/components/clear/BatchExecClearModal.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="批量执行 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.REL_ID.value">清理机器</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.REL_ID.value === submit.range">
|
||||
<span class="normal-label clear-label">清理机器</span>
|
||||
<MachineSelector class="param-input"
|
||||
placeholder="请选择清理的机器"
|
||||
@change="(e) => submit.relIdList[0] = e"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 管理员 -->
|
||||
<div class="all-user-wrapper" v-if="$isAdmin()">
|
||||
<span class="normal-label clear-label">执行用户</span>
|
||||
<a-checkbox class="param-input" v-model="iCreated">只清理我执行的</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
import MachineSelector from '@/components/machine/MachineSelector'
|
||||
|
||||
export default {
|
||||
name: 'BatchExecClearModal',
|
||||
components: { MachineSelector },
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
iCreated: true,
|
||||
submit: {
|
||||
range: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null,
|
||||
relIdList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.iCreated = true
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.relIdList = []
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.REL_ID.value) {
|
||||
if (!this.submit.relIdList.length) {
|
||||
this.$message.warning('请选择需要清理的机器')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
iCreated: this.iCreated ? 1 : 2,
|
||||
clearType: DATA_CLEAR_TYPE.BATCH_EXEC.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.all-user-wrapper {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
164
orion-ops-vue/src/components/clear/EventLogClearModal.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="操作日志 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.REL_ID.value">操作分类</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.REL_ID.value === submit.range">
|
||||
<span class="normal-label clear-label">操作分类</span>
|
||||
<a-select class="param-input"
|
||||
placeholder="请选择需要清理的操作分类"
|
||||
@change="(e) => submit.relIdList[0] = e">
|
||||
<a-select-option v-for="classify in EVENT_CLASSIFY" :key="classify.value" :value="classify.value">
|
||||
{{ classify.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, EVENT_CLASSIFY, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'EventLogClearModal',
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
EVENT_CLASSIFY,
|
||||
visible: false,
|
||||
loading: false,
|
||||
submit: {
|
||||
range: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null,
|
||||
relIdList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.relIdList = []
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.REL_ID.value) {
|
||||
if (!this.submit.relIdList.length) {
|
||||
this.$message.warning('请选择需要清理的操作分类')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
clearType: DATA_CLEAR_TYPE.USER_EVENT_LOG.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="报警记录 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'MachineAlarmHistoryClearModal',
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
submit: {
|
||||
range: null,
|
||||
machineId: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(machineId) {
|
||||
this.submit.machineId = machineId
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (!this.submit.machineId) {
|
||||
this.$message.warning('无机器id')
|
||||
return
|
||||
}
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
clearType: DATA_CLEAR_TYPE.MACHINE_ALARM_HISTORY.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
</style>
|
||||
151
orion-ops-vue/src/components/clear/SchedulerRecordClearModal.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="调度任务 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'SchedulerRecordClearModal',
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
submit: {
|
||||
range: null,
|
||||
relId: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(relId) {
|
||||
this.submit.relId = relId
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (!this.submit.relId) {
|
||||
this.$message.warning('无任务id')
|
||||
return
|
||||
}
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
clearType: DATA_CLEAR_TYPE.SCHEDULER_RECORD.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
</style>
|
||||
161
orion-ops-vue/src/components/clear/TerminalLogClearModal.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="终端日志 清理"
|
||||
okText="清理"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clear"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-clear-container">
|
||||
<!-- 清理区间 -->
|
||||
<div class="data-clear-range">
|
||||
<a-radio-group class="nowrap" v-model="submit.range">
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.DAY.value">保留天数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.TOTAL.value">保留条数</a-radio-button>
|
||||
<a-radio-button :value="DATA_CLEAR_RANGE.REL_ID.value">清理机器</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 清理参数 -->
|
||||
<div class="data-clear-params">
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.DAY.value === submit.range">
|
||||
<span class="normal-label clear-label">保留天数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveDay"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的天数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.TOTAL.value === submit.range">
|
||||
<span class="normal-label clear-label">保留条数</span>
|
||||
<a-input-number class="param-input"
|
||||
v-model="submit.reserveTotal"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="清理后数据所保留的条数"/>
|
||||
</div>
|
||||
<div class="data-clear-param" v-if="DATA_CLEAR_RANGE.REL_ID.value === submit.range">
|
||||
<span class="normal-label clear-label">清理机器</span>
|
||||
<MachineSelector class="param-input"
|
||||
placeholder="请选择清理的机器"
|
||||
@change="(e) => submit.relIdList[0] = e"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DATA_CLEAR_RANGE, DATA_CLEAR_TYPE } from '@/lib/enum'
|
||||
import MachineSelector from '@/components/machine/MachineSelector'
|
||||
|
||||
export default {
|
||||
name: 'TerminalLogClearModal',
|
||||
components: { MachineSelector },
|
||||
data: function() {
|
||||
return {
|
||||
DATA_CLEAR_RANGE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
submit: {
|
||||
range: null,
|
||||
reserveDay: null,
|
||||
reserveTotal: null,
|
||||
relIdList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.submit.reserveDay = null
|
||||
this.submit.reserveTotal = null
|
||||
this.submit.relIdList = []
|
||||
this.submit.range = DATA_CLEAR_RANGE.DAY.value
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
clear() {
|
||||
if (this.submit.range === DATA_CLEAR_RANGE.DAY.value) {
|
||||
if (this.submit.reserveDay === null) {
|
||||
this.$message.warning('请输入需要保留的天数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.TOTAL.value) {
|
||||
if (this.submit.reserveTotal === null) {
|
||||
this.$message.warning('请输入需要保留的条数')
|
||||
return
|
||||
}
|
||||
} else if (this.submit.range === DATA_CLEAR_RANGE.REL_ID.value) {
|
||||
if (!this.submit.relIdList.length) {
|
||||
this.$message.warning('请选择需要清理的机器')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认清理',
|
||||
content: '清理后数据将无法恢复, 确定要清理吗?',
|
||||
mask: false,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.doClear()
|
||||
}
|
||||
})
|
||||
},
|
||||
doClear() {
|
||||
this.loading = true
|
||||
this.$api.clearData({
|
||||
...this.submit,
|
||||
clearType: DATA_CLEAR_TYPE.TERMINAL_LOG.value
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$emit('clear')
|
||||
this.$message.info(`共清理 ${data}条数据`)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-clear-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-clear-range {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-clear-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
</style>
|
||||
51
orion-ops-vue/src/components/common/RightClickMenu.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<!-- 右键菜单 -->
|
||||
<div class="right-menu" ref="rightMenu" @contextmenu.prevent>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<span ref="rightMenuTrigger" class="right-menu-trigger" @contextmenu.prevent></span>
|
||||
<template #overlay>
|
||||
<a-menu @click="clickRightMenuItem" @contextmenu.prevent>
|
||||
<slot name="items"/>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RightClickMenu',
|
||||
props: {
|
||||
x: {
|
||||
type: Function,
|
||||
default: e => {
|
||||
return e.offsetX + 10
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: Function,
|
||||
default: e => {
|
||||
return e.clientY
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openRightMenu(e) {
|
||||
if (e.button === 2) {
|
||||
this.$refs.rightMenu.style.left = this.x(e) + 'px'
|
||||
this.$refs.rightMenu.style.top = this.y(e) + 'px'
|
||||
this.$refs.rightMenu.style.display = 'block'
|
||||
this.$refs.rightMenuTrigger.click()
|
||||
} else {
|
||||
this.$refs.rightMenu.style.display = 'none'
|
||||
}
|
||||
},
|
||||
clickRightMenuItem({ key }) {
|
||||
this.$emit('clickRight', key)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
153
orion-ops-vue/src/components/content/AddTemplateModal.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="650"
|
||||
:dialogStyle="{top: '64px'}"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="模板名称" hasFeedback>
|
||||
<a-input v-decorator="decorators.name" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模板内容">
|
||||
<Editor :height="350" v-decorator="decorators.value"/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模板描述">
|
||||
<a-textarea v-decorator="decorators.description" allowClear/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { pick } from 'lodash'
|
||||
import Editor from '@/components/editor/Editor'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 17 }
|
||||
}
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
name: ['name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入模板名称'
|
||||
}, {
|
||||
max: 32,
|
||||
message: '模板名称长度不能大于32位'
|
||||
}]
|
||||
}],
|
||||
value: ['value', {
|
||||
initialValue: undefined,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入模板内容'
|
||||
}, {
|
||||
max: 2048,
|
||||
message: '模板内容长度不能大于2048位'
|
||||
}]
|
||||
}],
|
||||
description: ['description', {
|
||||
rules: [{
|
||||
max: 64,
|
||||
message: '模板描述长度不能大于64位'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddTemplateModal',
|
||||
components: {
|
||||
Editor
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
layout,
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.title = '新增模板'
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改模板'
|
||||
this.$api.getTemplateDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.visible = true
|
||||
this.id = row.id
|
||||
this.record = pick(Object.assign({}, row), 'name', 'value', 'description')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addTemplate({
|
||||
...values
|
||||
})
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateTemplate({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
159
orion-ops-vue/src/components/content/AddWebhookModal.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="650"
|
||||
:dialogStyle="{top: '128px'}"
|
||||
:bodyStyle="{padding: '24px 8px 0 8px'}"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
:mask="mask"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="webhook 名称" hasFeedback>
|
||||
<a-input v-decorator="decorators.name" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="webhook url" hasFeedback>
|
||||
<a-input v-decorator="decorators.url" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="webhook 类型">
|
||||
<a-select v-decorator="decorators.type" placeholder="请选择">
|
||||
<a-select-option v-for="type of WEBHOOK_TYPE" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { pick } from 'lodash'
|
||||
import { WEBHOOK_TYPE } from '@/lib/enum'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 17 }
|
||||
}
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
name: ['name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入 webhook 名称'
|
||||
}, {
|
||||
max: 64,
|
||||
message: 'webhook 名称长度不能大于64位'
|
||||
}]
|
||||
}],
|
||||
url: ['url', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入webhook url'
|
||||
}, {
|
||||
max: 2048,
|
||||
message: 'webhook url 长度不能大于2048位'
|
||||
}]
|
||||
}],
|
||||
type: ['type', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择 webhook 类型'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddWebhookModal',
|
||||
props: {
|
||||
mask: Boolean
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
WEBHOOK_TYPE,
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
layout,
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add() {
|
||||
this.title = '新增 webhook'
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改 webhook'
|
||||
this.$api.getWebhookConfigDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.visible = true
|
||||
this.id = row.id
|
||||
this.record = pick(Object.assign({}, row), 'name', 'url', 'type')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addWebhookConfig({
|
||||
...values
|
||||
})
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateWebhookConfig({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
233
orion-ops-vue/src/components/content/EnvHistoryModal.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:width="1024"
|
||||
:dialogStyle="{top: '64px'}"
|
||||
:bodyStyle="{padding: '8px'}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@cancel="close">
|
||||
<!-- 历史值表格 -->
|
||||
<div class="table-main-container table-scroll-x-auto">
|
||||
<a-table :columns="columns"
|
||||
:dataSource="rows"
|
||||
:pagination="pagination"
|
||||
rowKey="id"
|
||||
@change="getList"
|
||||
:scroll="{x: '100%'}"
|
||||
:loading="loading"
|
||||
size="middle">
|
||||
<!-- beforeValue -->
|
||||
<template #beforeValue="record">
|
||||
<div class="auto-ellipsis">
|
||||
<a class="copy-icon-left" v-if="record.beforeValue" @click="$copy(record.beforeValue)">
|
||||
<a-icon type="copy"/>
|
||||
</a>
|
||||
<span class="pointer auto-ellipsis-item" title="预览" @click="preview(record.beforeValue)">
|
||||
{{ record.beforeValue }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- afterValue -->
|
||||
<template #afterValue="record">
|
||||
<div class="auto-ellipsis">
|
||||
<a class="copy-icon-left" @click="$copy(record.afterValue)">
|
||||
<a-icon type="copy"/>
|
||||
</a>
|
||||
<span class="pointer auto-ellipsis-item" title="预览" @click="preview(record.afterValue)">
|
||||
{{ record.afterValue }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 类型 -->
|
||||
<template #type="record">
|
||||
<a-tag class="m0" :color="record.type | formatType('color')">
|
||||
{{ record.type | formatType('label') }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 修改时间 -->
|
||||
<template #createTime="record">
|
||||
{{ record.createTime | formatDate }} ({{ record.createTimeAgo }})
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #action="record">
|
||||
<a @click="rollback(record)">回滚</a>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<!-- 历史值表格 -->
|
||||
<div class="history-event">
|
||||
<!-- 预览框 -->
|
||||
<TextPreview ref="preview"/>
|
||||
</div>
|
||||
<!-- 头部 -->
|
||||
<template #title>
|
||||
<span>历史记录</span>
|
||||
<span class="span-blue" style="margin-left: 8px">{{ env.key }}</span>
|
||||
<a @click="$copy(env.key)">
|
||||
<a-icon class="copy-icon-right" type="copy"/>
|
||||
</a>
|
||||
</template>
|
||||
<!-- 底部 -->
|
||||
<template #footer>
|
||||
<a-button @click="close">关闭</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, HISTORY_VALUE_OPTION_TYPE } from '@/lib/enum'
|
||||
import TextPreview from '@/components/preview/TextPreview'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'seq',
|
||||
width: 60,
|
||||
align: 'center',
|
||||
customRender: (text, record, index) => `${index + 1}`
|
||||
},
|
||||
{
|
||||
title: 'beforeValue',
|
||||
key: 'beforeValue',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => (a.beforeValue || '').localeCompare(b.beforeValue || ''),
|
||||
scopedSlots: { customRender: 'beforeValue' }
|
||||
},
|
||||
{
|
||||
title: 'afterValue',
|
||||
key: 'afterValue',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => (a.afterValue || '').localeCompare(b.afterValue || ''),
|
||||
scopedSlots: { customRender: 'afterValue' }
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: 'type',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
scopedSlots: { customRender: 'type' }
|
||||
},
|
||||
{
|
||||
title: '修改人',
|
||||
key: 'updateUserName',
|
||||
dataIndex: 'updateUserName',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '修改时间',
|
||||
key: 'createTime',
|
||||
width: 220,
|
||||
align: 'center',
|
||||
sorter: (a, b) => a.createTime - b.createTime,
|
||||
scopedSlots: { customRender: 'createTime' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'EnvHistoryModal',
|
||||
components: {
|
||||
TextPreview
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
loading: false,
|
||||
visible: false,
|
||||
env: {},
|
||||
rows: [],
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showTotal: function(total) {
|
||||
return `共 ${total} 条`
|
||||
}
|
||||
},
|
||||
columns
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(env) {
|
||||
this.env = env
|
||||
this.visible = true
|
||||
this.getList({})
|
||||
},
|
||||
getList(page = this.pagination) {
|
||||
this.loading = true
|
||||
this.$api.getHistoryValueList({
|
||||
valueId: this.env.valueId,
|
||||
valueType: this.env.valueType,
|
||||
page: page.current,
|
||||
limit: page.pageSize
|
||||
}).then(({ data }) => {
|
||||
const pagination = { ...this.pagination }
|
||||
pagination.total = data.total
|
||||
pagination.current = data.page
|
||||
this.rows = data.rows || []
|
||||
this.pagination = pagination
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
rollback(record) {
|
||||
var updateValue
|
||||
switch (record.type) {
|
||||
case HISTORY_VALUE_OPTION_TYPE.INSERT.value:
|
||||
updateValue = record.afterValue
|
||||
break
|
||||
default:
|
||||
updateValue = record.beforeValue
|
||||
}
|
||||
this.$confirm({
|
||||
title: '确认回滚',
|
||||
content: `是否回滚值为 ${updateValue}`,
|
||||
okText: '确认',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
this.loading = true
|
||||
this.$api.rollbackHistoryValue({
|
||||
id: record.id
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
this.$message.success('已回滚')
|
||||
this.getList({})
|
||||
this.$emit('rollback', record.id, updateValue)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
preview(value) {
|
||||
this.$refs.preview.preview(value)
|
||||
},
|
||||
close() {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.env = {}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatType(status, f) {
|
||||
return enumValueOf(HISTORY_VALUE_OPTION_TYPE, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
167
orion-ops-vue/src/components/content/TemplateSelector.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:width="1000"
|
||||
:dialogStyle="{top: '16px'}"
|
||||
:bodyStyle="{padding: '8px'}"
|
||||
:destroyOnClose="true"
|
||||
title="模板列表">
|
||||
<!-- 搜索列 -->
|
||||
<div class="table-tools-bar">
|
||||
<!-- 左侧 -->
|
||||
<div class="tools-fixed-left">
|
||||
<a-form-model layout="inline" class="command-template-search-form" ref="query" :model="query">
|
||||
<a-form-model-item label="模板名称" prop="name">
|
||||
<a-input v-model="query.name" allowClear/>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="模板内容" prop="value">
|
||||
<a-input v-model="query.value" allowClear/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</div>
|
||||
<!-- 右侧 -->
|
||||
<div class="tools-fixed-right">
|
||||
<a-icon type="search" class="tools-icon" title="查询" @click="getList({})"/>
|
||||
<a-icon type="reload" class="tools-icon" title="重置" @click="resetForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<div class="table-main-container table-scroll-x-auto">
|
||||
<a-table :columns="columns"
|
||||
:dataSource="rows"
|
||||
:pagination="pagination"
|
||||
rowKey="id"
|
||||
@change="getList"
|
||||
:scroll="{x: '100%'}"
|
||||
:loading="loading"
|
||||
size="middle">
|
||||
<!-- 模板内容 -->
|
||||
<template #value="record">
|
||||
<span :title="record.value">
|
||||
<a-icon class="span-blue pointer" type="copy" title="复制" @click="$copy(record.value)"/>
|
||||
{{ record.value }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #action="record">
|
||||
<!-- 选择 -->
|
||||
<a @click="selected(record.value)">选择</a>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<!-- 页脚 -->
|
||||
<template #footer>
|
||||
<a-button @click="() => visible = false">关闭</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
/**
|
||||
* 列
|
||||
*/
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'seq',
|
||||
width: 65,
|
||||
align: 'center',
|
||||
customRender: (text, record, index) => `${index + 1}`
|
||||
},
|
||||
{
|
||||
title: '模板名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.name.localeCompare(b.name)
|
||||
},
|
||||
{
|
||||
title: '模板内容',
|
||||
key: 'value',
|
||||
width: 240,
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.value.localeCompare(b.value),
|
||||
scopedSlots: { customRender: 'value' }
|
||||
},
|
||||
{
|
||||
title: '模板描述',
|
||||
key: 'description',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
dataIndex: 'description'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 60,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'TemplateSelector',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
query: {
|
||||
name: null,
|
||||
value: null,
|
||||
description: null
|
||||
},
|
||||
rows: [],
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showTotal: function(total) {
|
||||
return `共 ${total} 条`
|
||||
}
|
||||
},
|
||||
loading: false,
|
||||
columns
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.visible = true
|
||||
if (!this.rows.length) {
|
||||
this.getList({})
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
},
|
||||
getList(page = this.pagination) {
|
||||
this.loading = true
|
||||
this.$api.getTemplateList({
|
||||
...this.query,
|
||||
page: page.current,
|
||||
limit: page.pageSize
|
||||
}).then(({ data }) => {
|
||||
const pagination = { ...this.pagination }
|
||||
pagination.total = data.total
|
||||
pagination.current = data.page
|
||||
this.rows = data.rows || []
|
||||
this.pagination = pagination
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
resetForm() {
|
||||
this.$refs.query.resetFields()
|
||||
this.getList({})
|
||||
},
|
||||
selected(value) {
|
||||
this.$emit('selected', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
108
orion-ops-vue/src/components/editor/Editor.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<ace ref="editor"
|
||||
:value="value"
|
||||
:lang="lang"
|
||||
:width="width === 0 ? '100%' : width"
|
||||
:height="height === 0 ? '100%' : height"
|
||||
:theme="theme"
|
||||
:options="options"
|
||||
@init="initEditor"
|
||||
v-bind="config">
|
||||
</ace>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import ace from 'vue2-ace-editor'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
ace
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'iplastic'
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
default: 'sh'
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
enableLiveAutocompletion: true,
|
||||
fontSize: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
return {
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
showPrintMargin: false,
|
||||
fontSize: this.config.fontSize,
|
||||
enableLiveAutocompletion: this.config.enableLiveAutocompletion,
|
||||
readOnly: this.readOnly
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initEditor(editor) {
|
||||
require('brace/ext/language_tools')
|
||||
// 设置语言
|
||||
require('brace/mode/sh')
|
||||
require('brace/mode/json')
|
||||
require('brace/mode/xml')
|
||||
require('brace/mode/yaml')
|
||||
require('brace/mode/properties')
|
||||
require('brace/snippets/sh')
|
||||
require('brace/snippets/json')
|
||||
require('brace/snippets/xml')
|
||||
require('brace/snippets/yaml')
|
||||
require('brace/snippets/properties')
|
||||
// 设置主题
|
||||
// 浅色 iplastic sqlserver tomorrow xcode
|
||||
// 深色 dracula gruvbox idle_fingers merbivore terminal tomorrow_night_bright
|
||||
require('brace/theme/iplastic')
|
||||
// 监听值的变化
|
||||
editor.getSession().on('change', () => {
|
||||
this.$emit('change', editor.getValue())
|
||||
})
|
||||
},
|
||||
getValue() {
|
||||
return this.$refs.editor.editor.getValue()
|
||||
},
|
||||
setValue(value) {
|
||||
this.$refs.editor.editor.session.setValue(value)
|
||||
},
|
||||
clear() {
|
||||
this.$refs.editor.editor.session.setValue('')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
110
orion-ops-vue/src/components/exec/ExecTaskDetailModal.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="详情"
|
||||
width="700px"
|
||||
:dialogStyle="{top: '16px'}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true">
|
||||
<a-spin :spinning="loading">
|
||||
<div id="exec-task-descriptions">
|
||||
<a-descriptions bordered size="middle">
|
||||
<a-descriptions-item label="执行主机" :span="2">
|
||||
{{ `${detail.machineName} (${detail.machineHost})` }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行用户" :span="1">
|
||||
{{ detail.username }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行命令" :span="3">
|
||||
<Editor :height="300" :readOnly="true" :value="detail.command"/>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.description" label="执行描述" :span="3">
|
||||
{{ detail.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.createTime" label="创建时间" :span="2">
|
||||
{{ detail.createTime | formatDate }} ({{ detail.createTimeAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态" :span="1">
|
||||
<a-tag v-if="detail.status" :color="detail.status | formatExecStatus('color')">
|
||||
{{ detail.status | formatExecStatus('label') }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.startDate" label="开始时间" :span="detail.exitCode === null ? 3 : 2">
|
||||
{{ detail.startDate | formatDate }} ({{ detail.startDateAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.exitCode !== null" label="退出码" :span="1">
|
||||
<span :style="{'color': detail.exitCode === 0 ? '#4263EB' : '#E03131'}">
|
||||
{{ detail.exitCode }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.endDate" label="结束时间" :span="2">
|
||||
{{ detail.endDate | formatDate }} ({{ detail.endDateAgo }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.used" label="用时" :span="1">
|
||||
{{ `${detail.keepTime} (${detail.used}ms)` }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<a-button class="mr8" type="primary" @click="() => $copy(detail.command)">复制命令</a-button>
|
||||
<a-button @click="close">关闭</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatDate } from '@/lib/filters'
|
||||
import { enumValueOf, BATCH_EXEC_STATUS } from '@/lib/enum'
|
||||
import Editor from '@/components/editor/Editor'
|
||||
|
||||
export default {
|
||||
name: 'ExecTaskDetailModal',
|
||||
components: {
|
||||
Editor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
visible: false,
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.loading = true
|
||||
this.visible = true
|
||||
this.$api.getExecDetail({ id })
|
||||
.then(({ data }) => {
|
||||
this.loading = false
|
||||
this.detail = data
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.detail = {}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatDate,
|
||||
formatExecStatus(status, f) {
|
||||
return enumValueOf(BATCH_EXEC_STATUS, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep #exec-task-descriptions table th {
|
||||
padding: 14px;
|
||||
width: 95px;
|
||||
}
|
||||
|
||||
::v-deep #exec-task-descriptions table td {
|
||||
padding: 14px 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="应用环境 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppProfileExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.APP_PROFILE.value,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
100
orion-ops-vue/src/components/export/AppRepositoryExportModal.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="应用版本仓库 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 密码导出 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出密码</span>
|
||||
<a-checkbox class="param-input" v-model="exportPassword">是否导出密码 (密文, 仅用于导入)</a-checkbox>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'AppRepositoryExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
exportPassword: false,
|
||||
protectPassword: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.exportPassword = false
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.APP_REPOSITORY.value,
|
||||
exportPassword: this.exportPassword ? 1 : 2,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="应用信息 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'ApplicationExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.APPLICATION.value,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="命令模板 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'CommandTemplateExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.COMMAND_TEMPLATE.value,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="操作日志 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 导出分类 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出分类</span>
|
||||
<a-select class="param-input"
|
||||
placeholder="全部"
|
||||
@change="(e) => classify = e">
|
||||
<a-select-option v-for="classify in EVENT_CLASSIFY" :key="classify.value" :value="classify.value">
|
||||
{{ classify.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<!-- 选择用户 -->
|
||||
<div class="data-export-param mb16" v-if="$isAdmin() && manager">
|
||||
<span class="normal-label export-label">选择用户</span>
|
||||
<UserSelector class="param-input"
|
||||
placeholder="全部"
|
||||
:disabled="onlyMyself"
|
||||
@change="(e) => userId = e"/>
|
||||
</div>
|
||||
<!-- 只看自己 -->
|
||||
<div class="data-export-param mb16" v-if="$isAdmin() && manager">
|
||||
<span class="normal-label export-label">只看自己</span>
|
||||
<a-checkbox class="param-input" v-model="onlyMyself"/>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EVENT_CLASSIFY, EXPORT_TYPE } from '@/lib/enum'
|
||||
import UserSelector from '@/components/user/UserSelector'
|
||||
|
||||
export default {
|
||||
name: 'EventLogExportExportModal',
|
||||
components: { UserSelector },
|
||||
props: {
|
||||
manager: Boolean
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
EVENT_CLASSIFY,
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined,
|
||||
classify: undefined,
|
||||
onlyMyself: undefined,
|
||||
userId: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.protectPassword = undefined
|
||||
this.classify = undefined
|
||||
this.onlyMyself = undefined
|
||||
this.userId = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.USER_EVENT_LOG.value,
|
||||
protectPassword: this.protectPassword,
|
||||
classify: this.classify,
|
||||
userId: this.userId,
|
||||
onlyMyself: this.manager ? (this.onlyMyself ? 1 : 2) : 1
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="机器报警记录 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 导出机器 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出机器</span>
|
||||
<MachineSelector class="param-input"
|
||||
placeholder="全部"
|
||||
@change="(m) => machineId = m"/>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
import MachineSelector from '@/components/machine/MachineSelector'
|
||||
|
||||
export default {
|
||||
name: 'MachineAlarmHistoryExportModal',
|
||||
components: { MachineSelector },
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined,
|
||||
machineId: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.machineId = undefined
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.MACHINE_ALARM_HISTORY.value,
|
||||
protectPassword: this.protectPassword,
|
||||
machineId: this.machineId
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
100
orion-ops-vue/src/components/export/MachineExportModal.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="机器信息 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 密码导出 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出密码</span>
|
||||
<a-checkbox class="param-input" v-model="exportPassword">是否导出密码 (密文, 仅用于导入)</a-checkbox>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'MachineExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
exportPassword: false,
|
||||
protectPassword: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.exportPassword = false
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.MACHINE_INFO.value,
|
||||
exportPassword: this.exportPassword ? 1 : 2,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
100
orion-ops-vue/src/components/export/MachineProxyExportModal.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="机器代理 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 密码导出 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出密码</span>
|
||||
<a-checkbox class="param-input" v-model="exportPassword">是否导出密码 (密文, 仅用于导入)</a-checkbox>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'MachineProxyExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
exportPassword: false,
|
||||
protectPassword: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.exportPassword = false
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.MACHINE_PROXY.value,
|
||||
exportPassword: this.exportPassword ? 1 : 2,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
104
orion-ops-vue/src/components/export/TailFileExportModal.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="日志文件 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 导出机器 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出机器</span>
|
||||
<MachineSelector class="param-input"
|
||||
placeholder="全部"
|
||||
@change="(m) => machineId = m"/>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
import MachineSelector from '@/components/machine/MachineSelector'
|
||||
|
||||
export default {
|
||||
name: 'TailFileExportModal',
|
||||
components: { MachineSelector },
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined,
|
||||
machineId: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.machineId = undefined
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.TAIL_FILE.value,
|
||||
protectPassword: this.protectPassword,
|
||||
machineId: this.machineId
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
104
orion-ops-vue/src/components/export/TerminalLogExportModal.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="终端日志 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 导出机器 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">导出机器</span>
|
||||
<MachineSelector class="param-input"
|
||||
placeholder="全部"
|
||||
@change="(m) => machineId = m"/>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE } from '@/lib/enum'
|
||||
import MachineSelector from '@/components/machine/MachineSelector'
|
||||
|
||||
export default {
|
||||
name: 'TerminalLogExportModal',
|
||||
components: { MachineSelector },
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined,
|
||||
machineId: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.machineId = undefined
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
exportType: EXPORT_TYPE.TERMINAL_LOG.value,
|
||||
protectPassword: this.protectPassword,
|
||||
machineId: this.machineId
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
107
orion-ops-vue/src/components/export/WebhookExportModal.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="webhook 导出"
|
||||
okText="导出"
|
||||
:width="400"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="exportData"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="data-export-container">
|
||||
<!-- 导出参数 -->
|
||||
<div class="data-export-params">
|
||||
<!-- 导出分类 -->
|
||||
<div class="data-export-param mb16">
|
||||
<span class="normal-label export-label">类型</span>
|
||||
<a-select class="param-input"
|
||||
placeholder="全部"
|
||||
@change="(e) => type = e">
|
||||
<a-select-option v-for="type in WEBHOOK_TYPE" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-export-param">
|
||||
<span class="normal-label export-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导出文档的密码(数字及字母)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
import { EXPORT_TYPE, WEBHOOK_TYPE } from '@/lib/enum'
|
||||
|
||||
export default {
|
||||
name: 'WebhookExportModal',
|
||||
data: function() {
|
||||
return {
|
||||
WEBHOOK_TYPE,
|
||||
visible: false,
|
||||
loading: false,
|
||||
protectPassword: undefined,
|
||||
type: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.type = undefined
|
||||
this.protectPassword = undefined
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
exportData() {
|
||||
this.loading = true
|
||||
this.$api.exportData({
|
||||
type: this.type,
|
||||
exportType: EXPORT_TYPE.WEBHOOK.value,
|
||||
protectPassword: this.protectPassword
|
||||
}).then((e) => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('导出成功, 片刻后自动下载')
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.$message.error('导出失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-export-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.export-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-export-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
</style>
|
||||
241
orion-ops-vue/src/components/import/DataImportModal.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="importType.title"
|
||||
:okText="dataCheckPage ? '开始导入' : '导入'"
|
||||
:width="500"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clickImport"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 导入页面 -->
|
||||
<div class="data-import-container" v-if="!dataCheckPage">
|
||||
<!-- 导入提示 -->
|
||||
<a-alert class="import-alert-message" v-if="importType.tips" :message="importType.tips" type="info"/>
|
||||
<!-- 上传框 -->
|
||||
<div class="file-select-container">
|
||||
<!-- 文件选择 -->
|
||||
<div class="file-select-wrapper">
|
||||
<a-upload accept=".xlsx"
|
||||
:beforeUpload="selectFile"
|
||||
:customRequest="() => {}"
|
||||
:showUploadList="false">
|
||||
<a-button type="link">选择文件</a-button>
|
||||
</a-upload>
|
||||
<!-- 已选择的文件 -->
|
||||
<span class="selected-file-name" v-if="uploadFile" :title="uploadFile.name">
|
||||
{{ uploadFile.name }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 下载模板 -->
|
||||
<a-button type="link" @click="downloadTemplate">下载模板</a-button>
|
||||
</div>
|
||||
<!-- 参数 -->
|
||||
<div class="data-import-params">
|
||||
<!-- 文档密码 -->
|
||||
<div class="data-import-param">
|
||||
<span class="normal-label import-label">文档密码</span>
|
||||
<a-input class="param-input"
|
||||
v-model="protectPassword"
|
||||
:maxLength="10"
|
||||
placeholder="导入文档的密码"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 检查页面 -->
|
||||
<div class="data-check-container" v-else>
|
||||
<!-- 非法数据 -->
|
||||
<div class="check-data-container" v-if="checkData.illegalRows.length">
|
||||
<span class="normal-label check-label span-red">非法数据 (不会导入)</span>
|
||||
<div class="check-data-wrapper" v-for="row of checkData.illegalRows" :key="row.index">
|
||||
第 <span class="span-red">{{ row.row }}</span> 行, <span v-if="row.symbol" class="ml4 mr8">{{ row.symbol }}</span>
|
||||
<span class="span-red">{{ row.illegalMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 新增数据 -->
|
||||
<div class="check-data-container" v-if="checkData.insertRows.length">
|
||||
<span class="normal-label check-label span-blue">新增数据</span>
|
||||
<div class="check-data-wrapper" v-for="row of checkData.insertRows" :key="row.index">
|
||||
第 <span class="span-blue">{{ row.row }}</span> 行, <span class="ml4">{{ row.symbol }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 修改数据 -->
|
||||
<div class="check-data-container" v-if="checkData.updateRows.length">
|
||||
<span class="normal-label check-label span-blue">修改数据</span>
|
||||
<div class="check-data-wrapper" v-for="row of checkData.updateRows" :key="row.index">
|
||||
第 <span class="span-blue">{{ row.row }}</span> 行, <span class="ml4">{{ row.symbol }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
|
||||
export default {
|
||||
name: 'DataImportModal',
|
||||
props: {
|
||||
importType: Object
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
dataCheckPage: false,
|
||||
protectPassword: undefined,
|
||||
uploadFile: null,
|
||||
checkData: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dataCheckPage = false
|
||||
this.protectPassword = undefined
|
||||
this.uploadFile = null
|
||||
this.checkData = null
|
||||
this.loading = false
|
||||
this.visible = true
|
||||
},
|
||||
selectFile(e) {
|
||||
const suffix = e.name.substring(e.name.lastIndexOf('.') + 1)
|
||||
if (suffix !== 'xlsx') {
|
||||
this.$message.error('请选择 xlsx 表格进行导入')
|
||||
return false
|
||||
}
|
||||
this.uploadFile = e
|
||||
},
|
||||
clickImport() {
|
||||
if (this.dataCheckPage) {
|
||||
this.asyncImportData()
|
||||
} else {
|
||||
this.checkImportData()
|
||||
}
|
||||
},
|
||||
checkImportData() {
|
||||
if (!this.uploadFile) {
|
||||
this.$message.warning('请选择导入文件')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
const formData = new FormData()
|
||||
formData.append('file', this.uploadFile)
|
||||
formData.append('type', this.importType.value)
|
||||
formData.append('protectPassword', this.protectPassword)
|
||||
this.$api.checkImportData(formData).then(({ data }) => {
|
||||
this.loading = false
|
||||
this.dataCheckPage = true
|
||||
this.checkData = data
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
asyncImportData() {
|
||||
if (this.checkData.insertRows.length + this.checkData.updateRows.length === 0) {
|
||||
this.$message.warning('无可用导入数据, 无法导入')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$api.importData({
|
||||
importToken: this.checkData.importToken
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
this.visible = false
|
||||
this.$message.success('已开始导入')
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
downloadTemplate() {
|
||||
this.$api.getImportTemplate({
|
||||
type: this.importType.value
|
||||
}).then((e) => {
|
||||
downloadFile(e)
|
||||
}).catch(() => {
|
||||
this.$message.error('下载失败')
|
||||
})
|
||||
},
|
||||
close() {
|
||||
if (this.dataCheckPage) {
|
||||
this.$api.cancelImportData({
|
||||
importToken: this.checkData.importToken
|
||||
})
|
||||
}
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.uploadFile = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.data-import-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.import-alert-message {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-select-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected-file-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 294px;
|
||||
display: inline-flex;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.import-label {
|
||||
width: 64px;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.data-import-param {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
margin-left: 8px;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.data-check-container {
|
||||
margin: -12px 0;
|
||||
|
||||
.check-data-container {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.check-label {
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.check-data-wrapper {
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
padding-left: 16px;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
158
orion-ops-vue/src/components/layout/Header.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<a-layout-header class="header-main">
|
||||
<!-- 头部左侧 -->
|
||||
<div class="header-fixed-left">
|
||||
<!-- 折叠 -->
|
||||
<a-icon class="trigger-icon header-block-container header-block-fold"
|
||||
:type="fold ? 'menu-unfold' : 'menu-fold'"
|
||||
:title="fold ? '展开' : '折叠'"
|
||||
@click="changeFold"/>
|
||||
<!-- 左侧配置 -->
|
||||
<a-icon class="trigger-icon header-block-container"
|
||||
v-for="(prop, index) of leftProps"
|
||||
:key="index"
|
||||
:title="prop.title"
|
||||
:type="prop.icon"
|
||||
@click="handlerCall(prop)"/>
|
||||
</div>
|
||||
<!-- 头部右侧 -->
|
||||
<div class="header-fixed-right">
|
||||
<!-- 环境选择 -->
|
||||
<HeaderProfileSelect id="header-profile-selector"
|
||||
class="header-block-container"
|
||||
ref="profileSelect"
|
||||
v-show="profileSelectorVisible"
|
||||
@chooseProfile="chooseProfile"/>
|
||||
<!-- 站内信 -->
|
||||
<WebSideMessageDrawer id="web-side-message-drawer" class="header-block-container"/>
|
||||
<!-- 用户下拉 -->
|
||||
<HeaderUser id="header-user" class="header-block-container"/>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import HeaderProfileSelect from './HeaderProfileSelect'
|
||||
import HeaderUser from './HeaderUser'
|
||||
import WebSideMessageDrawer from '@/components/layout/WebSideMessageDrawer'
|
||||
|
||||
export default {
|
||||
name: 'Header',
|
||||
components: {
|
||||
WebSideMessageDrawer,
|
||||
HeaderProfileSelect,
|
||||
HeaderUser
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
fold: false,
|
||||
profileSelectorVisible: false,
|
||||
leftProps: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeFold() {
|
||||
this.fold = !this.fold
|
||||
this.$emit('changeFoldStatus')
|
||||
},
|
||||
handlerCall(prop) {
|
||||
prop.call && this[prop.call] && this[prop.call]()
|
||||
prop.event && this.$emit('onHeaderEvent', prop.event)
|
||||
},
|
||||
back() {
|
||||
this.$router.back(-1)
|
||||
},
|
||||
checkVisible(e = this.$route) {
|
||||
this.profileSelectorVisible = e.meta.visibleProfile === true
|
||||
this.leftProps = e.meta.leftProps || []
|
||||
},
|
||||
chooseProfile(profile) {
|
||||
this.$emit('chooseProfile', profile)
|
||||
},
|
||||
reloadProfile() {
|
||||
this.$refs.profileSelect.loadProfile()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.checkVisible()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.header-main {
|
||||
background: #FFF;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-fixed-left {
|
||||
display: flex;
|
||||
|
||||
.header-block-fold {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.trigger-icon {
|
||||
font-size: 18px;
|
||||
line-height: 48px;
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.trigger-icon:hover {
|
||||
color: #1890FF;
|
||||
}
|
||||
}
|
||||
|
||||
.header-fixed-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#header-profile-selector {
|
||||
padding: 0 8px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
|
||||
::v-deep i {
|
||||
padding-left: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
#web-side-message-drawer {
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
#header-user {
|
||||
padding: 0 8px;
|
||||
margin-right: 2px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-block-container {
|
||||
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header-block-container:hover {
|
||||
color: hsla(0, 0%, 100%, .2);
|
||||
background-color: #E7F5FF;
|
||||
color: #148EFF;
|
||||
}
|
||||
|
||||
</style>
|
||||
76
orion-ops-vue/src/components/layout/HeaderProfileSelect.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<a-dropdown v-if="profileList.length">
|
||||
<a class="ant-dropdown-link" id="current-profile" @click="e => e.preventDefault()">
|
||||
{{ currentProfile.name }}
|
||||
<a-icon type="down"/>
|
||||
</a>
|
||||
<template #overlay>
|
||||
<a-menu @click="chooseProfile">
|
||||
<a-menu-item v-for="profile in profileList" :key="JSON.stringify(profile)">
|
||||
{{ profile.name }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isEmptyStr } from '@/lib/utils'
|
||||
|
||||
export default {
|
||||
name: 'HeaderProfileSelect',
|
||||
data() {
|
||||
return {
|
||||
currentProfile: '',
|
||||
profileList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chooseProfile({ key }) {
|
||||
this.currentProfile = JSON.parse(key)
|
||||
this.$storage.set(this.$storage.keys.ACTIVE_PROFILE, key)
|
||||
this.$emit('chooseProfile', this.currentProfile)
|
||||
},
|
||||
loadProfile() {
|
||||
this.$api.fastGetProfileList().then(({ data }) => {
|
||||
if (!data || !data.length) {
|
||||
return
|
||||
}
|
||||
this.profileList = data
|
||||
}).then(() => {
|
||||
if (!this.profileList.length) {
|
||||
this.$storage.remove(this.$storage.keys.ACTIVE_PROFILE)
|
||||
return
|
||||
}
|
||||
let storageProfile = this.$storage.get(this.$storage.keys.ACTIVE_PROFILE)
|
||||
if (isEmptyStr(storageProfile)) {
|
||||
// 如果没有则拿到第一个
|
||||
storageProfile = this.profileList[0]
|
||||
} else {
|
||||
let matches = false
|
||||
storageProfile = JSON.parse(storageProfile)
|
||||
for (var profileValue of this.profileList) {
|
||||
if (profileValue.id === storageProfile.id) {
|
||||
matches = true
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
storageProfile = this.profileList[0]
|
||||
}
|
||||
}
|
||||
this.$storage.set(this.$storage.keys.ACTIVE_PROFILE, JSON.stringify(storageProfile))
|
||||
this.currentProfile = storageProfile
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadProfile()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#current-profile {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
100
orion-ops-vue/src/components/layout/HeaderUser.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<a-dropdown>
|
||||
<a class="ant-dropdown-link" @click="e => e.preventDefault()">
|
||||
<template v-if="user.avatar">
|
||||
<a-avatar :src="user.avatar" :size="36"/>
|
||||
</template>
|
||||
<template v-else-if="user.nickname">
|
||||
<a-avatar :size="36" :style="{backgroundColor: '#7265E6', verticalAlign: 'middle'}">
|
||||
{{ user.nickname.substring(user.nickname.length - 1) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div style="width: 36px; height: 36px"/>
|
||||
</template>
|
||||
</a>
|
||||
<template #overlay>
|
||||
<a-menu @click="chooseMenu">
|
||||
<a-menu-item key="nickname" id="user-nickname">
|
||||
<a-icon type="smile"/>
|
||||
{{ user.nickname }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="userInfo">
|
||||
<a-icon type="user"/>
|
||||
个人中心
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetPassword">
|
||||
<a-icon type="safety-certificate"/>
|
||||
修改密码
|
||||
<ResetPassword ref="resetModel" @resetSuccess="resetSuccess"/>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="logout">
|
||||
<a-icon type="export"/>
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ResetPassword from '../user/ResetPassword'
|
||||
|
||||
const menuItemHandler = {
|
||||
nickname() {
|
||||
},
|
||||
userInfo() {
|
||||
this.$router.push({ path: '/user/detail' })
|
||||
},
|
||||
resetPassword() {
|
||||
this.openResetModel()
|
||||
},
|
||||
async logout() {
|
||||
await this.$api.logout()
|
||||
this.$storage.clear()
|
||||
this.$storage.clearSession()
|
||||
this.$router.push({ path: '/login' })
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'HeaderUser',
|
||||
components: { ResetPassword },
|
||||
data: function() {
|
||||
return {
|
||||
user: {
|
||||
nickname: '',
|
||||
avatar: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chooseMenu({ key }) {
|
||||
menuItemHandler[key].call(this)
|
||||
},
|
||||
openResetModel() {
|
||||
this.$refs.resetModel.open()
|
||||
},
|
||||
resetSuccess() {
|
||||
this.$storage.clear()
|
||||
this.$storage.clearSession()
|
||||
this.$router.push('/login')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$api.getUserDetail()
|
||||
.then(({ data }) => {
|
||||
this.user.nickname = data.nickname
|
||||
this.user.avatar = data.avatar
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#user-nickname {
|
||||
color: #495057;
|
||||
padding-left: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
129
orion-ops-vue/src/components/layout/Layout.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<a-layout id="common-layout" v-if="validToken">
|
||||
<!-- 左侧 -->
|
||||
<a-layout-sider id="common-sider" v-model="collapsed" :trigger="null">
|
||||
<!-- <div class="logo"/> -->
|
||||
<!-- 左侧菜单 -->
|
||||
<Menu ref="menu"/>
|
||||
</a-layout-sider>
|
||||
<a-layout id="common-right">
|
||||
<!-- 头部菜单 -->
|
||||
<Header id="common-header"
|
||||
ref="header"
|
||||
@changeFoldStatus="collapsed = !collapsed"
|
||||
@chooseProfile="chooseProfile"
|
||||
@onHeaderEvent="onHeaderEvent"/>
|
||||
<!-- 主体部分 -->
|
||||
<a-layout-content id="common-content">
|
||||
<a-spin :spinning="globalLoading" :tip="globalLoadingTip">
|
||||
<router-view ref="route"
|
||||
:key="$route.fullPath"
|
||||
@reloadProfile="reloadProfile"
|
||||
@openLoading="openLoading"
|
||||
@closeLoading="closeLoading"/>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Menu from './Menu'
|
||||
import Header from './Header'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Menu,
|
||||
Header
|
||||
},
|
||||
watch: {
|
||||
$route(e) {
|
||||
// 设置菜单选中
|
||||
this.$refs.menu.chooseMenu(e)
|
||||
// 设置头部按钮
|
||||
this.$refs.header.checkVisible(e)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapsed: false,
|
||||
validToken: false,
|
||||
globalLoading: false,
|
||||
globalLoadingTip: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chooseProfile(e) {
|
||||
this.$refs.route && this.$refs.route.chooseProfile && this.$refs.route.chooseProfile(e)
|
||||
},
|
||||
onHeaderEvent(e) {
|
||||
this.$refs.route && this.$refs.route.onHeaderEvent && this.$refs.route.onHeaderEvent(e)
|
||||
},
|
||||
reloadProfile() {
|
||||
this.$refs.header.reloadProfile()
|
||||
},
|
||||
openLoading(tip = null) {
|
||||
this.globalLoading = true
|
||||
this.globalLoadingTip = tip
|
||||
},
|
||||
closeLoading() {
|
||||
this.globalLoading = false
|
||||
this.globalLoadingTip = null
|
||||
}
|
||||
},
|
||||
async beforeCreate() {
|
||||
if (this.$getUserId()) {
|
||||
await this.$api.validToken().then(() => {
|
||||
this.validToken = true
|
||||
}).catch(() => {
|
||||
this.validToken = false
|
||||
})
|
||||
} else {
|
||||
this.validToken = false
|
||||
}
|
||||
if (!this.validToken) {
|
||||
this.$storage.clear()
|
||||
this.$storage.clearSession()
|
||||
this.$router.push('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#common-layout {
|
||||
height: 100vh;
|
||||
|
||||
.logo {
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
#common-sider {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#common-right {
|
||||
overflow: hidden;
|
||||
|
||||
#common-content {
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
#common-header {
|
||||
z-index: 10;
|
||||
padding-right: 0;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
97
orion-ops-vue/src/components/layout/Menu.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<a-menu theme="dark"
|
||||
mode="inline"
|
||||
:selectedKeys="selectedKeys"
|
||||
:defaultOpenKeys="defaultOpenKeys">
|
||||
<template v-for="menuItem in menuList">
|
||||
<!-- 一级菜单 -->
|
||||
<a-menu-item v-if="!menuItem.children" :key="menuItem.id">
|
||||
<router-link :to="menuItem.path">
|
||||
<a-icon :type="menuItem.icon"/>
|
||||
<span>{{ menuItem.name }}</span>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<!-- 二级菜单 -->
|
||||
<a-sub-menu v-else :key="menuItem.id">
|
||||
<template #title>
|
||||
<a-icon :type="menuItem.icon"/>
|
||||
<span class="usn">{{ menuItem.name }}</span>
|
||||
</template>
|
||||
<a-menu-item v-for="subMenuItem in menuItem.children" :key="subMenuItem.id">
|
||||
<router-link :to="subMenuItem.path">
|
||||
<a-icon :type="subMenuItem.icon"/>
|
||||
<span>{{ subMenuItem.name }}</span>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Menu',
|
||||
data() {
|
||||
return {
|
||||
menuList: [],
|
||||
selectedKeys: [],
|
||||
defaultOpenKeys: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chooseMenu(route = this.$route) {
|
||||
const routerPath = route.path
|
||||
for (const menu of this.menuList) {
|
||||
if (menu.path && routerPath.startsWith(menu.path)) {
|
||||
// 一级菜单选中
|
||||
this.selectedKeys[0] = menu.id
|
||||
this.$forceUpdate()
|
||||
return
|
||||
}
|
||||
if (menu.children) {
|
||||
for (const child of menu.children) {
|
||||
if (child.path && routerPath.startsWith(child.path)) {
|
||||
// 二级菜单选中
|
||||
let present = false
|
||||
for (const defaultOpenKey of this.defaultOpenKeys) {
|
||||
if (defaultOpenKey === menu.id) {
|
||||
present = true
|
||||
break
|
||||
}
|
||||
}
|
||||
this.selectedKeys[0] = child.id
|
||||
if (!present) {
|
||||
this.defaultOpenKeys.push(menu.id)
|
||||
}
|
||||
this.$forceUpdate()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 加载菜单
|
||||
this.$api.getMenu().then(({ data }) => {
|
||||
let id = 0
|
||||
for (const menu of data) {
|
||||
menu.id = ++id
|
||||
const children = menu.children
|
||||
if (children) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
children[i].id = ++id
|
||||
}
|
||||
}
|
||||
this.menuList.push(menu)
|
||||
}
|
||||
// 选中
|
||||
this.chooseMenu()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
530
orion-ops-vue/src/components/layout/WebSideMessageDrawer.vue
Normal file
@@ -0,0 +1,530 @@
|
||||
<template>
|
||||
<div class="web-side-message-container pointer" @click="onOpen" title="消息">
|
||||
<!-- 触发器 -->
|
||||
<a class="icon-wrapper">
|
||||
<!-- 红点 -->
|
||||
<a-badge class="unread-message-dot" :dot="unreadCount > 0">
|
||||
<a-icon class="web-side-message-trigger" type="notification"/>
|
||||
</a-badge>
|
||||
</a>
|
||||
<!-- 侧边抽屉 -->
|
||||
<a-drawer :title="null"
|
||||
placement="right"
|
||||
:closable="false"
|
||||
:width="400"
|
||||
:maskStyle="{opacity: 0, animation: 'none'}"
|
||||
:bodyStyle="{padding: 0}"
|
||||
:maskClosable="true"
|
||||
:visible="visible"
|
||||
:afterVisibleChange="visibleChange"
|
||||
@close="onClose">
|
||||
<!-- 消息头 -->
|
||||
<div class="message-header-container">
|
||||
<!-- 头部左侧 -->
|
||||
<div class="message-header-left">
|
||||
<!-- 消息状态 -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<!-- 消息状态触发器 -->
|
||||
<a class="message-block-item message-status-trigger" @click="e => e.preventDefault()">
|
||||
<a-icon :type="messageStatus.icon"/>
|
||||
<span>{{ messageStatus.label }}</span>
|
||||
<a-icon type="down"/>
|
||||
</a>
|
||||
<!-- 消息类型下拉框 -->
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item v-for="item of messageStatusList" :key="item.label">
|
||||
<span class="header-dropdown-item message-type-dropdown-item" @click="changeStatus(item)">
|
||||
<a-icon :type="item.icon"/>
|
||||
<span>{{ item.label }}</span>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<!-- 头部右侧 -->
|
||||
<div class="message-header-right">
|
||||
<!-- 消息处理 -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<!-- 消息处理触发器 -->
|
||||
<a class="message-block-item" @click="e => e.preventDefault()">
|
||||
<a-icon class="message-action-trigger" type="ellipsis"/>
|
||||
</a>
|
||||
<!-- 消息处理下拉框 -->
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item key="1">
|
||||
<span class="header-dropdown-item message-action-dropdown-item" @click="readAllMessage()">
|
||||
<a-icon type="check"/>
|
||||
<span>标记所有消息为已读</span>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="2">
|
||||
<span class="header-dropdown-item message-action-dropdown-item" @click="deleteAllMessage()">
|
||||
<a-icon type="delete"/>
|
||||
<span>删除所有已读消息</span>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
<!-- 关闭消息 -->
|
||||
<a class="message-block-item" @click="onClose" title="关闭">
|
||||
<a-icon class="message-action-trigger" type="close"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list-container">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 消息 -->
|
||||
<template v-if="rows.length">
|
||||
<div class="message-wrapper" v-for="row of rows"
|
||||
:key="row.id"
|
||||
@click="clickMessage(row)"
|
||||
@mouseenter="addMessageActive(row)"
|
||||
@mouseleave="removeMessageActive(row)">
|
||||
<!-- 消息头 -->
|
||||
<div class="message-top-wrapper">
|
||||
<!-- 消息左侧 消息分类 -->
|
||||
<div class="message-top-left">
|
||||
<a-icon class="message-classify-icon" :type="row.classify | formatMessageClassify('icon')"/>
|
||||
<div class="message-classify-text-wrapper">
|
||||
<!-- 分类 -->
|
||||
<span class="message-classify-label">{{ row.classify | formatMessageClassify('label') }}</span>
|
||||
<span class="message-classify-divider">/</span>
|
||||
<!-- 类型 -->
|
||||
<span class="message-type-label">{{ row.type | formatMessageType('label') }}</span>
|
||||
<!-- 未读 -->
|
||||
<a-badge v-if="row.status === READ_STATUS.UNREAD.value" class="message-unread-dot" status="error"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息右侧 按钮-->
|
||||
<div class="message-top-right" v-show="row.visibleTools">
|
||||
<a-icon v-if="row.status === READ_STATUS.UNREAD.value"
|
||||
type="check"
|
||||
title="已读"
|
||||
@click.stop="readMessage(row)"/>
|
||||
<a-icon type="close" title="删除" @click.stop="deleteMessage(row.id)"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 消息 -->
|
||||
<div class="message-text-wrapper">
|
||||
<a-icon class="message-text-icon" type="bell" theme="twoTone"/>
|
||||
<span class="message-text" v-html="row.message"/>
|
||||
</div>
|
||||
<!-- 消息时间 -->
|
||||
<div class="message-date-wrapper">
|
||||
{{ row.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 无数据 -->
|
||||
<template v-else>
|
||||
<div class="message-empty-wrapper">
|
||||
<a-empty description="暂无消息"/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 加载更多 -->
|
||||
<div class="load-more-wrapper">
|
||||
<a-button v-if="hasMore" type="link" @click="loadMoreMessage">加载更多</a-button>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUUID, clearStainKeywords, dateFormat, replaceStainKeywords } from '@/lib/utils'
|
||||
import { enumValueOf, MESSAGE_CLASSIFY, MESSAGE_TYPE, READ_STATUS } from '@/lib/enum'
|
||||
|
||||
const messageStatusList = [{
|
||||
status: READ_STATUS.UNREAD.value,
|
||||
label: '未读消息',
|
||||
icon: 'tag'
|
||||
}, {
|
||||
status: undefined,
|
||||
label: '全部消息',
|
||||
icon: 'tags'
|
||||
}]
|
||||
|
||||
export default {
|
||||
name: 'WebSideMessageDrawer',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
unreadCount: 0,
|
||||
limit: 30,
|
||||
pollMaxId: null,
|
||||
minId: null,
|
||||
rows: [],
|
||||
pollId: null,
|
||||
messageStatus: messageStatusList[0],
|
||||
messageStatusList,
|
||||
READ_STATUS,
|
||||
hasMore: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen() {
|
||||
this.visible = true
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
},
|
||||
changeStatus(status) {
|
||||
this.messageStatus = status
|
||||
this.getMessages()
|
||||
},
|
||||
readAllMessage() {
|
||||
this.$api.setWebSideMessageAllRead().then(() => {
|
||||
this.rows.forEach(row => {
|
||||
row.status = READ_STATUS.READ.value
|
||||
})
|
||||
this.$message.success('操作成功')
|
||||
})
|
||||
},
|
||||
deleteAllMessage() {
|
||||
this.$api.deleteAllReadMessage().then(() => {
|
||||
this.rows = this.rows.filter(row => {
|
||||
return row.status === READ_STATUS.UNREAD.value
|
||||
})
|
||||
this.$message.success('操作成功')
|
||||
})
|
||||
},
|
||||
readMessage(row) {
|
||||
this.$api.setMessageRead({
|
||||
idList: [row.id]
|
||||
}).then(() => {
|
||||
row.status = READ_STATUS.READ.value
|
||||
})
|
||||
},
|
||||
deleteMessage(id) {
|
||||
this.$api.deleteWebSideMessage({
|
||||
idList: [id]
|
||||
}).then(() => {
|
||||
for (let i = 0; i < this.rows.length; i++) {
|
||||
if (this.rows[i].id === id) {
|
||||
this.rows.splice(i, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
clickMessage(row) {
|
||||
// 重定向
|
||||
this.$router.push(enumValueOf(MESSAGE_TYPE, row.type).redirect(row))
|
||||
this.onClose()
|
||||
if (row.status === READ_STATUS.READ.value) {
|
||||
return
|
||||
}
|
||||
// 修改状态
|
||||
row.status = READ_STATUS.READ.value
|
||||
this.$api.setMessageRead({
|
||||
idList: [row.id]
|
||||
})
|
||||
},
|
||||
addMessageActive(row) {
|
||||
row.visibleTools = true
|
||||
this.$forceUpdate()
|
||||
},
|
||||
removeMessageActive(row) {
|
||||
row.visibleTools = false
|
||||
this.$forceUpdate()
|
||||
},
|
||||
visibleChange(e) {
|
||||
if (!e) {
|
||||
return
|
||||
}
|
||||
this.loadNewMessage()
|
||||
},
|
||||
getMessages() {
|
||||
// 获取站内信
|
||||
this.$api.getWebSideMessageList({
|
||||
status: this.messageStatus.status,
|
||||
limit: this.limit
|
||||
}).then(({ data }) => {
|
||||
this.rows = data.rows || []
|
||||
this.rows.forEach(this.processMessage)
|
||||
this.hasMore = this.rows.length === this.limit
|
||||
this.loading = false
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
loadNewMessage() {
|
||||
this.$api.getNewMessage({
|
||||
maxId: this.rows.length ? this.rows[0].id : null,
|
||||
status: this.messageStatus.status
|
||||
}).then(({ data }) => {
|
||||
this.unreadCount = data.unreadCount
|
||||
const newMessages = data.newMessages
|
||||
if (newMessages && newMessages.length) {
|
||||
newMessages.forEach(this.processMessage)
|
||||
newMessages.forEach(row => this.rows.unshift(row))
|
||||
}
|
||||
})
|
||||
},
|
||||
loadMoreMessage() {
|
||||
this.loading = true
|
||||
this.$api.getMoreMessage({
|
||||
maxId: this.rows[this.rows.length - 1].id,
|
||||
status: this.messageStatus.status,
|
||||
limit: this.limit
|
||||
}).then(({ data }) => {
|
||||
this.loading = false
|
||||
const length = data.length
|
||||
this.hasMore = length === this.limit
|
||||
if (!length) {
|
||||
return
|
||||
}
|
||||
data.forEach(this.processMessage)
|
||||
data.forEach(row => this.rows.push(row))
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
processMessage(row) {
|
||||
// 格式化时间
|
||||
row.createTime = dateFormat(new Date(row.createTime), 'MM月dd日 HH:mm:ss')
|
||||
// 处理数据
|
||||
row.message = replaceStainKeywords(row.message)
|
||||
// 显示按钮
|
||||
row.visibleTools = false
|
||||
},
|
||||
pollWebSideMessage() {
|
||||
this.$api.pollWebSideMessage({
|
||||
maxId: this.pollMaxId
|
||||
}).then(({ data }) => {
|
||||
this.unreadCount = data.unreadCount
|
||||
this.pollMaxId = data.maxId
|
||||
const newMessages = data.newMessages
|
||||
if (newMessages && newMessages.length) {
|
||||
// 通知新消息
|
||||
for (const newMessage of newMessages) {
|
||||
setTimeout(() => {
|
||||
const messageType = enumValueOf(MESSAGE_TYPE, newMessage.type)
|
||||
const key = getUUID()
|
||||
this.$notification[messageType.notify]({
|
||||
key,
|
||||
message: messageType.label,
|
||||
description: clearStainKeywords(newMessage.message),
|
||||
duration: messageType.duration,
|
||||
onClick: () => {
|
||||
this.$notification.close(key)
|
||||
this.$router.push(messageType.redirect(newMessage))
|
||||
this.$api.setMessageRead({
|
||||
idList: [newMessage.id]
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatMessageClassify(classify, f) {
|
||||
return enumValueOf(MESSAGE_CLASSIFY, classify)[f]
|
||||
},
|
||||
formatMessageType(type, f) {
|
||||
return enumValueOf(MESSAGE_TYPE, type)[f]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollWebSideMessage()
|
||||
// 轮询
|
||||
this.pollId = setInterval(this.pollWebSideMessage, 15000)
|
||||
// 加载消息
|
||||
this.getMessages()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.web-side-message-trigger {
|
||||
font-size: 19px;
|
||||
color: #181E33;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
::v-deep .unread-message-dot .ant-badge-dot {
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
.message-header-container {
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid #CED4DA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.message-header-right {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.message-block-item {
|
||||
padding-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: grey !important;
|
||||
}
|
||||
|
||||
.message-block-item:hover {
|
||||
color: #1890FF !important;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.message-status-trigger {
|
||||
user-select: none;
|
||||
|
||||
span {
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
padding: 0 5px 0 7px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 17px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
i {
|
||||
font-size: 17px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-dropdown-item:hover {
|
||||
color: #1890FF !important;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.message-type-dropdown-item {
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
.message-action-dropdown-item {
|
||||
width: 186px;
|
||||
}
|
||||
|
||||
.message-action-trigger {
|
||||
margin: 0 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.message-list-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message-wrapper {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #CED4DA;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-wrapper:hover {
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
.message-empty-wrapper {
|
||||
margin: 32px 0 0 96px;
|
||||
}
|
||||
|
||||
.load-more-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.message-top-wrapper, .message-text-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-date-wrapper {
|
||||
font-size: 12px;
|
||||
margin-left: 32px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.message-classify-icon, .message-text-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 15px;
|
||||
color: #1890FF;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: #E6F7FF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message-top-wrapper {
|
||||
|
||||
.message-top-left, .message-top-right {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message-top-right > i {
|
||||
width: 23px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.message-top-right > i:hover {
|
||||
color: #1890FF !important;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.message-classify-text-wrapper {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message-classify-label, .message-type-label {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.message-classify-divider {
|
||||
color: #1890FF;
|
||||
font-weight: 500;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.message-unread-dot {
|
||||
display: flex;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
width: 332px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
263
orion-ops-vue/src/components/log/AddLogFileModal.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:title="title"
|
||||
:width="650"
|
||||
:dialogStyle="{top: '64px', padding: 0}"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="check"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form :form="form" v-bind="layout">
|
||||
<a-form-item label="机器">
|
||||
<MachineSelector ref="machineSelector"
|
||||
placeholder="请选择"
|
||||
@change="setDefaultConfig"
|
||||
:query="machineQuery"
|
||||
v-decorator="decorators.machineId"/>
|
||||
</a-form-item>
|
||||
<!-- 追踪模式 -->
|
||||
<a-form-item v-if="form.getFieldValue('machineId') === 1" label="追踪模式">
|
||||
<a-radio-group v-decorator="decorators.tailMode">
|
||||
<a-tooltip v-for="type of FILE_TAIL_MODE" :key="type.value" :title="type.tips">
|
||||
<a-radio :value="type.value">
|
||||
{{ type.label }}
|
||||
</a-radio>
|
||||
</a-tooltip>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" hasFeedback>
|
||||
<a-input v-decorator="decorators.name" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件路径" hasFeedback>
|
||||
<a-input v-decorator="decorators.path" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="命令" style="margin-bottom: 12px">
|
||||
<a-textarea v-decorator="decorators.command"
|
||||
:disabled="form.getFieldValue('machineId') === 1 &&
|
||||
form.getFieldValue('tailMode') === FILE_TAIL_MODE.TRACKER.value"
|
||||
allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件偏移量(行)" hasFeedback>
|
||||
<a-input v-decorator="decorators.offset" allowClear/>
|
||||
</a-form-item>
|
||||
<a-form-item label="文件编码">
|
||||
<a-input v-decorator="decorators.charset" allowClear/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pick } from 'lodash'
|
||||
import { ENABLE_STATUS, FILE_TAIL_MODE } from '@/lib/enum'
|
||||
import MachineSelector from '@/components/machine/MachineSelector'
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 17 }
|
||||
}
|
||||
|
||||
function getDecorators() {
|
||||
return {
|
||||
name: ['name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入名称'
|
||||
}, {
|
||||
max: 64,
|
||||
message: '名称长度不能大于64位'
|
||||
}]
|
||||
}],
|
||||
machineId: ['machineId', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择机器'
|
||||
}]
|
||||
}],
|
||||
tailMode: ['tailMode', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请选择追踪模式'
|
||||
}],
|
||||
initialValue: FILE_TAIL_MODE.TRACKER.value
|
||||
}],
|
||||
path: ['path', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入文件路径'
|
||||
}, {
|
||||
max: 1024,
|
||||
message: '文件路径长度不能大于1024位'
|
||||
}]
|
||||
}],
|
||||
command: ['command', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入命令'
|
||||
}, {
|
||||
max: 1024,
|
||||
message: '命令长度不能大于1024位'
|
||||
}]
|
||||
}],
|
||||
offset: ['offset', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入文件偏移量'
|
||||
}, {
|
||||
validator: this.validateOffset
|
||||
}]
|
||||
}],
|
||||
charset: ['charset', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: '请输入文件编码'
|
||||
}, {
|
||||
max: 16,
|
||||
message: '文件编码长度不能大于16位'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AddLogFileModal',
|
||||
components: {
|
||||
MachineSelector
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
FILE_TAIL_MODE,
|
||||
id: null,
|
||||
visible: false,
|
||||
title: null,
|
||||
loading: false,
|
||||
record: null,
|
||||
updateConfig: true,
|
||||
layout,
|
||||
decorators: getDecorators.call(this),
|
||||
form: this.$form.createForm(this),
|
||||
machineQuery: { status: ENABLE_STATUS.ENABLE.value }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateOffset(rule, value, callback) {
|
||||
if (value === '') {
|
||||
callback(new Error('请输入文件偏移量'))
|
||||
} else if (parseFloat(value) !== parseInt(value)) {
|
||||
callback(new Error('请输入文件偏移量必须为整数'))
|
||||
} else {
|
||||
if (value < 0 || value > 10000) {
|
||||
callback(new Error('文件偏移量必须在0-10000之间'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
},
|
||||
setDefaultConfig(machineId) {
|
||||
if (!this.updateConfig) {
|
||||
return
|
||||
}
|
||||
if (!machineId) {
|
||||
return
|
||||
}
|
||||
this.$api.getTailConfig({ machineId })
|
||||
.then(({ data }) => {
|
||||
const config = pick(Object.assign({}, data), 'command', 'offset', 'charset')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(config)
|
||||
})
|
||||
})
|
||||
},
|
||||
add() {
|
||||
this.title = '新增日志文件'
|
||||
this.updateConfig = true
|
||||
this.initRecord({})
|
||||
},
|
||||
update(id) {
|
||||
this.title = '修改日志文件'
|
||||
this.updateConfig = false
|
||||
this.$api.getTailDetail({
|
||||
id
|
||||
}).then(({ data }) => {
|
||||
this.initRecord(data)
|
||||
})
|
||||
},
|
||||
initRecord(row) {
|
||||
this.form.resetFields()
|
||||
this.visible = true
|
||||
this.id = row.id
|
||||
this.record = pick(Object.assign({}, row), 'machineId', 'name', 'path', 'command', 'offset', 'charset')
|
||||
// 设置数据
|
||||
new Promise((resolve) => {
|
||||
// 加载数据
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue(this.record)
|
||||
})
|
||||
resolve()
|
||||
}).then(() => {
|
||||
// 设置追踪类型
|
||||
if (this.record.machineId === 1) {
|
||||
this.$nextTick(() => {
|
||||
const tailMode = row.tailMode
|
||||
this.record.tailMode = tailMode
|
||||
this.form.setFieldsValue({ tailMode })
|
||||
})
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.updateConfig = true
|
||||
})
|
||||
},
|
||||
check() {
|
||||
this.loading = true
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.submit(values)
|
||||
})
|
||||
},
|
||||
async submit(values) {
|
||||
let res
|
||||
try {
|
||||
if (!this.id) {
|
||||
// 添加
|
||||
res = await this.$api.addTailFile({
|
||||
...values
|
||||
})
|
||||
} else {
|
||||
// 修改
|
||||
res = await this.$api.updateTailFile({
|
||||
...values,
|
||||
id: this.id
|
||||
})
|
||||
}
|
||||
if (!this.id) {
|
||||
this.$message.success('添加成功')
|
||||
this.$emit('added', res.data)
|
||||
} else {
|
||||
this.$message.success('修改成功')
|
||||
this.$emit('updated', res.data)
|
||||
}
|
||||
this.close()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.$refs.machineSelector.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
99
orion-ops-vue/src/components/log/AppActionLogAppender.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="app-action-container">
|
||||
<!-- 日志 -->
|
||||
<div class="app-action-log">
|
||||
<logAppender ref="appender"
|
||||
size="default"
|
||||
:height="appenderHeight"
|
||||
:relId="id"
|
||||
:tailType="FILE_TAIL_TYPE.APP_ACTION_LOG.value"
|
||||
:downloadType="FILE_DOWNLOAD_TYPE.APP_ACTION_LOG.value">
|
||||
<!-- 左侧工具 -->
|
||||
<template #left-tools>
|
||||
<div class="action-log-tools">
|
||||
<a-tag color="#5C7CFA" v-if="detail.actionName">
|
||||
{{ detail.actionName }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
</logAppender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ACTION_STATUS, FILE_DOWNLOAD_TYPE, FILE_TAIL_TYPE } from '@/lib/enum'
|
||||
import LogAppender from '@/components/log/LogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppActionLogAppender',
|
||||
components: { LogAppender },
|
||||
props: {
|
||||
appenderHeight: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FILE_TAIL_TYPE,
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
id: null,
|
||||
pollId: null,
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.id = id
|
||||
this.$api.getAppActionDetail({
|
||||
id: this.id
|
||||
}).then(({ data }) => {
|
||||
this.detail = data
|
||||
// 设置轮询状态
|
||||
if (this.detail.status === ACTION_STATUS.WAIT.value ||
|
||||
this.detail.status === ACTION_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 2000)
|
||||
}
|
||||
}).then(() => {
|
||||
this.$nextTick(() => this.$refs.appender.openTail())
|
||||
})
|
||||
},
|
||||
close() {
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 关闭tail
|
||||
this.$nextTick(() => {
|
||||
this.$refs.appender.clear()
|
||||
this.$refs.appender.dispose()
|
||||
})
|
||||
this.id = null
|
||||
this.detail = {}
|
||||
},
|
||||
pollStatus() {
|
||||
this.$api.getAppActionStatus({
|
||||
id: this.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
// 清除状态轮询
|
||||
if (this.detail.status !== ACTION_STATUS.WAIT.value &&
|
||||
this.detail.status !== ACTION_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.action-log-tools {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:closable="false"
|
||||
:title="null"
|
||||
:footer="null"
|
||||
:dialogStyle="{top: '32px', padding: 0}"
|
||||
:bodyStyle="{padding: 0}"
|
||||
:destroyOnClose="true"
|
||||
:forceRender="true"
|
||||
@cancel="close"
|
||||
width="96%">
|
||||
<!-- 日志面板 -->
|
||||
<AppActionLogAppender ref="logger" appenderHeight="calc(100vh - 106px)"/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppActionLogAppender from '@/components/log/AppActionLogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppActionLogAppenderModal',
|
||||
components: { AppActionLogAppender },
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
setTimeout(() => {
|
||||
this.$refs.logger.open(id)
|
||||
}, 300)
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.$refs.logger.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
238
orion-ops-vue/src/components/log/AppBuildLogAppender.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="app-build-container">
|
||||
<!-- 步骤 -->
|
||||
<div class="app-build-steps">
|
||||
<a-steps :current="current" :status="stepStatus">
|
||||
<template v-for="action in detail.actions">
|
||||
<a-step :key="action.id"
|
||||
:title="action.actionName"
|
||||
:subTitle="action.used ? `${action.used}ms` : ''">
|
||||
<template v-if="action.status === ACTION_STATUS.RUNNABLE.value" #icon>
|
||||
<a-icon type="loading"/>
|
||||
</template>
|
||||
</a-step>
|
||||
</template>
|
||||
</a-steps>
|
||||
</div>
|
||||
<!-- 日志 -->
|
||||
<div class="app-build-log">
|
||||
<logAppender ref="appender"
|
||||
size="default"
|
||||
:height="appenderHeight"
|
||||
:relId="id"
|
||||
:tailType="FILE_TAIL_TYPE.APP_BUILD_LOG.value"
|
||||
:downloadType="FILE_DOWNLOAD_TYPE.APP_BUILD_LOG.value">
|
||||
<!-- 左侧工具 -->
|
||||
<template #left-tools>
|
||||
<div class="build-log-tools">
|
||||
<a-tag color="#5C7CFA" v-if="detail.seq">
|
||||
#{{ detail.seq }}
|
||||
</a-tag>
|
||||
<a-tag color="#40C057" v-if="detail.appName">
|
||||
{{ detail.appName }}
|
||||
</a-tag>
|
||||
<a-tag color="#845EF7" v-if="detail.profileName">
|
||||
{{ detail.profileName }}
|
||||
</a-tag>
|
||||
<!-- 命令输入 -->
|
||||
<a-input-search class="command-write-input"
|
||||
size="default"
|
||||
v-if="BUILD_STATUS.RUNNABLE.value === detail.status"
|
||||
v-model="command"
|
||||
placeholder="输入"
|
||||
@search="sendCommand">
|
||||
<template #enterButton>
|
||||
<a-icon type="forward"/>
|
||||
</template>
|
||||
<!-- 发送 lf -->
|
||||
<template #suffix>
|
||||
<a-icon :class="{'send-lf-trigger': true, 'send-lf-trigger-enable': sendLf}"
|
||||
title="是否发送 \n"
|
||||
type="pull-request"
|
||||
@click="() => sendLf = !sendLf"/>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<!-- 停止 -->
|
||||
<a-popconfirm v-if="BUILD_STATUS.RUNNABLE.value === detail.status"
|
||||
title="是否要停止执行?"
|
||||
placement="bottomLeft"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="terminate">
|
||||
<a-button icon="close">停止</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 下载 -->
|
||||
<div class="download-bundle-wrapper" v-if="detail.status === BUILD_STATUS.FINISH.value">
|
||||
<a-button v-if="!downloadUrl" icon="link" size="small" @click="loadDownloadUrl">获取产物链接</a-button>
|
||||
<a target="_blank" :href="downloadUrl" @click="clearDownloadUrl" v-else>
|
||||
<a-button icon="download">下载产物</a-button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</logAppender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumValueOf, ACTION_STATUS, BUILD_STATUS, FILE_DOWNLOAD_TYPE, FILE_TAIL_TYPE } from '@/lib/enum'
|
||||
import LogAppender from '@/components/log/LogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppBuildLogAppender',
|
||||
components: { LogAppender },
|
||||
props: {
|
||||
appenderHeight: String
|
||||
},
|
||||
computed: {
|
||||
stepStatus() {
|
||||
if (this.detail.status) {
|
||||
return enumValueOf(BUILD_STATUS, this.detail.status).stepStatus
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FILE_TAIL_TYPE,
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
BUILD_STATUS,
|
||||
ACTION_STATUS,
|
||||
id: null,
|
||||
current: 0,
|
||||
detail: {},
|
||||
command: null,
|
||||
sendLf: true,
|
||||
pollId: null,
|
||||
downloadUrl: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.id = id
|
||||
this.$api.getAppBuildDetail({
|
||||
id: this.id
|
||||
}).then(({ data }) => {
|
||||
this.detail = data
|
||||
this.setStepsCurrent()
|
||||
// 设置轮询状态
|
||||
if (this.detail.status === BUILD_STATUS.WAIT.value ||
|
||||
this.detail.status === BUILD_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 2000)
|
||||
}
|
||||
}).then(() => {
|
||||
this.$nextTick(() => this.$refs.appender.openTail())
|
||||
})
|
||||
},
|
||||
close() {
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 关闭tail
|
||||
this.$nextTick(() => {
|
||||
this.$refs.appender.clear()
|
||||
this.$refs.appender.dispose()
|
||||
})
|
||||
this.id = null
|
||||
this.current = 0
|
||||
this.detail = {}
|
||||
this.downloadUrl = null
|
||||
},
|
||||
pollStatus() {
|
||||
this.$api.getAppBuildStatus({
|
||||
id: this.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
// 清除状态轮询
|
||||
if (this.detail.status !== BUILD_STATUS.WAIT.value &&
|
||||
this.detail.status !== BUILD_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 设置action
|
||||
if (!data.actions || !data.actions.length || !this.detail.actions || !this.detail.actions.length) {
|
||||
return
|
||||
}
|
||||
for (const actionStatus of data.actions) {
|
||||
this.detail.actions.filter(s => s.id === actionStatus.id).forEach(e => {
|
||||
e.status = actionStatus.status
|
||||
e.used = actionStatus.used
|
||||
})
|
||||
}
|
||||
// 设置当前操作
|
||||
this.setStepsCurrent()
|
||||
})
|
||||
},
|
||||
terminate() {
|
||||
this.$api.terminateAppBuild({
|
||||
id: this.detail.id
|
||||
}).then(() => {
|
||||
this.$message.success('已停止')
|
||||
})
|
||||
},
|
||||
sendCommand() {
|
||||
let command = this.command || ''
|
||||
if (this.sendLf) {
|
||||
command += '\n'
|
||||
}
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
this.command = ''
|
||||
this.$api.writeAppBuild({
|
||||
id: this.detail.id,
|
||||
command
|
||||
})
|
||||
},
|
||||
setStepsCurrent() {
|
||||
const len = this.detail.actions.length
|
||||
let curr = len - 1
|
||||
for (let i = 0; i < len; i++) {
|
||||
const status = this.detail.actions[i].status
|
||||
if (status !== ACTION_STATUS.FINISH.value) {
|
||||
curr = i
|
||||
break
|
||||
}
|
||||
}
|
||||
this.current = curr
|
||||
},
|
||||
async loadDownloadUrl() {
|
||||
try {
|
||||
const downloadUrl = await this.$api.getFileDownloadToken({
|
||||
type: FILE_DOWNLOAD_TYPE.APP_BUILD_BUNDLE.value,
|
||||
id: this.id
|
||||
})
|
||||
this.downloadUrl = this.$api.fileDownloadExec({ token: downloadUrl.data })
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
clearDownloadUrl() {
|
||||
setTimeout(() => {
|
||||
this.downloadUrl = null
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-build-steps {
|
||||
height: 46px;
|
||||
padding: 8px 12px 0 12px;
|
||||
}
|
||||
|
||||
.build-log-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:closable="false"
|
||||
:title="null"
|
||||
:footer="null"
|
||||
:dialogStyle="{top: '16px', padding: 0}"
|
||||
:bodyStyle="{padding: 0}"
|
||||
:destroyOnClose="true"
|
||||
:forceRender="true"
|
||||
@cancel="close"
|
||||
width="96%">
|
||||
<!-- 日志面板 -->
|
||||
<AppBuildLogAppender ref="logger" appenderHeight="calc(100vh - 122px)"/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppBuildLogAppender from '@/components/log/AppBuildLogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppBuildLogAppenderModal',
|
||||
components: { AppBuildLogAppender },
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
setTimeout(() => {
|
||||
this.$refs.logger.open(id)
|
||||
}, 300)
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.$refs.logger.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
335
orion-ops-vue/src/components/log/AppReleaseLogAppender.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="app-release-container" :style="{height}">
|
||||
<!-- 菜单 -->
|
||||
<div class="release-machines-menu gray-box-shadow">
|
||||
<a-menu mode="inline" v-model="selectedKeys">
|
||||
<a-menu-item v-for="machine in record.machines"
|
||||
:key="machine.id"
|
||||
:title="machine.machineName"
|
||||
@click="chooseMachineLog(machine.id)">
|
||||
<div class="menu-item-machine-wrapper">
|
||||
<!-- 机器名称 -->
|
||||
<span class="menu-item-machine-name auto-ellipsis-item">{{ machine.machineName }}</span>
|
||||
<!-- 状态 -->
|
||||
<span class="menu-item-machine-status">
|
||||
<a-tag :color="machine.status | formatActionStatus('color')">
|
||||
{{ machine.status | formatActionStatus('label') }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
<!-- 机器 -->
|
||||
<div class="release-machine-container gray-box-shadow">
|
||||
<div class="release-machine"
|
||||
v-for="machine in record.machines"
|
||||
v-show="machine.id === selectedKeys[0]"
|
||||
:key="machine.id">
|
||||
<!-- 进度 -->
|
||||
<div class="machine-steps">
|
||||
<a-steps :current="current[machine.id]"
|
||||
:status="machine.status | formatActionStatus('stepStatus')">
|
||||
<template v-for="action in machine.actions">
|
||||
<a-step :key="action.id"
|
||||
:title="action.actionName"
|
||||
:subTitle="action.used ? `${action.used}ms` : ''">
|
||||
<template v-if="action.status === ACTION_STATUS.RUNNABLE.value" #icon>
|
||||
<a-icon type="loading"/>
|
||||
</template>
|
||||
</a-step>
|
||||
</template>
|
||||
</a-steps>
|
||||
</div>
|
||||
<!-- 日志 -->
|
||||
<div class="machine-logger-appender">
|
||||
<LogAppender :ref="`appender-${machine.id}`"
|
||||
size="default"
|
||||
:height="appenderHeight"
|
||||
:relId="machine.id"
|
||||
:tailType="FILE_TAIL_TYPE.APP_RELEASE_LOG.value"
|
||||
:downloadType="FILE_DOWNLOAD_TYPE.APP_RELEASE_MACHINE_LOG.value">
|
||||
<template #left-tools>
|
||||
<!-- 命令输入 -->
|
||||
<a-input-search class="command-write-input"
|
||||
size="default"
|
||||
v-if="ACTION_STATUS.RUNNABLE.value === machine.status"
|
||||
v-model="command"
|
||||
placeholder="输入"
|
||||
@search="sendCommand(machine.id)">
|
||||
<template #enterButton>
|
||||
<a-icon type="forward"/>
|
||||
</template>
|
||||
<!-- 发送 lf -->
|
||||
<template #suffix>
|
||||
<a-icon :class="{'send-lf-trigger': true, 'send-lf-trigger-enable': sendLf}"
|
||||
title="是否发送 \n"
|
||||
type="pull-request"
|
||||
@click="() => sendLf = !sendLf"/>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<!-- 停止 -->
|
||||
<a-popconfirm v-if="ACTION_STATUS.RUNNABLE.value === machine.status"
|
||||
title="是否要停止执行?"
|
||||
placement="bottomLeft"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="terminateMachine(machine.id)">
|
||||
<a-button icon="close">停止</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 跳过 -->
|
||||
<a-popconfirm v-if="ACTION_STATUS.WAIT.value === machine.status"
|
||||
title="是否要跳过执行?"
|
||||
placement="bottomLeft"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="skipMachine(machine.id)">
|
||||
<a-button icon="stop">跳过</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</LogAppender>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumValueOf, ACTION_STATUS, FILE_DOWNLOAD_TYPE, FILE_TAIL_TYPE, RELEASE_STATUS } from '@/lib/enum'
|
||||
import LogAppender from '@/components/log/LogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseLogAppender',
|
||||
components: { LogAppender },
|
||||
props: {
|
||||
appenderHeight: String,
|
||||
height: String
|
||||
},
|
||||
computed: {},
|
||||
data() {
|
||||
return {
|
||||
FILE_TAIL_TYPE,
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
ACTION_STATUS,
|
||||
id: null,
|
||||
loading: false,
|
||||
record: {},
|
||||
command: null,
|
||||
sendLf: true,
|
||||
pollId: null,
|
||||
selectedKeys: [],
|
||||
openedFiles: [],
|
||||
current: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open(id) {
|
||||
this.id = id
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 加载明细
|
||||
this.loading = true
|
||||
await this.$api.getAppReleaseDetail({
|
||||
id: id,
|
||||
queryAction: 1
|
||||
}).then(({ data }) => {
|
||||
// 设置数据
|
||||
this.record = data
|
||||
this.selectedKeys[0] = data.machines[0].id
|
||||
this.loading = false
|
||||
this.setStepsCurrent()
|
||||
}).then(() => {
|
||||
// 打开日志
|
||||
for (const machine of this.record.machines) {
|
||||
if (machine.status !== ACTION_STATUS.WAIT.value &&
|
||||
machine.status !== ACTION_STATUS.SKIPPED.value) {
|
||||
this.$refs[`appender-${machine.id}`][0].openTail()
|
||||
this.openedFiles.push(machine.id)
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
// 设置轮询
|
||||
if (this.record.status === RELEASE_STATUS.RUNNABLE.value) {
|
||||
// 轮询状态
|
||||
this.pollId = setInterval(this.pollStatus, 2000)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
close() {
|
||||
if (!this.record.machines || !this.record.machines.length) {
|
||||
return
|
||||
}
|
||||
// 关闭tail
|
||||
for (const machine of this.record.machines) {
|
||||
const appender = this.$refs[`appender-${machine.id}`][0]
|
||||
appender.clear()
|
||||
appender.dispose()
|
||||
}
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
this.id = null
|
||||
this.record = {}
|
||||
this.selectedKeys = []
|
||||
this.openedFiles = []
|
||||
this.current = {}
|
||||
},
|
||||
chooseMachineLog(id) {
|
||||
setTimeout(() => {
|
||||
this.$nextTick(() => this.$refs[`appender-${id}`][0].fitTerminal())
|
||||
}, 100)
|
||||
},
|
||||
terminateMachine(releaseMachineId) {
|
||||
this.$api.terminateAppReleaseMachine({
|
||||
releaseMachineId: releaseMachineId
|
||||
}).then(() => {
|
||||
this.$message.success('已停止')
|
||||
})
|
||||
},
|
||||
skipMachine(releaseMachineId) {
|
||||
this.$api.skipAppReleaseMachine({
|
||||
releaseMachineId: releaseMachineId
|
||||
}).then(() => {
|
||||
this.$message.success('已跳过')
|
||||
})
|
||||
},
|
||||
sendCommand(releaseMachineId) {
|
||||
let command = this.command || ''
|
||||
if (this.sendLf) {
|
||||
command += '\n'
|
||||
}
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
this.command = ''
|
||||
this.$api.writeAppReleaseMachine({
|
||||
releaseMachineId: releaseMachineId,
|
||||
command
|
||||
})
|
||||
},
|
||||
async pollStatus() {
|
||||
if (!this.record.machines || !this.record.machines.length) {
|
||||
return
|
||||
}
|
||||
const pollId = this.record.machines.map(s => s.id)
|
||||
this.$api.getAppReleaseMachineListStatus({
|
||||
releaseMachineIdList: pollId
|
||||
}).then(({ data }) => {
|
||||
if (!data || !data.length) {
|
||||
return
|
||||
}
|
||||
const notFinish = data.map(s => s.status).filter(s => s === ACTION_STATUS.WAIT.value ||
|
||||
s === ACTION_STATUS.RUNNABLE.value)
|
||||
// 清除状态轮询
|
||||
if (notFinish.length === 0) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 设置机器状态
|
||||
for (const machineStatus of data) {
|
||||
this.record.machines.filter(s => s.id === machineStatus.id).forEach(s => {
|
||||
s.status = machineStatus.status
|
||||
// 检查打开tail
|
||||
const opened = this.openedFiles.filter(e => e === s.id).length > 0
|
||||
if (!opened && s.status !== ACTION_STATUS.WAIT.value &&
|
||||
s.status !== ACTION_STATUS.SKIPPED.value) {
|
||||
// 打开日志
|
||||
this.$refs[`appender-${s.id}`][0].openTail()
|
||||
this.openedFiles.push(s.id)
|
||||
}
|
||||
// 设置action
|
||||
if (!machineStatus.actions || !machineStatus.actions.length || !s.actions || !s.actions.length) {
|
||||
return
|
||||
}
|
||||
for (const actionStatus of machineStatus.actions) {
|
||||
s.actions.filter(a => a.id === actionStatus.id).forEach(e => {
|
||||
e.status = actionStatus.status
|
||||
e.used = actionStatus.used
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// 设置当前操作
|
||||
this.setStepsCurrent()
|
||||
})
|
||||
},
|
||||
setStepsCurrent() {
|
||||
if (!this.record.machines || !this.record.machines.length) {
|
||||
return
|
||||
}
|
||||
for (const machine of this.record.machines) {
|
||||
const len = machine.actions.length
|
||||
let curr = len - 1
|
||||
for (let i = 0; i < len; i++) {
|
||||
const status = machine.actions[i].status
|
||||
if (status !== ACTION_STATUS.FINISH.value) {
|
||||
curr = i
|
||||
break
|
||||
}
|
||||
}
|
||||
this.current[machine.id] = curr
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatActionStatus(status, f) {
|
||||
return enumValueOf(ACTION_STATUS, status)[f]
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.app-release-container {
|
||||
display: flex;
|
||||
background: #F0F2F5;
|
||||
|
||||
.release-machines-menu {
|
||||
width: 240px;
|
||||
margin: 16px;
|
||||
padding: 8px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 2px;
|
||||
|
||||
::v-deep .ant-menu-item {
|
||||
padding: 0 0 0 12px !important;
|
||||
}
|
||||
|
||||
.menu-item-machine-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.menu-item-machine-name {
|
||||
width: 147px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.release-machine-container {
|
||||
width: calc(100% - 287px);
|
||||
margin: 16px 16px 16px 0;
|
||||
background: #FFF;
|
||||
border-radius: 2px;
|
||||
|
||||
.machine-steps {
|
||||
height: 46px;
|
||||
padding: 8px 12px 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:closable="false"
|
||||
:title="null"
|
||||
:footer="null"
|
||||
:dialogStyle="{top: '16px', padding: 0}"
|
||||
:bodyStyle="{padding: 0}"
|
||||
:destroyOnClose="true"
|
||||
:forceRender="true"
|
||||
@cancel="close"
|
||||
width="96%">
|
||||
<!-- 日志面板 -->
|
||||
<AppReleaseLogAppender ref="logger" appenderHeight="calc(100vh - 152px)" height="calc(100vh - 34px)"/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppReleaseLogAppender from '@/components/log/AppReleaseLogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseLogAppenderModal',
|
||||
components: {
|
||||
AppReleaseLogAppender
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
setTimeout(() => {
|
||||
this.$refs.logger.open(id)
|
||||
}, 300)
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.$refs.logger.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="app-release-container">
|
||||
<!-- 步骤 -->
|
||||
<div class="app-release-steps">
|
||||
<a-steps :current="current" :status="detail.status | formatActionStatus('stepStatus')">
|
||||
<template v-for="action in detail.actions">
|
||||
<a-step :key="action.id"
|
||||
:title="action.actionName"
|
||||
:subTitle="action.used ? `${action.used}ms` : ''">
|
||||
<template v-if="action.status === ACTION_STATUS.RUNNABLE.value" #icon>
|
||||
<a-icon type="loading"/>
|
||||
</template>
|
||||
</a-step>
|
||||
</template>
|
||||
</a-steps>
|
||||
</div>
|
||||
<!-- 日志 -->
|
||||
<div class="machine-release-log">
|
||||
<logAppender ref="appender"
|
||||
size="default"
|
||||
:height="appenderHeight"
|
||||
:relId="id"
|
||||
:tailType="FILE_TAIL_TYPE.APP_RELEASE_LOG.value"
|
||||
:downloadType="FILE_DOWNLOAD_TYPE.APP_RELEASE_MACHINE_LOG.value">
|
||||
<!-- 左侧工具 -->
|
||||
<template #left-tools>
|
||||
<div class="machine-log-tools">
|
||||
<a-tag color="#5C7CFA" v-if="detail.machineName">
|
||||
{{ detail.machineName }}
|
||||
</a-tag>
|
||||
<a-tag color="#40C057" v-if="detail.machineHost">
|
||||
{{ detail.machineHost }}
|
||||
</a-tag>
|
||||
<!-- 命令输入 -->
|
||||
<a-input-search class="command-write-input"
|
||||
size="default"
|
||||
v-if="ACTION_STATUS.RUNNABLE.value === detail.status"
|
||||
v-model="command"
|
||||
placeholder="输入"
|
||||
@search="sendCommand">
|
||||
<template #enterButton>
|
||||
<a-icon type="forward"/>
|
||||
</template>
|
||||
<!-- 发送 lf -->
|
||||
<template #suffix>
|
||||
<a-icon :class="{'send-lf-trigger': true, 'send-lf-trigger-enable': sendLf}"
|
||||
title="是否发送 \n"
|
||||
type="pull-request"
|
||||
@click="() => sendLf = !sendLf"/>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<!-- 停止 -->
|
||||
<a-popconfirm v-if="ACTION_STATUS.RUNNABLE.value === detail.status"
|
||||
title="是否要停止执行?"
|
||||
placement="bottomLeft"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="terminateMachine">
|
||||
<a-button icon="close">停止</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</logAppender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumValueOf, ACTION_STATUS, FILE_DOWNLOAD_TYPE, FILE_TAIL_TYPE } from '@/lib/enum'
|
||||
import LogAppender from '@/components/log/LogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseMachineLogAppender',
|
||||
components: { LogAppender },
|
||||
props: {
|
||||
appenderHeight: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FILE_TAIL_TYPE,
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
ACTION_STATUS,
|
||||
id: null,
|
||||
current: 0,
|
||||
detail: {},
|
||||
command: null,
|
||||
sendLf: true,
|
||||
pollId: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.id = id
|
||||
this.$api.getAppReleaseMachineDetail({
|
||||
releaseMachineId: this.id
|
||||
}).then(({ data }) => {
|
||||
this.detail = data
|
||||
this.setStepsCurrent()
|
||||
// 设置轮询状态
|
||||
if (this.detail.status === ACTION_STATUS.WAIT.value ||
|
||||
this.detail.status === ACTION_STATUS.RUNNABLE.value) {
|
||||
this.pollId = setInterval(this.pollStatus, 2000)
|
||||
}
|
||||
}).then(() => {
|
||||
this.$nextTick(() => this.$refs.appender.openTail())
|
||||
})
|
||||
},
|
||||
close() {
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 关闭tail
|
||||
this.$nextTick(() => {
|
||||
this.$refs.appender.clear()
|
||||
this.$refs.appender.dispose()
|
||||
})
|
||||
this.id = null
|
||||
this.current = 0
|
||||
this.detail = {}
|
||||
},
|
||||
terminateMachine() {
|
||||
this.$api.terminateAppReleaseMachine({
|
||||
releaseMachineId: this.detail.id
|
||||
}).then(() => {
|
||||
this.$message.success('已停止')
|
||||
})
|
||||
},
|
||||
sendCommand() {
|
||||
let command = this.command || ''
|
||||
if (this.sendLf) {
|
||||
command += '\n'
|
||||
}
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
this.command = ''
|
||||
this.$api.writeAppReleaseMachine({
|
||||
releaseMachineId: this.detail.id,
|
||||
command
|
||||
})
|
||||
},
|
||||
pollStatus() {
|
||||
this.$api.getAppReleaseMachineStatus({
|
||||
releaseMachineId: this.id
|
||||
}).then(({ data }) => {
|
||||
this.detail.status = data.status
|
||||
// 清除状态轮询
|
||||
if (this.detail.status !== ACTION_STATUS.WAIT.value &&
|
||||
this.detail.status !== ACTION_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 设置action
|
||||
if (!data.actions || !data.actions.length || !this.detail.actions || !this.detail.actions.length) {
|
||||
return
|
||||
}
|
||||
for (const actionStatus of data.actions) {
|
||||
this.detail.actions.filter(s => s.id === actionStatus.id).forEach(e => {
|
||||
e.status = actionStatus.status
|
||||
e.used = actionStatus.used
|
||||
})
|
||||
}
|
||||
// 设置当前操作
|
||||
this.setStepsCurrent()
|
||||
})
|
||||
},
|
||||
setStepsCurrent() {
|
||||
const len = this.detail.actions.length
|
||||
let curr = len - 1
|
||||
for (let i = 0; i < len; i++) {
|
||||
const status = this.detail.actions[i].status
|
||||
if (status !== ACTION_STATUS.FINISH.value) {
|
||||
curr = i
|
||||
break
|
||||
}
|
||||
}
|
||||
this.current = curr
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatActionStatus(status, f) {
|
||||
return enumValueOf(ACTION_STATUS, status)[f]
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-release-steps {
|
||||
height: 46px;
|
||||
padding: 8px 12px 0 12px;
|
||||
}
|
||||
|
||||
.machine-log-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:closable="false"
|
||||
:title="null"
|
||||
:footer="null"
|
||||
:dialogStyle="{top: '16px', padding: 0}"
|
||||
:bodyStyle="{padding: 0}"
|
||||
:destroyOnClose="true"
|
||||
:forceRender="true"
|
||||
@cancel="close"
|
||||
width="96%">
|
||||
<!-- 日志面板 -->
|
||||
<AppReleaseMachineLogAppender ref="logger" appenderHeight="calc(100vh - 122px)"/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import AppReleaseMachineLogAppender from '@/components/log/AppReleaseMachineLogAppender'
|
||||
|
||||
export default {
|
||||
name: 'AppReleaseMachineLogAppenderModal',
|
||||
components: { AppReleaseMachineLogAppender },
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
setTimeout(() => {
|
||||
this.$refs.logger.open(id)
|
||||
}, 300)
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.$refs.logger.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
181
orion-ops-vue/src/components/log/ExecLoggerAppender.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="exec-logger-appender">
|
||||
<LogAppender ref="appender"
|
||||
size="default"
|
||||
:height="appenderHeight"
|
||||
:relId="execId"
|
||||
:tailType="FILE_TAIL_TYPE.EXEC_LOG.value"
|
||||
:downloadType="FILE_DOWNLOAD_TYPE.EXEC_LOG.value">
|
||||
<!-- 左侧工具栏 -->
|
||||
<template #left-tools>
|
||||
<div class="appender-left-tools">
|
||||
<!-- 状态 -->
|
||||
<a-tag class="machine-exec-status" v-if="status" :color="status | formatExecStatus('color')">
|
||||
{{ status | formatExecStatus('label') }}
|
||||
</a-tag>
|
||||
<!-- used -->
|
||||
<span class="mx8" title="用时" v-if="BATCH_EXEC_STATUS.COMPLETE.value === status && keepTime">
|
||||
{{ `${keepTime} (${used}ms)` }}
|
||||
</span>
|
||||
<!-- exitCode -->
|
||||
<span class="mx8" title="退出码"
|
||||
v-if="exitCode !== null"
|
||||
:style="{'color': exitCode === 0 ? '#4263EB' : '#E03131'}">
|
||||
{{ exitCode }}
|
||||
</span>
|
||||
<!-- 命令输入 -->
|
||||
<a-input-search class="command-write-input"
|
||||
size="default"
|
||||
v-if="BATCH_EXEC_STATUS.RUNNABLE.value === status"
|
||||
v-model="command"
|
||||
placeholder="输入"
|
||||
@search="sendCommand">
|
||||
<template #enterButton>
|
||||
<a-icon type="forward"/>
|
||||
</template>
|
||||
<!-- 发送 lf -->
|
||||
<template #suffix>
|
||||
<a-icon :class="{'send-lf-trigger': true, 'send-lf-trigger-enable': sendLf}"
|
||||
title="是否发送 \n"
|
||||
type="pull-request"
|
||||
@click="() => sendLf = !sendLf"/>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<!-- 停止 -->
|
||||
<a-popconfirm v-if="BATCH_EXEC_STATUS.RUNNABLE.value === status"
|
||||
title="是否要停止执行?"
|
||||
placement="bottomLeft"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="terminate">
|
||||
<a-button icon="close">停止</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</LogAppender>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enumValueOf, BATCH_EXEC_STATUS, FILE_DOWNLOAD_TYPE, FILE_TAIL_TYPE } from '@/lib/enum'
|
||||
import LogAppender from './LogAppender'
|
||||
|
||||
export default {
|
||||
name: 'ExecLoggerAppender',
|
||||
components: {
|
||||
LogAppender
|
||||
},
|
||||
props: {
|
||||
appenderHeight: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FILE_TAIL_TYPE,
|
||||
FILE_DOWNLOAD_TYPE,
|
||||
BATCH_EXEC_STATUS,
|
||||
status: null,
|
||||
execId: null,
|
||||
command: null,
|
||||
sendLf: true,
|
||||
keepTime: null,
|
||||
used: null,
|
||||
exitCode: null,
|
||||
pollId: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open(execId) {
|
||||
this.execId = execId
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
// 打开日志
|
||||
this.$nextTick(() => this.$refs.appender.openTail())
|
||||
// 获取状态
|
||||
await this.getStatus()
|
||||
// 检查轮询状态
|
||||
if (this.status === BATCH_EXEC_STATUS.WAITING.value ||
|
||||
this.status === BATCH_EXEC_STATUS.RUNNABLE.value) {
|
||||
// 轮询状态
|
||||
this.pollId = setInterval(this.pollStatus, 2000)
|
||||
}
|
||||
},
|
||||
close() {
|
||||
// 关闭tail
|
||||
this.$refs.appender.clear()
|
||||
this.$refs.appender.dispose()
|
||||
// 关闭轮询
|
||||
if (this.pollId) {
|
||||
clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
this.status = null
|
||||
this.execId = null
|
||||
this.command = null
|
||||
this.keepTime = null
|
||||
this.used = null
|
||||
this.exitCode = null
|
||||
},
|
||||
sendCommand() {
|
||||
let command = this.command || ''
|
||||
if (this.sendLf) {
|
||||
command += '\n'
|
||||
}
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
this.command = ''
|
||||
this.$api.writeExecTask({
|
||||
id: this.execId,
|
||||
command
|
||||
})
|
||||
},
|
||||
terminate() {
|
||||
this.status = BATCH_EXEC_STATUS.TERMINATED.value
|
||||
this.$api.terminateExecTask({
|
||||
id: this.execId
|
||||
}).then(() => {
|
||||
this.$message.success('已停止')
|
||||
})
|
||||
},
|
||||
async getStatus() {
|
||||
await this.$api.getExecTaskStatus({
|
||||
idList: [this.execId]
|
||||
}).then(({ data }) => {
|
||||
if (data && data.length) {
|
||||
const status = data[0]
|
||||
this.status = status.status
|
||||
this.exitCode = status.exitCode
|
||||
this.used = status.used
|
||||
this.keepTime = status.keepTime
|
||||
}
|
||||
})
|
||||
},
|
||||
async pollStatus() {
|
||||
await this.getStatus()
|
||||
if (this.status !== BATCH_EXEC_STATUS.WAITING.value &&
|
||||
this.status !== BATCH_EXEC_STATUS.RUNNABLE.value) {
|
||||
clearInterval(this.pollId)
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatExecStatus(status, f) {
|
||||
return enumValueOf(BATCH_EXEC_STATUS, status)[f]
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.pollId !== null && clearInterval(this.pollId)
|
||||
this.pollId = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.appender-left-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
50
orion-ops-vue/src/components/log/ExecLoggerAppenderModal.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
:closable="false"
|
||||
:title="null"
|
||||
:footer="null"
|
||||
:dialogStyle="{top: '32px', padding: 0}"
|
||||
:bodyStyle="{padding: 0}"
|
||||
:destroyOnClose="true"
|
||||
:forceRender="true"
|
||||
@cancel="close"
|
||||
width="96%">
|
||||
<!-- 日志面板 -->
|
||||
<ExecLoggerAppender ref="logger" appenderHeight="calc(100vh - 106px)"/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import ExecLoggerAppender from './ExecLoggerAppender'
|
||||
|
||||
export default {
|
||||
name: 'ExecLoggerAppenderModal',
|
||||
components: {
|
||||
ExecLoggerAppender
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.visible = true
|
||||
setTimeout(() => {
|
||||
this.$refs.logger.open(id)
|
||||
}, 300)
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.$nextTick(() => {
|
||||
this.$refs.logger.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
98
orion-ops-vue/src/components/log/FileAnsiCleanModal.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<a-modal v-model="visible"
|
||||
title="清除文件 ASNI 码"
|
||||
okText="清除"
|
||||
:width="400"
|
||||
:dialogStyle="{top: '64px', padding: 0}"
|
||||
:bodyStyle="{padding: '16px'}"
|
||||
:okButtonProps="{props: {disabled: loading}}"
|
||||
:maskClosable="false"
|
||||
:destroyOnClose="true"
|
||||
@ok="clean"
|
||||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<!-- 提示 -->
|
||||
<a-alert message="清除日志文件的 ANSI 着色码, 恢复为普通的日志文件"/>
|
||||
<!-- 文件上传拖拽框 -->
|
||||
<div class="upload-event-trigger mt8">
|
||||
<a-upload-dragger class="upload-drag"
|
||||
accept=".log,.txt"
|
||||
:beforeUpload="selectFile"
|
||||
:multiple="true"
|
||||
:fileList="fileList"
|
||||
:showUploadList="true">
|
||||
<p id="upload-trigger-icon" class="ant-upload-drag-icon">
|
||||
<a-icon type="inbox"/>
|
||||
</p>
|
||||
<p class="ant-upload-text">单击或拖动文件到此区域</p>
|
||||
</a-upload-dragger>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadFile } from '@/lib/utils'
|
||||
|
||||
export default {
|
||||
name: 'FileAnsiCleanModal',
|
||||
data: function() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
fileList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.visible = true
|
||||
},
|
||||
selectFile(e) {
|
||||
this.fileList.push(e)
|
||||
return false
|
||||
},
|
||||
removeFile(e) {
|
||||
for (let i = 0; i < this.fileList.length; i++) {
|
||||
if (this.fileList[i] === e) {
|
||||
this.fileList.splice(i, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
async clean() {
|
||||
if (!this.fileList.length) {
|
||||
this.$message.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
for (const file of this.fileList) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
this.$message.info(`开始处理 ${file.name}`)
|
||||
await this.$api.cleanFileAnsiCode(formData).then((e) => {
|
||||
this.$message.success(`${file.name} 处理完成, 片刻后自动下载`)
|
||||
downloadFile(e, file.name)
|
||||
this.fileList.splice(0, 1)
|
||||
}).catch(() => {
|
||||
this.$message.error(`${file.name} 处理失败`)
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
close() {
|
||||
this.visible = false
|
||||
this.loading = false
|
||||
this.fileList = []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#upload-trigger-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::v-deep .upload-drag .ant-upload span {
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
389
orion-ops-vue/src/components/log/LogAppender.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="logger-view-container-wrapper">
|
||||
<!-- 日志面板 -->
|
||||
<div class="logger-view-container">
|
||||
<!-- 工具 -->
|
||||
<div class="log-tools" v-if="toolsProps.visible !== false">
|
||||
<!-- 左侧工具 -->
|
||||
<div class="log-tools-fixed-left">
|
||||
<slot name="left-tools" v-if="toolsProps.visibleLeft !== false"/>
|
||||
</div>
|
||||
<!-- 右侧工具 -->
|
||||
<div class="log-tools-fixed-right" v-if="toolsProps.visibleRight !== false">
|
||||
<!-- 复制 -->
|
||||
<a-button class="mr8"
|
||||
v-if="rightToolsProps.copy !== false"
|
||||
:size="size"
|
||||
type="primary"
|
||||
icon="copy"
|
||||
@click="copy">
|
||||
复制
|
||||
</a-button>
|
||||
<!-- 清空 -->
|
||||
<a-button class="mr8"
|
||||
v-if="rightToolsProps.clean !== false"
|
||||
:size="size"
|
||||
type="default"
|
||||
icon="delete"
|
||||
@click="clear">
|
||||
清空
|
||||
</a-button>
|
||||
<!-- 下载 -->
|
||||
<div class="log-download-wrapper" v-if="rightToolsProps.download !== false">
|
||||
<a-button :size="size" v-if="!downloadUrl" type="default" icon="link" @click="loadDownloadUrl">获取下载链接</a-button>
|
||||
<a target="_blank" :href="downloadUrl" @click="clearDownloadUrl" v-else>
|
||||
<a-button :size="size" type="default" icon="download">下载</a-button>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 固定日志 -->
|
||||
<div class="log-fixed-wrapper nowrap" v-if="rightToolsProps.fixed !== false">
|
||||
<span class="log-fixed-label normal-label ml8 usn">固定</span>
|
||||
<a-switch class="log-fixed-switch" v-model="fixedLog" :size="size"/>
|
||||
</div>
|
||||
<!-- 状态 -->
|
||||
<div class="log-status-wrapper nowrap" v-if="rightToolsProps.status !== false">
|
||||
<!-- 状态 执行中 -->
|
||||
<template v-if="LOG_TAIL_STATUS.RUNNABLE.value === status">
|
||||
<a-popconfirm title="确认关闭当前日志连接?"
|
||||
placement="topRight"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="close">
|
||||
<a-badge class="pointer usn"
|
||||
:status="LOG_TAIL_STATUS.RUNNABLE.status"
|
||||
:text="LOG_TAIL_STATUS.RUNNABLE.label"/>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<!-- 状态 其他 -->
|
||||
<template v-else>
|
||||
<a-badge class="usn"
|
||||
:status="status | formatLogStatus('status')"
|
||||
:text="status | formatLogStatus('label')"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 日志容器 -->
|
||||
<div class="log-container" :style="{height}" ref="logContainer">
|
||||
<!-- 右键菜单 -->
|
||||
<a-dropdown v-model="visibleRightMenu" :trigger="['contextmenu']">
|
||||
<!-- 日志终端 -->
|
||||
<div class="log-terminal" ref="logTerminal" @click="clickTerminal"/>
|
||||
<!-- 下拉菜单 -->
|
||||
<template #overlay>
|
||||
<a-menu @click="clickRightMenu">
|
||||
<a-menu-item key="copy">
|
||||
<span class="right-menu-item"><a-icon type="copy"/>复制</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="selectAll">
|
||||
<span class="right-menu-item"><a-icon type="profile"/>全选</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clear">
|
||||
<span class="right-menu-item"><a-icon type="stop"/>清空</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="toTop">
|
||||
<span class="right-menu-item"><a-icon type="vertical-align-top"/>去顶部</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="toBottom">
|
||||
<span class="right-menu-item"><a-icon type="vertical-align-bottom"/>去底部</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 搜索框 -->
|
||||
<TerminalSearch ref="search" :searchPlugin="plugin.search"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import { SearchAddon } from 'xterm-addon-search'
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links'
|
||||
import { enumValueOf, LOG_TAIL_STATUS } from '@/lib/enum'
|
||||
|
||||
import 'xterm/css/xterm.css'
|
||||
import TerminalSearch from '@/components/terminal/TerminalSearch'
|
||||
|
||||
/**
|
||||
* 右键菜单操作
|
||||
*/
|
||||
const rightMenuHandler = {
|
||||
selectAll() {
|
||||
this.term.selectAll()
|
||||
},
|
||||
copy() {
|
||||
this.copySelection(false)
|
||||
},
|
||||
clear() {
|
||||
this.clear()
|
||||
},
|
||||
toTop() {
|
||||
this.term.scrollToTop()
|
||||
},
|
||||
toBottom() {
|
||||
this.term.scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LogAppender',
|
||||
components: { TerminalSearch },
|
||||
props: {
|
||||
config: Object,
|
||||
height: String,
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small'
|
||||
},
|
||||
relId: Number,
|
||||
tailType: Number,
|
||||
downloadType: Number,
|
||||
toolsProps: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
visible: true,
|
||||
visibleLeft: true,
|
||||
visibleRight: true
|
||||
}
|
||||
}
|
||||
},
|
||||
rightToolsProps: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
copy: true,
|
||||
clean: true,
|
||||
fixed: true,
|
||||
download: true,
|
||||
status: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
LOG_TAIL_STATUS,
|
||||
client: null,
|
||||
fixedLog: true,
|
||||
status: LOG_TAIL_STATUS.WAITING.value,
|
||||
downloadUrl: null,
|
||||
visibleRightMenu: false,
|
||||
term: null,
|
||||
termConfig: {
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true,
|
||||
cursorStyle: 'bar',
|
||||
cursorBlink: false,
|
||||
fastScrollModifier: 'shift',
|
||||
fontSize: 13,
|
||||
rendererType: 'canvas',
|
||||
fontFamily: 'courier-new, courier, monospace',
|
||||
lineHeight: 1.08,
|
||||
convertEol: true,
|
||||
theme: {
|
||||
foreground: '#FFFFFF',
|
||||
background: '#212529'
|
||||
}
|
||||
},
|
||||
plugin: {
|
||||
fit: null,
|
||||
search: null,
|
||||
links: null
|
||||
},
|
||||
token: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openTail() {
|
||||
this.$api.getTailToken({
|
||||
type: this.tailType,
|
||||
relId: this.relId
|
||||
}).then(({ data }) => {
|
||||
this.token = data
|
||||
this.initLogTailView(data)
|
||||
})
|
||||
},
|
||||
initLogTailView(data) {
|
||||
this.$nextTick(() => {
|
||||
// 打开日志模块
|
||||
this.term = new Terminal({ ...this.termConfig })
|
||||
this.term.open(this.$refs.logTerminal)
|
||||
// 注册自适应组件
|
||||
this.plugin.fit = new FitAddon()
|
||||
this.term.loadAddon(this.plugin.fit)
|
||||
// 注册搜索组件
|
||||
this.plugin.search = new SearchAddon()
|
||||
this.term.loadAddon(this.plugin.search)
|
||||
// 注册 url link组件
|
||||
this.plugin.links = new WebLinksAddon()
|
||||
this.term.loadAddon(this.plugin.links)
|
||||
// 注册自适应监听器
|
||||
window.addEventListener('resize', this.fitTerminal)
|
||||
// 注册快捷键
|
||||
this.term.attachCustomKeyEventHandler((ev) => {
|
||||
// 注册全选键 ctrl + a
|
||||
if (ev.keyCode === 65 && ev.ctrlKey && ev.type === 'keydown') {
|
||||
setTimeout(() => {
|
||||
this.term.selectAll()
|
||||
}, 10)
|
||||
}
|
||||
// 注册复制键 ctrl + c
|
||||
if (ev.keyCode === 67 && ev.ctrlKey && ev.type === 'keydown') {
|
||||
this.copySelection(false)
|
||||
}
|
||||
// 注册搜索键 ctrl + shift + f
|
||||
if (ev.keyCode === 70 && ev.ctrlKey && ev.shiftKey && ev.type === 'keydown') {
|
||||
this.$refs.search.open()
|
||||
}
|
||||
})
|
||||
// 隐藏光标
|
||||
this.term.write('\x1b[?25l')
|
||||
// 调整大小
|
||||
this.fitTerminal()
|
||||
// 建立连接
|
||||
this.initSocket(data)
|
||||
})
|
||||
},
|
||||
initSocket(data) {
|
||||
// 打开websocket
|
||||
this.client = new WebSocket(this.$api.fileTail({ token: data.token }))
|
||||
this.client.onopen = () => {
|
||||
this.status = LOG_TAIL_STATUS.RUNNABLE.value
|
||||
this.$emit('open')
|
||||
}
|
||||
this.client.onerror = () => {
|
||||
this.status = LOG_TAIL_STATUS.ERROR.value
|
||||
}
|
||||
this.client.onclose = (e) => {
|
||||
this.status = LOG_TAIL_STATUS.CLOSE.value
|
||||
if (e.code > 4000 && e.code < 5000) {
|
||||
// 自定义错误信息
|
||||
this.term.write(`\x1b[93m${e.reason}\x1b[0m`)
|
||||
}
|
||||
this.$emit('close')
|
||||
}
|
||||
this.client.onmessage = async event => {
|
||||
this.term.write(await event.data.text())
|
||||
if (!this.fixedLog) {
|
||||
this.term.scrollToBottom()
|
||||
}
|
||||
}
|
||||
},
|
||||
fitTerminal() {
|
||||
const dimensions = this.plugin.fit && this.plugin.fit.proposeDimensions()
|
||||
if (!dimensions) {
|
||||
return
|
||||
}
|
||||
if (dimensions?.cols && dimensions?.rows) {
|
||||
this.term.resize(dimensions.cols, dimensions.rows)
|
||||
}
|
||||
},
|
||||
clickTerminal() {
|
||||
this.visibleRightMenu = false
|
||||
},
|
||||
copy() {
|
||||
this.term.selectAll()
|
||||
this.$copy(this.term.getSelection())
|
||||
this.term.clearSelection()
|
||||
},
|
||||
copySelection(tips = undefined) {
|
||||
this.$copy(this.term.getSelection(), tips)
|
||||
},
|
||||
clear() {
|
||||
this.term && this.term.clear()
|
||||
},
|
||||
close() {
|
||||
this.client && this.client.readyState === 1 && this.client.close()
|
||||
},
|
||||
dispose() {
|
||||
this.term && this.term.dispose()
|
||||
this.plugin.fit && this.plugin.fit.dispose()
|
||||
this.plugin.search && this.plugin.search.dispose()
|
||||
this.plugin.links && this.plugin.links.dispose()
|
||||
this.client && this.client.readyState === 1 && this.client.close()
|
||||
window.removeEventListener('resize', this.fitTerminal)
|
||||
},
|
||||
clickRightMenu({ key }) {
|
||||
this.visibleRightMenu = false
|
||||
rightMenuHandler[key].call(this)
|
||||
},
|
||||
async loadDownloadUrl() {
|
||||
try {
|
||||
const downloadUrl = await this.$api.getFileDownloadToken({
|
||||
type: this.downloadType,
|
||||
id: this.relId
|
||||
})
|
||||
this.downloadUrl = this.$api.fileDownloadExec({ token: downloadUrl.data })
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
clearDownloadUrl() {
|
||||
setTimeout(() => {
|
||||
this.downloadUrl = null
|
||||
})
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
formatLogStatus(status, f) {
|
||||
return enumValueOf(LOG_TAIL_STATUS, status)[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logger-view-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
padding: 4px 8px;
|
||||
|
||||
.log-tools-fixed-left {
|
||||
min-width: 20%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-tools-fixed-right {
|
||||
min-width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.log-fixed-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.log-fixed-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.log-fixed-switch {
|
||||
margin: 2px 12px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-container {
|
||||
width: 100%;
|
||||
background: #212529;
|
||||
padding: 4px 0 0 4px;
|
||||
|
||||
.log-terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||