Compare commits

..

40 Commits
v2.8 ... v2.9.2

Author SHA1 Message Date
Orange
9736d94762 fix: 临时移除 Database 协议的用户名与密码相同 选项 2021-04-19 04:00:28 -05:00
Jiangjie.Bai
d8566d2f9e Merge pull request #753 from jumpserver/dev
v2.9.0 发版
2021-04-15 21:05:14 +08:00
fit2bot
d74da503c8 fix: 修复不能禁用的问题 (#751)
perf: 优化系统用户创建

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-15 19:41:07 +08:00
Orange
e1d8e4aea6 fix: 修复v2.9测试中的bugs 2021-04-15 06:40:30 -05:00
Orange
d21559599f fix: 修复系统设置邮件bugs 2021-04-15 06:39:42 -05:00
Orange
f8cadb545f fix: 修复首页面板链接跳转有误的问题
Closes https://github.com/jumpserver/jumpserver/issues/5850
2021-04-15 06:39:07 -05:00
Orange
2bc4b53159 Merge pull request #747 from jumpserver/pr@dev@fix_create_asset_node_lost
fix(assets): 修复选中资产创建时,没有默认选中节点的问题
2021-04-15 16:19:51 +08:00
ibuler
7f28cc0aad fix(assets): 修复选中资产创建时,没有默认选中节点的问题 2021-04-15 15:39:26 +08:00
Orange
7e95e38d24 Merge pull request #744 from jumpserver/dev
v2.9.0 rc3
2021-04-14 18:55:07 +08:00
Orange
57bafc01e3 fix: 修改翻译 2021-04-14 18:54:34 +08:00
Orange
834033f2fd fix: 隐藏终端详情中的端口数据
Closes https://github.com/jumpserver/trello/issues/986
2021-04-14 05:30:25 -05:00
Orange
9a41ccbdd7 fix: 禁止全局组织中批量更新用户组 2021-04-14 05:30:04 -05:00
ibuler
3793370c9c fix: 修复导入id重复的bug 2021-04-14 05:29:38 -05:00
Orange
4486dc55a7 Merge pull request #740 from jumpserver/dev
v2.9.0 rc2
2021-04-13 19:30:56 +08:00
fit2bot
bdb63b865a fix: 修复v2.9 bugs (#739)
* fix: 修复资产列表协议组显示的问题

* fix: 全局组织批量更新用户禁止更新用户组

* fix: 修复网关无法克隆的Bug

* fix: 修复平台列表更新bug

* fix: 修复翻译问题

* fix: 修复更新管理用户账户秘钥的问题

Co-authored-by: Orange <orangemtony@gmail.com>
2021-04-13 19:30:24 +08:00
fit2bot
2d17b48b86 fix(import): 修复导入编辑,空格无法编辑的问题 (#735)
fix: 修复点击节点导入出问题的bug

perf(import): 优化使用分页导入

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-13 14:19:07 +08:00
老广
67091d5a22 Merge pull request #733 from jumpserver/dev
Merge Dev v2.9
2021-04-08 06:24:13 -05:00
liuboF2c
798c4ca64e feat:支持配置全局组织的显示名称 (#731)
Co-authored-by: liubo <liubo@fit2cloud.com>
2021-04-08 13:57:26 +08:00
fit2bot
eddd27e95d perf: 优化 csv/xlsx 导入 (#725)
* perf: 优化导入csv

* perf: 优化导入

stash

perf: 优化导入

perf: 更新导入

perf: 优化导入

feat: 完成导入优化

perf: 修复bug

* perf: 继续优化导入,性能提高三倍

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-08 10:09:58 +08:00
dependabot[bot]
12ffa363c1 chore(deps): bump lodash from 4.17.15 to 4.17.19 (#213)
* chore: 更新Submodule指向

* fix: 修复Build失败的问题

* fix(preload): 开启Preload

开启preload,提高首屏加载速度

Co-authored-by: Orange <orangemtony@gmail.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
Co-authored-by: 老广 <ibuler@qq.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-07 15:51:21 +08:00
dependabot[bot]
cd79246f0d build(deps): bump http-proxy from 1.18.0 to 1.18.1 (#368)
* build(deps): bump http-proxy from 1.18.0 to 1.18.1

Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:50:29 +08:00
dependabot[bot]
94583e2156 build(deps): bump ini from 1.3.5 to 1.3.7 (#545)
* build(deps): bump ini from 1.3.5 to 1.3.7

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:44:08 +08:00
dependabot[bot]
ca602a8052 build(deps): bump axios from 0.18.1 to 0.21.1 (#567)
* build(deps): bump axios from 0.18.1 to 0.21.1

Bumps [axios](https://github.com/axios/axios) from 0.18.1 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.1...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:43:11 +08:00
dependabot[bot]
5bdc4e4e3a build(deps): bump elliptic from 6.5.2 to 6.5.4 (#649)
* build(deps): bump elliptic from 6.5.2 to 6.5.4

Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:41:53 +08:00
dependabot[bot]
da1b73d3fd build(deps): bump y18n from 3.2.1 to 3.2.2 (#721)
* build(deps): bump y18n from 3.2.1 to 3.2.2

Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)
2021-04-07 15:35:50 +08:00
Orange
4bc4012520 feat: 支持首页PDF导出,优化打印样式 2021-04-07 15:32:28 +08:00
Orange
b8f1cb7a8e feat: 支持首页PDF导出,优化打印样式 2021-04-07 15:32:28 +08:00
liubo
ee6a3c6d68 feat: 支持添加nutanix云账号 2021-04-07 15:19:26 +08:00
Orange
be176ad408 perf: 用户来源不是本地时禁用更新密码 2021-04-07 11:17:50 +08:00
Orange
73c17fccbe perf: 优化翻译批量命令选择资产时提示资产不支持SSH协议;连接 2021-04-07 11:04:40 +08:00
Orange
30c1284a41 fix: 完善页面邮箱地址校验规则
Closes https://github.com/jumpserver/trello/issues/933
2021-04-07 11:03:09 +08:00
Orange
191900381a fix: 修复资产更多信息更新失败的问题 2021-04-07 11:02:08 +08:00
Orange
91e04a8d18 perf: 优化编译命令 2021-03-29 19:29:06 +08:00
Orange
1b223f0486 fix: 修复命令存储更新失败的问题 2021-03-29 19:27:58 +08:00
Orange
22eb78339e Merge pull request #719 from jumpserver/pr@dev@perf_status
perf: 优化系统监控页面
2021-03-29 19:23:00 +08:00
ibuler
5831cb326c perf: 优化系统监控页面 2021-03-29 16:59:33 +08:00
Orange
52a4c1824f fix: 修复创建K8S系统用户的权限问题 2021-03-23 17:55:06 +08:00
Orange
c155e5a59b perf: 优化全局组织下禁止用户更新用户组 2021-03-23 17:54:15 +08:00
Orange
ff90e56763 fix: 修复创建远程应用默认Path丢失的问题 2021-03-23 17:50:54 +08:00
Orange
14000317b9 fix: 恢复RDP系统用户user_group字段 2021-03-23 17:49:52 +08:00
53 changed files with 2853 additions and 930 deletions

View File

@@ -1,5 +1,8 @@
module.exports = {
presets: [
'@vue/app'
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
]
}

View File

@@ -1,6 +1,16 @@
server {
listen 80;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
gzip_comp_level 8;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_static on;
gzip_disable "MSIE [1-6].";
location /ui/ {
try_files $uri / /ui/index.html;
alias /opt/lina/;

View File

@@ -18,13 +18,15 @@
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.18.1",
"axios": "0.21.1",
"axios-retry": "^3.1.9",
"deepmerge": "^4.2.2",
"echarts": "^4.7.0",
"element-ui": "2.13.2",
"eslint-plugin-html": "^6.0.0",
"install": "^0.13.0",
"jquery": "^3.5.0",
"js-cookie": "2.2.0",
"less": "^3.10.3",
@@ -43,6 +45,7 @@
"lodash.values": "^4.3.0",
"moment-parseformat": "^3.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
@@ -73,6 +76,7 @@
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"element-theme-chalk": "^2.13.1",
"eslint": "^5.15.3",

View File

@@ -1,51 +1,52 @@
<template><div>
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="MFAInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="MFAInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
<el-form label-position="right" label-width="80px" :model="dialogInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="MFAInfo.hostname" disabled />
<el-input v-model="dialogInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
<el-input v-model="dialogInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="MFAInfo.password" type="password" show-password />
<el-input v-model="dialogInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.sshkey')">
<input type="file" @change="Onchange">
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
<el-form label-position="right" label-width="80px" :model="dialogInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="dialogInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="dialogInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="dialogInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.sshkey')">
<input type="file" @change="Onchange">
</el-form-item>
</el-form>
</Dialog>
</Dialog>
</div>
</div>
</div>
</template>
<script>
@@ -111,7 +112,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
},
tableConfig: {
url: this.url,
@@ -316,7 +317,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
@@ -326,7 +327,7 @@ export default {
// TODO 校验文件类型
const reader = new FileReader()
reader.onload = function() {
vm.dialogInfo.key = this.result
vm.dialogInfo.private_key = this.result
}
reader.readAsText(
e.target.files[0]
@@ -340,8 +341,8 @@ export default {
if (this.dialogInfo.password !== '') {
data.password = this.dialogInfo.password
}
if (this.dialogInfo.key !== '') {
data.key = this.dialogInfo.key
if (this.dialogInfo.private_key !== '') {
data.private_key = this.dialogInfo.private_key
}
this.$axios.post(
`/api/v1/assets/asset-users/`,
@@ -356,7 +357,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()

View File

@@ -2,6 +2,7 @@ import Vue from 'vue'
import Select2 from '@/components/Select2'
import NestedField from '@/components/AutoDataForm/components/NestedField'
import rules from '@/components/DataForm/rules'
import { assignIfNot } from '@/utils/common'
export class FormFieldGenerator {
constructor() {
@@ -109,19 +110,21 @@ export class FormFieldGenerator {
return field
}
generateField(name, fieldsMeta, remoteFieldsMeta) {
let field = { id: name, prop: name, el: {}, attrs: {}}
let field = { id: name, prop: name, el: {}, attrs: {}, rules: [] }
const remoteFieldMeta = remoteFieldsMeta[name] || {}
Vue.$log.debug('FieldsMeta: ', fieldsMeta, name)
const fieldMeta = fieldsMeta[name] || {}
Vue.$log.debug('FieldMeta is: ', fieldMeta)
field.label = remoteFieldMeta.label
field.helpText = remoteFieldMeta.help_text
field = this.generateFieldByType(remoteFieldMeta.type, field, fieldMeta, remoteFieldMeta)
field = this.generateFieldByName(name, field)
field = this.generateFieldByOther(field, fieldMeta, remoteFieldMeta)
const el = Object.assign(field.el || {}, fieldMeta.el || {})
field = Object.assign(field, fieldMeta || {}, { el: el })
const el = assignIfNot(fieldMeta.el || {}, field.el)
const rules = fieldMeta.rules || field.rules
field = Object.assign(field, fieldMeta)
field.el = el
field.rules = rules
_.set(field, 'attrs.error', '')
Vue.$log.debug('Generate field: ', name, field)
return field
}
generateFieldGroup(field, fieldsMeta, remoteFieldsMeta) {

View File

@@ -78,6 +78,10 @@ export default {
$('body').unbind('mousedown')
},
methods: {
refreshTree: function() {
const refreshIconRef = $('#tree-refresh')
refreshIconRef.click()
},
editTreeNode: function() {
this.hideRMenu()
const currentNode = this.zTree.getSelectedNodes()[0]
@@ -100,15 +104,19 @@ export default {
if (this.setting.url.indexOf('?') !== -1) {
combinator = '&'
}
let url = ''
const query = Object.assign({}, this.$route.query)
if (treeNode.meta.type === 'node') {
this.currentNode = treeNode
this.currentNodeId = treeNode.meta.node.id
this.$route.query['node'] = this.currentNodeId
this.$emit('urlChange', `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`)
query['node'] = this.currentNodeId
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`
} else if (treeNode.meta.type === 'asset') {
this.$route.query['asset'] = treeNode.meta.asset.id
this.$emit('urlChange', `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`)
query['asset'] = treeNode.meta.asset.id
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`
}
this.$router.push({ query })
this.$emit('urlChange', url)
},
removeTreeNode: function() {
this.hideRMenu()
@@ -121,6 +129,7 @@ export default {
).then(() => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.zTree.removeNode(currentNode)
this.refreshTree()
}).catch(() => {
// this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
@@ -141,7 +150,7 @@ export default {
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
this.zTree.updateNode(treeNode)
this.$message.success(this.$t('common.updateSuccessMsg'))
})
}).finally(() => { this.refreshTree() })
},
onBodyMouseDown: function(event) {
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
@@ -210,7 +219,7 @@ export default {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
})
}).finally(() => this.refreshTree())
},
createTreeNode: function() {
this.hideRMenu()
@@ -242,7 +251,6 @@ export default {
})
},
refresh: function() {
},
getSelectedNodes: function() {
return this.zTree.getSelectedNodes()

View File

@@ -8,7 +8,14 @@ export const RequiredChange = {
required: true, message: i18n.t('common.fieldRequiredError'), trigger: 'change'
}
export const EmailCheck = {
type: 'email',
message: i18n.t('common.InputEmailAddress'),
trigger: ['blur', 'change']
}
export default {
Required,
RequiredChange
RequiredChange,
EmailCheck
}

View File

@@ -167,7 +167,7 @@ import getLocatedSlotKeys from './utils/extract-keys'
import transformSearchImmediatelyItem from './utils/search-immediately-item'
import isFalsey from './utils/is-falsey'
import merge from 'deepmerge'
const defaultFirstPage = 0
const defaultFirstPage = 1
const noPaginationDataPath = 'payload'
export default {
@@ -723,6 +723,10 @@ export default {
default(row, index) {
return true
}
},
totalData: {
type: Array,
default: null
}
},
data() {
@@ -810,6 +814,13 @@ export default {
},
_searchForm() {
return transformSearchImmediatelyItem(this.collapseForm, this)
},
lastPageNum() {
// page
const pageOffset = this.firstPage - defaultFirstPage
const pageCount = Math.ceil(this.total / this.size)
const lastPageNum = pageCount + pageOffset
return lastPageNum
}
},
watch: {
@@ -828,6 +839,13 @@ export default {
* @property {array} rows - 已选中的行数据的数组
*/
this.$emit('selection-change', val)
},
totalData(val) {
if (val) {
this.page = defaultFirstPage
this.total = val.length
this.getList()
}
}
},
mounted() {
@@ -877,12 +895,54 @@ export default {
}
return query
},
getPageData() {
return this.data
},
async gotoNextPage() {
if (!this.hasNextPage()) {
return false
}
this.page += 1
await this.getList({ loading: true })
},
hasNextPage() {
return this.page < this.lastPageNum
},
getList({ loading = true } = {}) {
const { url } = this
if (url) {
return this.getListFromRemote({ loading: loading })
}
if (this.totalData) {
return this.getListFromStaticData({ loading: true })
}
// this.$log.debug("last page is: ", this.lastPageNum)
},
getListFromStaticData({ loading = true } = {}) {
if (loading) {
this.loading = true
}
if (!this.hasPagination) {
this.data = this.totalData
this.loading = false
return this.data
}
// page
const pageOffset = this.firstPage - defaultFirstPage
const page = this.page === 0 ? 1 : this.page
const start = (page + pageOffset - 1) * this.size
const end = (page + pageOffset) * this.size
this.$log.debug(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
this.data = this.totalData.slice(start, end)
this.loading = false
return this.data
},
/**
* 手动刷新列表数据,选项的默认值为: { loading: true }
* @public
* @param {object} options 方法选项
*/
getList({ loading = true } = {}) {
getListFromRemote({ loading = true } = {}) {
const { url } = this
if (!url) {
return

View File

@@ -100,6 +100,9 @@ export default {
iListeners() {
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
},
dataTable() {
return this.$refs.table
},
...mapGetters({
'globalTableConfig': 'tableConfig'
})

View File

@@ -55,7 +55,6 @@ export default {
},
data() {
return {
}
},
methods: {
@@ -74,4 +73,8 @@ export default {
/*padding-top: 10px;*/
}
.dialog-footer {
padding-right: 50px;
}
</style>

View File

@@ -1,64 +1,74 @@
<template>
<Dialog
:title="$t('common.Import')"
:title="importTitle"
:visible.sync="showImportDialog"
:destroy-on-close="true"
:close-on-click-modal="false"
:loading-status="loadStatus"
@confirm="handleImportConfirm"
@cancel="handleImportCancel()"
width="80%"
class="importDialog"
:confirm-title="confirmTitle"
:show-cancel="false"
:show-confirm="false"
@close="handleImportCancel"
>
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="importTypeOption">
<el-radio v-for="option of importTypeOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form v-if="!showTable" label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
<el-radio v-model="importOption" class="export-item" label="1">{{ this.$t('common.Create') }}</el-radio>
<el-radio v-model="importOption" class="export-item" label="2">{{ this.$t('common.Update') }}</el-radio>
<el-radio v-model="importOption" class="export-item" label="create">{{ this.$t('common.Create') }}</el-radio>
<el-radio v-model="importOption" class="export-item" label="update">{{ this.$t('common.Update') }}</el-radio>
<div style="line-height: 1.5">
<span v-if="importOption==='1'" class="el-upload__tip">
{{ this.$t('common.imExport.downloadImportTemplateMsg') }}
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link>
</span>
<span v-else class="el-upload__tip">
{{ this.$t('common.imExport.downloadUpdateTemplateMsg') }}
<el-link type="success" :underline="false" @click="downloadUpdateTempUrl">{{ this.$t('common.Download') }}</el-link>
<span class="el-upload__tip">
{{ downloadTemplateTitle }}
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('csv')"> CSV </el-link>
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
</span>
</div>
</el-form-item>
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'">
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'" class="file-uploader">
<el-upload
ref="upload"
drag
action="string"
list-type="text/csv"
:http-request="handleImport"
:limit="1"
:auto-upload="false"
:on-change="onFileChange"
:before-upload="beforeUpload"
accept=".csv,.xlsx"
>
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button>
<!-- <div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>-->
<i class="el-icon-upload" />
<div class="el-upload__text">{{ $t('common.imExport.dragUploadFileInfo') }}</div>
<div slot="tip" class="el-upload__tip">
<span :class="{'hasError': hasFileFormatOrSizeError }">{{ $t('common.imExport.uploadCsvLth10MHelpText') }}</span>
<div v-if="renderError" class="hasError">{{ renderError }}</div>
</div>
</el-upload>
</el-form-item>
</el-form>
<div v-if="errorMsg" class="error-msg error-results">
<ul v-if="typeof errorMsg === 'object'">
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li>
</ul>
<span v-else>{{ errorMsg }}</span>
<div v-else class="importTableZone">
<ImportTable
ref="importTable"
:json-data="jsonData"
:import-option="importOption"
:url="url"
@cancel="cancelUpload"
@finish="closeDialog"
/>
</div>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import ImportTable from '@/components/ListTable/TableAction/ImportTable'
import { getErrorResponseMsg } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
export default {
name: 'ImportDialog',
components: {
Dialog
Dialog,
ImportTable
},
props: {
selectedRows: {
@@ -73,45 +83,49 @@ export default {
data() {
return {
showImportDialog: false,
importOption: '1',
isCsv: true,
importOption: 'create',
errorMsg: '',
loadStatus: false,
importTypeOption: 'csv'
importTypeOption: 'csv',
importTypeIsCsv: true,
showTable: false,
renderError: '',
hasFileFormatOrSizeError: false,
jsonData: {}
}
},
computed: {
hasSelected() {
return this.selectedRows.length > 0
},
importTypeOptions() {
return [
{
label: 'CSV',
value: 'csv',
can: true
},
{
label: 'Excel',
value: 'xlsx',
can: true
}
]
},
upLoadUrl() {
return this.url
},
downloadImportTempUrl() {
const format = this.importTypeOption === 'csv' ? 'format=csv&template=import&limit=1' : 'format=xlsx&template=import&limit=1'
const url = (this.url.indexOf('?') === -1) ? `${this.url}?${format}` : `${this.url}&${format}`
return url
},
uploadHelpTextClass() {
const cls = ['el-upload__tip']
if (!this.isCsv) {
cls.push('error-msg')
}
return cls
},
downloadTemplateTitle() {
if (this.importOption === 'create') {
return this.$t('common.imExport.downloadImportTemplateMsg')
} else {
return this.$t('common.imExport.downloadUpdateTemplateMsg')
}
},
importTitle() {
if (this.importOption === 'create') {
return this.$t('common.Import') + this.$t('common.Create')
} else {
return this.$t('common.Import') + this.$t('common.Update')
}
},
confirmTitle() {
return '导入'
}
},
watch: {
importOption(val) {
this.showTable = false
}
},
mounted() {
@@ -120,56 +134,69 @@ export default {
})
},
methods: {
performUpdate(item) {
this.$axios.put(
this.upLoadUrl,
item.file,
{ headers: { 'Content-Type': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length })
this.onSuccess(msg)
closeDialog() {
this.showImportDialog = false
},
cancelUpload() {
this.showTable = false
this.renderError = ''
this.jsonData = {}
},
onFileChange(file, fileList) {
fileList.splice(0, fileList.length)
if (file.status !== 'ready') {
return
}
// const isCsv = file.raw.type = 'text/csv'
if (!this.beforeUpload(file)) {
return
}
const isCsv = file.name.indexOf('csv') > -1
const url = new URL(this.url, 'http://localhost')
url.pathname += 'render-to-json/'
const renderToJsonUrl = url.toString().replace('http://localhost', '')
this.$axios.post(
renderToJsonUrl,
file.raw,
{ headers: { 'Content-Type': isCsv ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
).then(data => {
this.jsonData = data
this.showTable = true
}).catch(error => {
this.catchError(error)
fileList.splice(0, fileList.length)
this.renderError = getErrorResponseMsg(error)
}).finally(() => {
this.loadStatus = false
})
},
performCreate(item) {
this.$axios.post(
this.upLoadUrl,
item.file,
{ headers: { 'Content-Type': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.catchError(error)
}).finally(() => {
this.loadStatus = false
})
beforeUpload(file) {
const isLt30M = file.size / 1024 / 1024 < 30
if (!isLt30M) {
this.hasFileFormatOrSizeError = true
}
return isLt30M
},
async downloadTemplateFile(tp) {
const downloadUrl = await this.getDownloadTemplateUrl(tp)
window.open(downloadUrl)
},
async getDownloadTemplateUrl(tp) {
const template = this.importOption === 'create' ? 'import' : 'update'
let query = `format=${tp}&template=${template}`
if (this.importOption === 'update' && this.selectedRows.length > 0) {
const resources = []
for (const item of this.selectedRows) {
resources.push(item.id)
}
const resp = await createSourceIdCache(resources)
query += `&spm=${resp.spm}`
} else {
query += '&limit=1'
}
return this.url.indexOf('?') === -1 ? `${this.url}?${query}` : `${this.url}&${query}`
},
catchError(error) {
this.$refs.upload.clearFiles()
if (error.response && error.response.status === 400) {
const errorData = error.response.data
const totalErrorMsg = []
errorData.forEach((value, index) => {
if (typeof value === 'string') {
totalErrorMsg.push(`line ${index}. ${value}`)
} else {
const errorMsg = [`line ${index}. `]
for (const [k, v] of Object.entries(value)) {
if (v) {
errorMsg.push(`${k}: ${v}`)
}
}
if (errorMsg.length > 1) {
totalErrorMsg.push(errorMsg.join(' '))
}
}
})
this.errorMsg = totalErrorMsg
}
console.log(error)
},
onSuccess(msg) {
this.errorMsg = ''
@@ -181,40 +208,14 @@ export default {
a.click()
window.URL.revokeObjectURL(url)
},
handleImport(item) {
this.loadStatus = true
if (this.importOption === '1') {
this.performCreate(item)
} else {
this.performUpdate(item)
}
},
async downloadUpdateTempUrl() {
var resources = []
const data = this.selectedRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
const baseUrl = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
const format = this.importTypeOption === 'csv' ? '?format=csv&template=update&spm=' : '?format=xlsx&template=update&spm='
const url = `${baseUrl}${format}` + spm.spm
return this.downloadCsv(url)
},
async handleImportConfirm() {
this.$refs.upload.submit()
this.$refs['importTable'].performUpload()
},
handleImportCancel() {
this.showImportDialog = false
},
beforeUpload(file) {
this.isCsv = this.importTypeOption === 'csv' ? _.endsWith(file.name, 'csv') : _.endsWith(file.name, 'xlsx')
if (!this.isCsv) {
this.$message.error(
this.$t('common.NeedSpecifiedFile')
)
}
return this.isCsv
this.showTable = false
this.renderError = ''
this.jsonData = {}
}
}
}
@@ -231,4 +232,49 @@ export default {
overflow: auto
}
.importDialog >>> .el-form-item.file-uploader {
padding-right: 150px;
}
.file-uploader >>> .el-upload {
width: 100%;
//padding-right: 150px;
}
.file-uploader >>> .el-upload-dragger {
width: 100%;
}
.importTableZone {
padding: 0 20px;
.importTable {
overflow: auto;
}
.tableFilter {
padding-bottom: 10px;
}
}
.importTable >>> .el-dialog__body {
padding-bottom: 20px;
}
.export-item {
margin-left: 80px;
}
.export-item:first-child {
margin-left: 0;
}
.hasError {
color: $--color-danger;
}
.el-upload__tip {
line-height: 1.5;
padding-top: 0;
}
</style>

View File

@@ -0,0 +1,426 @@
<template>
<div>
<el-row>
<el-col :span="8">
<div class="tableFilter">
<el-radio-group v-model="importStatusFilter" size="small">
<el-radio-button label="all">{{ $t('common.Total') }}</el-radio-button>
<el-radio-button label="ok">{{ $t('common.Success') }}</el-radio-button>
<el-radio-button label="error">{{ $t('common.Failed') }}</el-radio-button>
<el-radio-button label="pending">{{ $t('common.Pending') }}</el-radio-button>
</el-radio-group>
</div>
</el-col>
<el-col :span="8" style="text-align: center">
<span class="summary-item summary-total"> {{ $t('common.Total') }}: {{ totalCount }}</span>
<span class="summary-item summary-success"> {{ $t('common.Success') }}: {{ successCount }}</span>
<span class="summary-item summary-failed"> {{ $t('common.Failed') }}: {{ failedCount }}</span>
<span class="summary-item summary-pending"> {{ $t('common.Pending') }}: {{ pendingCount }}</span>
</el-col>
</el-row>
<div class="row">
<el-progress :percentage="processedPercent" />
</div>
<DataTable v-if="tableGenDone" id="importTable" ref="dataTable" :config="tableConfig" class="importTable" />
<div class="row" style="padding-top: 20px">
<div style="float: right">
<el-button size="small" @click="performCancel">{{ $t('common.Cancel') }}</el-button>
<el-button size="small" type="primary" @click="performImportAction">{{ importActionTitle }}</el-button>
</div>
</div>
</div>
</template>
<script>
import DataTable from '@/components/DataTable'
import { sleep } from '@/utils/common'
import { EditableInputFormatter, StatusFormatter } from '@/components/ListTable/formatters'
export default {
name: 'ImportTable',
components: {
DataTable
},
props: {
jsonData: {
type: Object,
default: () => ({})
},
url: {
type: String,
required: true
},
importOption: {
type: String,
required: true
}
},
data() {
return {
columns: [],
importStatusFilter: 'all',
iTotalData: [],
tableConfig: {
hasSelection: false,
// hasPagination: false,
columns: [],
totalData: [],
paginationSize: 10,
paginationSizes: [10],
tableAttrs: {
stripe: true, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark'
}
},
tableGenDone: false,
importTaskStatus: 'pending', // pending, started, stopped, done
importTaskResult: '', // success, hasError
hasImport: false,
hasContinueButton: false,
importActions: {
import: this.$t('common.Import'),
continue: this.$t('common.Continue'),
stop: this.$t('common.Stop'),
finished: this.$t('common.Finished')
}
}
},
computed: {
tableColumnNameMapper() {
const mapper = {}
for (const column of this.tableConfig.columns) {
mapper[column['prop']] = column['label']
}
return mapper
},
importAction() {
switch (this.importTaskStatus) {
case 'pending':
return 'import'
case 'started':
return 'stop'
}
if (this.totalCount === this.successCount) {
return 'finished'
} else {
return 'continue'
}
},
importActionTitle() {
return this.importActions[this.importAction]
},
successData() {
return this.iTotalData.filter((item) => {
return item['@status'] === 'ok'
})
},
failedData() {
return this.iTotalData.filter((item) => {
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
})
},
pendingData() {
return this.iTotalData.filter((item) => {
return item['@status'] === 'pending'
})
},
totalCount() {
return this.iTotalData.length
},
successCount() {
return this.successData.length
},
failedCount() {
return this.failedData.length
},
pendingCount() {
return this.pendingData.length
},
processedCount() {
return this.totalCount - this.pendingCount
},
processedPercent() {
if (this.totalCount === 0) {
return 0
}
return Math.round(this.processedCount / this.totalCount * 100)
},
elDataTable() {
return this.$refs['dataTable'].dataTable
}
},
watch: {
importStatusFilter(val) {
if (val === 'all') {
this.tableConfig.totalData = this.iTotalData
} else if (val === 'error') {
this.tableConfig.totalData = this.failedData
} else {
this.tableConfig.totalData = this.iTotalData.filter((item) => {
return item['@status'] === val
})
}
}
},
mounted() {
this.generateTable()
},
methods: {
generateTableColumns(tableTitles, tableData) {
const vm = this
const columns = [{
prop: '@status',
label: vm.$t('common.Status'),
width: '80px',
align: 'center',
formatter: StatusFormatter,
formatterArgs: {
iconChoices: {
ok: 'fa-check text-primary',
error: 'fa-times text-danger',
pending: 'fa-clock-o'
},
getChoicesKey(val) {
if (val === 'ok' || val === 'pending') {
return val
}
return 'error'
},
getTip(val) {
if (val === 'ok') {
return vm.$t('common.Success')
} else if (val === 'pending') {
return vm.$t('common.Pending')
} else if (val && val.name === 'error') {
return val.error
}
return ''
},
hasTips: true
}
}]
for (const item of tableTitles) {
const dataItemLens = tableData.map(d => {
const prop = item[1]
const itemColData = d[prop]
if (!d) {
return 0
}
if (!itemColData || !itemColData.length) {
return 0
}
return itemColData.length
})
let colMaxWidth = Math.max(...dataItemLens) * 10
if (colMaxWidth === 0) {
continue
}
colMaxWidth = Math.min(180, colMaxWidth)
colMaxWidth = Math.max(colMaxWidth, 100)
columns.push({
prop: item[1],
label: item[0],
minWidth: colMaxWidth + 'px',
showOverflowTooltip: true,
formatter: EditableInputFormatter,
formatterArgs: {
onEnter: ({ row, col, oldValue, newValue }) => {
const prop = col.prop
row['@status'] = 'pending'
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
this.$set(row, prop, newValue)
}
}
})
}
return columns
},
generateTableData(tableTitles, tableData) {
const totalData = []
tableData.forEach(item => {
this.$set(item, '@status', 'pending')
totalData.push(item)
})
return totalData
},
generateTable() {
const tableTitles = this.jsonData['title']
const tableData = this.jsonData['data']
const columns = this.generateTableColumns(tableTitles, tableData)
const totalData = this.generateTableData(tableTitles, tableData)
this.tableConfig.columns = columns
this.tableGenDone = true
setTimeout(() => {
this.iTotalData = totalData
this.tableConfig.totalData = totalData
}, 200)
},
beautifyErrorData(errorData) {
if (typeof errorData === 'string') {
return errorData
} else if (Array.isArray(errorData)) {
return errorData
} else if (typeof errorData !== 'object') {
return errorData
}
const data = []
// eslint-disable-next-line prefer-const
for (let [key, value] of Object.entries(errorData)) {
if (typeof value === 'object') {
value = this.beautifyErrorData(value)
}
let label = this.tableColumnNameMapper[key]
if (!label) {
label = key
}
data.push(`${label}: ${value}`)
}
return data
},
performCancel() {
this.performStop()
this.$emit('cancel')
},
performFinish() {
this.performStop()
this.$emit('finish')
},
taskIsStopped() {
return this.importTaskStatus === 'stopped'
},
performImportAction() {
switch (this.importAction) {
case 'continue':
return this.performContinue()
case 'import':
return this.performUpload()
case 'stop':
return this.performStop()
case 'finished':
return this.performFinish()
}
},
performContinue() {
if (this.importTaskStatus === 'done') {
for (const item of this.failedData) {
item['@status'] = 'pending'
}
this.tableConfig.totalData = this.pendingData
}
this.importTaskStatus = 'started'
setTimeout(() => {
this.performUpload()
}, 100)
},
performStop() {
this.importTaskStatus = 'stopped'
},
async performUploadCurrentPageData() {
const currentData = this.elDataTable.getPageData()
for (const item of currentData) {
if (item['@status'] !== 'pending') {
continue
}
if (this.taskIsStopped()) {
return
}
await this.performUploadObject(item)
await sleep(100)
}
},
async performUpload() {
this.importTaskStatus = 'started'
this.importStatusFilter = 'pending'
while (!this.taskIsStopped()) {
await this.performUploadCurrentPageData()
const hasNextPage = this.elDataTable.hasNextPage()
if (hasNextPage && !this.taskIsStopped()) {
await this.elDataTable.gotoNextPage()
await sleep(100)
} else {
break
}
}
if (this.pendingCount === 0) {
this.importTaskStatus = 'done'
}
if (this.failedCount > 0) {
this.$message.error(this.$t('common.imExport.hasImportErrorItemMsg') + '')
}
},
async performUpdateObject(item) {
const updateUrl = `${this.url}${item.id}/`
return this.$axios.put(
updateUrl,
item,
{ disableFlashErrorMsg: true }
)
},
async performUploadObject(item) {
let handler = this.performCreateObject
if (this.importOption === 'update') {
handler = this.performUpdateObject
}
try {
await handler.bind(this)(item)
item['@status'] = 'ok'
} catch (error) {
const errorData = error?.response?.data
const _error = this.beautifyErrorData(errorData)
item['@status'] = {
name: 'error',
error: _error
}
}
},
async performCreateObject(item) {
return this.$axios.post(
this.url,
item,
{ disableFlashErrorMsg: true }
)
},
keepElementInViewport() {
const tableRef = document.getElementById('importTable')
const pendingRef = tableRef?.getElementsByClassName('pendingStatus')[0]
if (!pendingRef) {
return
}
const parentTdRef = pendingRef.parentElement.parentElement.parentElement.parentElement
const rect = parentTdRef.getBoundingClientRect()
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
windowInnerHeight = windowInnerHeight * 0.97 - 150
const inViewport = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowInnerHeight
)
if (!inViewport) {
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
}
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/element-variables.scss";
.summary-item {
padding: 0 10px
}
.summary-success {
color: $--color-primary;
}
.summary-failed {
color: $--color-danger;
}
.importTable >>> .cell {
min-height: 20px;
height: 100%;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div style="width: 100%;min-height: 20px" @click.stop="editCell">
<el-input
v-if="inEditMode"
v-model="value"
size="mini"
class="editInput"
@keyup.enter.native="onInputEnter"
@blur="onInputEnter"
/>
<template v-else>
<span>{{ cellValue }}</span>
</template>
</div>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'EditableInputFormatter',
components: {
},
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
trigger: 'click',
onEnter: ({ row, col, oldValue, newValue }) => {
const prop = col.prop
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
this.$set(row, prop, newValue)
}
}
}
}
},
data() {
const valueIsString = typeof this.cellValue === 'string'
const jsonValue = this.cellValue ? JSON.stringify(this.cellValue) : ''
return {
inEditMode: false,
value: valueIsString ? this.cellValue || '' : jsonValue,
valueIsString: valueIsString,
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
methods: {
editCell() {
this.inEditMode = true
},
onInputEnter() {
let validValue = this.value
try {
validValue = JSON.parse(validValue)
} catch (e) {
// pass
}
this.formatterArgs.onEnter({
row: this.row, col: this.col,
oldValue: this.cellValue,
newValue: validValue
})
this.inEditMode = false
},
cancelEdit() {
this.inEditMode = false
}
}
}
</script>
<style scoped>
.editInput >>> .el-input__inner {
padding: 2px;
line-height: 12px;
}
.editInput {
padding: -6px;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div>
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
<div slot="content">
<template v-if="tipsIsArray">
<div v-for="tip of tips" :key="tip">
<span>{{ tip }}</span>
<br>
</div>
</template>
<span v-else>
{{ tips }}
</span>
</div>
<i :class="'fa ' + iconClass" />
</el-tooltip>
<i v-else :class="'fa ' + iconClass" />
</div>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'StatusFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
iconChoices: {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
},
getChoicesKey(val) {
return !!val
},
getTip(val, col) {
},
hasTips: false
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
computed: {
iconClass() {
const key = this.formatterArgs.getChoicesKey(this.cellValue)
return this.formatterArgs.iconChoices[key] + ' ' + key + 'Status'
},
tips() {
const vm = this
return this.formatterArgs.getTip(this.cellValue, vm)
},
tipsIsArray() {
return Array.isArray(this.tips)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -10,6 +10,8 @@ import SystemUserFormatter from './GrantedSystemUsersShowFormatter'
import ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
import DialogDetailFormatter from './DialogDetailFormatter'
import LoadingActionsFormatter from './LoadingActionsFormatter'
import EditableInputFormatter from './EditableInputFormatter'
import StatusFormatter from './StatusFormatter'
export default {
DetailFormatter,
@@ -23,7 +25,9 @@ export default {
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter,
ArrayFormatter
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
}
export {
@@ -38,5 +42,7 @@ export {
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter,
ArrayFormatter
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
}

View File

@@ -45,17 +45,6 @@ export default {
dataTable() {
return this.$refs.dataTable.$refs.dataTable
},
// hasCreateAction() {
// const hasLeftAction = this.headerActions.hasLeftActions
// if (hasLeftAction === false) {
// return false
// }
// const hasCreate = this.headerActions.hasCreate
// if (hasCreate === false) {
// return false
// }
// return true
// },
iTableConfig() {
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
this.$log.debug('Header actions', this.headerActions)

View File

@@ -72,7 +72,6 @@ export default {
watch: {
treeConfig: {
handler(val) {
},
deep: true
}

View File

@@ -179,7 +179,7 @@
"TestAssetsConnective": "测试资产可连接性",
"TestConnection": "测试连接",
"Type": "类型",
"UnselectedAssets": "未选择资产",
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
"UnselectedNodes": "未选择节点",
"UpdateAssetUserToken": "更新资产用户认证信息",
"Username": "用户名",
@@ -258,6 +258,10 @@
"EnterForSearch": "按回车进行搜索",
"Export": "导出",
"Import": "导入",
"ContinueImport": "继续导入",
"Continue": "继续",
"Stop": "停止",
"Finished": "完成",
"Refresh": "刷新",
"Info": "提示",
"MFAConfirm": "MFA 认证",
@@ -320,16 +324,25 @@
"fieldRequiredError": "这个字段是必填项",
"getErrorMsg": "获取失败",
"MFAErrorMsg": "MFA错误请检查",
"Total": "总共",
"Success": "成功",
"Failed": "失败",
"Pending": "等待",
"Status": "状态",
"InputEmailAddress": "请输入正确的邮箱地址",
"imExport": {
"ExportAll": "导出所有",
"ExportOnlyFiltered": "仅导出搜索结果",
"ExportOnlySelectedItems": "仅导出选择项",
"ExportRange": "导出范围",
"createSuccessMsg": "导入创建成功,总共:{count}",
"downloadImportTemplateMsg": "下载导入模板",
"downloadImportTemplateMsg": "下载创建模板",
"downloadUpdateTemplateMsg": "下载更新模板",
"onlyCSVFilesTips": "仅支持csv文件导入",
"updateSuccessMsg": "导入更新成功,总共:{count}"
"updateSuccessMsg": "导入更新成功,总共:{count}",
"uploadCsvLth10MHelpText": "只能上传 csv/xlsx, 且不超过 10M",
"dragUploadFileInfo": "将文件拖到此处,或点击上传",
"hasImportErrorItemMsg": "存在导入失败项,点击左侧 x 查看失败原因,点击表格编辑后,可以继续导入失败项"
},
"fileType": "文件类型",
"isValid": "有效",
@@ -554,6 +567,7 @@
"Acl": "访问控制",
"UserAclList": "用户登录",
"UserAclCreate": "创建用户登录规则",
"UserAclLists": "用户登录规则",
"UserAclUpdate": "更新用户登录规则",
"UserAclDetail": "用户登录规则详情",
"AssetAclList": "登录资产",

View File

@@ -178,7 +178,7 @@
"TestAssetsConnective": "Test assets connective",
"TestConnection": "Test connection",
"Type": "Type",
"UnselectedAssets": "Unselected assets",
"UnselectedAssets": "No asset selected or the selected asset does not support SSH protocol connection",
"UnselectedNodes": "Unselected nodes",
"UpdateAssetUserToken": "Update asset user auth",
"Username": "Username",
@@ -257,6 +257,10 @@
"EnterForSearch": "Press enter to search",
"Export": "Export",
"Import": "Import",
"ContinueImport": "ContinueImport",
"Continue": "Continue",
"Stop": "Stop",
"Finished": "Finished",
"Refresh": "Refresh",
"Info": "Info",
"MFAConfirm": "MFA Confirm",
@@ -277,11 +281,13 @@
"Reset": "Reset",
"Search": "Search",
"MFAErrorMsg": "MFA Errorplease check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select",
"SelectFile": "Select file",
"Show": "Show",
"Submit": "Submit",
"Test": "Test",
"SaveAndAddAnother":"Save and add another",
"TestSuccessMsg": "Test Success",
"To": "To",
"Update": "Update",
@@ -318,6 +324,11 @@
"fieldRequiredError": "This field is required",
"getErrorMsg": "Get failed",
"fileType": "File type",
"Status": "Status",
"Total": "Total",
"Success": "Success",
"Failed": "Failed",
"Pending": "Pending",
"imExport": {
"ExportAll": "Export all",
"ExportOnlyFiltered": "Export only filtered",
@@ -327,7 +338,10 @@
"downloadImportTemplateMsg": "Download import template",
"downloadUpdateTemplateMsg": "Download update template",
"onlyCSVFilesTips": "Only csv supported",
"updateSuccessMsg": "Update success, total: {count}"
"updateSuccessMsg": "Update success, total: {count}",
"dragUploadFileInfo": "Drag file here or click to upload",
"uploadCsvLth10MHelpText": "csv/xlsx files with a size less than 10M",
"hasImportErrorItemMsg": "There is an error item, click the x icon to view the details, and continue to import after editing"
},
"isValid": "Is valid",
"nav": {
@@ -552,6 +566,7 @@
"UserAclList": "User acl list",
"UserAclCreate": "User acl create",
"UserAclUpdate": "User acl update",
"UserAclLists": "User acl lists",
"UserAclDetail": "User acl detail",
"AssetAclList": "Asset acl list",
"AssetAclCreate": "Asset acl create",
@@ -587,6 +602,10 @@
"RemoteAppPermissionCreate": "Remote apps permission create",
"RemoteAppPermissionDetail": "Remote apps permissions detail",
"RemoteAppPermissionUpdate": "Remote app permission update",
"ApplicationDetail": "Application detail",
"ApplicationPermissionCreate": "Application permission create",
"ApplicationPermissionDetail": "Application permission detail",
"ApplicationPermissionUpdate": "Application permission update",
"RemoteAppUpdate": "Remote app update",
"ReplayStorageUpdate": "Replay storage update",
"SessionDetail": "Sessions detail",

View File

@@ -1,6 +1,6 @@
<template>
<div class="page">
<PageHeading>
<PageHeading class="disabled-when-print">
<slot name="title">{{ iTitle }}</slot>
<template #rightSide>
<slot name="headingRightSide" />
@@ -42,5 +42,15 @@ export default {
</script>
<style scoped>
@media print {
.disabled-when-print{
display: none;
}
.enabled-when-print{
display: inherit !important;
}
.print-margin{
margin-top: 10px;
}
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<NavBar class="sidebar-container" />
<NavBar class="sidebar-container disabled-when-print" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<div :class="{'fixed-header':fixedHeader}" class="disabled-when-print">
<NavHeader />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<Footer />
<Footer class="disabled-when-print" />
</div>
</div>
</template>
@@ -98,4 +98,28 @@ export default {
.mobile .fixed-header {
width: 100%;
}
@media print {
.disabled-when-print{
display: none;
width: 100%;
}
.enabled-when-print{
display: inherit !important;
}
.print-margin{
margin-top: 10px;
}
.drawer-bg{
display: none;
}
.main-container{
margin-left: 0px !important;
}
//.fixed-header{
// width: 100% !important;
//}
//.hideSidebar .fixed-header{
// width: 100% !important;
//}
}
</style>

View File

@@ -1,3 +1,5 @@
const _ = require('lodash')
function getTimeUnits(u) {
const units = {
'd': '天',
@@ -187,8 +189,28 @@ export function getDayFuture(days, now) {
return new Date(now.getTime() + 3600 * 1000 * 24 * days)
}
export function getErrorResponseMsg(error) {
let msg = ''
const data = error.response && error.response.data || {}
if (data && (data.error || data.msg || data.detail)) {
msg = data.error || data.msg || data.detail
}
return msg
}
export function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
function customizer(objValue, srcValue) {
return _.isUndefined(objValue) ? srcValue : objValue
}
export const assignIfNot = _.partialRight(_.assignInWith, customizer)
const scheme = document.location.protocol
const port = document.location.port ? ':' + document.location.port : ''
const BASE_URL = scheme + '//' + document.location.hostname + port
export { BASE_URL }

View File

@@ -1,6 +1,7 @@
import axios from 'axios'
import i18n from '@/i18n/i18n'
import { getTokenFromCookie } from '@/utils/auth'
import { getErrorResponseMsg } from '@/utils/common'
import { refreshSessionIdAge } from '@/api/users'
import { Message, MessageBox } from 'element-ui'
import store from '@/store'
@@ -84,11 +85,8 @@ function ifBadRequest({ response, error }) {
export function flashErrorMsg({ response, error }) {
if (!response.config.disableFlashErrorMsg) {
let msg = error.message
const data = response.data
if (data && (data.error || data.msg || data.detail)) {
msg = data.error || data.msg || data.detail
}
const responseErrorMsg = getErrorResponseMsg(error)
const msg = responseErrorMsg || error.message
Message({
message: msg,
type: 'error',

View File

@@ -122,6 +122,11 @@ export default {
},
showOverflowTooltip: true
},
protocols: {
formatter: function(row) {
return <span> {row.protocols.toString()} </span>
}
},
ip: {
sortable: 'custom',
width: '140px'

View File

@@ -85,7 +85,17 @@ export default {
}
}.bind(this)
}
]
],
onClone: function({ row, col }) {
const cloneRoute = {
name: 'GatewayCreate',
query: {
domain: this.object.id,
clone_from: row.id
}
}
this.$router.push(cloneRoute)
}.bind(this)
}
}

View File

@@ -1,5 +1,5 @@
<template>
<GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" :perform-submit="performSubmit.bind(this)" />
<GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" />
</template>
<script>
@@ -17,65 +17,48 @@ export default {
charset: 'utf8'
},
fields: [
[this.$t('common.Basic'), ['name', 'base', 'charset', 'security', 'console', 'comment']]
[this.$t('common.Basic'), ['name', 'base', 'charset', 'meta', 'comment']]
],
fieldsMeta: {
security: {
type: 'select',
label: 'RDP security',
options: [{
label: 'RDP',
value: 'rdp'
meta: {
fields: ['security', 'console'],
fieldsMeta: {
security: {
prop: 'meta.security',
type: 'select',
label: 'RDP security',
options: [{
label: 'RDP',
value: 'rdp'
},
{
label: 'NLA',
value: 'nla'
},
{
label: 'TLS',
value: 'tls'
},
{
label: 'Any',
value: 'any'
}]
},
console: {
type: 'select',
label: 'RDP console',
options: [{
label: this.$t('common.Yes'),
value: 'true'
}, {
label: this.$t('common.No'),
value: 'false'
}]
}
},
{
label: 'NLA',
value: 'nla'
},
{
label: 'TLS',
value: 'tls'
},
{
label: 'Any',
value: 'any'
}],
hidden: form => form.base !== 'Windows'
},
console: {
type: 'select',
label: 'RDP console',
options: [{
label: '是',
value: 'true'
}, {
label: '否',
value: 'false'
}],
hidden: form => form.base !== 'Windows'
}
},
performSubmit: function(formdata) {
var postData = {}
if (formdata.base === 'Windows') {
postData.meta = {}
postData.meta.security = formdata.security
postData.meta.console = (formdata.console === 'true')
}
postData.name = formdata.name
postData.base = formdata.base
postData.charset = formdata.charset
postData.comment = formdata.comment || ''
const params = this.$route.params
if (params.id) {
return this.$axios.put(
`${this.url}${params.id}/`, postData
)
} else {
return this.$axios.post(
this.url, postData
)
}
},
url: '/api/v1/assets/platforms/'
}

View File

@@ -4,141 +4,34 @@
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import UploadKey from '@/components/UploadKey'
import { Required } from '@/components/DataForm/rules'
import getFields from './fields'
export default {
name: 'SystemUserCreateUpdate',
components: { GenericCreateUpdatePage },
data() {
const fields = getFields.bind(this)()
return {
initial: {
login_mode: 'auto',
protocol: this.$route.query.protocol,
username_same_with_user: false,
auto_generate_key: false,
auto_push: false
},
fields: [
[this.$t('common.Basic'), ['name', 'login_mode', 'username', 'username_same_with_user', 'priority', 'protocol']],
[this.$t('common.Basic'), ['name', 'login_mode', 'username', 'priority', 'protocol']],
[this.$t('common.Auth'), ['update_password', 'password']],
[this.$t('common.Other'), ['comment']]
],
fieldsMeta: {
login_mode: {
helpText: this.$t('assets.LoginModeHelpMessage'),
hidden: (form) => {
if (form.protocol === 'k8s') {
return true
}
},
on: {
input: ([value], updateForm) => {
if (value === 'manual') {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
username: {
el: {
disabled: false
},
rules: [Required],
hidden: (form) => {
if (form.login_mode === 'auto') {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
if (!form.username_same_with_user) {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
if (['mysql', 'postgresql', 'mariadb', 'oracle'].indexOf(form.protocol) !== -1) {
this.fieldsMeta.username.rules = [Required]
this.fieldsMeta.username.rules[0].required = true
}
}
},
private_key: {
component: UploadKey,
hidden: (form) => {
if (form.login_mode !== 'auto') {
return true
}
if (form.protocol === 'k8s') {
return true
}
return form.auto_generate_key === true
}
},
username_same_with_user: {
type: 'switch',
helpText: this.$t('assets.UsernameHelpMessage'),
hidden: (form) => {
this.fieldsMeta.username.el.disabled = form.username_same_with_user
return form.protocol === 'k8s'
},
el: {
disabled: false
}
},
protocol: {
rules: [Required],
el: {
disabled: true,
style: 'width:100%'
},
on: {
input: ([value], updateForm) => {
if (['ssh', 'rdp'].indexOf(value) === -1) {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
update_password: {
label: this.$t('users.UpdatePassword'),
type: 'checkbox',
hidden: (formValue) => {
if (formValue.update_password || formValue.protocol === 'k8s') {
return true
}
if (formValue.login_mode === 'manual') {
return true
}
return !this.$route.params.id
}
},
password: {
helpText: this.$t('assets.PasswordHelpMessage'),
hidden: form => {
if (form.login_mode !== 'auto' || form.protocol === 'k8s' || form.auto_generate_key) {
return true
}
if (!this.$route.params.id) {
return false
}
return !form.update_password
}
}
login_mode: fields.login_mode,
username: fields.username,
private_key: fields.private_key,
protocol: fields.protocol,
update_password: fields.update_password,
password: fields.password
},
url: '/api/v1/assets/system-users/',
authHiden: false
}
},
method: {
},
mounted() {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.username_same_with_user.el.disabled = true
url: '/api/v1/assets/system-users/'
}
}
}

View File

@@ -0,0 +1,179 @@
import { Required } from '@/components/DataForm/rules'
import UploadKey from '@/components/UploadKey'
import i18n from '@/i18n/i18n'
import { Select2 } from '@/components'
function getFields() {
const login_mode = {
helpText: i18n.t('assets.LoginModeHelpMessage'),
on: {
input: ([value], updateForm) => {
if (value === 'manual') {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
}
const username = {
el: {
disabled: false
},
on: {
input: ([value], updateForm) => {
updateForm({ home: `/home/${value}` })
}
},
rules: [Object.assign({}, Required)],
hidden: (form) => {
if (form.login_mode === 'manual' || form.username_same_with_user) {
this.fieldsMeta.username.rules[0].required = false
} else {
this.fieldsMeta.username.rules[0].required = true
}
if (form.username_same_with_user) {
this.fieldsMeta.username.el.disabled = true
} else {
this.fieldsMeta.username.el.disabled = false
}
}
}
const private_key = {
component: UploadKey,
hidden: (form) => {
if (form.login_mode !== 'auto') {
return true
}
return form.auto_generate_key === true
}
}
const username_same_with_user = {
type: 'switch',
helpText: this.$t('assets.UsernameHelpMessage'),
el: {
disabled: false
},
hidden: form => {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.username_same_with_user.el.disabled = true
}
}
}
const auto_generate_key = {
type: 'switch',
label: this.$t('assets.AutoGenerateKey'),
hidden: form => {
if (JSON.stringify(this.$route.params) !== '{}') {
return true
}
if (form.protocol === 'k8s') {
return true
}
if (form.login_mode === 'manual') {
this.fieldsMeta.auto_generate_key.el.disabled = true
} else {
this.fieldsMeta.auto_generate_key.el.disabled = false
}
},
el: {
disabled: false
}
}
const protocol = {
rules: [Required],
el: {
style: 'width:100%',
disabled: true
}
}
const cmd_filters = {
component: Select2,
el: {
multiple: true,
value: [],
ajax: {
url: '/api/v1/assets/cmd-filters/'
}
}
}
const auto_push = {
type: 'switch',
el: {
disabled: false
},
hidden: form => {
if (form.login_mode === 'manual') {
this.fieldsMeta.auto_push.el.disabled = true
} else {
this.fieldsMeta.auto_push.el.disabled = false
}
},
on: {
input: ([value], updateForm) => {
if (!value) {
updateForm({ auto_generate_key: value })
}
}
}
}
const update_password = {
label: this.$t('users.UpdatePassword'),
type: 'checkbox',
hidden: (formValue) => {
if (formValue.update_password) {
return true
}
if (formValue.login_mode === 'manual') {
return true
}
return !this.$route.params.id
}
}
const password = {
helpText: this.$t('assets.PasswordHelpMessage'),
hidden: form => {
if (form.login_mode !== 'auto' || form.auto_generate_key) {
return true
}
if (!this.$route.params.id) {
return false
}
return !form.update_password
}
}
const system_groups = {
label: this.$t('assets.LinuxUserAffiliateGroup'),
hidden: (item) => !item.auto_push || item.username_same_with_user,
helpText: this.$t('assets.GroupsHelpMessage')
}
return {
login_mode: login_mode,
username: username,
private_key: private_key,
username_same_with_user: username_same_with_user,
auto_generate_key: auto_generate_key,
protocol: protocol,
cmd_filters: cmd_filters,
auto_push: auto_push,
update_password: update_password,
password: password,
system_groups: system_groups
}
}
export default getFields

View File

@@ -26,10 +26,17 @@ export default {
],
fieldsMeta: {
token: {
rules: [Required],
rules: [Object.assign({}, Required)],
el: {
type: 'textarea',
autosize: { minRows: 3 }
},
hidden: form => {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.token.rules[0].required = false
}
}
},
protocol: {
@@ -40,18 +47,7 @@ export default {
}
}
},
url: '/api/v1/assets/system-users/',
authHiden: false
}
},
method: {
},
mounted() {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.token.rules[0].required = false
url: '/api/v1/assets/system-users/'
}
}
}

View File

@@ -4,12 +4,13 @@
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import { Required } from '@/components/DataForm/rules'
import getFields from './fields'
export default {
name: 'SystemUserCreateUpdate',
components: { GenericCreateUpdatePage },
data() {
const fields = getFields.bind(this)()
return {
initial: {
login_mode: 'auto',
@@ -28,131 +29,23 @@ export default {
[this.$t('common.Other'), ['comment']]
],
fieldsMeta: {
login_mode: {
helpText: this.$t('assets.LoginModeHelpMessage'),
hidden: (form) => {
if (form.protocol === 'k8s') {
return true
}
},
on: {
input: ([value], updateForm) => {
if (value === 'manual') {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
username: {
el: {
disabled: false
},
rules: [Required],
hidden: (form) => {
if (form.login_mode === 'auto') {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
if (!form.username_same_with_user) {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
}
},
username_same_with_user: {
type: 'switch',
helpText: this.$t('assets.UsernameHelpMessage'),
hidden: (form) => {
this.fieldsMeta.username.el.disabled = form.username_same_with_user
return form.protocol === 'k8s'
},
el: {
disabled: false
}
},
auto_push: {
type: 'switch',
el: {
disabled: false
},
hidden: form => {
if (form.login_mode === 'manual') { this.fieldsMeta.auto_push.el.disabled = true }
},
on: {
input: ([value], updateForm) => {
if (!value) {
updateForm({ auto_generate_key: value })
}
}
}
},
protocol: {
rules: [Required],
el: {
style: 'width:100%',
disabled: true
},
on: {
input: ([value], updateForm) => {
if (['ssh', 'rdp'].indexOf(value) === -1) {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
login_mode: fields.login_mode,
username: fields.username,
username_same_with_user: fields.username_same_with_user,
auto_push: fields.auto_push,
protocol: fields.protocol,
ad_domain: {
label: this.$t('assets.AdDomain'),
hidden: (form) => ['rdp'].indexOf(form.protocol) === -1,
helpText: this.$t('assets.AdDomainHelpText')
},
update_password: {
label: this.$t('users.UpdatePassword'),
type: 'checkbox',
hidden: (formValue) => {
if (formValue.update_password || formValue.protocol === 'k8s') {
return true
}
if (formValue.login_mode === 'manual') {
return true
}
return !this.$route.params.id
}
},
password: {
helpText: this.$t('assets.PasswordHelpMessage'),
hidden: form => {
if (form.login_mode !== 'auto' || form.protocol === 'k8s' || form.auto_generate_key) {
return true
}
if (!this.$route.params.id) {
return false
}
return !form.update_password
}
},
system_groups: {
label: this.$t('assets.LinuxUserAffiliateGroup'),
hidden: (item) => ['ssh', 'rdp'].indexOf(item.protocol) === -1 || !item.auto_push || item.username_same_with_user,
helpText: this.$t('assets.GroupsHelpMessage')
}
update_password: fields.update_password,
password: fields.password,
system_groups: fields.system_groups
},
url: '/api/v1/assets/system-users/',
authHiden: false
url: '/api/v1/assets/system-users/'
}
},
method: {
},
mounted() {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.username_same_with_user.el.disabled = true
}
}
}
</script>

View File

@@ -4,17 +4,14 @@
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import UploadKey from '@/components/UploadKey'
import { Select2 } from '@/components'
import { Required } from '@/components/DataForm/rules'
// const asciiProtocols = ['ssh', 'telnet', 'mysql']
const graphProtocols = ['vnc', 'rdp', 'k8s']
import getFields from './fields'
export default {
name: 'SystemUserCreateUpdate',
components: { GenericCreateUpdatePage },
data() {
const fields = getFields.bind(this)()
return {
initial: {
login_mode: 'auto',
@@ -29,185 +26,30 @@ export default {
fields: [
[this.$t('common.Basic'), ['name', 'login_mode', 'username', 'username_same_with_user', 'priority', 'protocol']],
[this.$t('assets.AutoPush'), ['auto_push', 'sudo', 'shell', 'home', 'system_groups']],
[this.$t('common.Auth'), ['auto_generate_key', 'update_password', 'password', 'private_key', 'token', 'ad_domain']],
[this.$t('common.Auth'), ['auto_generate_key', 'update_password', 'password', 'private_key']],
[this.$t('common.Command filter'), ['cmd_filters']],
[this.$t('common.Other'), ['sftp_root', 'comment']]
],
fieldsMeta: {
login_mode: {
helpText: this.$t('assets.LoginModeHelpMessage'),
hidden: (form) => {
if (form.protocol === 'k8s') {
return true
}
},
on: {
input: ([value], updateForm) => {
if (value === 'manual') {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
username: {
el: {
disabled: false
},
on: {
input: ([value], updateForm) => {
updateForm({ home: `/home/${value}` })
}
},
rules: [Required],
hidden: (form) => {
if (form.login_mode === 'auto') {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
if (!form.username_same_with_user) {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
if (['mysql', 'postgresql', 'mariadb', 'oracle'].indexOf(form.protocol) !== -1) {
this.fieldsMeta.username.rules = [Required]
this.fieldsMeta.username.rules[0].required = true
}
}
},
private_key: {
component: UploadKey,
hidden: (form) => {
if (form.login_mode !== 'auto') {
return true
}
if (form.protocol === 'k8s') {
return true
}
return form.auto_generate_key === true
}
},
username_same_with_user: {
type: 'switch',
helpText: this.$t('assets.UsernameHelpMessage'),
hidden: (form) => {
this.fieldsMeta.username.el.disabled = form.username_same_with_user
return form.protocol === 'k8s'
},
el: {
disabled: false
}
},
auto_generate_key: {
type: 'switch',
label: this.$t('assets.AutoGenerateKey'),
hidden: form => {
this.fieldsMeta.auto_generate_key.el.disabled = ['ssh', 'rdp'].indexOf(form.protocol) === -1 || form.login_mode === 'manual'
if (JSON.stringify(this.$route.params) !== '{}') {
return true
}
if (form.protocol === 'k8s') {
return true
}
},
el: {
disabled: false
}
},
token: {
rules: [Required],
el: {
type: 'textarea',
autosize: { minRows: 3 }
},
hidden: form => {
return form.protocol !== 'k8s'
}
},
protocol: {
rules: [Required],
el: {
style: 'width:100%',
disabled: true
},
on: {
input: ([value], updateForm) => {
if (['ssh', 'rdp'].indexOf(value) === -1) {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
ad_domain: {
label: this.$t('assets.AdDomain'),
hidden: (form) => ['rdp'].indexOf(form.protocol) === -1,
helpText: this.$t('assets.AdDomainHelpText')
},
cmd_filters: {
component: Select2,
hidden: (form) => graphProtocols.indexOf(form.protocol) !== -1,
el: {
multiple: true,
value: [],
ajax: {
url: '/api/v1/assets/cmd-filters/'
}
}
},
auto_push: {
type: 'switch',
el: {
disabled: false
},
hidden: form => {
this.fieldsMeta.auto_push.el.disabled = ['ssh', 'rdp'].indexOf(form.protocol) === -1 || form.login_mode === 'manual'
},
on: {
input: ([value], updateForm) => {
if (!value) {
updateForm({ auto_generate_key: value })
}
}
}
},
login_mode: fields.login_mode,
username: fields.username,
private_key: fields.private_key,
username_same_with_user: fields.username_same_with_user,
auto_generate_key: fields.auto_generate_key,
protocol: fields.protocol,
cmd_filters: fields.cmd_filters,
auto_push: fields.auto_push,
sftp_root: {
rules: [Required],
helpText: this.$t('assets.SFTPHelpMessage'),
hidden: (item) => item.protocol !== 'ssh'
helpText: this.$t('assets.SFTPHelpMessage')
},
sudo: {
rules: [Required],
helpText: this.$t('assets.SudoHelpMessage'),
hidden: (item) => item.protocol !== 'ssh' || !item.auto_push
},
update_password: {
label: this.$t('users.UpdatePassword'),
type: 'checkbox',
hidden: (formValue) => {
if (formValue.update_password || formValue.protocol === 'k8s') {
return true
}
if (formValue.login_mode === 'manual') {
return true
}
return !this.$route.params.id
}
},
password: {
helpText: this.$t('assets.PasswordHelpMessage'),
hidden: form => {
if (form.login_mode !== 'auto' || form.protocol === 'k8s' || form.auto_generate_key) {
return true
}
if (!this.$route.params.id) {
return false
}
return !form.update_password
}
},
update_password: fields.update_password,
password: fields.password,
shell: {
hidden: (item) => item.protocol !== 'ssh' || !item.auto_push,
rules: [Required]
@@ -217,25 +59,12 @@ export default {
hidden: (item) => item.protocol !== 'ssh' || !item.auto_push || item.username_same_with_user,
helpText: this.$t('assets.HomeHelpMessage')
},
system_groups: {
label: this.$t('assets.LinuxUserAffiliateGroup'),
hidden: (item) => ['ssh', 'rdp'].indexOf(item.protocol) === -1 || !item.auto_push || item.username_same_with_user,
helpText: this.$t('assets.GroupsHelpMessage')
}
system_groups: fields.system_groups
},
url: '/api/v1/assets/system-users/',
authHiden: false
url: '/api/v1/assets/system-users/'
}
},
method: {
},
mounted() {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.username_same_with_user.el.disabled = true
}
}
}
</script>

View File

@@ -4,7 +4,7 @@
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import { Required } from '@/components/DataForm/rules'
import getFields from '@/views/assets/SystemUser/SystemUserCreate/fields'
// const asciiProtocols = ['ssh', 'telnet', 'mysql']
@@ -12,6 +12,7 @@ export default {
name: 'SystemUserCreateUpdate',
components: { GenericCreateUpdatePage },
data() {
const fields = getFields.bind(this)()
return {
initial: {
login_mode: 'auto',
@@ -24,90 +25,14 @@ export default {
[this.$t('common.Other'), ['comment']]
],
fieldsMeta: {
login_mode: {
helpText: this.$t('assets.LoginModeHelpMessage')
},
username: {
el: {
disabled: false
},
rules: [Required],
hidden: (form) => {
if (form.login_mode === 'auto') {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
if (!form.username_same_with_user) {
this.fieldsMeta.username.rules = [Required]
} else {
this.fieldsMeta.username.rules[0].required = false
}
}
},
username_same_with_user: {
type: 'switch',
helpText: this.$t('assets.UsernameHelpMessage'),
hidden: (form) => {
this.fieldsMeta.username.el.disabled = form.username_same_with_user
},
el: {
disabled: false
}
},
protocol: {
rules: [Required],
el: {
disabled: true,
style: 'width:100%'
},
on: {
input: ([value], updateForm) => {
if (['ssh', 'rdp'].indexOf(value) === -1) {
updateForm({ auto_push: false })
updateForm({ auto_generate_key: false })
}
}
}
},
update_password: {
label: this.$t('users.UpdatePassword'),
type: 'checkbox',
hidden: (formValue) => {
if (formValue.update_password || formValue.protocol === 'k8s') {
return true
}
if (formValue.login_mode === 'manual') {
return true
}
return !this.$route.params.id
}
},
password: {
helpText: this.$t('assets.PasswordHelpMessage'),
hidden: form => {
if (form.login_mode !== 'auto' || form.protocol === 'k8s' || form.auto_generate_key) {
return true
}
if (!this.$route.params.id) {
return false
}
return !form.update_password
}
}
login_mode: fields.login_mode,
username: fields.username,
username_same_with_user: fields.username_same_with_user,
protocol: fields.protocol,
update_password: fields.update_password,
password: fields.password
},
url: '/api/v1/assets/system-users/',
authHiden: false
}
},
method: {
},
mounted() {
const params = this.$route.params
const method = params.id ? 'update' : 'create'
if (method === 'update') {
this.fieldsMeta.username_same_with_user.el.disabled = true
url: '/api/v1/assets/system-users/'
}
}
}

View File

@@ -1,12 +1,28 @@
<template>
<div class="statistic-box">
<h4>{{ $t('dashboard.ActiveUserAssetsRatioTitle') }}</h4>
<el-row :gutter="10">
<el-col :md="12" :sm="24">
<el-row :gutter="2">
<el-col :md="12" :sm="10">
<echarts :options="userOption" :autoresize="true" />
<div style="" class="print-display">
<div class="circle-icon" style="background: #1ab394;" />
<label>{{ $t('dashboard.ActiveUser') }}</label>
<div class="circle-icon" style="background: #1C84C6;" />
<label>{{ $t('dashboard.DisabledUser') }}</label>
<div class="circle-icon" style="background: #9CC3DA;" />
<label>{{ $t('dashboard.InActiveUser') }}</label>
</div>
</el-col>
<el-col :md="12" :sm="24">
<el-col :md="12" :sm="10">
<echarts :options="AssetOption" :autoresize="true" />
<div style="" class="print-display">
<div class="circle-icon" style="background: #1ab394;" />
<label>{{ $t('dashboard.ActiveAsset') }}</label>
<div class="circle-icon" style="background: #1C84C6;" />
<label>{{ $t('dashboard.DisabledAsset') }}</label>
<div class="circle-icon" style="background: #9CC3DA;" />
<label>{{ $t('dashboard.InActiveAsset') }}</label>
</div>
</el-col>
</el-row>
</div>
@@ -153,4 +169,23 @@ export default {
width: 100%;
height: 250px;
}
.print-display {
display: none;
}
.circle-icon {
width: 14px;
height: 14px;
-moz-border-radius: 7px;
-webkit-border-radius: 7px;
border-radius: 7px;
display:inline-block;
}
@media print {
.el-col-24{
width: 50% !important;
}
.print-display {
display: inherit;
}
}
</style>

View File

@@ -1,5 +1,8 @@
<template>
<echarts :options="options" :autoresize="true" theme="light" />
<div>
<echarts ref="echarts" :options="options" :autoresize="true" theme="light" class="disabled-when-print" @finished="getDataUrl" />
<img v-if="dataUrl" :src="dataUrl" class="enabled-when-print" style="display: none;width: 100%;">
</div>
</template>
<script>
@@ -15,6 +18,7 @@ export default {
},
data: function() {
return {
dataUrl: '',
metricsData: {
dates_metrics_date: [],
dates_metrics_total_count_active_assets: [],
@@ -106,6 +110,11 @@ export default {
url = `${url}&monthly=1`
}
this.metricsData = await this.$axios.get(url)
},
getDataUrl() {
this.dataUrl = this.$refs.echarts.getDataURL({
})
}
}
}
@@ -116,4 +125,15 @@ export default {
width: 100%;
height: 300px;
}
@media print {
.disabled-when-print{
display: none;
}
.enabled-when-print{
display: inherit !important;
}
.print-margin{
margin-top: 10px;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="white-bg dashboard-header">
<div class="white-bg dashboard-header print-margin">
<el-row>
<el-col :span="12">
<h2>{{ $t('dashboard.LoginOverview') }}</h2>
@@ -65,5 +65,16 @@ export default {
border: 1px solid #d2d2d2;
-webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
}
@media print {
.disabled-when-print{
display: none;
}
.enabled-when-print{
display: inherit !important;
}
.print-margin{
margin-top: 20px;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<el-row :gutter="10">
<el-col v-for="item of summaryItems" :key="item.title" :md="6" :sm="12">
<el-col v-for="item of summaryItems" :key="item.title" :md="6" :sm="12" :xs="12">
<SummaryCard :title="item.title" :right-side-label="item.rightSideLabel" :body="item.body" />
</el-col>
</el-row>
@@ -56,7 +56,7 @@ export default {
type: 'primary'
},
body: {
route: `/terminal/session`,
route: { name: `SessionList`, params: { activeMenu: 'OnlineList' }},
count: this.counter.total_count_online_users,
comment: 'Online users'
}
@@ -68,7 +68,7 @@ export default {
type: 'danger'
},
body: {
route: `/terminal/session`,
route: { name: `SessionList`, params: { activeMenu: 'OnlineList' }},
count: this.counter.total_count_online_sessions,
comment: 'Online sessions'
}
@@ -95,4 +95,10 @@ export default {
}
}
@media print {
.el-col-24{
width: 50% !important;
}
}
</style>

View File

@@ -3,11 +3,11 @@
<el-col :md="8" :sm="12">
<TopUser />
</el-col>
<el-col :md="8" :sm="12">
<el-col :md="8" :sm="12" class="print-margin-top">
<TopAssets />
</el-col>
<el-col :md="8" :sm="12">
<Latest10Sessions class="card-item" />
<Latest10Sessions class="card-item print-margin-top" />
</el-col>
</el-row>
</template>
@@ -28,5 +28,9 @@ export default {
</script>
<style scoped>
@media print {
.print-margin-top{
margin-top: 10px;
}
}
</style>

View File

@@ -108,7 +108,7 @@ export default {
hasLeftActions: false,
hasImport: false,
hasDatePicker: true,
canExportSelected: false,
canExportSelected: true,
datePicker: {
dateStart: dateFrom,
dateEnd: dateTo

View File

@@ -41,6 +41,17 @@ export default {
Title() {
return this.$t('route.Sessions')
}
},
mounted() {
const params = this.$route.params
switch (params.activeMenu) {
case 'OnlineList':
this.config.activeMenu = 'OnlineList'
break
case 'OfflineList':
this.config.activeMenu = 'OfflineList'
break
}
}
}
</script>

View File

@@ -55,14 +55,14 @@ export default {
key: this.$t('sessions.remoteAddr'),
value: this.terminalData.remote_addr
},
{
key: this.$t('sessions.sshPort'),
value: this.terminalData.ssh_port
},
{
key: this.$t('sessions.httpPort'),
value: this.terminalData.http_port
},
// {
// key: this.$t('sessions.sshPort'),
// value: this.terminalData.ssh_port
// },
// {
// key: this.$t('sessions.httpPort'),
// value: this.terminalData.http_port
// },
{
key: this.$t('sessions.dateCreated'),
value: this.terminalData.date_created

View File

@@ -94,8 +94,8 @@ export default {
url: '/api/v1/terminal/terminals/',
columns: [
'name', 'remote_addr', 'session_online',
'state.session_active_count', 'state.system_cpu_load_1',
'state.system_disk_used_percent', 'state.system_memory_used_percent',
'stat.cpu_load',
'stat.disk_used', 'stat.memory_used',
'status_display',
'is_active', 'is_alive', 'actions'
],
@@ -103,19 +103,15 @@ export default {
name: {
sortable: 'custom'
},
'state.session_active_count': {
label: this.$t('sessions.sessionActiveCount'),
width: '120px'
},
'state.system_cpu_load_1': {
'stat.cpu_load': {
label: this.$t('sessions.systemCpuLoad'),
width: '120px'
},
'state.system_disk_used_percent': {
'stat.disk_used': {
label: this.$t('sessions.systemDiskUsedPercent'),
width: '120px'
},
'state.system_memory_used_percent': {
'stat.memory_used': {
label: this.$t('sessions.systemMemoryUsedPercent'),
width: '120px'
},

View File

@@ -7,6 +7,7 @@
:create-success-next-route="successUrl"
:get-method="getMethod"
:has-detail-in-msg="false"
:on-perform-success="onPerformSuccess"
/>
</IBox>
</template>
@@ -23,7 +24,7 @@ export default {
data() {
return {
fields: [
[this.$t('common.BasicInfo'), ['SITE_URL', 'USER_GUIDE_URL', 'FORGOT_PASSWORD_URL']]
[this.$t('common.BasicInfo'), ['SITE_URL', 'USER_GUIDE_URL', 'FORGOT_PASSWORD_URL', 'GLOBAL_ORG_DISPLAY_NAME']]
],
successUrl: { name: 'Settings', params: { activeMenu: 'Basic' }},
url: '/api/v1/settings/setting/?category=basic'
@@ -32,6 +33,9 @@ export default {
methods: {
getMethod() {
return 'put'
},
onPerformSuccess() {
setTimeout(() => window.location.reload(), 500)
}
}
}

View File

@@ -3,7 +3,9 @@
<GenericCreateUpdateForm
:fields="fields"
:url="url"
:perform-submit="performSubmit"
:get-method="getMethod"
:fields-meta="fieldsMeta"
:more-buttons="moreButtons"
:has-detail-in-msg="false"
/>
@@ -14,6 +16,7 @@
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
import { testEmailSetting } from '@/api/settings'
import { IBox } from '@/components'
import rules from '@/components/DataForm/rules'
export default {
name: 'Email',
@@ -51,6 +54,24 @@ export default {
]
]
],
fieldsMeta: {
EMAIL_HOST_USER: {
rules: [
rules.EmailCheck,
rules.Required
]
},
EMAIL_FROM: {
rules: [
rules.EmailCheck
]
},
EMAIL_RECIPIENT: {
rules: [
rules.EmailCheck
]
}
},
url: '/api/v1/settings/setting/?category=email',
moreButtons: [
{
@@ -72,7 +93,19 @@ export default {
methods: {
getMethod() {
return 'put'
},
performSubmit(validValues) {
Object.keys(validValues).forEach(
function(key) {
if (validValues[key] === null) {
delete validValues[key]
}
}
)
return this.$axios['put'](`/api/v1/settings/setting/?category=email`, validValues)
}
}
}

View File

@@ -38,10 +38,16 @@ export default {
url: '/api/v1/users/users/',
fieldsMeta: {
password_strategy: {
hidden: () => {
return this.$route.params.id
hidden: (formValue) => {
return this.$route.params.id || formValue.source !== 'local'
}
},
email: {
rules: [
rules.EmailCheck,
rules.Required
]
},
update_password: {
label: this.$t('users.UpdatePassword'),
type: 'checkbox',
@@ -58,7 +64,7 @@ export default {
if (formValue.password_strategy) {
return false
}
return !formValue.update_password
return !formValue.update_password || formValue.source !== 'local'
},
el: {
required: false
@@ -71,12 +77,12 @@ export default {
if (formValue.set_public_key) {
return true
}
return this.$route.meta.action !== 'update'
return this.$route.meta.action !== 'update' || formValue.source !== 'local'
}
},
public_key: {
hidden: (formValue) => {
return !formValue.set_public_key
return !formValue.set_public_key || formValue.source !== 'local'
}
},
role: {

View File

@@ -65,7 +65,7 @@ export default {
name: 'UserApplicationPermissionRules'
},
{
title: '用户登录规则',
title: this.$t('route.UserAclLists'),
name: 'UserAclList'
}
]

View File

@@ -164,7 +164,7 @@ export default {
{
name: 'updateSelected',
title: this.$t('common.updateSelected'),
can: ({ selectedRows }) => selectedRows.length > 0,
can: ({ selectedRows }) => selectedRows.length > 0 && !vm.currentOrgIsRoot,
callback: ({ selectedRows, reloadTable }) => {
vm.updateSelectedDialogSetting.dialogSetting.dialogVisible = true
vm.updateSelectedDialogSetting.selectedRows = selectedRows
@@ -187,7 +187,7 @@ export default {
fieldsMeta: {
groups: {
label: this.$t('users.UserGroups'),
hidden: () => false,
hidden: () => vm.currentOrgIsRoot,
el: {
multiple: true,
ajax: {

View File

@@ -4,7 +4,7 @@
<script type="text/jsx">
import GenericListTable from '@/layout/components/GenericListTable'
import { ACCOUNT_PROVIDER_ATTRS_MAP, aliyun, aws_china, aws_international, huaweicloud, qcloud, azure, azure_international, vmware } from '../const'
import { ACCOUNT_PROVIDER_ATTRS_MAP, aliyun, aws_china, aws_international, huaweicloud, qcloud, azure, azure_international, vmware, nutanix } from '../const'
import { BooleanFormatter, DetailFormatter } from '@/components/ListTable/formatters'
export default {
@@ -109,6 +109,10 @@ export default {
{
name: vmware,
title: ACCOUNT_PROVIDER_ATTRS_MAP[vmware].title
},
{
name: nutanix,
title: ACCOUNT_PROVIDER_ATTRS_MAP[nutanix].title
}
]
}

View File

@@ -119,7 +119,7 @@ export default {
// 更新获取链接
if (params.id) {
const form = await this.$refs.createUpdatePage.$refs.createUpdateForm.getFormValue()
this.fieldsMeta.regions.el.ajax.url = `/api/v1/xpack/cloud/regions/?account_id=${form.account}`
this.fieldsMeta.regions.el.ajax.url = form.account ? `/api/v1/xpack/cloud/regions/?account_id=${form.account}` : `/api/v1/xpack/cloud/regions/`
}
}
}

View File

@@ -8,6 +8,7 @@ export const qcloud = 'qcloud'
export const azure = 'azure'
export const azure_international = 'azure_international'
export const vmware = 'vmware'
export const nutanix = 'nutanix'
export const ACCOUNT_PROVIDER_ATTRS_MAP = {
[aliyun]: {
@@ -49,5 +50,10 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
name: vmware,
title: 'VMware',
attrs: ['host', 'port', 'username', 'password']
},
[nutanix]: {
name: nutanix,
title: 'Nutanix',
attrs: ['access_key_id', 'access_key_secret', 'api_endpoint']
}
}

View File

@@ -1,9 +1,7 @@
<template>
<div>
<div style="font-size: 24px;font-weight: 300">
<span v-if="type === 'omnidb'">{{ `OmniDB ( ${serviceData.total} )` }}</span>
<span v-else-if="type === 'guacamole'">{{ `Guacamole ( ${serviceData.total} )` }}</span>
<span v-else>{{ `KoKo ( ${serviceData.total} )` }}</span>
<span>{{ componentName }} ( {{ componentMetric.total }} )</span>
</div>
<el-card class="box-card" shadow="never">
<el-row :gutter="10">
@@ -14,40 +12,40 @@
<div
class="progress-bar progress-bar-success"
role="progressbar"
:style="{'width':toPercent(serviceData.normal) }"
:style="{'width':toPercent(componentMetric.normal) }"
/>
<div
class="progress-bar progress-bar-warning"
role="progressbar"
:style="{'width':toPercent(serviceData.high) }"
:style="{'width':toPercent(componentMetric.high) }"
/>
<div
class="progress-bar progress-bar-danger"
role="progressbar"
:style="{'width':toPercent(serviceData.critical) }"
:style="{'width':toPercent(componentMetric.critical) }"
/>
<div
class="progress-bar progress-bar-offline"
role="progressbar"
:style="{'width':toPercent(serviceData.offline) }"
:style="{'width':toPercent(componentMetric.offline) }"
/>
</div>
<div style="display: flex;justify-content: space-around;font-size: 14px;">
<span>
<i class="el-icon-circle-check" style="color: #13CE66;" />
{{ $t('xpack.NormalLoad') }}: {{ serviceData.normal }}
{{ $t('xpack.NormalLoad') }}: {{ componentMetric.normal }}
</span>
<span>
<i class="el-icon-bell" style="color: #E6A23C;" />
{{ $t('xpack.HighLoad') }}: {{ serviceData.high }}
{{ $t('xpack.HighLoad') }}: {{ componentMetric.high }}
</span>
<span>
<i class="el-icon-message-solid" style="color: #FF4949;" />
{{ $t('xpack.CriticalLoad') }}: {{ serviceData.critical }}
{{ $t('xpack.CriticalLoad') }}: {{ componentMetric.critical }}
</span>
<span>
<i class="el-icon-circle-close" style="color: #bfbaba;" />
{{ $t('xpack.Offline') }}: {{ serviceData.offline }}
{{ $t('xpack.Offline') }}: {{ componentMetric.offline }}
</span>
</div>
</div>
@@ -56,7 +54,7 @@
<div style="height: 100%;width: 100%;padding-top: 8px;">
<h2 style="text-align: center;font-weight: 350">{{ $t('dashboard.OnlineSessions') }}</h2>
<div style="text-align: center;font-size: 30px;">
{{ serviceData.session_active }}
{{ componentMetric.session_active }}
</div>
</div>
</el-col>
@@ -70,7 +68,6 @@
export default {
name: 'MonitorCard',
components: {
},
props: {
// koko/guacamole/omnidb/core
@@ -78,29 +75,25 @@ export default {
type: String,
default: 'koko',
required: true
}
},
data() {
return {
baseUrl: `/api/v1/terminal/components/metrics/?type=`,
serviceData: {
}
},
componentMetric: {
type: Object,
default: () => ({})
}
},
computed: {
},
mounted() {
this.getServiceData()
componentName() {
const nameMapper = {
koko: 'KoKo',
guacamole: 'Guacamole',
omnidb: 'OmniDB'
}
return nameMapper[this.componentMetric.type]
}
},
methods: {
async getServiceData() {
const url = `${this.baseUrl}${this.type}`
this.serviceData = await this.$axios.get(url)
},
toPercent(num) {
return (Math.round(num / this.serviceData.total * 10000) / 100.00 + '%')// 小数点后两位百分比
return (Math.round(num / this.componentMetric.total * 10000) / 100.00 + '%')// 小数点后两位百分比
}
}
}

View File

@@ -1,18 +1,12 @@
<template>
<Page>
<el-row :gutter="40">
<el-col :lg="12" :md="24">
<MonitorCard type="koko" class="monitorCard" />
</el-col>
<el-col :lg="12" :md="24">
<MonitorCard type="guacamole" class="monitorCard" />
</el-col>
<el-col :lg="12" :md="24">
<MonitorCard type="omnidb" class="monitorCard" />
<el-row v-if="loaded" :gutter="40">
<el-col v-for="metric of metricsData" :key="metric.type" :lg="12" :md="24">
<MonitorCard :type="metric.type" :component-metric="metric" class="monitorCard" />
</el-col>
</el-row>
</Page>
</template>lg
</template>
<script>
import Page from '@/layout/components/Page/index'
@@ -26,9 +20,24 @@ export default {
},
data() {
return {
metricsData: [],
loaded: false
}
},
computed: {
},
mounted() {
this.getMetricsData()
},
methods: {
async getMetricsData() {
const url = '/api/v1/terminal/components/metrics/'
this.$axios.get(url).then((data) => {
this.metricsData = data
}).finally(() => {
this.loaded = true
})
}
}
}
</script>

View File

@@ -1,6 +1,9 @@
'use strict'
const path = require('path')
const defaultSettings = require('./src/settings.js')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = /\.(js|css|json|txt|ico|svg)(\?.*)?$/i
function resolve(dir) {
return path.join(__dirname, dir)
@@ -82,7 +85,15 @@ module.exports = {
alias: {
'@': resolve('src')
}
}
},
plugins: [
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: productionGzipExtensions, // 处理所有匹配此 {RegExp} 的资源
threshold: 10240, // 只处理比这个值大的资源。按字节计算(楼主设置10K以上进行压缩)
minRatio: 0.8 // 只有压缩率比这个值小的资源才会被处理
})
]
},
chainWebpack(config) {
// it can improve the speed of the first screen, it is recommended to turn on preload

1370
yarn.lock

File diff suppressed because it is too large Load Diff