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 = {
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

@@ -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

@@ -723,6 +723,10 @@ export default {
default(row, index) {
return true
}
},
totalData: {
type: Array,
default: null
}
},
data() {
@@ -828,6 +832,12 @@ export default {
* @property {array} rows - 已选中的行数据的数组
*/
this.$emit('selection-change', val)
},
totalData(val) {
if (val) {
this.total = val.length
this.getList()
}
}
},
mounted() {
@@ -877,12 +887,34 @@ export default {
}
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 }
* @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,67 @@ 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 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 => {
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 +206,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 +230,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,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 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

@@ -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": "存在导入失败项,点击表格编辑后,可以继续导入失败项"
},
"fileType": "文件类型",
"isValid": "有效",

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,6 +281,7 @@
"Reset": "Reset",
"Search": "Search",
"MFAErrorMsg": "MFA Errorplease check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select",
"SelectFile": "Select file",
"Show": "Show",
@@ -318,6 +323,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 +337,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": "Has import failed item, click to edit it and continue upload"
},
"isValid": "Is valid",
"nav": {

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

@@ -187,6 +187,19 @@ 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))
}
const scheme = document.location.protocol
const port = document.location.port ? ':' + document.location.port : ''
const BASE_URL = scheme + '//' + document.location.hostname + port

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

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

View File

@@ -1,5 +1,5 @@
<template>
<GenericCreateUpdatePage v-bind="$data" />
<GenericCreateUpdatePage v-bind="$data" :perform-submit="performSubmit" />
</template>
<script>
@@ -94,6 +94,19 @@ export default {
updateSuccessNextRoute: { 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>

View File

@@ -23,7 +23,7 @@ export default {
},
fields: [
[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.Other'), ['comment']]
],
@@ -133,6 +133,11 @@ export default {
}
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/',

View File

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

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>
@@ -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

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

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

@@ -4,6 +4,7 @@
:fields="fields"
:url="url"
:get-method="getMethod"
:fields-meta="fieldsMeta"
:more-buttons="moreButtons"
:has-detail-in-msg="false"
/>
@@ -14,6 +15,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 +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',
moreButtons: [
{

View File

@@ -10,6 +10,7 @@ import { GenericCreateUpdatePage } from '@/layout/components'
import UserPassword from '@/components/UserPassword'
import RoleCheckbox from '@/views/users/User/components/RoleCheckbox'
import rules from '@/components/DataForm/rules'
import { mapGetters } from 'vuex'
export default {
components: {
@@ -37,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',
@@ -57,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
@@ -70,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: {
@@ -109,6 +116,14 @@ export default {
}
}
},
computed: {
...mapGetters(['currentOrgIsRoot'])
},
mounted() {
if (this.currentOrgIsRoot) {
this.fieldsMeta.groups.el.disabled = true
}
},
methods: {
cleanFormValue(value) {
const method = this.getMethod()

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

@@ -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