first commit
This commit is contained in:
154
orion-ops-vue/src/components/app/AddAppEnvModal.vue
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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>
|
||||
Reference in New Issue
Block a user