Merge pull request #733 from jumpserver/dev

Merge Dev v2.9
This commit is contained in:
老广
2021-04-08 06:24:13 -05:00
committed by GitHub
39 changed files with 2406 additions and 286 deletions

View File

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

View File

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

View File

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

View File

@@ -723,6 +723,10 @@ export default {
default(row, index) { default(row, index) {
return true return true
} }
},
totalData: {
type: Array,
default: null
} }
}, },
data() { data() {
@@ -828,6 +832,12 @@ export default {
* @property {array} rows - 已选中的行数据的数组 * @property {array} rows - 已选中的行数据的数组
*/ */
this.$emit('selection-change', val) this.$emit('selection-change', val)
},
totalData(val) {
if (val) {
this.total = val.length
this.getList()
}
} }
}, },
mounted() { mounted() {
@@ -877,12 +887,34 @@ export default {
} }
return query return query
}, },
getList({ loading = true } = {}) {
const { url } = this
if (url) {
return this.getListFromRemote({ loading: loading })
}
if (this.totalData) {
this.getListFromStaticData()
}
},
getListFromStaticData() {
if (!this.hasPagination) {
this.data = this.totalData
return
}
// 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
console.log(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
this.data = this.totalData.slice(start, end)
},
/** /**
* 手动刷新列表数据,选项的默认值为: { loading: true } * 手动刷新列表数据,选项的默认值为: { loading: true }
* @public * @public
* @param {object} options 方法选项 * @param {object} options 方法选项
*/ */
getList({ loading = true } = {}) { getListFromRemote({ loading = true } = {}) {
const { url } = this const { url } = this
if (!url) { if (!url) {
return return

View File

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

View File

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

View File

@@ -1,64 +1,74 @@
<template> <template>
<Dialog <Dialog
:title="$t('common.Import')" :title="importTitle"
:visible.sync="showImportDialog" :visible.sync="showImportDialog"
:destroy-on-close="true" :destroy-on-close="true"
:close-on-click-modal="false"
:loading-status="loadStatus" :loading-status="loadStatus"
@confirm="handleImportConfirm" width="80%"
@cancel="handleImportCancel()" class="importDialog"
:confirm-title="confirmTitle"
:show-cancel="false"
:show-confirm="false"
@close="handleImportCancel"
> >
<el-form label-position="left" style="padding-left: 50px"> <el-form v-if="!showTable" 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-item :label="$t('common.Import' )" :label-width="'100px'"> <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="create">{{ 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="update">{{ this.$t('common.Update') }}</el-radio>
<div style="line-height: 1.5"> <div style="line-height: 1.5">
<span v-if="importOption==='1'" class="el-upload__tip"> <span class="el-upload__tip">
{{ this.$t('common.imExport.downloadImportTemplateMsg') }} {{ downloadTemplateTitle }}
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link> <el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('csv')"> CSV </el-link>
</span> <el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
<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> </span>
</div> </div>
</el-form-item> </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 <el-upload
ref="upload" ref="upload"
drag
action="string" action="string"
list-type="text/csv" list-type="text/csv"
:http-request="handleImport"
:limit="1" :limit="1"
:auto-upload="false" :auto-upload="false"
:on-change="onFileChange"
:before-upload="beforeUpload" :before-upload="beforeUpload"
accept=".csv,.xlsx"
> >
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button> <i class="el-icon-upload" />
<!-- <div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>--> <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-upload>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div v-if="errorMsg" class="error-msg error-results"> <div v-else class="importTableZone">
<ul v-if="typeof errorMsg === 'object'"> <ImportTable
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li> ref="importTable"
</ul> :json-data="jsonData"
<span v-else>{{ errorMsg }}</span> :import-option="importOption"
:url="url"
@cancel="cancelUpload"
@finish="closeDialog"
/>
</div> </div>
</Dialog> </Dialog>
</template> </template>
<script> <script>
import Dialog from '@/components/Dialog' import Dialog from '@/components/Dialog'
import ImportTable from '@/components/ListTable/TableAction/ImportTable'
import { getErrorResponseMsg } from '@/utils/common'
import { createSourceIdCache } from '@/api/common' import { createSourceIdCache } from '@/api/common'
export default { export default {
name: 'ImportDialog', name: 'ImportDialog',
components: { components: {
Dialog Dialog,
ImportTable
}, },
props: { props: {
selectedRows: { selectedRows: {
@@ -73,45 +83,49 @@ export default {
data() { data() {
return { return {
showImportDialog: false, showImportDialog: false,
importOption: '1', importOption: 'create',
isCsv: true,
errorMsg: '', errorMsg: '',
loadStatus: false, loadStatus: false,
importTypeOption: 'csv' importTypeOption: 'csv',
importTypeIsCsv: true,
showTable: false,
renderError: '',
hasFileFormatOrSizeError: false,
jsonData: {}
} }
}, },
computed: { computed: {
hasSelected() { hasSelected() {
return this.selectedRows.length > 0 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() { uploadHelpTextClass() {
const cls = ['el-upload__tip'] const cls = ['el-upload__tip']
if (!this.isCsv) { if (!this.isCsv) {
cls.push('error-msg') cls.push('error-msg')
} }
return cls 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() { mounted() {
@@ -120,56 +134,67 @@ export default {
}) })
}, },
methods: { methods: {
performUpdate(item) { closeDialog() {
this.$axios.put( this.showImportDialog = false
this.upLoadUrl, },
item.file, cancelUpload() {
{ headers: { 'Content-Type': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true } this.showTable = false
).then((data) => { this.renderError = ''
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length }) this.jsonData = {}
this.onSuccess(msg) },
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 renderToJsonUrl = this.url + 'render-to-json/'
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 => { }).catch(error => {
this.catchError(error) fileList.splice(0, fileList.length)
this.renderError = getErrorResponseMsg(error)
}).finally(() => { }).finally(() => {
this.loadStatus = false this.loadStatus = false
}) })
}, },
performCreate(item) { beforeUpload(file) {
this.$axios.post( const isLt30M = file.size / 1024 / 1024 < 30
this.upLoadUrl, if (!isLt30M) {
item.file, this.hasFileFormatOrSizeError = true
{ headers: { 'Content-Type': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true } }
).then((data) => { return isLt30M
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length }) },
this.onSuccess(msg) async downloadTemplateFile(tp) {
}).catch(error => { const downloadUrl = await this.getDownloadTemplateUrl(tp)
this.catchError(error) window.open(downloadUrl)
}).finally(() => { },
this.loadStatus = false 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) { catchError(error) {
this.$refs.upload.clearFiles() console.log(error)
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
}
}, },
onSuccess(msg) { onSuccess(msg) {
this.errorMsg = '' this.errorMsg = ''
@@ -181,40 +206,14 @@ export default {
a.click() a.click()
window.URL.revokeObjectURL(url) 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() { async handleImportConfirm() {
this.$refs.upload.submit() this.$refs['importTable'].performUpload()
}, },
handleImportCancel() { handleImportCancel() {
this.showImportDialog = false this.showImportDialog = false
}, this.showTable = false
beforeUpload(file) { this.renderError = ''
this.isCsv = this.importTypeOption === 'csv' ? _.endsWith(file.name, 'csv') : _.endsWith(file.name, 'xlsx') this.jsonData = {}
if (!this.isCsv) {
this.$message.error(
this.$t('common.NeedSpecifiedFile')
)
}
return this.isCsv
} }
} }
} }
@@ -231,4 +230,49 @@ export default {
overflow: auto 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> </style>

View File

@@ -0,0 +1,385 @@
<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>
</el-col>
</el-row>
<div class="row">
<el-progress :percentage="processedPercent" />
</div>
<DataTable v-if="tableGenDone" id="importTable" :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: [],
tableAttrs: {
stripe: true, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
height: '60vh'
}
},
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)
}
},
watch: {
importStatusFilter(val) {
if (val === 'all') {
this.tableConfig.totalData = this.iTotalData
} else if (val === 'error') {
this.tableConfig.totalData = this.iTotalData.filter((item) => {
return item['@status'].name === 'error'
})
} 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, col) {
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
})
}
return columns
},
generateTableData(tableTitles, tableData) {
const totalData = []
tableData.forEach((item, index) => {
if (!item.id) {
item.id = index
}
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.$emit('cancel')
},
performFinish() {
this.$emit('finish')
},
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() {
for (const item of this.failedData) {
item['@status'] = 'pending'
}
setTimeout(() => {
this.performUpload()
}, 100)
},
performStop() {
this.importTaskStatus = 'stopped'
},
async performUpload() {
this.importTaskStatus = 'started'
for (const item of this.pendingData) {
if (this.importTaskStatus === 'stopped') {
return
}
await this.performUploadObject(item)
await sleep(100)
}
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
}
} finally {
const tableRef = document.getElementById('importTable')
const pendingRef = tableRef?.getElementsByClassName('pendingStatus')[0]
if (pendingRef) {
const parentTdRef = pendingRef.parentElement.parentElement.parentElement.parentElement
if (!this.isElementInViewport(parentTdRef)) {
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
}
}
}
},
async performCreateObject(item) {
return this.$axios.post(
this.url,
item,
{ disableFlashErrorMsg: true }
)
},
isElementInViewport(el) {
const rect = el.getBoundingClientRect()
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
windowInnerHeight = windowInnerHeight * 0.97 - 150
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowInnerHeight
)
}
}
}
</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;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div @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
row[prop] = newValue
}
}
}
}
},
data() {
const valueIsString = typeof this.cellValue === 'string'
const jsonValue = 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 = ''
if (this.valueIsString) {
validValue = this.value
} else {
validValue = JSON.parse(validValue)
}
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 ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
import DialogDetailFormatter from './DialogDetailFormatter' import DialogDetailFormatter from './DialogDetailFormatter'
import LoadingActionsFormatter from './LoadingActionsFormatter' import LoadingActionsFormatter from './LoadingActionsFormatter'
import EditableInputFormatter from './EditableInputFormatter'
import StatusFormatter from './StatusFormatter'
export default { export default {
DetailFormatter, DetailFormatter,
@@ -23,7 +25,9 @@ export default {
ShowKeyFormatter, ShowKeyFormatter,
DialogDetailFormatter, DialogDetailFormatter,
LoadingActionsFormatter, LoadingActionsFormatter,
ArrayFormatter ArrayFormatter,
EditableInputFormatter,
StatusFormatter
} }
export { export {
@@ -38,5 +42,7 @@ export {
ShowKeyFormatter, ShowKeyFormatter,
DialogDetailFormatter, DialogDetailFormatter,
LoadingActionsFormatter, LoadingActionsFormatter,
ArrayFormatter ArrayFormatter,
EditableInputFormatter,
StatusFormatter
} }

View File

@@ -45,17 +45,6 @@ export default {
dataTable() { dataTable() {
return this.$refs.dataTable.$refs.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() { iTableConfig() {
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery }) const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
this.$log.debug('Header actions', this.headerActions) this.$log.debug('Header actions', this.headerActions)

View File

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

View File

@@ -178,7 +178,7 @@
"TestAssetsConnective": "Test assets connective", "TestAssetsConnective": "Test assets connective",
"TestConnection": "Test connection", "TestConnection": "Test connection",
"Type": "Type", "Type": "Type",
"UnselectedAssets": "Unselected assets", "UnselectedAssets": "No asset selected or the selected asset does not support SSH protocol connection",
"UnselectedNodes": "Unselected nodes", "UnselectedNodes": "Unselected nodes",
"UpdateAssetUserToken": "Update asset user auth", "UpdateAssetUserToken": "Update asset user auth",
"Username": "Username", "Username": "Username",
@@ -257,6 +257,10 @@
"EnterForSearch": "Press enter to search", "EnterForSearch": "Press enter to search",
"Export": "Export", "Export": "Export",
"Import": "Import", "Import": "Import",
"ContinueImport": "ContinueImport",
"Continue": "Continue",
"Stop": "Stop",
"Finished": "Finished",
"Refresh": "Refresh", "Refresh": "Refresh",
"Info": "Info", "Info": "Info",
"MFAConfirm": "MFA Confirm", "MFAConfirm": "MFA Confirm",
@@ -277,6 +281,7 @@
"Reset": "Reset", "Reset": "Reset",
"Search": "Search", "Search": "Search",
"MFAErrorMsg": "MFA Errorplease check", "MFAErrorMsg": "MFA Errorplease check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select", "Select": "Select",
"SelectFile": "Select file", "SelectFile": "Select file",
"Show": "Show", "Show": "Show",
@@ -318,6 +323,11 @@
"fieldRequiredError": "This field is required", "fieldRequiredError": "This field is required",
"getErrorMsg": "Get failed", "getErrorMsg": "Get failed",
"fileType": "File type", "fileType": "File type",
"Status": "Status",
"Total": "Total",
"Success": "Success",
"Failed": "Failed",
"Pending": "Pending",
"imExport": { "imExport": {
"ExportAll": "Export all", "ExportAll": "Export all",
"ExportOnlyFiltered": "Export only filtered", "ExportOnlyFiltered": "Export only filtered",
@@ -327,7 +337,10 @@
"downloadImportTemplateMsg": "Download import template", "downloadImportTemplateMsg": "Download import template",
"downloadUpdateTemplateMsg": "Download update template", "downloadUpdateTemplateMsg": "Download update template",
"onlyCSVFilesTips": "Only csv supported", "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": "Has import failed item, click to edit it and continue upload"
}, },
"isValid": "Is valid", "isValid": "Is valid",
"nav": { "nav": {

View File

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

View File

@@ -1,14 +1,14 @@
<template> <template>
<div :class="classObj" class="app-wrapper"> <div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> <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="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}"> <div :class="{'fixed-header':fixedHeader}" class="disabled-when-print">
<NavHeader /> <NavHeader />
<tags-view v-if="needTagsView" /> <tags-view v-if="needTagsView" />
</div> </div>
<app-main /> <app-main />
<Footer /> <Footer class="disabled-when-print" />
</div> </div>
</div> </div>
</template> </template>
@@ -98,4 +98,28 @@ export default {
.mobile .fixed-header { .mobile .fixed-header {
width: 100%; 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> </style>

View File

@@ -187,6 +187,19 @@ export function getDayFuture(days, now) {
return new Date(now.getTime() + 3600 * 1000 * 24 * days) 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))
}
const scheme = document.location.protocol const scheme = document.location.protocol
const port = document.location.port ? ':' + document.location.port : '' const port = document.location.port ? ':' + document.location.port : ''
const BASE_URL = scheme + '//' + document.location.hostname + port const BASE_URL = scheme + '//' + document.location.hostname + port

View File

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

View File

@@ -19,7 +19,9 @@ export default {
return { return {
initial: { initial: {
type: appTypeMeta.name, type: appTypeMeta.name,
path: pathInitial attrs: {
path: pathInitial
}
}, },
fields: [ fields: [
[this.$t('common.Basic'), ['name', 'type']], [this.$t('common.Basic'), ['name', 'type']],

View File

@@ -1,5 +1,5 @@
<template> <template>
<GenericCreateUpdatePage v-bind="$data" /> <GenericCreateUpdatePage v-bind="$data" :perform-submit="performSubmit" />
</template> </template>
<script> <script>
@@ -94,6 +94,19 @@ export default {
updateSuccessNextRoute: { name: 'AssetList' }, updateSuccessNextRoute: { name: 'AssetList' },
createSuccessNextRoute: { name: 'AssetList' } createSuccessNextRoute: { name: 'AssetList' }
} }
},
methods: {
getUrl() {
const params = this.$route.params
let url = this.url
if (params.id) {
url = `${url}${params.id}/`
}
return url
},
performSubmit(validValues) {
return this.$axios['patch'](this.getUrl(), validValues)
}
} }
} }
</script> </script>

View File

@@ -23,7 +23,7 @@ export default {
}, },
fields: [ fields: [
[this.$t('common.Basic'), ['name', 'login_mode', 'username', 'username_same_with_user', 'priority', 'protocol']], [this.$t('common.Basic'), ['name', 'login_mode', 'username', 'username_same_with_user', 'priority', 'protocol']],
[this.$t('assets.AutoPush'), ['auto_push']], [this.$t('assets.AutoPush'), ['auto_push', 'system_groups']],
[this.$t('common.Auth'), ['update_password', 'password', 'ad_domain']], [this.$t('common.Auth'), ['update_password', 'password', 'ad_domain']],
[this.$t('common.Other'), ['comment']] [this.$t('common.Other'), ['comment']]
], ],
@@ -133,6 +133,11 @@ export default {
} }
return !form.update_password 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')
} }
}, },
url: '/api/v1/assets/system-users/', url: '/api/v1/assets/system-users/',

View File

@@ -115,7 +115,6 @@ export default {
name: 'K8S', name: 'K8S',
title: 'K8S', title: 'K8S',
type: 'primary', type: 'primary',
has: this.$store.getters.hasValidLicense,
group: this.$t('assets.OtherProtocol') group: this.$t('assets.OtherProtocol')
} }
] ]

View File

@@ -1,12 +1,28 @@
<template> <template>
<div class="statistic-box"> <div class="statistic-box">
<h4>{{ $t('dashboard.ActiveUserAssetsRatioTitle') }}</h4> <h4>{{ $t('dashboard.ActiveUserAssetsRatioTitle') }}</h4>
<el-row :gutter="10"> <el-row :gutter="2">
<el-col :md="12" :sm="24"> <el-col :md="12" :sm="10">
<echarts :options="userOption" :autoresize="true" /> <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>
<el-col :md="12" :sm="24"> <el-col :md="12" :sm="10">
<echarts :options="AssetOption" :autoresize="true" /> <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-col>
</el-row> </el-row>
</div> </div>
@@ -153,4 +169,23 @@ export default {
width: 100%; width: 100%;
height: 250px; 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> </style>

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="white-bg dashboard-header"> <div class="white-bg dashboard-header print-margin">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<h2>{{ $t('dashboard.LoginOverview') }}</h2> <h2>{{ $t('dashboard.LoginOverview') }}</h2>
@@ -65,5 +65,16 @@ export default {
border: 1px solid #d2d2d2; border: 1px solid #d2d2d2;
-webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.125); -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> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<el-row :gutter="10"> <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" /> <SummaryCard :title="item.title" :right-side-label="item.rightSideLabel" :body="item.body" />
</el-col> </el-col>
</el-row> </el-row>
@@ -95,4 +95,10 @@ export default {
} }
} }
@media print {
.el-col-24{
width: 50% !important;
}
}
</style> </style>

View File

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

View File

@@ -4,6 +4,7 @@
:create-success-next-route="successUrl" :create-success-next-route="successUrl"
:update-success-next-route="successUrl" :update-success-next-route="successUrl"
:has-detail-in-msg="false" :has-detail-in-msg="false"
:after-get-form-value="afterGetFormValue"
:perform-submit="performSubmit" :perform-submit="performSubmit"
/> />
</template> </template>
@@ -59,6 +60,10 @@ export default {
}, },
methods: { methods: {
afterGetFormValue(validValues) {
validValues.meta.HOSTS = validValues.meta.HOSTS.toString()
return validValues
},
performSubmit(validValues) { performSubmit(validValues) {
const method = this.getMethod() const method = this.getMethod()
validValues.meta.HOSTS = validValues.meta.HOSTS.split(',').map(item => (item.trim())) validValues.meta.HOSTS = validValues.meta.HOSTS.split(',').map(item => (item.trim()))

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
:fields="fields" :fields="fields"
:url="url" :url="url"
:get-method="getMethod" :get-method="getMethod"
:fields-meta="fieldsMeta"
:more-buttons="moreButtons" :more-buttons="moreButtons"
:has-detail-in-msg="false" :has-detail-in-msg="false"
/> />
@@ -14,6 +15,7 @@
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm' import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
import { testEmailSetting } from '@/api/settings' import { testEmailSetting } from '@/api/settings'
import { IBox } from '@/components' import { IBox } from '@/components'
import rules from '@/components/DataForm/rules'
export default { export default {
name: 'Email', name: 'Email',
@@ -51,6 +53,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', url: '/api/v1/settings/setting/?category=email',
moreButtons: [ moreButtons: [
{ {

View File

@@ -10,6 +10,7 @@ import { GenericCreateUpdatePage } from '@/layout/components'
import UserPassword from '@/components/UserPassword' import UserPassword from '@/components/UserPassword'
import RoleCheckbox from '@/views/users/User/components/RoleCheckbox' import RoleCheckbox from '@/views/users/User/components/RoleCheckbox'
import rules from '@/components/DataForm/rules' import rules from '@/components/DataForm/rules'
import { mapGetters } from 'vuex'
export default { export default {
components: { components: {
@@ -37,10 +38,16 @@ export default {
url: '/api/v1/users/users/', url: '/api/v1/users/users/',
fieldsMeta: { fieldsMeta: {
password_strategy: { password_strategy: {
hidden: () => { hidden: (formValue) => {
return this.$route.params.id return this.$route.params.id || formValue.source !== 'local'
} }
}, },
email: {
rules: [
rules.EmailCheck,
rules.Required
]
},
update_password: { update_password: {
label: this.$t('users.UpdatePassword'), label: this.$t('users.UpdatePassword'),
type: 'checkbox', type: 'checkbox',
@@ -57,7 +64,7 @@ export default {
if (formValue.password_strategy) { if (formValue.password_strategy) {
return false return false
} }
return !formValue.update_password return !formValue.update_password || formValue.source !== 'local'
}, },
el: { el: {
required: false required: false
@@ -70,12 +77,12 @@ export default {
if (formValue.set_public_key) { if (formValue.set_public_key) {
return true return true
} }
return this.$route.meta.action !== 'update' return this.$route.meta.action !== 'update' || formValue.source !== 'local'
} }
}, },
public_key: { public_key: {
hidden: (formValue) => { hidden: (formValue) => {
return !formValue.set_public_key return !formValue.set_public_key || formValue.source !== 'local'
} }
}, },
role: { role: {
@@ -109,6 +116,14 @@ export default {
} }
} }
}, },
computed: {
...mapGetters(['currentOrgIsRoot'])
},
mounted() {
if (this.currentOrgIsRoot) {
this.fieldsMeta.groups.el.disabled = true
}
},
methods: { methods: {
cleanFormValue(value) { cleanFormValue(value) {
const method = this.getMethod() const method = this.getMethod()

View File

@@ -4,7 +4,7 @@
<script type="text/jsx"> <script type="text/jsx">
import GenericListTable from '@/layout/components/GenericListTable' 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' import { BooleanFormatter, DetailFormatter } from '@/components/ListTable/formatters'
export default { export default {
@@ -109,6 +109,10 @@ export default {
{ {
name: vmware, name: vmware,
title: ACCOUNT_PROVIDER_ATTRS_MAP[vmware].title title: ACCOUNT_PROVIDER_ATTRS_MAP[vmware].title
},
{
name: nutanix,
title: ACCOUNT_PROVIDER_ATTRS_MAP[nutanix].title
} }
] ]
} }

View File

@@ -8,6 +8,7 @@ export const qcloud = 'qcloud'
export const azure = 'azure' export const azure = 'azure'
export const azure_international = 'azure_international' export const azure_international = 'azure_international'
export const vmware = 'vmware' export const vmware = 'vmware'
export const nutanix = 'nutanix'
export const ACCOUNT_PROVIDER_ATTRS_MAP = { export const ACCOUNT_PROVIDER_ATTRS_MAP = {
[aliyun]: { [aliyun]: {
@@ -49,5 +50,10 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
name: vmware, name: vmware,
title: 'VMware', title: 'VMware',
attrs: ['host', 'port', 'username', 'password'] 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> <template>
<div> <div>
<div style="font-size: 24px;font-weight: 300"> <div style="font-size: 24px;font-weight: 300">
<span v-if="type === 'omnidb'">{{ `OmniDB ( ${serviceData.total} )` }}</span> <span>{{ componentName }} ( {{ componentMetric.total }} )</span>
<span v-else-if="type === 'guacamole'">{{ `Guacamole ( ${serviceData.total} )` }}</span>
<span v-else>{{ `KoKo ( ${serviceData.total} )` }}</span>
</div> </div>
<el-card class="box-card" shadow="never"> <el-card class="box-card" shadow="never">
<el-row :gutter="10"> <el-row :gutter="10">
@@ -14,40 +12,40 @@
<div <div
class="progress-bar progress-bar-success" class="progress-bar progress-bar-success"
role="progressbar" role="progressbar"
:style="{'width':toPercent(serviceData.normal) }" :style="{'width':toPercent(componentMetric.normal) }"
/> />
<div <div
class="progress-bar progress-bar-warning" class="progress-bar progress-bar-warning"
role="progressbar" role="progressbar"
:style="{'width':toPercent(serviceData.high) }" :style="{'width':toPercent(componentMetric.high) }"
/> />
<div <div
class="progress-bar progress-bar-danger" class="progress-bar progress-bar-danger"
role="progressbar" role="progressbar"
:style="{'width':toPercent(serviceData.critical) }" :style="{'width':toPercent(componentMetric.critical) }"
/> />
<div <div
class="progress-bar progress-bar-offline" class="progress-bar progress-bar-offline"
role="progressbar" role="progressbar"
:style="{'width':toPercent(serviceData.offline) }" :style="{'width':toPercent(componentMetric.offline) }"
/> />
</div> </div>
<div style="display: flex;justify-content: space-around;font-size: 14px;"> <div style="display: flex;justify-content: space-around;font-size: 14px;">
<span> <span>
<i class="el-icon-circle-check" style="color: #13CE66;" /> <i class="el-icon-circle-check" style="color: #13CE66;" />
{{ $t('xpack.NormalLoad') }}: {{ serviceData.normal }} {{ $t('xpack.NormalLoad') }}: {{ componentMetric.normal }}
</span> </span>
<span> <span>
<i class="el-icon-bell" style="color: #E6A23C;" /> <i class="el-icon-bell" style="color: #E6A23C;" />
{{ $t('xpack.HighLoad') }}: {{ serviceData.high }} {{ $t('xpack.HighLoad') }}: {{ componentMetric.high }}
</span> </span>
<span> <span>
<i class="el-icon-message-solid" style="color: #FF4949;" /> <i class="el-icon-message-solid" style="color: #FF4949;" />
{{ $t('xpack.CriticalLoad') }}: {{ serviceData.critical }} {{ $t('xpack.CriticalLoad') }}: {{ componentMetric.critical }}
</span> </span>
<span> <span>
<i class="el-icon-circle-close" style="color: #bfbaba;" /> <i class="el-icon-circle-close" style="color: #bfbaba;" />
{{ $t('xpack.Offline') }}: {{ serviceData.offline }} {{ $t('xpack.Offline') }}: {{ componentMetric.offline }}
</span> </span>
</div> </div>
</div> </div>
@@ -56,7 +54,7 @@
<div style="height: 100%;width: 100%;padding-top: 8px;"> <div style="height: 100%;width: 100%;padding-top: 8px;">
<h2 style="text-align: center;font-weight: 350">{{ $t('dashboard.OnlineSessions') }}</h2> <h2 style="text-align: center;font-weight: 350">{{ $t('dashboard.OnlineSessions') }}</h2>
<div style="text-align: center;font-size: 30px;"> <div style="text-align: center;font-size: 30px;">
{{ serviceData.session_active }} {{ componentMetric.session_active }}
</div> </div>
</div> </div>
</el-col> </el-col>
@@ -70,7 +68,6 @@
export default { export default {
name: 'MonitorCard', name: 'MonitorCard',
components: { components: {
}, },
props: { props: {
// koko/guacamole/omnidb/core // koko/guacamole/omnidb/core
@@ -78,29 +75,25 @@ export default {
type: String, type: String,
default: 'koko', default: 'koko',
required: true required: true
} },
}, componentMetric: {
data() { type: Object,
return { default: () => ({})
baseUrl: `/api/v1/terminal/components/metrics/?type=`,
serviceData: {
}
} }
}, },
computed: { computed: {
componentName() {
}, const nameMapper = {
mounted() { koko: 'KoKo',
this.getServiceData() guacamole: 'Guacamole',
omnidb: 'OmniDB'
}
return nameMapper[this.componentMetric.type]
}
}, },
methods: { methods: {
async getServiceData() {
const url = `${this.baseUrl}${this.type}`
this.serviceData = await this.$axios.get(url)
},
toPercent(num) { 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> <template>
<Page> <Page>
<el-row :gutter="40"> <el-row v-if="loaded" :gutter="40">
<el-col :lg="12" :md="24"> <el-col v-for="metric of metricsData" :key="metric.type" :lg="12" :md="24">
<MonitorCard type="koko" class="monitorCard" /> <MonitorCard :type="metric.type" :component-metric="metric" 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-col> </el-col>
</el-row> </el-row>
</Page> </Page>
</template>lg </template>
<script> <script>
import Page from '@/layout/components/Page/index' import Page from '@/layout/components/Page/index'
@@ -26,9 +20,24 @@ export default {
}, },
data() { data() {
return { return {
metricsData: [],
loaded: false
} }
}, },
computed: { 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> </script>

View File

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