mirror of
https://github.com/jumpserver/lina.git
synced 2025-11-11 21:01:57 +00:00
* perf: ldap import user error msg * fix: Duplication on new code --------- Co-authored-by: wangruidong <940853815@qq.com>
497 lines
14 KiB
Vue
497 lines
14 KiB
Vue
<template>
|
|
<div>
|
|
<div class="tableFilter">
|
|
<el-radio-group v-model="importStatusFilter" size="small">
|
|
<el-radio-button label="all">{{ $t('Total') }}: {{ totalCount }}</el-radio-button>
|
|
<el-radio-button label="ok">{{ $t('Success') }}: {{ successCount }}</el-radio-button>
|
|
<el-radio-button label="error">{{ $t('Failed') }}: {{ failedCount }}</el-radio-button>
|
|
<el-radio-button label="pending">{{ $t('Pending') }}: {{ pendingCount }}</el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
<div class="row">
|
|
<el-progress :percentage="processedPercent" />
|
|
</div>
|
|
<DataTable v-if="tableGenDone" id="importTable" ref="dataTable" :config="tableConfig" class="importTable" />
|
|
<div class="row" style="padding-top: 20px">
|
|
<div class="btn-groups" style="float: right">
|
|
<el-button v-if="showCancel" size="small" @click="performCancel">{{ $t('Cancel') }}</el-button>
|
|
<el-button
|
|
v-show="!disableImportBtn"
|
|
size="small"
|
|
type="primary"
|
|
@click="performImportAction"
|
|
>
|
|
{{ importActionTitle }}
|
|
</el-button>
|
|
<el-button
|
|
v-for="button in moreButtons"
|
|
v-show="!button.hidden"
|
|
:key="button.title"
|
|
:loading="button.loading"
|
|
:disabled="disableImportBtn"
|
|
size="small"
|
|
v-bind="button"
|
|
@click="handleClick(button)"
|
|
>
|
|
{{ button.title }}
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import DataTable from '@/components/Table/DataTable/index.vue'
|
|
import { getUpdateObjURL } from '@/utils/common'
|
|
import { sleep } from '@/utils/time'
|
|
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
|
|
import { encryptPassword } from '@/utils/crypto'
|
|
import getStatusColumnMeta from '@/components/Table/ListTable/TableAction/const'
|
|
|
|
export default {
|
|
name: 'ImportTable',
|
|
components: {
|
|
DataTable
|
|
},
|
|
props: {
|
|
jsonData: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
url: {
|
|
type: String,
|
|
default: () => ''
|
|
},
|
|
importOption: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
showButtons: {
|
|
type: Boolean,
|
|
default: () => true
|
|
},
|
|
config: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
performUploadObject: {
|
|
type: Function,
|
|
default: null
|
|
},
|
|
canEdit: {
|
|
type: Boolean,
|
|
default: () => true
|
|
},
|
|
showCancel: {
|
|
type: Boolean,
|
|
default: () => true
|
|
},
|
|
moreButtons: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
disableImportBtn: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
origin: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
columns: [],
|
|
importStatusFilter: 'all',
|
|
iTotalData: [],
|
|
defaultTableConfig: {
|
|
hasSelection: false,
|
|
// hasPagination: false,
|
|
columns: [],
|
|
totalData: [],
|
|
paginationSize: 10,
|
|
paginationSizes: [10],
|
|
tableAttrs: {
|
|
stripe: true, // 斑马纹表格
|
|
border: true, // 表格边框
|
|
fit: true, // 宽度自适应,
|
|
tooltipEffect: 'dark',
|
|
maxHeight: this.tableHeight
|
|
}
|
|
},
|
|
tableGenDone: false,
|
|
importTaskStatus: 'pending', // pending, started, stopped, done
|
|
importTaskResult: '', // success, hasError
|
|
hasImport: false,
|
|
hasContinueButton: false,
|
|
importActions: {
|
|
import: this.$t('Import'),
|
|
continue: this.$t('Continue'),
|
|
stop: this.$t('Stop'),
|
|
finished: this.$t('Finished')
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
tableColumnNameMapper() {
|
|
const mapper = {}
|
|
for (const column of this.tableConfig.columns) {
|
|
mapper[column['prop']] = column['label']
|
|
}
|
|
return mapper
|
|
},
|
|
importAction() {
|
|
switch (this.importTaskStatus) {
|
|
case 'pending':
|
|
return 'import'
|
|
case 'started':
|
|
return 'stop'
|
|
}
|
|
if (this.totalCount === this.successCount) {
|
|
return 'finished'
|
|
} else {
|
|
return 'continue'
|
|
}
|
|
},
|
|
importActionTitle() {
|
|
return this.importActions[this.importAction]
|
|
},
|
|
successData() {
|
|
return this.iTotalData.filter((item) => {
|
|
return item['@status'] === 'ok'
|
|
})
|
|
},
|
|
failedData() {
|
|
return this.iTotalData.filter((item) => {
|
|
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
|
|
})
|
|
},
|
|
pendingData() {
|
|
return this.iTotalData.filter((item) => {
|
|
return item['@status'] === 'pending'
|
|
})
|
|
},
|
|
totalCount() {
|
|
return this.iTotalData.length
|
|
},
|
|
successCount() {
|
|
return this.successData.length
|
|
},
|
|
failedCount() {
|
|
return this.failedData.length
|
|
},
|
|
pendingCount() {
|
|
return this.pendingData.length
|
|
},
|
|
processedCount() {
|
|
return this.totalCount - this.pendingCount
|
|
},
|
|
processedPercent() {
|
|
if (this.totalCount === 0) {
|
|
return 0
|
|
}
|
|
return Math.round(this.processedCount / this.totalCount * 100)
|
|
},
|
|
elDataTable() {
|
|
return this.$refs['dataTable'].dataTable
|
|
},
|
|
tableConfig() {
|
|
const tableDefaultConfig = this.defaultTableConfig
|
|
let tableAttrs = tableDefaultConfig.tableAttrs
|
|
if (this.config.tableAttrs) {
|
|
tableAttrs = Object.assign(tableAttrs, this.config.tableAttrs)
|
|
}
|
|
const config = Object.assign(tableDefaultConfig, this.config)
|
|
config.tableAttrs = tableAttrs
|
|
return config
|
|
}
|
|
},
|
|
watch: {
|
|
importStatusFilter(val) {
|
|
if (val === 'all') {
|
|
this.tableConfig.totalData = this.iTotalData
|
|
} else if (val === 'error') {
|
|
this.tableConfig.totalData = this.failedData
|
|
} else {
|
|
this.tableConfig.totalData = this.iTotalData.filter((item) => {
|
|
return item['@status'] === val
|
|
})
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.generateTable()
|
|
},
|
|
methods: {
|
|
generateTableColumns(tableTitles, tableData) {
|
|
const columns = [{ ...getStatusColumnMeta.bind(this)().status }]
|
|
for (const item of tableTitles) {
|
|
const dataItemLens = tableData.map(d => {
|
|
if (!d) {
|
|
return 0
|
|
}
|
|
const prop = item[1]
|
|
const itemColData = d[prop]
|
|
if (typeof itemColData === 'boolean') {
|
|
return 5 // boolean is 5 characters long 并且 boolean.length 是 undefined
|
|
} else if (typeof itemColData === 'number') {
|
|
return itemColData.toString().length
|
|
} else if (typeof itemColData === 'string') {
|
|
return itemColData.length
|
|
} else if (typeof itemColData === 'object') {
|
|
if (!itemColData || itemColData.length === 0) {
|
|
return 0
|
|
} else {
|
|
return JSON.stringify(itemColData).length
|
|
}
|
|
}
|
|
return 0
|
|
})
|
|
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',
|
|
formatter: EditableInputFormatter,
|
|
showOverflowTooltip: true,
|
|
formatterArgs: {
|
|
canEdit: this.canEdit,
|
|
onEnter: ({ row, col, oldValue, newValue }) => {
|
|
const prop = col.prop
|
|
row['@status'] = 'pending'
|
|
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
|
|
this.$set(row, prop, newValue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return columns
|
|
},
|
|
generateTableData(tableTitles, tableData) {
|
|
const totalData = []
|
|
tableData.forEach(item => {
|
|
this.$set(item, '@status', 'pending')
|
|
const encryptFields = ['password', 'secret', 'private_key']
|
|
for (const field of encryptFields) {
|
|
if (item[field]) {
|
|
item[field] = encryptPassword(item[field])
|
|
}
|
|
}
|
|
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 (Array.isArray(value)) {
|
|
value = JSON.stringify(value)
|
|
} else if (typeof value === 'object') {
|
|
value = this.beautifyErrorData(value)
|
|
}
|
|
let label = this.tableColumnNameMapper[key]
|
|
if (!label) {
|
|
label = key
|
|
}
|
|
data.push(`${label}: ${value}`)
|
|
}
|
|
return data
|
|
},
|
|
performCancel() {
|
|
this.performStop()
|
|
this.$emit('cancel')
|
|
},
|
|
performFinish() {
|
|
this.performStop()
|
|
this.$emit('finish')
|
|
},
|
|
taskIsStopped() {
|
|
return this.importTaskStatus === 'stopped'
|
|
},
|
|
performImportAction() {
|
|
switch (this.importAction) {
|
|
case 'continue':
|
|
return this.performContinue()
|
|
case 'import':
|
|
return this.performUpload()
|
|
case 'stop':
|
|
return this.performStop()
|
|
case 'finished':
|
|
return this.performFinish()
|
|
}
|
|
},
|
|
performContinue() {
|
|
if (this.importTaskStatus === 'done') {
|
|
for (const item of this.failedData) {
|
|
item['@status'] = 'pending'
|
|
}
|
|
this.tableConfig.totalData = this.pendingData
|
|
}
|
|
this.importTaskStatus = 'started'
|
|
setTimeout(() => {
|
|
this.performUpload()
|
|
}, 100)
|
|
},
|
|
performStop() {
|
|
this.importTaskStatus = 'stopped'
|
|
},
|
|
async performUploadCurrentPageData() {
|
|
let currentData
|
|
if (this.tableConfig.hasSelection) {
|
|
currentData = this.elDataTable.selected
|
|
} else {
|
|
currentData = this.elDataTable.getPageData()
|
|
}
|
|
for (const item of currentData) {
|
|
if (item['@status'] !== 'pending') {
|
|
continue
|
|
}
|
|
if (this.taskIsStopped()) {
|
|
return
|
|
}
|
|
const handler = this.performUploadObject || this.defaultPerformUploadObject
|
|
await handler(item)
|
|
await sleep(100)
|
|
}
|
|
},
|
|
async performUpload() {
|
|
this.importTaskStatus = 'started'
|
|
this.importStatusFilter = 'pending'
|
|
while (!this.taskIsStopped()) {
|
|
await this.performUploadCurrentPageData()
|
|
const hasNextPage = this.elDataTable.hasNextPage()
|
|
if (hasNextPage && !this.taskIsStopped()) {
|
|
await this.elDataTable.gotoNextPage()
|
|
await sleep(100)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
this.importTaskStatus = 'done'
|
|
|
|
// 在不影响其他组件使用本组件的基础上,对云同步中导入按钮的变化
|
|
if (this.origin === 'cloudSync') {
|
|
this.tableConfig.totalData = this.pendingData
|
|
this.importTaskStatus = 'pending'
|
|
}
|
|
|
|
if (this.failedCount > 0) {
|
|
this.$message.error(this.$tc('HasImportErrorItemMsg') + '')
|
|
}
|
|
},
|
|
async performUpdateObject(item) {
|
|
const updateUrl = getUpdateObjURL(this.url, item.id)
|
|
return this.$axios.patch(
|
|
updateUrl,
|
|
item,
|
|
{ disableFlashErrorMsg: true }
|
|
)
|
|
},
|
|
async defaultPerformUploadObject(item) {
|
|
let handler = this.performCreateObject
|
|
if (this.importOption === 'update') {
|
|
handler = this.performUpdateObject
|
|
}
|
|
try {
|
|
await handler.bind(this)(item)
|
|
item['@status'] = 'ok'
|
|
} catch (error) {
|
|
const errorData = error?.response?.data
|
|
const _error = this.beautifyErrorData(errorData)
|
|
item['@status'] = {
|
|
name: 'error',
|
|
error: _error
|
|
}
|
|
}
|
|
},
|
|
async performCreateObject(item) {
|
|
return this.$axios.post(
|
|
this.url,
|
|
item,
|
|
{ disableFlashErrorMsg: true }
|
|
)
|
|
},
|
|
keepElementInViewport() {
|
|
const tableRef = document.getElementById('importTable')
|
|
const pendingRef = tableRef?.getElementsByClassName('pendingStatus')[0]
|
|
if (!pendingRef) {
|
|
return
|
|
}
|
|
const parentTdRef = pendingRef.parentElement.parentElement.parentElement.parentElement
|
|
const rect = parentTdRef.getBoundingClientRect()
|
|
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
|
|
windowInnerHeight = windowInnerHeight * 0.97 - 150
|
|
const inViewport = (
|
|
rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= windowInnerHeight
|
|
)
|
|
if (!inViewport) {
|
|
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
|
|
}
|
|
},
|
|
addTableItem(item) {
|
|
this.tableConfig.totalData.push(item)
|
|
},
|
|
handleClick(btn) {
|
|
const callback = btn.callback || function() {
|
|
}
|
|
callback(btn)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "~@/styles/variables";
|
|
|
|
.summary-item {
|
|
padding: 0 10px
|
|
}
|
|
|
|
.summary-success {
|
|
color: $--color-primary;
|
|
}
|
|
|
|
.summary-failed {
|
|
color: $--color-danger;
|
|
}
|
|
|
|
.importTable ::v-deep .cell {
|
|
min-height: 20px;
|
|
height: 100%;
|
|
max-height: 160px;
|
|
}
|
|
|
|
.btn-groups {
|
|
::v-deep .el-button.is-disabled {
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
</style>
|