mirror of
https://github.com/jumpserver/lina.git
synced 2026-05-18 05:35:11 +00:00
perf: update report
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
lina/
|
||||
ui/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -18,4 +19,4 @@ tests/**/coverage/
|
||||
.env.development
|
||||
.python-version
|
||||
|
||||
helper.json
|
||||
helper.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM jumpserver/lina-base:20260114_045651 AS stage-build
|
||||
FROM jumpserver/lina-base:20260331_081153 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"npm": "^7.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"sm-crypto": "^0.4.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"v-sanitize": "^0.0.13",
|
||||
"vue": "2.7.16",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<AutoDataForm
|
||||
v-if="!loading"
|
||||
ref="AutoDataForm"
|
||||
:class="addTemplate? '': 'account-add'"
|
||||
:class="addTemplate ? '' : 'account-add'"
|
||||
:submit-btn-text="submitBtnText"
|
||||
v-bind="$data"
|
||||
@submit="confirm"
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
|
||||
export default {
|
||||
@@ -59,16 +59,27 @@ export default {
|
||||
]
|
||||
},
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
form: Object.assign({ 'on_invalid': 'error' }, this.account || {}),
|
||||
form: Object.assign({ on_invalid: 'error' }, this.account || {}),
|
||||
encryptedFields: ['secret'],
|
||||
fields: [
|
||||
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
|
||||
[
|
||||
this.$t('Basic'),
|
||||
['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']
|
||||
],
|
||||
[this.$t('Asset'), ['nodes', 'assets']],
|
||||
[this.$t('Secret'), [
|
||||
'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key',
|
||||
'secret_reset'
|
||||
]],
|
||||
[
|
||||
this.$t('Secret'),
|
||||
[
|
||||
'secret_type',
|
||||
'password',
|
||||
'ssh_key',
|
||||
'token',
|
||||
'access_key',
|
||||
'passphrase',
|
||||
'api_key',
|
||||
'secret_reset'
|
||||
]
|
||||
],
|
||||
[this.$t('Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
|
||||
],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
@@ -170,7 +181,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
<style lang="scss" scoped>
|
||||
.account-add {
|
||||
::v-deep .el-form-item {
|
||||
//margin-bottom: 5px;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<script>
|
||||
import { GenericUpdateFormDialog } from '@/layout/components'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export default {
|
||||
name: 'AccountBulkUpdateDialog',
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
},
|
||||
selectedRows: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
hasSaveContinue: false,
|
||||
fields: [],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
cleanOtherFormValue: (formValue) => {
|
||||
cleanOtherFormValue: formValue => {
|
||||
for (const value of formValue) {
|
||||
Object.keys(value).forEach((item, index, arr) => {
|
||||
if (['ssh_key', 'token', 'access_key', 'api_key', 'password'].includes(item)) {
|
||||
@@ -82,6 +82,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
|
||||
export default {
|
||||
@@ -46,8 +46,14 @@ export default {
|
||||
const accountMeta = accountFieldsMeta(this)
|
||||
return {
|
||||
fields: [
|
||||
'name', 'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key'
|
||||
'name',
|
||||
'secret_type',
|
||||
'password',
|
||||
'ssh_key',
|
||||
'token',
|
||||
'access_key',
|
||||
'passphrase',
|
||||
'api_key'
|
||||
],
|
||||
fieldsMeta: {
|
||||
...accountMeta,
|
||||
@@ -80,18 +86,19 @@ export default {
|
||||
const data = {
|
||||
secret: encryptPassword(form[secretType])
|
||||
}
|
||||
this.$axios.patch(
|
||||
`/api/v1/accounts/accounts/${this.account.id}/`,
|
||||
data,
|
||||
{ disableFlashErrorMsg: true }
|
||||
).then(res => {
|
||||
this.$message.success(this.$tc('UpdateSuccessMsg'))
|
||||
this.iVisible = false
|
||||
}).catch(err => {
|
||||
const errMsg = Object.values(err.response.data).join(', ')
|
||||
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
|
||||
this.iVisible = false
|
||||
})
|
||||
this.$axios
|
||||
.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data, {
|
||||
disableFlashErrorMsg: true
|
||||
})
|
||||
.then(res => {
|
||||
this.$message.success(this.$tc('UpdateSuccessMsg'))
|
||||
this.iVisible = false
|
||||
})
|
||||
.catch(err => {
|
||||
const errMsg = Object.values(err.response.data).join(', ')
|
||||
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
|
||||
this.iVisible = false
|
||||
})
|
||||
},
|
||||
handleCancel() {
|
||||
this.$emit('update:visible', false)
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
<el-form-item :label="secretTypeLabel">
|
||||
<SecretViewerFormatter
|
||||
:cell-value="secretInfo.secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: account['name'],
|
||||
secretType: secretType || ''
|
||||
}}"
|
||||
:col="{
|
||||
formatterArgs: {
|
||||
name: account['name'],
|
||||
secretType: secretType || ''
|
||||
}
|
||||
}"
|
||||
@input="onShowKeyCopyFormatterChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -36,12 +38,12 @@
|
||||
<el-form-item :label="$tc('DateUpdated')">
|
||||
<span>{{ account['date_updated'] | date }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showPasswordRecord" v-perms="'accounts.view_accountsecret'" :label="$tc('PasswordRecord')">
|
||||
<el-link
|
||||
:underline="false"
|
||||
type="success"
|
||||
@click="showHistoryDialog"
|
||||
>
|
||||
<el-form-item
|
||||
v-if="showPasswordRecord"
|
||||
v-perms="'accounts.view_accountsecret'"
|
||||
:label="$tc('PasswordRecord')"
|
||||
>
|
||||
<el-link :underline="false" type="success" @click="showHistoryDialog">
|
||||
<span style="padding-right: 30px">
|
||||
{{ versions }}
|
||||
</span>
|
||||
@@ -61,7 +63,7 @@
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
|
||||
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
@@ -90,7 +92,7 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$tc('Detail')
|
||||
}
|
||||
},
|
||||
@@ -144,7 +146,8 @@ export default {
|
||||
name: this.secretInfo.name,
|
||||
secret: encryptPassword(this.modifiedSecret)
|
||||
}
|
||||
const url = this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
|
||||
const url =
|
||||
this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
|
||||
this.$axios.patch(`${url}/${this.account.id}/`, params).then(() => {
|
||||
this.$message.success(this.$tc('UpdateSuccessMsg'))
|
||||
})
|
||||
@@ -154,7 +157,7 @@ export default {
|
||||
this.$message.warning(this.$tc('AccountSecretReadDisabled'))
|
||||
return
|
||||
}
|
||||
return this.$axios.get(this.url).then((res) => {
|
||||
return this.$axios.get(this.url).then(res => {
|
||||
this.secretInfo = res
|
||||
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
|
||||
this.showSecret = true
|
||||
@@ -180,7 +183,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@
|
||||
</template>
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { logout as redirectToLogout } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'UserConfirmDialog',
|
||||
@@ -179,7 +180,7 @@ export default {
|
||||
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
|
||||
this.smsWidth = val === 'sms' ? 6 : 0
|
||||
},
|
||||
performConfirm: _.debounce(function({ response, callback, cancel }) {
|
||||
performConfirm: _.debounce(function ({ response, callback, cancel }) {
|
||||
if (this.processing || this.visible) {
|
||||
return
|
||||
}
|
||||
@@ -224,7 +225,7 @@ export default {
|
||||
})
|
||||
}, 500),
|
||||
logout() {
|
||||
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
|
||||
redirectToLogout()
|
||||
},
|
||||
sendCode() {
|
||||
this.$axios
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Dialog
|
||||
v-if="exportDialogShow"
|
||||
:destroy-on-close="true"
|
||||
:title="$tc('Export')"
|
||||
:title="iDialogTitle"
|
||||
:visible.sync="exportDialogShow"
|
||||
width="700px"
|
||||
@close="handleExportCancel"
|
||||
@@ -14,10 +14,10 @@
|
||||
{{ tips }}
|
||||
</el-alert>
|
||||
<el-form label-position="left" style="padding-left: 20px">
|
||||
<el-form-item :label="$tc('FileType' )" :label-width="'100px'">
|
||||
<el-form-item v-if="showFileType" :label="$tc('FileType' )" :label-width="'100px'">
|
||||
<el-radio-group v-model="exportTypeOption">
|
||||
<el-radio
|
||||
v-for="option of exportTypeOptions"
|
||||
v-for="option of iExportTypeOptions"
|
||||
:key="option.value"
|
||||
:disabled="!option.can"
|
||||
:label="option.value"
|
||||
@@ -99,6 +99,34 @@ export default {
|
||||
tipsType: {
|
||||
type: String,
|
||||
default: 'success'
|
||||
},
|
||||
triggerEvent: {
|
||||
type: String,
|
||||
default: 'showExportDialog'
|
||||
},
|
||||
dialogTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showFileType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
defaultExportType: {
|
||||
type: String,
|
||||
default: 'csv'
|
||||
},
|
||||
fixedExportType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fileTypeOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
extraQuery: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -137,6 +165,9 @@ export default {
|
||||
tableHasQuery() {
|
||||
return Object.keys(this.tableQuery).length > 0
|
||||
},
|
||||
iDialogTitle() {
|
||||
return this.dialogTitle || this.$tc('Export')
|
||||
},
|
||||
exportOptions() {
|
||||
return [
|
||||
{
|
||||
@@ -156,7 +187,10 @@ export default {
|
||||
}
|
||||
]
|
||||
},
|
||||
exportTypeOptions() {
|
||||
iExportTypeOptions() {
|
||||
if (this.fileTypeOptions.length > 0) {
|
||||
return this.fileTypeOptions
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: 'CSV',
|
||||
@@ -172,10 +206,10 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('showExportDialog', this.showExportDialogHandler)
|
||||
this.$eventBus.$off(this.triggerEvent, this.showExportDialogHandler)
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('showExportDialog', this.showExportDialogHandler)
|
||||
this.$eventBus.$on(this.triggerEvent, this.showExportDialogHandler)
|
||||
},
|
||||
methods: {
|
||||
showExportDialogHandler({ selectedRows, url, name }) {
|
||||
@@ -184,6 +218,9 @@ export default {
|
||||
}
|
||||
},
|
||||
showExportDialog() {
|
||||
this.exportTypeOption = this.fixedExportType || this.defaultExportType
|
||||
this.exportOption = 'all'
|
||||
|
||||
if (!this.mfaVerifyRequired) {
|
||||
this.exportDialogShow = true
|
||||
|
||||
@@ -199,6 +236,13 @@ export default {
|
||||
}
|
||||
this.$axios.get('/api/v1/authentication/confirm/check/?confirm_type=mfa').then(() => {
|
||||
this.exportDialogShow = true
|
||||
if (this.hasSelected) {
|
||||
this.exportOption = 'selected'
|
||||
}
|
||||
|
||||
if (this.tableHasQuery) {
|
||||
this.exportOption = 'filtered'
|
||||
}
|
||||
})
|
||||
},
|
||||
downloadCsv(url) {
|
||||
@@ -216,7 +260,8 @@ export default {
|
||||
const spm = await createSourceIdCache(resources)
|
||||
query['spm'] = spm.spm
|
||||
}
|
||||
query['format'] = exportTypeOption
|
||||
Object.assign(query, this.extraQuery)
|
||||
query['format'] = exportTypeOption || this.fixedExportType || this.defaultExportType
|
||||
const queryStr =
|
||||
(url.indexOf('?') > -1 ? '&' : '?') +
|
||||
queryUtil.stringify(query, '=', '&')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<ExportDialog :selected-rows="selectedRows" v-bind="exportOptions" v-on="$listeners" />
|
||||
<ExportDialog :selected-rows="selectedRows" v-bind="reportExportOptions" v-on="$listeners" />
|
||||
<ImportDialog :selected-rows="selectedRows" v-bind="importOptions" v-on="$listeners" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -24,6 +25,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
reportExportOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
importOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
<div class="row">
|
||||
<el-progress :percentage="processedPercent" />
|
||||
</div>
|
||||
<DataTable v-if="tableGenDone" id="importTable" ref="dataTable" :config="tableConfig" class="importTable" />
|
||||
<DataTable
|
||||
v-if="tableGenDone"
|
||||
id="importTable"
|
||||
ref="dataTable"
|
||||
:config="tableConfig"
|
||||
class="importTable"
|
||||
/>
|
||||
<div class="row" style="padding-top: 20px">
|
||||
<div class="btn-groups">
|
||||
<el-button v-if="showCancel" size="small" @click="performCancel">{{ $t('Cancel') }}</el-button>
|
||||
<el-button v-if="showCancel" size="small" @click="performCancel">{{
|
||||
$t('Cancel')
|
||||
}}</el-button>
|
||||
<el-button
|
||||
v-show="!disableImportBtn"
|
||||
size="small"
|
||||
@@ -45,7 +53,7 @@ import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
import { getUpdateObjURL } from '@/utils/common/index'
|
||||
import { sleep } from '@/utils/common/time'
|
||||
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import getStatusColumnMeta from '@/components/Table/ListTable/TableAction/const'
|
||||
|
||||
export default {
|
||||
@@ -162,17 +170,17 @@ export default {
|
||||
return this.importActions[this.importAction]
|
||||
},
|
||||
successData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return this.iTotalData.filter(item => {
|
||||
return item['@status'] === 'ok'
|
||||
})
|
||||
},
|
||||
failedData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return this.iTotalData.filter(item => {
|
||||
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
|
||||
})
|
||||
},
|
||||
pendingData() {
|
||||
return this.iTotalData.filter((item) => {
|
||||
return this.iTotalData.filter(item => {
|
||||
return item['@status'] === 'pending'
|
||||
})
|
||||
},
|
||||
@@ -195,7 +203,7 @@ export default {
|
||||
if (this.totalCount === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(this.processedCount / this.totalCount * 100)
|
||||
return Math.round((this.processedCount / this.totalCount) * 100)
|
||||
},
|
||||
elDataTable() {
|
||||
return this.$refs['dataTable'].dataTable
|
||||
@@ -218,7 +226,7 @@ export default {
|
||||
} else if (val === 'error') {
|
||||
this.tableConfig.totalData = this.failedData
|
||||
} else {
|
||||
this.tableConfig.totalData = this.iTotalData.filter((item) => {
|
||||
this.tableConfig.totalData = this.iTotalData.filter(item => {
|
||||
return item['@status'] === val
|
||||
})
|
||||
}
|
||||
@@ -278,7 +286,8 @@ export default {
|
||||
return columns
|
||||
},
|
||||
getEncryptFields() {
|
||||
const fromProp = Array.isArray(this.encryptFields) && this.encryptFields.length ? this.encryptFields : null
|
||||
const fromProp =
|
||||
Array.isArray(this.encryptFields) && this.encryptFields.length ? this.encryptFields : null
|
||||
return fromProp || ['password', 'secret', 'private_key']
|
||||
},
|
||||
generateTableData(tableTitles, tableData) {
|
||||
@@ -415,11 +424,7 @@ export default {
|
||||
},
|
||||
async performUpdateObject(item) {
|
||||
const updateUrl = getUpdateObjURL(this.url, item.id)
|
||||
return this.$axios.patch(
|
||||
updateUrl,
|
||||
item,
|
||||
{ disableFlashErrorMsg: true }
|
||||
)
|
||||
return this.$axios.patch(updateUrl, item, { disableFlashErrorMsg: true })
|
||||
},
|
||||
async defaultPerformUploadObject(item) {
|
||||
let handler = this.performCreateObject
|
||||
@@ -439,11 +444,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async performCreateObject(item) {
|
||||
return this.$axios.post(
|
||||
this.url,
|
||||
item,
|
||||
{ disableFlashErrorMsg: true }
|
||||
)
|
||||
return this.$axios.post(this.url, item, { disableFlashErrorMsg: true })
|
||||
},
|
||||
keepElementInViewport() {
|
||||
const tableRef = document.getElementById('importTable')
|
||||
@@ -455,11 +456,7 @@ export default {
|
||||
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
|
||||
)
|
||||
const inViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowInnerHeight
|
||||
if (!inViewport) {
|
||||
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
|
||||
}
|
||||
@@ -468,8 +465,7 @@ export default {
|
||||
this.tableConfig.totalData.push(item)
|
||||
},
|
||||
handleClick(btn) {
|
||||
const callback = btn.callback || function() {
|
||||
}
|
||||
const callback = btn.callback || function () {}
|
||||
callback(btn)
|
||||
}
|
||||
}
|
||||
@@ -477,10 +473,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/variables";
|
||||
@import '~@/styles/variables';
|
||||
|
||||
.summary-item {
|
||||
padding: 0 10px
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.summary-success {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<ImExportDialog
|
||||
v-if="dialogExportVisible"
|
||||
:export-options="iExportOptions"
|
||||
:report-export-options="iReportExportOptions"
|
||||
:import-options="iImportOptions"
|
||||
:selected-rows="selectedRows"
|
||||
v-bind="$attrs"
|
||||
@@ -20,6 +21,7 @@ import { cleanActions } from './utils'
|
||||
import { assignIfNot } from '@/utils/common/index'
|
||||
|
||||
const defaultTrue = { type: [Boolean, Function, String], default: true }
|
||||
const defaultFalse = { type: [Boolean, Function, String], default: false }
|
||||
|
||||
export default {
|
||||
name: 'RightSide',
|
||||
@@ -48,6 +50,22 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
hasReportExport: defaultFalse,
|
||||
reportExportOptions: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
handleReportExportClick: {
|
||||
type: Function,
|
||||
default: function({ selectedRows }) {
|
||||
const url = this.iReportExportOptions.url
|
||||
const triggerEvent = this.iReportExportOptions.triggerEvent || 'showReportExportDialog'
|
||||
this.dialogExportVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$eventBus.$emit(triggerEvent, { selectedRows, url, name: this.name })
|
||||
})
|
||||
}
|
||||
},
|
||||
hasImport: defaultTrue,
|
||||
importOptions: {
|
||||
type: Object,
|
||||
@@ -133,9 +151,16 @@ export default {
|
||||
name: 'actionExport',
|
||||
icon: 'download',
|
||||
tip: this.$t('Export'),
|
||||
has: this.hasExport,
|
||||
has: this.showLegacyExport.bind(this),
|
||||
callback: this.handleExportClick.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'actionReportExport',
|
||||
icon: 'fa-file-excel-o',
|
||||
tip: this.$t('ReportExport'),
|
||||
has: this.showReportExport.bind(this),
|
||||
callback: this.handleReportExportClick.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'actionRefresh',
|
||||
icon: 'refresh',
|
||||
@@ -179,9 +204,53 @@ export default {
|
||||
url: this.tableUrl,
|
||||
...this.exportOptions
|
||||
}
|
||||
},
|
||||
iReportExportOptions() {
|
||||
const reportExportOptions = this.reportExportOptions || {}
|
||||
const {
|
||||
extraQuery = {},
|
||||
fileTypeOptions,
|
||||
...restReportExportOptions
|
||||
} = reportExportOptions
|
||||
|
||||
return {
|
||||
url: this.tableUrl,
|
||||
triggerEvent: 'showReportExportDialog',
|
||||
dialogTitle: this.$t('ReportExport'),
|
||||
fixedExportType: 'xlsx',
|
||||
showFileType: false,
|
||||
...restReportExportOptions,
|
||||
extraQuery: {
|
||||
export_mode: 'report',
|
||||
...extraQuery
|
||||
},
|
||||
fileTypeOptions: fileTypeOptions || [
|
||||
{
|
||||
label: 'Excel',
|
||||
value: 'xlsx',
|
||||
can: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getActionVisibility(value, args) {
|
||||
if (typeof value === 'function') {
|
||||
return value(args)
|
||||
}
|
||||
return value
|
||||
},
|
||||
showLegacyExport(args) {
|
||||
const reportExportVisibility = this.getActionVisibility(this.hasReportExport, args)
|
||||
if (reportExportVisibility) {
|
||||
return false
|
||||
}
|
||||
return this.getActionVisibility(this.hasExport, args)
|
||||
},
|
||||
showReportExport(args) {
|
||||
return this.getActionVisibility(this.hasReportExport, args)
|
||||
},
|
||||
handleFilterClick() {
|
||||
this.$emit('update:quick-filter-expand', !this.quickFilterExpand)
|
||||
},
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
<script>
|
||||
import BaseFormatter from './base.vue'
|
||||
import { addBasePath } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
name: 'AccountConnectFormatter',
|
||||
@@ -56,12 +57,12 @@ export default {
|
||||
can: () => true,
|
||||
getConnectUrl: (row, protocol, asset) => {
|
||||
const assetId = asset ? asset.id : row.asset.id
|
||||
return `/luna/admin-connect/?
|
||||
return addBasePath(`/luna/admin-connect/?
|
||||
asset=${assetId}
|
||||
&account=${row.id}
|
||||
&protocol=${protocol}
|
||||
&org_id=${this.$store.getters.currentOrg.id}
|
||||
`.replace(/\s+/g, '')
|
||||
`.replace(/\s+/g, ''))
|
||||
},
|
||||
asset: null,
|
||||
assetUrl: '/api/v1/assets/assets/{id}/',
|
||||
|
||||
@@ -46,7 +46,7 @@ Vue.prototype.$tr = key => {
|
||||
|
||||
export async function fetchTranslationsFromAPI() {
|
||||
try {
|
||||
const res = await axios.get(`${window.__BASE_PATH__}/api/v1/settings/i18n/lina/?lang=${lang}&flat=0`)
|
||||
const res = await axios.get(`/api/v1/settings/i18n/lina/?lang=${lang}&flat=0`)
|
||||
const data = res.data
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
|
||||
@@ -34,6 +34,10 @@ export default {
|
||||
let key
|
||||
if (this.$route.query['_']) {
|
||||
key = this.$route.query['_']
|
||||
} else if (this.$route.path.startsWith('/audit/reports/')) {
|
||||
// 报表页面:只用路径作为 key,让同一路径的组件实例被复用
|
||||
// 包含 query 会导致每次 query 变化都创建新的缓存实例,积累的 deactivated 实例会同时响应路由变化形成循环
|
||||
key = _.trimEnd(this.$route.path, '/')
|
||||
} else if (this.$route.name.toLowerCase().includes('list')) {
|
||||
key = _.trimEnd(this.$route.path, '/') + '?' + new URLSearchParams(query).toString()
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm'
|
||||
import { getUpdateObjURL } from '@/utils/common/index'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import deepmerge from 'deepmerge'
|
||||
|
||||
export default {
|
||||
@@ -45,12 +45,12 @@ export default {
|
||||
},
|
||||
afterGetFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
default: value => value
|
||||
},
|
||||
// 提交前,清理form的值
|
||||
cleanFormValue: {
|
||||
type: Function,
|
||||
default: (value) => value
|
||||
default: value => value
|
||||
},
|
||||
// 获取 meta
|
||||
afterGetRemoteMeta: {
|
||||
@@ -76,28 +76,28 @@ export default {
|
||||
// 创建成功的msg
|
||||
createSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('CreateSuccessMsg')
|
||||
}
|
||||
},
|
||||
// 保存成功,继续添加的msg
|
||||
saveSuccessContinueMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('SaveSuccessContinueMsg')
|
||||
}
|
||||
},
|
||||
// 更新成功的msg
|
||||
updateSuccessMsg: {
|
||||
type: String,
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('UpdateSuccessMsg')
|
||||
}
|
||||
},
|
||||
// 创建成功的跳转路由
|
||||
createSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const routeName = this.$route.name?.replace('Create', 'List')
|
||||
return { name: routeName }
|
||||
}
|
||||
@@ -105,16 +105,15 @@ export default {
|
||||
// 更新成功的跳转路由
|
||||
updateSuccessNextRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const routeName = this.$route.name?.replace('Update', 'List')
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
objectDetailRoute: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
const routeName = this.$route.name?.replace('Update', 'Detail')
|
||||
.replace('Create', 'Detail')
|
||||
default: function () {
|
||||
const routeName = this.$route.name?.replace('Update', 'Detail').replace('Create', 'Detail')
|
||||
return { name: routeName }
|
||||
}
|
||||
},
|
||||
@@ -127,7 +126,7 @@ export default {
|
||||
},
|
||||
cloneNameSuffix: {
|
||||
type: [String, Number],
|
||||
default: function() {
|
||||
default: function () {
|
||||
return this.$t('Duplicate').toLowerCase()
|
||||
}
|
||||
},
|
||||
@@ -139,7 +138,7 @@ export default {
|
||||
// 获取创建和更新的url function
|
||||
getUrl: {
|
||||
type: Function,
|
||||
default: function() {
|
||||
default: function () {
|
||||
const objectId = this.getUpdateId()
|
||||
let url = this.url
|
||||
if (objectId) {
|
||||
@@ -180,12 +179,16 @@ export default {
|
||||
msg = msg[0].toLowerCase() + msg.slice(1)
|
||||
this.$message({
|
||||
message: h('p', null, [
|
||||
h('el-link', {
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
h(
|
||||
'el-link',
|
||||
{
|
||||
on: {
|
||||
click: () => this.$router.push(detailRoute)
|
||||
},
|
||||
style: { 'vertical-align': 'top', 'margin-right': '5px' }
|
||||
},
|
||||
style: { 'vertical-align': 'top', 'margin-right': '5px' }
|
||||
}, msgLinkName),
|
||||
msgLinkName
|
||||
),
|
||||
h('span', {}, msg)
|
||||
]),
|
||||
type: 'success'
|
||||
@@ -200,9 +203,12 @@ export default {
|
||||
default(res, method, vm, addContinue) {
|
||||
const route = this.getNextRoute(res, method)
|
||||
if (!(route.params && route.params.id)) {
|
||||
route['params'] = deepmerge(route['params'] || {}, { 'id': res.id })
|
||||
route['params'] = deepmerge(route['params'] || {}, { id: res.id })
|
||||
}
|
||||
route['query'] = deepmerge(route['query'], { 'order': this.extraQueryOrder, 'updated': new Date().getTime() })
|
||||
route['query'] = deepmerge(route['query'], {
|
||||
order: this.extraQueryOrder,
|
||||
updated: new Date().getTime()
|
||||
})
|
||||
|
||||
this.$emit('submitSuccess', res)
|
||||
|
||||
@@ -344,7 +350,7 @@ export default {
|
||||
encryptFields(values) {
|
||||
// 批量提交,clean 后可能是个数组
|
||||
if (values instanceof Array) {
|
||||
return values.map((item) => this.encryptFields(item))
|
||||
return values.map(item => this.encryptFields(item))
|
||||
}
|
||||
values = { ...values }
|
||||
for (const field of this.encryptedFields) {
|
||||
@@ -372,8 +378,8 @@ export default {
|
||||
defaultOnSubmit(validValues, formName, addContinue) {
|
||||
this.isSubmitting = true
|
||||
this.performSubmit(validValues)
|
||||
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch((error) => this.onPerformError(error, this.method, this))
|
||||
.then(res => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
|
||||
.catch(error => this.onPerformError(error, this.method, this))
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.isSubmitting = false
|
||||
@@ -383,7 +389,7 @@ export default {
|
||||
},
|
||||
async getCloneForm(cloneFrom) {
|
||||
const [curUrl, query] = this.url.split('?')
|
||||
const url = `${curUrl}${cloneFrom}/${query ? ('?' + query) : ''}`
|
||||
const url = `${curUrl}${cloneFrom}/${query ? '?' + query : ''}`
|
||||
try {
|
||||
const object = await this.getObjectDetail(url)
|
||||
let name = ''
|
||||
@@ -438,5 +444,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { logout as redirectToLogout } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'AccountDropdown',
|
||||
@@ -74,7 +75,7 @@ export default {
|
||||
if (currentOrg && (currentOrg.autoEnter || currentOrg.is_system)) {
|
||||
await this.$store.dispatch('users/setCurrentOrg', this.$store.getters.preOrg)
|
||||
}
|
||||
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
|
||||
redirectToLogout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { message } from '@/utils/vue/message'
|
||||
import { scopedLocalStorage as localStorage } from '@/utils/storage'
|
||||
import { getBasePath, scopedLocalStorage as localStorage } from '@/utils/storage'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
@@ -194,6 +194,38 @@ export function newURL(url) {
|
||||
return obj
|
||||
}
|
||||
|
||||
export function addBasePath(path = '') {
|
||||
if (!path || /^https?:\/\//i.test(path)) {
|
||||
return path
|
||||
}
|
||||
|
||||
const basePath = getBasePath()
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
|
||||
if (!basePath) {
|
||||
return normalizedPath
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedPath === basePath ||
|
||||
normalizedPath.startsWith(basePath + '/') ||
|
||||
normalizedPath.startsWith(basePath + '?') ||
|
||||
normalizedPath.startsWith(basePath + '#')
|
||||
) {
|
||||
return normalizedPath
|
||||
}
|
||||
|
||||
return `${basePath}${normalizedPath}`
|
||||
}
|
||||
|
||||
export function getCurrentPageUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return '/'
|
||||
}
|
||||
const { pathname, search, hash } = window.location
|
||||
return `${pathname}${search}${hash}`
|
||||
}
|
||||
|
||||
export function getUpdateObjURL(url, objId) {
|
||||
const urlObj = new URL(url, location.origin)
|
||||
let pathname = urlObj.pathname
|
||||
@@ -231,7 +263,7 @@ export const assignIfNot = _.partialRight(_.assignInWith, customizer)
|
||||
|
||||
const scheme = document.location.protocol
|
||||
const port = document.location.port ? ':' + document.location.port : ''
|
||||
const BASE_URL = scheme + '//' + document.location.hostname + port
|
||||
const BASE_URL = scheme + '//' + document.location.hostname + port + getBasePath()
|
||||
|
||||
export function groupedDropdownToCascader(group) {
|
||||
const firstType = group[0]
|
||||
@@ -427,7 +459,7 @@ export function openNewWindow(url) {
|
||||
let params = 'toolbar=yes,scrollbars=yes,resizable=yes'
|
||||
params = params + `,top=${top},left=${left},width=${screen.width / 3},height=${screen.height / 3}`
|
||||
window.sessionStorage.setItem('newWindowCount', `${count + 1}`)
|
||||
window.open(url, '_blank', params)
|
||||
window.open(addBasePath(url), '_blank', params)
|
||||
}
|
||||
|
||||
export function getDrawerWidth() {
|
||||
@@ -548,8 +580,10 @@ export function randomString(length, includeSymbols = false) {
|
||||
}
|
||||
|
||||
export function createWsUrl(path) {
|
||||
if (/^wss?:\/\//i.test(path)) {
|
||||
return path
|
||||
}
|
||||
const scheme = location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const port = location.port ? ':' + location.port : ''
|
||||
const base = window.__BASE_PATH__ || ''
|
||||
return scheme + '://' + location.hostname + port + base + path
|
||||
return scheme + '://' + location.hostname + port + addBasePath(path)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function openOrReuseWindow(
|
||||
export function openTaskPage(taskId, taskType, taskUrl) {
|
||||
taskType = taskType || 'celery'
|
||||
if (!taskUrl) {
|
||||
taskUrl = `${window.__BASE_PATH__}/core/ops/${taskType}/task/${taskId}/log/?type=${taskType}`
|
||||
taskUrl = `/core/ops/${taskType}/task/${taskId}/log/?type=${taskType}`
|
||||
}
|
||||
openOrReuseWindow(taskUrl)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios'
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { eventBus } from '@/utils/vue/eventbus'
|
||||
import { getTokenFromCookie } from '@/utils/jms/auth'
|
||||
import { getErrorResponseMsg } from '@/utils/common'
|
||||
import { addBasePath, getCurrentPageUrl, getErrorResponseMsg } from '@/utils/common'
|
||||
import { MessageBox } from 'element-ui'
|
||||
import { message } from '@/utils/vue/message'
|
||||
import store from '@/store'
|
||||
@@ -70,9 +70,8 @@ service.interceptors.request.use(
|
||||
|
||||
function goToLogin() {
|
||||
setTimeout(() => {
|
||||
const base = window.__BASE_PATH__ || ''
|
||||
const next = base ? window.location.pathname.replace(base, '') : window.location.pathname
|
||||
window.location = `${base}${process.env.VUE_APP_LOGIN_PATH}?next=${next}`
|
||||
const next = encodeURIComponent(getCurrentPageUrl())
|
||||
window.location = `${addBasePath(process.env.VUE_APP_LOGIN_PATH)}?next=${next}`
|
||||
}, 200)
|
||||
localStorage.setItem('next', window.location.hash.replace('#', ''))
|
||||
}
|
||||
@@ -114,9 +113,8 @@ function ifBadRequest({ response, error }) {
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
const base = window.__BASE_PATH__ || ''
|
||||
const next = base ? location.pathname.replace(base, '') : location.pathname
|
||||
window.location.href = `${base}${process.env.VUE_APP_LOGOUT_PATH}?next=${next}`
|
||||
const next = encodeURIComponent(getCurrentPageUrl())
|
||||
window.location.href = `${addBasePath(process.env.VUE_APP_LOGOUT_PATH)}?next=${next}`
|
||||
}
|
||||
|
||||
export function flashErrorMsg({ response, error }) {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* Created by PanJiaChen on 16/11/18.
|
||||
*/
|
||||
import { encryptPassword } from './session-encrypt'
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
@@ -43,60 +41,6 @@ const options = {
|
||||
}
|
||||
}
|
||||
const filter = new xss.FilterXSS(options)
|
||||
|
||||
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { vueCookie as VueCookie } from '@/utils/storage'
|
||||
|
||||
export function fillKey(key) {
|
||||
const KeyLength = 16
|
||||
if (key.length > KeyLength) {
|
||||
key = key.slice(0, KeyLength)
|
||||
}
|
||||
const filledKey = Buffer.alloc(KeyLength)
|
||||
const keys = Buffer.from(key)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
filledKey[i] = keys[i]
|
||||
}
|
||||
return filledKey
|
||||
}
|
||||
|
||||
export function aesEncrypt(text, originKey) {
|
||||
const key = CryptoJS.enc.Utf8.parse(fillKey(originKey))
|
||||
return CryptoJS.AES.encrypt(text, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.ZeroPadding
|
||||
}).toString()
|
||||
}
|
||||
|
||||
export function rsaEncrypt(text, pubKey) {
|
||||
const jsEncrypt = new JSEncrypt()
|
||||
jsEncrypt.setPublicKey(pubKey)
|
||||
return jsEncrypt.encrypt(text)
|
||||
}
|
||||
|
||||
export function getCookie(name) {
|
||||
return VueCookie.get(name)
|
||||
}
|
||||
|
||||
export function encryptPassword(password) {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
let rsaPublicKeyText = getCookie('jms_public_key')
|
||||
if (!rsaPublicKeyText) {
|
||||
return password
|
||||
}
|
||||
const aesKey = (Math.random() + 1).toString(36).substring(2)
|
||||
// public key 是 base64 存储的
|
||||
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '')
|
||||
const rsaPublicKey = atob(rsaPublicKeyText)
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
|
||||
const passwordCipher = aesEncrypt(String(password), aesKey)
|
||||
return `${keyCipher}:${passwordCipher}`
|
||||
}
|
||||
|
||||
window.aesEncrypt = aesEncrypt
|
||||
window.fillKey = fillKey
|
||||
|
||||
export default filter
|
||||
|
||||
window.encryptPassword = encryptPassword
|
||||
|
||||
160
src/utils/session-encrypt.js
Normal file
160
src/utils/session-encrypt.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { vueCookie as VueCookie } from '@/utils/storage'
|
||||
import { sm2, sm4 } from 'sm-crypto'
|
||||
|
||||
export function getCookie(name) {
|
||||
return VueCookie.get(name)
|
||||
}
|
||||
|
||||
export function fillKey(key) {
|
||||
const KeyLength = 16
|
||||
if (key.length > KeyLength) {
|
||||
key = key.slice(0, KeyLength)
|
||||
}
|
||||
const filledKey = Buffer.alloc(KeyLength)
|
||||
const keys = Buffer.from(key)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
filledKey[i] = keys[i]
|
||||
}
|
||||
return filledKey
|
||||
}
|
||||
|
||||
function aesEncrypt(text, originKey) {
|
||||
const key = CryptoJS.enc.Utf8.parse(fillKey(originKey))
|
||||
return CryptoJS.AES.encrypt(text, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.ZeroPadding
|
||||
}).toString()
|
||||
}
|
||||
|
||||
function rsaEncrypt(text, pubKey) {
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
const jsEncrypt = new JSEncrypt()
|
||||
jsEncrypt.setPublicKey(pubKey)
|
||||
return jsEncrypt.encrypt(text)
|
||||
}
|
||||
|
||||
function rsaDecrypt(cipher, pkey) {
|
||||
const jsEncrypt = new JSEncrypt()
|
||||
jsEncrypt.setPrivateKey(pkey)
|
||||
return jsEncrypt.decrypt(cipher)
|
||||
}
|
||||
|
||||
window.rsaEncrypt = rsaEncrypt
|
||||
window.rsaDecrypt = rsaDecrypt
|
||||
|
||||
function hexToBytes(hex) {
|
||||
if (!hex) return new Uint8Array([])
|
||||
hex = hex.toString().trim().toLowerCase()
|
||||
if (hex.startsWith('0x')) {
|
||||
hex = hex.slice(2)
|
||||
}
|
||||
// 确保是偶数长度
|
||||
const len = Math.floor(hex.length / 2)
|
||||
const bytes = new Uint8Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
// Uint8Array -> base64(标准 base64)
|
||||
let binary = ''
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i])
|
||||
}
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
function rsaEncryptPassword(password, rsaPublicKey) {
|
||||
const aesKey = (Math.random() + 1).toString(36).substring(2)
|
||||
// public key 是 base64 存储的
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
|
||||
const passwordCipher = aesEncrypt(password, aesKey)
|
||||
return `${keyCipher}:${passwordCipher}`
|
||||
}
|
||||
|
||||
function ensureSm2PublicKey(sm2PublicKey) {
|
||||
// sm2.min.js 的 doEncrypt 需要能被 decodePointHex 解析的公钥:
|
||||
// 通常为非压缩点 hex,格式 `04||x||y`(总长度 130)。
|
||||
// 但后端生成/下发的公钥有时是 `x||y`(长度 128),这里做归一化补齐 `04` 前缀。
|
||||
if (typeof sm2PublicKey === 'string') {
|
||||
sm2PublicKey = sm2PublicKey.replaceAll('"', '').trim()
|
||||
if (sm2PublicKey.startsWith('0x')) {
|
||||
sm2PublicKey = sm2PublicKey.slice(2)
|
||||
}
|
||||
// 后端下发的 SM2 公钥常见是 x||y(128 hex),sm-crypto 需要 04||x||y(130 hex)
|
||||
if (sm2PublicKey.length === 128 && !sm2PublicKey.startsWith('04')) {
|
||||
sm2PublicKey = '04' + sm2PublicKey
|
||||
}
|
||||
}
|
||||
return sm2PublicKey
|
||||
}
|
||||
|
||||
function gmEncryptPassword(password, sm2PublicKey) {
|
||||
sm2PublicKey = ensureSm2PublicKey(sm2PublicKey)
|
||||
// 只适配前端,不改后端:
|
||||
// 直接生成 16 字符 key(后端 padding_key 会保持原样,不再补齐)
|
||||
const sm4KeyRaw = randomString(16)
|
||||
const sm4KeyHex = Buffer.from(sm4KeyRaw).toString('hex')
|
||||
|
||||
let keyCipher = ''
|
||||
try {
|
||||
// 与后端 gmssl.sm2.CryptSM2 默认 decrypt 的 mode 对齐:
|
||||
// gmssl 解析的格式是 C1C2C3(mode=0),前端这里输出也用 mode=0。
|
||||
keyCipher = sm2.doEncrypt(sm4KeyRaw, sm2PublicKey, 0)
|
||||
} catch (e) {
|
||||
console.error('gmEncryptPassword sm2.doEncrypt failed:', e)
|
||||
// 避免前端崩溃:失败时返回明文,由后端按原值流程处理(至少可继续登录/看报错)
|
||||
return password
|
||||
}
|
||||
|
||||
const passwordCipher = sm4.encrypt(password, sm4KeyHex)
|
||||
// sm2/sm4 默认输出是 hex,但后端 gm.py/session.py 需要 base64:
|
||||
// - sm2_decrypt: base64.b64decode
|
||||
// - sm4 decrypt: base64.urlsafe_b64decode
|
||||
const keyCipherB64 = bytesToBase64(hexToBytes(keyCipher))
|
||||
const passwordCipherB64 = bytesToBase64(hexToBytes(passwordCipher))
|
||||
return `${keyCipherB64}:${passwordCipherB64}`
|
||||
}
|
||||
|
||||
export function encryptPassword(password) {
|
||||
if (!password) {
|
||||
console.log('password is empty')
|
||||
return ''
|
||||
}
|
||||
let publicKeyText = getCookie('jms_public_key')
|
||||
if (!publicKeyText) {
|
||||
console.log('publicKeyText is empty')
|
||||
return password
|
||||
}
|
||||
publicKeyText = publicKeyText.replaceAll('"', '')
|
||||
publicKeyText = atob(publicKeyText)
|
||||
let cipher = ''
|
||||
let jmsGMSSL = getCookie('jms_gm_ssl')
|
||||
if (publicKeyText.includes('PUBLIC KEY')) {
|
||||
jmsGMSSL = '0'
|
||||
}
|
||||
if (jmsGMSSL === '1') {
|
||||
cipher = gmEncryptPassword(password, publicKeyText)
|
||||
} else {
|
||||
cipher = rsaEncryptPassword(password, publicKeyText)
|
||||
}
|
||||
|
||||
return cipher
|
||||
}
|
||||
|
||||
export function randomString(length) {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let result = ''
|
||||
const charactersLength = characters.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<script>
|
||||
import { GenericCreateUpdatePage } from '@/layout/components'
|
||||
import getChangeSecretFields from '@/views/accounts/AccountBackup/fields'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { periodicMeta } from '@/components/const'
|
||||
|
||||
export default {
|
||||
@@ -20,7 +20,8 @@ export default {
|
||||
url: '/api/v1/accounts/account-backup-plans/',
|
||||
fields: [
|
||||
[this.$t('Basic'), ['name', 'types']],
|
||||
[this.$t('Backup'),
|
||||
[
|
||||
this.$t('Backup'),
|
||||
[
|
||||
'backup_type',
|
||||
'is_password_divided_by_email',
|
||||
@@ -91,6 +92,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<GenericCreateUpdatePage
|
||||
v-bind="$data"
|
||||
@getObjectDone="getObjectDone"
|
||||
/>
|
||||
<GenericCreateUpdatePage v-bind="$data" @getObjectDone="getObjectDone" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
|
||||
import { templateFields, templateFieldsMeta } from './const.js'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export default {
|
||||
name: 'GatewayCreateUpdate',
|
||||
@@ -27,7 +24,7 @@ export default {
|
||||
return {
|
||||
initial: {
|
||||
secret_type: 'password',
|
||||
push_params: { }
|
||||
push_params: {}
|
||||
},
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
hasDetailInMsg: false,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script>
|
||||
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
import { getUpdateObjURL, setUrlParam } from '@/utils/common/index'
|
||||
import { assetFieldsMeta } from '@/views/assets/const'
|
||||
|
||||
@@ -38,7 +38,7 @@ export default {
|
||||
},
|
||||
updateInitial: {
|
||||
type: Function,
|
||||
default: (initial) => {
|
||||
default: initial => {
|
||||
return initial
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
delete values['accounts']
|
||||
} else {
|
||||
const accounts = values?.accounts || []
|
||||
values.accounts = accounts.map((item) => {
|
||||
values.accounts = accounts.map(item => {
|
||||
item['secret'] = encryptPassword(item['secret'])
|
||||
return item
|
||||
})
|
||||
@@ -179,5 +179,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export const gcp = 'gcp'
|
||||
export const aliyun = 'aliyun'
|
||||
@@ -55,8 +55,18 @@ export const publicHostProviders = [
|
||||
export const publicDBProviders = [aliyun]
|
||||
|
||||
export const privateCloudProviders = [
|
||||
vmware, qingcloud_private, huaweicloud_private, ctyun_private,
|
||||
openstack, zstack, nutanix, fc, scp, apsara_stack, smartx, proxmox
|
||||
vmware,
|
||||
qingcloud_private,
|
||||
huaweicloud_private,
|
||||
ctyun_private,
|
||||
openstack,
|
||||
zstack,
|
||||
nutanix,
|
||||
fc,
|
||||
scp,
|
||||
apsara_stack,
|
||||
smartx,
|
||||
proxmox
|
||||
]
|
||||
|
||||
export const ACCOUNT_PROVIDER_ATTRS_MAP = {
|
||||
|
||||
@@ -95,6 +95,7 @@ export default {
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasReportExport: true,
|
||||
hasDatePicker: true,
|
||||
hasImport: false,
|
||||
searchConfig: {
|
||||
|
||||
@@ -73,6 +73,7 @@ export default {
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasReportExport: true,
|
||||
hasImport: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasReportExport: true,
|
||||
hasDatePicker: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: true
|
||||
|
||||
@@ -72,7 +72,11 @@ export default {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasDatePicker: true
|
||||
hasReportExport: true,
|
||||
hasDatePicker: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ export default {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasDatePicker: true
|
||||
hasReportExport: true,
|
||||
hasDatePicker: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
:title="title"
|
||||
:nav="nav"
|
||||
:name="name"
|
||||
:charts="charts"
|
||||
:tables="tables"
|
||||
:show-display-mode-toggle="true"
|
||||
:display-mode.sync="displayMode"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="charts-grid">
|
||||
<ReportToolbar
|
||||
:filters="currentFilters"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<template v-if="showChart">
|
||||
<div class="chart-container full-width">
|
||||
<div class="chart-container-title">
|
||||
@@ -19,17 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
|
||||
<div v-if="!isCustomReport" class="chart-container full-width">
|
||||
<div class="chart-container-title">
|
||||
<div class="chart-container-title-text">{{ $t('RiskyAccount') }}</div>
|
||||
@@ -76,17 +72,6 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<ReportToolbar
|
||||
v-if="tableData.length"
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<div v-for="(t, idx) in tableData.slice(1)" :key="t.name || idx" class="report-table-wrap full-width">
|
||||
<el-card class="report-card" shadow="hover">
|
||||
<div v-if="t.name" class="chart-container-title">
|
||||
@@ -101,16 +86,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<el-table :data="tableData.rows" border>
|
||||
<el-table-column v-for="column in tableData.columns" :key="column.key" :label="column.label" :prop="column.key" min-width="140" />
|
||||
</el-table>
|
||||
@@ -151,6 +126,16 @@ export default {
|
||||
return {
|
||||
title: this.$t('AccountAutomationReport'),
|
||||
name: 'AccountAutomationReport',
|
||||
charts: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'TaskExecutionTrends', title: this.$t('TaskExecutionTrends') },
|
||||
{ name: 'AccountResult', title: this.$t('AccountResult') }
|
||||
],
|
||||
tables: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'TaskExecutionTrends', title: this.$t('TaskExecutionTrends') },
|
||||
{ name: 'AccountResult', title: this.$t('AccountResult') }
|
||||
],
|
||||
description: '-',
|
||||
days: localStorage.getItem(this.name) || '7',
|
||||
automation_stats: {
|
||||
@@ -283,18 +268,10 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
days() {
|
||||
this.getData()
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getData()
|
||||
},
|
||||
methods: {
|
||||
onChange(val) {
|
||||
this.days = val
|
||||
},
|
||||
async getData() {
|
||||
const data = await this.fetchReportData('/api/v1/reports/reports/account-automation/')
|
||||
await this.loadTableData('/api/v1/reports/reports/account-automation/')
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
:title="title"
|
||||
:nav="nav"
|
||||
:name="name"
|
||||
:charts="charts"
|
||||
:tables="tables"
|
||||
:show-display-mode-toggle="true"
|
||||
:display-mode.sync="displayMode"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="charts-grid">
|
||||
<ReportToolbar
|
||||
:filters="currentFilters"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<template v-if="showChart">
|
||||
<div class="chart-container full-width">
|
||||
<div class="chart-container-title">
|
||||
@@ -19,17 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-container-title">
|
||||
<div class="chart-container-title-text">{{ $t('AccountCreationSourceDistribution') }}</div>
|
||||
@@ -97,17 +93,6 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<ReportToolbar
|
||||
v-if="tableData.length"
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<div v-for="(t, idx) in tableData.slice(1)" :key="t.name || idx" class="report-table-wrap full-width">
|
||||
<el-card class="report-card" shadow="hover">
|
||||
<div v-if="t.name" class="chart-container-title">
|
||||
@@ -128,16 +113,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<el-table :data="tableData.rows" border>
|
||||
<el-table-column
|
||||
v-for="column in tableData.columns"
|
||||
@@ -183,6 +158,22 @@ export default {
|
||||
return {
|
||||
title: this.$t('AccountStatisticsReport'),
|
||||
name: 'AccountStatistics',
|
||||
charts: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'AccountCreationSourceDistribution', title: this.$t('AccountCreationSourceDistribution') },
|
||||
{ name: 'AccountConnectivityStatusDistribution', title: this.$t('AccountConnectivityStatusDistribution') },
|
||||
{ name: 'AccountPasswordChangeTrends', title: this.$t('AccountPasswordChangeTrends') },
|
||||
{ name: 'RankByNumberOfAssetAccounts', title: this.$t('RankByNumberOfAssetAccounts') },
|
||||
{ name: 'AccountAndPasswordChangeRank', title: this.$t('AccountAndPasswordChangeRank') }
|
||||
],
|
||||
tables: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'AccountCreationSourceDistribution', title: this.$t('AccountCreationSourceDistribution') },
|
||||
{ name: 'AccountConnectivityStatusDistribution', title: this.$t('AccountConnectivityStatusDistribution') },
|
||||
{ name: 'AccountPasswordChangeTrends', title: this.$t('AccountPasswordChangeTrends') },
|
||||
{ name: 'RankByNumberOfAssetAccounts', title: this.$t('RankByNumberOfAssetAccounts') },
|
||||
{ name: 'AccountAndPasswordChangeRank', title: this.$t('AccountAndPasswordChangeRank') }
|
||||
],
|
||||
days: '30',
|
||||
account_stats: {
|
||||
'total': 0,
|
||||
|
||||
@@ -38,14 +38,16 @@
|
||||
|
||||
<script>
|
||||
import Page from '@/layout/components/Page'
|
||||
import { resolveRoute } from '@/utils/vue/index'
|
||||
import { appendQuery, isSameReportQuery } from '@/views/reports/base/reportUtils'
|
||||
import AccountStatistics from '@/views/reports/accounts/AccountStatistics.vue'
|
||||
import AccountAutomation from '@/views/reports/accounts/AccountAutomation.vue'
|
||||
import { appendQuery, buildCustomReportRouteQuery, reportDebugLog } from '@/views/reports/base/reportUtils'
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
key: 'AccountStatistics',
|
||||
titleKey: 'AccountStatisticsReport',
|
||||
routeName: 'AccountStatistics',
|
||||
component: AccountStatistics,
|
||||
path: '/reports/accounts/account-statistics',
|
||||
icon: 'fa fa-users',
|
||||
perm: 'rbac.view_accountstatisticsreport',
|
||||
reportType: 'AccountStatistics'
|
||||
@@ -53,7 +55,8 @@ const MENU_ITEMS = [
|
||||
{
|
||||
key: 'AccountAutomationReport',
|
||||
titleKey: 'AccountAutomationReport',
|
||||
routeName: 'AccountAutomationReport',
|
||||
component: AccountAutomation,
|
||||
path: '/reports/accounts/account-automation',
|
||||
icon: 'fa fa-cogs',
|
||||
perm: 'rbac.view_accountautomationreport',
|
||||
reportType: 'AccountAutomationReport'
|
||||
@@ -72,17 +75,36 @@ export default {
|
||||
component: '',
|
||||
componentKey: '',
|
||||
selectedChartKey: '',
|
||||
chartItems: []
|
||||
chartItems: [],
|
||||
catalogLoaded: false,
|
||||
lastSyncQueryKey: '',
|
||||
isPageActive: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.fullPath'() {
|
||||
if (!this.isPageActive) return
|
||||
const routeKey = this.buildRouteSyncKey(this.$route.query)
|
||||
if (routeKey === this.lastSyncQueryKey) return
|
||||
this.lastSyncQueryKey = routeKey
|
||||
reportDebugLog('accounts.index.route.fullPath', {
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query,
|
||||
selectedChartKey: this.selectedChartKey
|
||||
})
|
||||
this.syncSelectedFromRoute()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadCatalog()
|
||||
},
|
||||
activated() {
|
||||
this.isPageActive = true
|
||||
this.syncSelectedFromRoute()
|
||||
},
|
||||
deactivated() {
|
||||
this.isPageActive = false
|
||||
},
|
||||
methods: {
|
||||
getBaseItems() {
|
||||
return MENU_ITEMS
|
||||
@@ -90,15 +112,20 @@ export default {
|
||||
.map(item => ({
|
||||
key: item.key,
|
||||
title: this.$t(item.titleKey),
|
||||
routeName: item.routeName,
|
||||
component: item.component,
|
||||
path: item.path,
|
||||
icon: item.icon,
|
||||
isCustom: false,
|
||||
reportType: item.reportType,
|
||||
query: {},
|
||||
query: {
|
||||
chart_key: item.key
|
||||
},
|
||||
children: []
|
||||
}))
|
||||
},
|
||||
async loadCatalog() {
|
||||
reportDebugLog('accounts.index.loadCatalog.start', { routePath: this.$route.path, query: this.$route.query })
|
||||
this.catalogLoaded = false
|
||||
const items = this.getBaseItems()
|
||||
const itemMap = items.reduce((acc, item) => {
|
||||
if (item.reportType) {
|
||||
@@ -116,16 +143,24 @@ export default {
|
||||
target.children = (group.children || []).map(child => ({
|
||||
key: `report-${child.id}`,
|
||||
title: child.name,
|
||||
routeName: target.routeName,
|
||||
reportId: child.id,
|
||||
component: target.component,
|
||||
path: target.path,
|
||||
reportId: String(child.id),
|
||||
isCustom: true,
|
||||
query: { report_id: child.id }
|
||||
query: {
|
||||
chart_key: target.key,
|
||||
...buildCustomReportRouteQuery(child)
|
||||
}
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('load report catalog failed', error)
|
||||
}
|
||||
this.chartItems = items
|
||||
this.catalogLoaded = true
|
||||
reportDebugLog('accounts.index.loadCatalog.done', {
|
||||
items: items.map(item => ({ key: item.key, childCount: (item.children || []).length }))
|
||||
})
|
||||
this.syncSelectedFromRoute()
|
||||
},
|
||||
syncSelectedFromRoute() {
|
||||
@@ -135,32 +170,75 @@ export default {
|
||||
if (reportId) {
|
||||
target = this.chartItems
|
||||
.flatMap(item => item.children || [])
|
||||
.find(item => item.reportId === reportId)
|
||||
.find(item => String(item.reportId) === String(reportId))
|
||||
if (!target) {
|
||||
this.loadCatalog()
|
||||
if (!this.catalogLoaded) {
|
||||
return
|
||||
}
|
||||
const nextQuery = { ...(this.$route.query || {}) }
|
||||
delete nextQuery.report_id
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery })
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
target = this.chartItems[0]
|
||||
const chartKey = this.$route.query.chart_key
|
||||
target = this.chartItems.find(item => item.key === chartKey)
|
||||
|| this.chartItems.find(item => item.key === this.selectedChartKey)
|
||||
|| this.chartItems[0]
|
||||
}
|
||||
if (target) {
|
||||
if (target && (this.selectedChartKey !== target.key || !this.component)) {
|
||||
this.applyChart(target)
|
||||
}
|
||||
reportDebugLog('accounts.index.syncSelectedFromRoute', {
|
||||
reportId,
|
||||
selectedChartKey: this.selectedChartKey,
|
||||
targetKey: target?.key || ''
|
||||
})
|
||||
},
|
||||
buildRouteSyncKey(query = {}) {
|
||||
return JSON.stringify({
|
||||
report_id: query.report_id || '',
|
||||
chart_key: query.chart_key || '',
|
||||
days: query.days || ''
|
||||
})
|
||||
},
|
||||
isActive(item) {
|
||||
return this.selectedChartKey === item.key
|
||||
},
|
||||
applyChart(chart) {
|
||||
this.selectedChartKey = chart.key
|
||||
const route = resolveRoute({ name: chart.routeName }, this.$router)
|
||||
this.component = route.components.default
|
||||
this.componentKey = `${chart.key}-${this.$route.fullPath}`
|
||||
this.url = appendQuery('/ui/#' + route.path, chart.query || {})
|
||||
if (!chart.component || !chart.path) {
|
||||
return
|
||||
}
|
||||
this.component = chart.component
|
||||
this.componentKey = chart.key
|
||||
this.url = appendQuery('/ui/#' + chart.path, chart.query || {})
|
||||
reportDebugLog('accounts.index.applyChart', {
|
||||
chartKey: chart.key,
|
||||
reportId: chart.reportId || '',
|
||||
url: this.url,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
},
|
||||
handleChangeChart(chart) {
|
||||
const nextQuery = chart.query || {}
|
||||
if (isSameReportQuery(this.$route.query, nextQuery)) {
|
||||
const nextQuery = {
|
||||
...(this.$route.query.days ? { days: this.$route.query.days } : {}),
|
||||
...(chart.query || {})
|
||||
}
|
||||
reportDebugLog('accounts.index.handleChangeChart', {
|
||||
chartKey: chart.key,
|
||||
reportId: chart.reportId || '',
|
||||
nextQuery,
|
||||
currentQuery: this.$route.query
|
||||
})
|
||||
const normalize = (query = {}) => ({
|
||||
days: query.days || '',
|
||||
report_id: query.report_id || '',
|
||||
chart_key: query.chart_key || ''
|
||||
})
|
||||
if (JSON.stringify(normalize(this.$route.query)) === JSON.stringify(normalize(nextQuery))) {
|
||||
this.applyChart(chart)
|
||||
return
|
||||
}
|
||||
@@ -231,7 +309,7 @@ h5 {
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list li.active menu-link {
|
||||
.folder-list li.active > .menu-link {
|
||||
color: var(--color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
:title="title"
|
||||
:nav="nav"
|
||||
:name="name"
|
||||
:charts="charts"
|
||||
:tables="tables"
|
||||
:show-display-mode-toggle="true"
|
||||
:display-mode.sync="displayMode"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="charts-grid">
|
||||
<ReportToolbar
|
||||
:filters="currentFilters"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<template v-if="showChart">
|
||||
<div class="chart-container full-width">
|
||||
<div class="chart-container-title">
|
||||
@@ -19,16 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-container-title">
|
||||
<UserAssetActivity :days="days" :metrics="user_asset_activity_metrics" />
|
||||
@@ -98,16 +95,6 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<ReportToolbar
|
||||
v-if="tableData.length"
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<div v-for="(t, idx) in tableData.slice(1)" :key="t.name || idx" class="report-table-wrap">
|
||||
<el-card class="report-card" shadow="hover">
|
||||
<div v-if="t.name" class="chart-container-title">
|
||||
@@ -122,15 +109,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<el-table :data="tableData.rows" border>
|
||||
<el-table-column v-for="column in tableData.columns" :key="column.key" :label="column.label" :prop="column.key" min-width="140" />
|
||||
</el-table>
|
||||
@@ -170,6 +148,22 @@ export default {
|
||||
return {
|
||||
title: this.$t('AssetActivityReport'),
|
||||
name: 'AssetReport',
|
||||
charts: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'UserAssetActivity', title: this.$t('UserAssetActivity') },
|
||||
{ name: 'DistributionOfAssetLoginMethods', title: this.$t('DistributionOfAssetLoginMethods') },
|
||||
{ name: 'RemoteLoginProtocolUsageDistribution', title: this.$t('RemoteLoginProtocolUsageDistribution') },
|
||||
{ name: 'OperatingSystemDistributionOfLoginAssets', title: this.$t('OperatingSystemDistributionOfLoginAssets') },
|
||||
{ name: 'AssetLoginTrends', title: this.$t('AssetLoginTrends') }
|
||||
],
|
||||
tables: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'UserAssetActivity', title: this.$t('UserAssetActivity') },
|
||||
{ name: 'DistributionOfAssetLoginMethods', title: this.$t('DistributionOfAssetLoginMethods') },
|
||||
{ name: 'RemoteLoginProtocolUsageDistribution', title: this.$t('RemoteLoginProtocolUsageDistribution') },
|
||||
{ name: 'OperatingSystemDistributionOfLoginAssets', title: this.$t('OperatingSystemDistributionOfLoginAssets') },
|
||||
{ name: 'AssetLoginTrends', title: this.$t('AssetLoginTrends') }
|
||||
],
|
||||
days: localStorage.getItem(this.name) || '7',
|
||||
session_stats: {
|
||||
'total': 0,
|
||||
@@ -431,18 +425,10 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
days() {
|
||||
this.getData()
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getData()
|
||||
},
|
||||
methods: {
|
||||
onChange(val) {
|
||||
this.days = val
|
||||
},
|
||||
conversionData(data) {
|
||||
return data.map(item => {
|
||||
return {
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
:title="title"
|
||||
:nav="nav"
|
||||
:name="name"
|
||||
:charts="charts"
|
||||
:tables="tables"
|
||||
:show-display-mode-toggle="true"
|
||||
:display-mode.sync="displayMode"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="charts-grid">
|
||||
<ReportToolbar
|
||||
:filters="currentFilters"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<template v-if="showChart">
|
||||
<div class="chart-container full-width">
|
||||
<div class="chart-container-title">
|
||||
@@ -19,17 +26,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
|
||||
<div class="chart-container full-width">
|
||||
<div class="chart-container-title">
|
||||
<div class="chart-container-title-text">{{ $t('AssetTypeDistribution') }}</div>
|
||||
@@ -74,17 +70,6 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<ReportToolbar
|
||||
v-if="tableData.length"
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<div v-for="(t, idx) in tableData.slice(1)" :key="t.name || idx" class="report-table-wrap full-width">
|
||||
<el-card class="report-card" shadow="hover">
|
||||
<div v-if="t.name" class="chart-container-title">
|
||||
@@ -105,16 +90,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
:show-date-controls="false"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<el-table :data="tableData.rows" border>
|
||||
<el-table-column
|
||||
v-for="column in tableData.columns"
|
||||
@@ -158,6 +133,16 @@ export default {
|
||||
return {
|
||||
title: this.$t('AssetStatisticsReport'),
|
||||
name: 'AssetStatistics',
|
||||
charts: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'AssetTypeDistribution', title: this.$t('AssetTypeDistribution') },
|
||||
{ name: 'WeeklyGrowthTrend', title: this.$t('WeeklyGrowthTrend') }
|
||||
],
|
||||
tables: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'AssetTypeDistribution', title: this.$t('AssetTypeDistribution') },
|
||||
{ name: 'WeeklyGrowthTrend', title: this.$t('WeeklyGrowthTrend') }
|
||||
],
|
||||
days: '7',
|
||||
asset_stats: {
|
||||
'total': 0,
|
||||
|
||||
@@ -38,14 +38,16 @@
|
||||
|
||||
<script>
|
||||
import Page from '@/layout/components/Page'
|
||||
import { resolveRoute } from '@/utils/vue/index'
|
||||
import { appendQuery, isSameReportQuery } from '@/views/reports/base/reportUtils'
|
||||
import AssetActivity from '@/views/reports/assets/AssetActivity.vue'
|
||||
import AssetStatistics from '@/views/reports/assets/AssetStatistics.vue'
|
||||
import { appendQuery, buildCustomReportRouteQuery, reportDebugLog } from '@/views/reports/base/reportUtils'
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{
|
||||
key: 'AssetStatistics',
|
||||
titleKey: 'AssetStatisticsReport',
|
||||
routeName: 'AssetStatistics',
|
||||
component: AssetStatistics,
|
||||
path: '/reports/assets/asset-statistics',
|
||||
icon: 'fa fa-database',
|
||||
perm: 'rbac.view_assetstatisticsreport',
|
||||
reportType: 'AssetStatistics'
|
||||
@@ -53,7 +55,8 @@ const MENU_ITEMS = [
|
||||
{
|
||||
key: 'AssetReport',
|
||||
titleKey: 'AssetActivityReport',
|
||||
routeName: 'AssetReport',
|
||||
component: AssetActivity,
|
||||
path: '/reports/assets/asset-activity',
|
||||
icon: 'fa fa-exchange',
|
||||
perm: 'rbac.view_assetactivityreport',
|
||||
reportType: 'AssetReport'
|
||||
@@ -72,17 +75,36 @@ export default {
|
||||
component: '',
|
||||
componentKey: '',
|
||||
selectedChartKey: '',
|
||||
chartItems: []
|
||||
chartItems: [],
|
||||
catalogLoaded: false,
|
||||
lastSyncQueryKey: '',
|
||||
isPageActive: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.fullPath'() {
|
||||
if (!this.isPageActive) return
|
||||
const routeKey = this.buildRouteSyncKey(this.$route.query)
|
||||
if (routeKey === this.lastSyncQueryKey) return
|
||||
this.lastSyncQueryKey = routeKey
|
||||
reportDebugLog('assets.index.route.fullPath', {
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query,
|
||||
selectedChartKey: this.selectedChartKey
|
||||
})
|
||||
this.syncSelectedFromRoute()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadCatalog()
|
||||
},
|
||||
activated() {
|
||||
this.isPageActive = true
|
||||
this.syncSelectedFromRoute()
|
||||
},
|
||||
deactivated() {
|
||||
this.isPageActive = false
|
||||
},
|
||||
methods: {
|
||||
getBaseItems() {
|
||||
return MENU_ITEMS
|
||||
@@ -90,15 +112,20 @@ export default {
|
||||
.map(item => ({
|
||||
key: item.key,
|
||||
title: this.$t(item.titleKey),
|
||||
routeName: item.routeName,
|
||||
component: item.component,
|
||||
path: item.path,
|
||||
icon: item.icon,
|
||||
isCustom: false,
|
||||
reportType: item.reportType,
|
||||
query: {},
|
||||
query: {
|
||||
chart_key: item.key
|
||||
},
|
||||
children: []
|
||||
}))
|
||||
},
|
||||
async loadCatalog() {
|
||||
reportDebugLog('assets.index.loadCatalog.start', { routePath: this.$route.path, query: this.$route.query })
|
||||
this.catalogLoaded = false
|
||||
const items = this.getBaseItems()
|
||||
const itemMap = items.reduce((acc, item) => {
|
||||
if (item.reportType) {
|
||||
@@ -116,16 +143,24 @@ export default {
|
||||
target.children = (group.children || []).map(child => ({
|
||||
key: `report-${child.id}`,
|
||||
title: child.name,
|
||||
routeName: target.routeName,
|
||||
reportId: child.id,
|
||||
component: target.component,
|
||||
path: target.path,
|
||||
reportId: String(child.id),
|
||||
isCustom: true,
|
||||
query: { report_id: child.id }
|
||||
query: {
|
||||
chart_key: target.key,
|
||||
...buildCustomReportRouteQuery(child)
|
||||
}
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('load report catalog failed', error)
|
||||
}
|
||||
this.chartItems = items
|
||||
this.catalogLoaded = true
|
||||
reportDebugLog('assets.index.loadCatalog.done', {
|
||||
items: items.map(item => ({ key: item.key, childCount: (item.children || []).length }))
|
||||
})
|
||||
this.syncSelectedFromRoute()
|
||||
},
|
||||
syncSelectedFromRoute() {
|
||||
@@ -135,32 +170,75 @@ export default {
|
||||
if (reportId) {
|
||||
target = this.chartItems
|
||||
.flatMap(item => item.children || [])
|
||||
.find(item => item.reportId === reportId)
|
||||
.find(item => String(item.reportId) === String(reportId))
|
||||
if (!target) {
|
||||
this.loadCatalog()
|
||||
if (!this.catalogLoaded) {
|
||||
return
|
||||
}
|
||||
const nextQuery = { ...(this.$route.query || {}) }
|
||||
delete nextQuery.report_id
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery })
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
target = this.chartItems[0]
|
||||
const chartKey = this.$route.query.chart_key
|
||||
target = this.chartItems.find(item => item.key === chartKey)
|
||||
|| this.chartItems.find(item => item.key === this.selectedChartKey)
|
||||
|| this.chartItems[0]
|
||||
}
|
||||
if (target) {
|
||||
if (target && (this.selectedChartKey !== target.key || !this.component)) {
|
||||
this.applyChart(target)
|
||||
}
|
||||
reportDebugLog('assets.index.syncSelectedFromRoute', {
|
||||
reportId,
|
||||
selectedChartKey: this.selectedChartKey,
|
||||
targetKey: target?.key || ''
|
||||
})
|
||||
},
|
||||
isActive(item) {
|
||||
return this.selectedChartKey === item.key
|
||||
},
|
||||
applyChart(chart) {
|
||||
this.selectedChartKey = chart.key
|
||||
const route = resolveRoute({ name: chart.routeName }, this.$router)
|
||||
this.component = route.components.default
|
||||
this.componentKey = `${chart.key}-${this.$route.fullPath}`
|
||||
this.url = appendQuery('/ui/#' + route.path, chart.query || {})
|
||||
if (!chart.component || !chart.path) {
|
||||
return
|
||||
}
|
||||
this.component = chart.component
|
||||
this.componentKey = chart.key
|
||||
this.url = appendQuery('/ui/#' + chart.path, chart.query || {})
|
||||
reportDebugLog('assets.index.applyChart', {
|
||||
chartKey: chart.key,
|
||||
reportId: chart.reportId || '',
|
||||
url: this.url,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
},
|
||||
buildRouteSyncKey(query = {}) {
|
||||
return JSON.stringify({
|
||||
report_id: query.report_id || '',
|
||||
chart_key: query.chart_key || '',
|
||||
days: query.days || ''
|
||||
})
|
||||
},
|
||||
handleChangeChart(chart) {
|
||||
const nextQuery = chart.query || {}
|
||||
if (isSameReportQuery(this.$route.query, nextQuery)) {
|
||||
const nextQuery = {
|
||||
...(this.$route.query.days ? { days: this.$route.query.days } : {}),
|
||||
...(chart.query || {})
|
||||
}
|
||||
reportDebugLog('assets.index.handleChangeChart', {
|
||||
chartKey: chart.key,
|
||||
reportId: chart.reportId || '',
|
||||
nextQuery,
|
||||
currentQuery: this.$route.query
|
||||
})
|
||||
const normalize = (query = {}) => ({
|
||||
days: query.days || '',
|
||||
report_id: query.report_id || '',
|
||||
chart_key: query.chart_key || ''
|
||||
})
|
||||
if (JSON.stringify(normalize(this.$route.query)) === JSON.stringify(normalize(nextQuery))) {
|
||||
this.applyChart(chart)
|
||||
return
|
||||
}
|
||||
@@ -231,7 +309,7 @@ h5 {
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list li.active .menu-link {
|
||||
.folder-list li.active > .menu-link {
|
||||
color: var(--color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
<Logo />
|
||||
</div>
|
||||
<RightAction
|
||||
:chart-options="chartOptions"
|
||||
:name="name"
|
||||
:selected-chart-names="selectedChartNames"
|
||||
:selected-table-names="selectedTableNames"
|
||||
:table-options="tableOptions"
|
||||
:title="title"
|
||||
:show-operation-dropdown="!isCustomReportPage"
|
||||
:force-default-actions="nav && isCustomReportPage"
|
||||
@@ -21,7 +25,7 @@
|
||||
[{{ new Date().toLocaleString() }}]
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="customizeMode" class="report-visibility-panel">
|
||||
<div v-if="customizeMode || isCustomReportPage" class="report-visibility-panel">
|
||||
<div class="report-visibility-row">
|
||||
<el-checkbox :value="isDisplayModeEnabled('chart')" @change="handleModeToggle('chart', $event)">
|
||||
{{ $t('ChartReport') }}
|
||||
@@ -61,12 +65,16 @@
|
||||
</div>
|
||||
<div v-if="!nav" class="title-right">
|
||||
<RightAction
|
||||
:chart-options="chartOptions"
|
||||
:name="name"
|
||||
:title="title"
|
||||
:editor-only="true"
|
||||
:selected-chart-names="selectedChartNames"
|
||||
:selected-table-names="selectedTableNames"
|
||||
:show-editor-button="false"
|
||||
:show-custom-actions-in-editor="isCustomReportPage"
|
||||
:show-operation-only-in-editor="true"
|
||||
:table-options="tableOptions"
|
||||
/>
|
||||
<span v-if="url && showReportExportBtn" class="export-btn inline-export-btn">
|
||||
<el-button type="text" @click="openNewWindow">
|
||||
|
||||
@@ -16,43 +16,47 @@
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('ReportPeriodicExecution')">
|
||||
<el-switch v-model="form.is_periodic" />
|
||||
<el-form-item :label="$t('TimeRange')" prop="days">
|
||||
<el-select v-model="form.days" style="width: 100%">
|
||||
<el-option
|
||||
v-for="option in presetOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="form.is_periodic">
|
||||
<el-form-item :label="$t('Interval')" prop="interval">
|
||||
<el-input-number v-model="form.interval" :min="1" :precision="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('ChartReport')" prop="visibleCharts">
|
||||
<el-checkbox-group v-model="form.visibleCharts">
|
||||
<el-checkbox
|
||||
v-for="item in normalizedChartOptions"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.title }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('Crontab')">
|
||||
<CronTab v-model="form.crontab" />
|
||||
<div class="form-help-text">{{ $t('ReportSchedulePriorityTip') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('ReportRecipientsLabel')" prop="recipients">
|
||||
<Select2 v-model="form.recipients" v-bind="recipientSelect" />
|
||||
<div class="form-help-text">{{ $t('ReportRecipientsTip') }}</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item :label="$t('TableDetails')" prop="visibleTables">
|
||||
<el-checkbox-group v-model="form.visibleTables">
|
||||
<el-checkbox
|
||||
v-for="item in normalizedTableOptions"
|
||||
:key="item.name"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.title }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import CronTab from '@/components/Form/FormFields/CronTab/index.vue'
|
||||
import { REPORT_PRESET_DAYS_MAP, REPORT_RANGE_PRESET_OPTIONS } from './reportUtils'
|
||||
|
||||
const FILTER_FIELD_MAP = {
|
||||
UserLoginReport: 'user_id',
|
||||
UserChangePasswordReport: 'user_id',
|
||||
AssetStatistics: 'asset_id',
|
||||
AssetReport: 'asset_id',
|
||||
AccountStatistics: 'account',
|
||||
AccountAutomationReport: 'account'
|
||||
}
|
||||
import { REPORT_RANGE_PRESET_OPTIONS, normalizeReportDays } from './reportUtils'
|
||||
|
||||
function getDefaultName(title) {
|
||||
const now = new Date()
|
||||
@@ -60,22 +64,10 @@ function getDefaultName(title) {
|
||||
return `${title}-${date}`
|
||||
}
|
||||
|
||||
function normalizeRecipients(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
if (value && Array.isArray(value.ids)) {
|
||||
return value.ids
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'CreateReportDialog',
|
||||
components: {
|
||||
CronTab,
|
||||
Dialog,
|
||||
Select2
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
@@ -97,52 +89,33 @@ export default {
|
||||
defaultDays: {
|
||||
type: [String, Number],
|
||||
default: '7'
|
||||
},
|
||||
chartOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tableOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
defaultVisibleCharts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
defaultVisibleTables: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const validateCustomRange = (rule, value, callback) => {
|
||||
if (this.form.range_preset !== 'custom') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(value) || value.length !== 2 || !value[0] || !value[1]) {
|
||||
callback(new Error(this.$t('SelectStartAndEndDate')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
const validateRecipients = (rule, value, callback) => {
|
||||
if (!this.form.is_periodic) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
callback(new Error(this.$t('PleaseSelectRecipients')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
const validateSchedule = (rule, value, callback) => {
|
||||
if (!this.form.is_periodic) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!this.form.interval && !this.form.crontab) {
|
||||
callback(new Error(this.$t('RequireIntervalOrCrontabSetting')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
return {
|
||||
submitting: false,
|
||||
form: this.getInitialForm(),
|
||||
presetOptions: REPORT_RANGE_PRESET_OPTIONS,
|
||||
rules: {
|
||||
name: [{ required: true, message: this.$t('ThisFieldIsRequired'), trigger: 'blur' }],
|
||||
range_preset: [{ required: true, message: this.$t('PleaseSelectTimeRange'), trigger: 'change' }],
|
||||
date_range: [{ validator: validateCustomRange, trigger: 'change' }],
|
||||
interval: [{ validator: validateSchedule, trigger: 'change' }],
|
||||
recipients: [{ validator: validateRecipients, trigger: 'change' }]
|
||||
visibleCharts: [{ validator: this.validateVisibleReports, trigger: 'change' }],
|
||||
visibleTables: [{ validator: this.validateVisibleReports, trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -155,62 +128,14 @@ export default {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
filterField() {
|
||||
return FILTER_FIELD_MAP[this.reportType] || ''
|
||||
},
|
||||
isEdit() {
|
||||
return !!this.report?.id
|
||||
},
|
||||
filterLabel() {
|
||||
return {
|
||||
user_id: this.$t('UserFilterLabel'),
|
||||
asset_id: this.$t('AssetFilterLabel'),
|
||||
account: this.$t('AccountFilterLabel')
|
||||
}[this.filterField] || ''
|
||||
normalizedChartOptions() {
|
||||
return this.normalizeOptions(this.chartOptions)
|
||||
},
|
||||
recipientSelect() {
|
||||
return {
|
||||
ajax: {
|
||||
url: '/api/v1/users/users/?fields_size=mini',
|
||||
transformOption: (item) => ({ label: `${item.name}(${item.username})`, value: item.id })
|
||||
}
|
||||
}
|
||||
},
|
||||
filterSelect() {
|
||||
if (this.filterField === 'user_id') {
|
||||
return {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/users/users/suggestions/',
|
||||
transformOption: (item) => ({ label: `${item.name}(${item.username})`, value: item.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.filterField === 'asset_id') {
|
||||
return {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/assets/assets/?fields_size=mini',
|
||||
transformOption: (item) => ({
|
||||
label: item.name || item.address || item.hostname || item.id,
|
||||
value: item.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.filterField === 'account') {
|
||||
return {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/accounts/?fields_size=mini',
|
||||
transformOption: (item) => ({
|
||||
label: item.asset ? `${item.username} @ ${item.asset.name}` : item.username,
|
||||
value: item.username
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
normalizedTableOptions() {
|
||||
return this.normalizeOptions(this.tableOptions)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -221,58 +146,61 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
normalizeDays(days) {
|
||||
return normalizeReportDays(days, '7')
|
||||
},
|
||||
normalizeOptions(items = []) {
|
||||
if (!Array.isArray(items)) {
|
||||
return []
|
||||
}
|
||||
return items
|
||||
.filter(item => item && item.name)
|
||||
.map(item => ({
|
||||
name: String(item.name),
|
||||
title: String(item.title || item.name)
|
||||
}))
|
||||
},
|
||||
normalizeSelection(raw, options = []) {
|
||||
const safeOptions = Array.isArray(options) ? options : []
|
||||
const optionNames = safeOptions.map(item => item.name)
|
||||
const selected = Array.isArray(raw)
|
||||
? raw.map(item => String(item).trim()).filter(Boolean)
|
||||
: []
|
||||
const filtered = selected.filter(name => optionNames.includes(name))
|
||||
return filtered.length ? filtered : optionNames
|
||||
},
|
||||
validateVisibleReports(rule, value, callback) {
|
||||
const total = (this.form.visibleCharts || []).length + (this.form.visibleTables || []).length
|
||||
if (total <= 0) {
|
||||
callback(new Error(this.$t('PleaseSelectAtLeastOneReportSection')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
getInitialForm() {
|
||||
const report = this.report || {}
|
||||
const reportDays = this.normalizeDays(report.days || this.defaultDays || '7')
|
||||
const filters = report.filters || {}
|
||||
const preset = Object.entries(REPORT_PRESET_DAYS_MAP).find(([, days]) => String(days) === String(this.defaultDays))
|
||||
let filterValue = ''
|
||||
if (this.filterField) {
|
||||
// Only accept backend-provided user id options; ignore legacy username strings
|
||||
if (report._filter_user_options && report._filter_user_options.user_id && report._filter_user_options.user_id.length) {
|
||||
filterValue = report._filter_user_options.user_id[0].id
|
||||
} else if (filters[this.filterField]) {
|
||||
filterValue = filters[this.filterField]
|
||||
} else {
|
||||
filterValue = ''
|
||||
}
|
||||
}
|
||||
const chartOptions = this.normalizedChartOptions
|
||||
const tableOptions = this.normalizedTableOptions
|
||||
return {
|
||||
name: report.name || getDefaultName(this.reportTitle || this.reportType || 'report'),
|
||||
range_preset: filters.range_preset || (filters.start && filters.end ? 'custom' : (preset ? preset[0] : 'last_week')),
|
||||
date_range: filters.start && filters.end ? [filters.start, filters.end] : [],
|
||||
filter_value: filterValue,
|
||||
is_periodic: !!report.is_periodic,
|
||||
interval: report.interval || 24,
|
||||
crontab: report.crontab || '',
|
||||
recipients: normalizeRecipients(report.recipients)
|
||||
days: reportDays,
|
||||
visibleCharts: this.normalizeSelection(filters.visible_charts || this.defaultVisibleCharts, chartOptions),
|
||||
visibleTables: this.normalizeSelection(filters.visible_tables || this.defaultVisibleTables, tableOptions)
|
||||
}
|
||||
},
|
||||
getPayload() {
|
||||
const filters = {}
|
||||
let rangeDays = REPORT_PRESET_DAYS_MAP[this.form.range_preset] || parseInt(this.defaultDays) || 7
|
||||
if (this.form.range_preset === 'custom') {
|
||||
filters.start = this.form.date_range[0]
|
||||
filters.end = this.form.date_range[1]
|
||||
filters.range_preset = ''
|
||||
const start = new Date(`${filters.start}T00:00:00`)
|
||||
const end = new Date(`${filters.end}T00:00:00`)
|
||||
rangeDays = Math.max(1, Math.round((end - start) / 86400000) + 1)
|
||||
} else {
|
||||
filters.range_preset = this.form.range_preset
|
||||
}
|
||||
if (this.filterField && this.form.filter_value) {
|
||||
filters[this.filterField] = this.form.filter_value
|
||||
}
|
||||
const rangeDays = parseInt(this.normalizeDays(this.form.days), 10)
|
||||
return {
|
||||
name: this.form.name,
|
||||
tp: this.reportType,
|
||||
is_active: true,
|
||||
range_days: rangeDays,
|
||||
filters,
|
||||
is_periodic: this.form.is_periodic,
|
||||
interval: this.form.is_periodic ? this.form.interval : null,
|
||||
crontab: this.form.is_periodic ? (this.form.crontab || '') : '',
|
||||
recipients: this.form.recipients.length > 0 ? { type: 'ids', ids: this.form.recipients } : {}
|
||||
days: rangeDays,
|
||||
filters: {
|
||||
visible_charts: this.form.visibleCharts,
|
||||
visible_tables: this.form.visibleTables
|
||||
}
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Drawer
|
||||
:title="`${reportName || $t('Report')} - ${$t('ExecutionRecords')}`"
|
||||
:visible.sync="iVisible"
|
||||
size="980px"
|
||||
@close-drawer="handleClose"
|
||||
>
|
||||
<div v-loading="loading" class="report-execution-drawer">
|
||||
<el-table :data="executions" border height="calc(100vh - 160px)">
|
||||
<el-table-column :label="$t('ID')" min-width="180" prop="id" />
|
||||
<el-table-column :label="$t('Status')" min-width="120" prop="status" />
|
||||
<el-table-column :label="$t('Trigger')" min-width="120" prop="trigger" />
|
||||
<el-table-column :label="$t('DateStart')" min-width="180" prop="date_start" />
|
||||
<el-table-column :label="$t('DateFinished')" min-width="180" prop="date_finished" />
|
||||
<el-table-column :label="$t('DurationSeconds')" min-width="100" prop="duration" />
|
||||
<el-table-column :label="$t('SendRecords')" min-width="100">
|
||||
<template slot-scope="{ row }">
|
||||
{{ row.send_record_count || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('Actions')" min-width="120" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button size="mini" type="text" @click="openDetail(row)">
|
||||
{{ $t('Detail') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<Dialog
|
||||
v-if="detailVisible"
|
||||
:destroy-on-close="true"
|
||||
:show-confirm="false"
|
||||
:title="detailTitle"
|
||||
:visible.sync="detailVisible"
|
||||
top="8vh"
|
||||
width="900px"
|
||||
@cancel="detailVisible = false"
|
||||
>
|
||||
<div v-loading="detailLoading">
|
||||
<el-descriptions v-if="selectedExecution" :column="2" border class="detail-summary">
|
||||
<el-descriptions-item :label="$t('ID')">{{ selectedExecution.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('Status')">{{ selectedExecution.status }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('Trigger')">{{ selectedExecution.trigger }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('DurationSeconds')">{{ selectedExecution.duration || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('DateStart')">{{ selectedExecution.date_start || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('DateFinished')">{{ selectedExecution.date_finished || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-table :data="sendRecords" border max-height="360">
|
||||
<el-table-column :label="$t('Receiver')" min-width="120" prop="receiver" />
|
||||
<el-table-column :label="$t('Backend')" min-width="120" prop="backend" />
|
||||
<el-table-column :label="$t('Result')" min-width="100">
|
||||
<template slot-scope="{ row }">
|
||||
<span :class="row.is_success ? 'text-primary' : 'text-danger'">
|
||||
{{ row.is_success ? $t('Success') : $t('Failed') }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('CreatedTime')" min-width="180" prop="date_created" />
|
||||
<el-table-column :label="$t('Actions')" min-width="100" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button size="mini" type="text" @click="showRecordLog(row)">
|
||||
{{ $t('Log') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="selectedExecution && selectedExecution.summary" class="execution-summary">
|
||||
<h4>{{ $t('ExecutionSummary') }}</h4>
|
||||
<pre>{{ formatJson(selectedExecution.summary) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
v-if="logVisible"
|
||||
:destroy-on-close="true"
|
||||
:show-confirm="false"
|
||||
:title="$t('ExecutionLog')"
|
||||
:visible.sync="logVisible"
|
||||
top="15vh"
|
||||
width="760px"
|
||||
@cancel="logVisible = false"
|
||||
>
|
||||
<pre class="log-detail">{{ selectedLog }}</pre>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import Drawer from '@/components/Drawer'
|
||||
|
||||
export default {
|
||||
name: 'ReportExecutionDrawer',
|
||||
components: {
|
||||
Dialog,
|
||||
Drawer
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
reportId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
reportName: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
detailLoading: false,
|
||||
detailVisible: false,
|
||||
logVisible: false,
|
||||
executions: [],
|
||||
sendRecords: [],
|
||||
selectedExecution: null,
|
||||
selectedLog: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
detailTitle() {
|
||||
return this.selectedExecution ? `${this.$t('ExecutionDetail')} - ${this.selectedExecution.id}` : this.$t('ExecutionDetail')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
this.loadExecutions()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadExecutions() {
|
||||
if (!this.reportId) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await this.$axios.get('/api/v1/reports/report-executions/', {
|
||||
params: {
|
||||
report: this.reportId
|
||||
}
|
||||
})
|
||||
this.executions = data.results || data
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async openDetail(row) {
|
||||
this.detailVisible = true
|
||||
this.detailLoading = true
|
||||
this.selectedExecution = row
|
||||
try {
|
||||
const data = await this.$axios.get(`/api/v1/reports/report-executions/${row.id}/`)
|
||||
this.selectedExecution = data
|
||||
this.sendRecords = data.send_records || []
|
||||
} finally {
|
||||
this.detailLoading = false
|
||||
}
|
||||
},
|
||||
showRecordLog(row) {
|
||||
this.selectedLog = row.detail || row.error || '-'
|
||||
this.logVisible = true
|
||||
},
|
||||
formatJson(value) {
|
||||
return JSON.stringify(value || {}, null, 2)
|
||||
},
|
||||
handleClose() {
|
||||
this.iVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.report-execution-drawer {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.execution-summary {
|
||||
margin-top: 16px;
|
||||
|
||||
pre {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
background: #111827;
|
||||
color: #f9fafb;
|
||||
min-height: 220px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,78 +1,35 @@
|
||||
<template>
|
||||
<div class="report-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<template v-if="showDateControls">
|
||||
<div class="toolbar-item">
|
||||
<span class="toolbar-label">{{ $t('TimeRange') }}</span>
|
||||
<el-select v-model="localRangePreset" size="mini" style="width: 130px" @change="emitChange">
|
||||
<el-option v-for="option in presetOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
|
||||
<DatetimeRangePicker
|
||||
v-if="localRangePreset === 'custom'"
|
||||
:date-start="localDateRange?.[0]"
|
||||
:date-end="localDateRange?.[1]"
|
||||
@dateChange="onDateChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="filterField" class="toolbar-item">
|
||||
<span class="toolbar-label">{{ filterLabel }}</span>
|
||||
<Select2
|
||||
v-model="localFilterValue"
|
||||
v-bind="filterSelect"
|
||||
style="width: 220px"
|
||||
@change="emitChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SwitchDate :name="storageKey" :days="localDays" @change="emitChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import DatetimeRangePicker from '@/components/Form/FormFields/DatetimeRangePicker.vue'
|
||||
import { REPORT_RANGE_PRESET_OPTIONS } from './reportUtils'
|
||||
import SwitchDate from '@/components/Dashboard/SwitchDate.vue'
|
||||
|
||||
export default {
|
||||
name: 'ReportToolbar',
|
||||
components: {
|
||||
Select2,
|
||||
DatetimeRangePicker
|
||||
SwitchDate
|
||||
},
|
||||
props: {
|
||||
showDateControls: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isCustomReport: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filterField: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
filterLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
filterSelect: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
reportName: {
|
||||
type: String,
|
||||
default: 'reportDays'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
presetOptions: REPORT_RANGE_PRESET_OPTIONS,
|
||||
localRangePreset: 'last_week',
|
||||
localDateRange: [],
|
||||
localFilterValue: ''
|
||||
localDays: '7'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
storageKey() {
|
||||
return this.reportName || this.$route?.name || 'reportDays'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -80,29 +37,14 @@ export default {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(val) {
|
||||
this.localRangePreset = val.range_preset || (val.start && val.end ? 'custom' : 'last_week')
|
||||
this.localDateRange = val.start && val.end ? [new Date(val.start), new Date(val.end)] : []
|
||||
this.localFilterValue = val.filter_value || ''
|
||||
this.localDays = String(val.days || '7')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDateChange(val) {
|
||||
this.localDateRange = val
|
||||
this.emitChange()
|
||||
},
|
||||
emitChange() {
|
||||
const payload = {
|
||||
filter_value: this.localFilterValue
|
||||
}
|
||||
if (this.showDateControls) {
|
||||
payload.range_preset = this.localRangePreset
|
||||
if (this.localRangePreset === 'custom') {
|
||||
payload.start = this.localDateRange?.[0] ? this.localDateRange[0].toISOString() : ''
|
||||
payload.end = this.localDateRange?.[1] ? this.localDateRange[1].toISOString() : ''
|
||||
}
|
||||
}
|
||||
this.$emit('filter-change', payload)
|
||||
emitChange(days) {
|
||||
this.localDays = String(days || '7')
|
||||
this.$emit('filter-change', { days: this.localDays })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,50 +53,8 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.report-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
&.chart-container {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.toolbar-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.el-select,
|
||||
.select2,
|
||||
.el-date-editor {
|
||||
min-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<i class="el-icon-arrow-down el-icon--right" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="history">{{ $t('ExecutionHistory') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canDeleteReport" command="delete" divided>{{ $t('Delete') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canSaveReport" command="edit">{{ $t('Edit') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canDeleteReport" :divided="canSaveReport" command="delete">{{ $t('Delete') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<template v-if="!showOperationOnlyInEditor || !editorOnly">
|
||||
@@ -45,10 +45,14 @@
|
||||
</el-button-group>
|
||||
|
||||
<CreateReportDialog
|
||||
:chart-options="chartOptions"
|
||||
:default-days="getDaysParam()"
|
||||
:default-visible-charts="selectedChartNames"
|
||||
:default-visible-tables="selectedTableNames"
|
||||
:report="editingReport"
|
||||
:report-title="title"
|
||||
:report-type="name"
|
||||
:table-options="tableOptions"
|
||||
:visible.sync="showCreateDialog"
|
||||
@created="handleCreated"
|
||||
/>
|
||||
@@ -58,20 +62,14 @@
|
||||
:report-query="$route.query"
|
||||
:visible.sync="showExportDialog"
|
||||
/>
|
||||
<ReportExecutionDrawer
|
||||
:report-id="reportId"
|
||||
:report-name="title"
|
||||
:visible.sync="showExecutionDrawer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { download } from '@/utils/common'
|
||||
import CreateReportDialog from './CreateReportDialog.vue'
|
||||
import ReportExecutionDrawer from './ReportExecutionDrawer.vue'
|
||||
import ReportExportDialog from './ReportExportDialog.vue'
|
||||
import { appendQuery, pickReportQuery } from './reportUtils'
|
||||
import { appendQuery, pickReportQuery, buildCustomReportRouteQuery, normalizeReportDays, fetchReportDetailShared } from './reportUtils'
|
||||
|
||||
const REPORT_ACTION_PERM_MAP = {
|
||||
UserLoginReport: {
|
||||
@@ -104,7 +102,6 @@ export default {
|
||||
name: 'RightAction',
|
||||
components: {
|
||||
CreateReportDialog,
|
||||
ReportExecutionDrawer,
|
||||
ReportExportDialog
|
||||
},
|
||||
props: {
|
||||
@@ -139,6 +136,22 @@ export default {
|
||||
forceDefaultActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
chartOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tableOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedChartNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedTableNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -146,7 +159,6 @@ export default {
|
||||
exportLoading: false,
|
||||
reportData: null,
|
||||
showCreateDialog: false,
|
||||
showExecutionDrawer: false,
|
||||
showExportDialog: false
|
||||
}
|
||||
},
|
||||
@@ -187,43 +199,26 @@ export default {
|
||||
},
|
||||
editingReport() {
|
||||
const query = this.$route.query || {}
|
||||
const filters = {}
|
||||
if (query.range_preset) {
|
||||
filters.range_preset = query.range_preset
|
||||
}
|
||||
if (query.start) {
|
||||
filters.start = query.start
|
||||
}
|
||||
if (query.end) {
|
||||
filters.end = query.end
|
||||
}
|
||||
const key = this.filterField
|
||||
const filterValue = key ? query[key] : ''
|
||||
if (key && filterValue) {
|
||||
filters[key] = filterValue
|
||||
}
|
||||
const reportDays = parseInt(normalizeReportDays(query.days || this.reportData?.days || this.getDaysParam(), '7'), 10)
|
||||
if (this.isCustomReport) {
|
||||
return {
|
||||
...(this.reportData || {}),
|
||||
days: reportDays,
|
||||
filters: {
|
||||
...(this.reportData?.filters || {}),
|
||||
...filters
|
||||
// 使用当前页面的勾选状态,而不是已保存的状态
|
||||
visible_charts: this.selectedChartNames,
|
||||
visible_tables: this.selectedTableNames
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
filters
|
||||
days: reportDays,
|
||||
filters: {
|
||||
visible_charts: this.selectedChartNames,
|
||||
visible_tables: this.selectedTableNames
|
||||
}
|
||||
}
|
||||
},
|
||||
filterField() {
|
||||
return {
|
||||
UserLoginReport: 'user_id',
|
||||
UserChangePasswordReport: 'user_id',
|
||||
AssetStatistics: 'asset_id',
|
||||
AssetReport: 'asset_id',
|
||||
AccountStatistics: 'account',
|
||||
AccountAutomationReport: 'account'
|
||||
}[this.name] || ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -240,7 +235,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async loadReportDetail() {
|
||||
this.reportData = await this.$axios.get(`/api/v1/reports/reports/${this.reportId}/`)
|
||||
this.reportData = await fetchReportDetailShared(this.$axios, this.reportId)
|
||||
},
|
||||
checkName() {
|
||||
if (!this.name) {
|
||||
@@ -250,7 +245,7 @@ export default {
|
||||
return true
|
||||
},
|
||||
getDaysParam() {
|
||||
return this.$route.query.days || localStorage.getItem(this.name) || '7'
|
||||
return normalizeReportDays(this.$route.query.days || localStorage.getItem(this.name), '7')
|
||||
},
|
||||
exportPdf() {
|
||||
if (!this.checkName()) {
|
||||
@@ -302,8 +297,8 @@ export default {
|
||||
this.showCreateDialog = true
|
||||
},
|
||||
handleCommand(command) {
|
||||
if (command === 'history') {
|
||||
this.showExecutionDrawer = true
|
||||
if (command === 'edit' && this.canSaveReport) {
|
||||
this.openEditor()
|
||||
return
|
||||
}
|
||||
if (command === 'delete' && this.canDeleteReport) {
|
||||
@@ -320,11 +315,15 @@ export default {
|
||||
this.$router.replace({ path: this.$route.path, query: {} })
|
||||
},
|
||||
handleCreated(report) {
|
||||
const query = {
|
||||
...buildCustomReportRouteQuery(report)
|
||||
}
|
||||
if (this.$route.query.chart_key) {
|
||||
query.chart_key = this.$route.query.chart_key
|
||||
}
|
||||
this.$router.push({
|
||||
path: this.$route.path,
|
||||
query: {
|
||||
report_id: report.id
|
||||
}
|
||||
query
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import { appendQuery, pickReportQuery } from './reportUtils'
|
||||
|
||||
const FILTER_FIELD_MAP = {
|
||||
UserLoginReport: 'user_id',
|
||||
UserChangePasswordReport: 'user_id',
|
||||
AssetReport: 'asset_id',
|
||||
AssetStatistics: 'asset_id',
|
||||
AccountStatistics: 'account',
|
||||
AccountAutomationReport: 'account'
|
||||
}
|
||||
import { appendQuery, normalizeReportDays, normalizeVisibleFilterList, pickReportQuery, reportDebugLog, fetchReportDetailShared } from './reportUtils'
|
||||
|
||||
const TABLE_LABEL_KEY_MAP = {
|
||||
user_stats: 'Overview',
|
||||
@@ -18,6 +9,9 @@ const TABLE_LABEL_KEY_MAP = {
|
||||
user_login_time_metrics: 'VisitTimeDistribution',
|
||||
session_stats: 'Overview',
|
||||
asset_login_log_metrics: 'AssetLoginTrends',
|
||||
asset_login_by_type: 'OperatingSystemDistributionOfLoginAssets',
|
||||
asset_login_by_from: 'DistributionOfAssetLoginMethods',
|
||||
asset_login_by_protocol: 'RemoteLoginProtocolUsageDistribution',
|
||||
user_asset_activity_metrics: 'UserAssetActivity',
|
||||
asset_stats: 'Overview',
|
||||
assets_by_type_category: 'AssetTypeDistribution',
|
||||
@@ -73,17 +67,44 @@ export default {
|
||||
reportDetail: null,
|
||||
displayMode: ['chart', 'table'],
|
||||
// now supports multiple tables: array of { name, columns, rows }
|
||||
tableData: []
|
||||
tableData: [],
|
||||
reportFetchInFlight: Object.create(null),
|
||||
reportFetchCache: Object.create(null),
|
||||
lastGetDataRouteKey: '',
|
||||
lastFetchedReportId: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.fullPath'() {
|
||||
const routeKey = this.buildGetDataRouteKey(this.$route.query)
|
||||
if (routeKey === this.lastGetDataRouteKey) {
|
||||
return
|
||||
}
|
||||
// reportId 变了说明切换到了不同的报告,父组件会改变 :key 导致本组件被销毁并重建
|
||||
// 新组件的 mounted() 会负责初始化加载,此处不重复 getData
|
||||
if (this.reportId !== this.lastFetchedReportId) {
|
||||
this.lastGetDataRouteKey = routeKey
|
||||
this.lastFetchedReportId = this.reportId
|
||||
return
|
||||
}
|
||||
this.lastGetDataRouteKey = routeKey
|
||||
this.lastFetchedReportId = this.reportId
|
||||
reportDebugLog('mixin.route.fullPath', {
|
||||
name: this.name,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
this.reportDetail = null
|
||||
if (typeof this.getData === 'function') {
|
||||
this.getData()
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 预填当前路由 key,防止 mounted() 调用 getData 后,route watcher 对同一 key 重复触发
|
||||
this.lastGetDataRouteKey = this.buildGetDataRouteKey(this.$route.query)
|
||||
this.lastFetchedReportId = this.reportId
|
||||
},
|
||||
computed: {
|
||||
displayModes() {
|
||||
const modes = Array.isArray(this.displayMode) ? this.displayMode : [this.displayMode]
|
||||
@@ -107,27 +128,48 @@ export default {
|
||||
reportTitle() {
|
||||
return this.reportDetail?.name || this.title
|
||||
},
|
||||
filterField() {
|
||||
return FILTER_FIELD_MAP[this.name] || ''
|
||||
},
|
||||
filterLabel() {
|
||||
return {
|
||||
user_id: this.$t('UserFilterLabel'),
|
||||
asset_id: this.$t('AssetFilterLabel'),
|
||||
account: this.$t('AccountFilterLabel')
|
||||
}[this.filterField] || ''
|
||||
},
|
||||
currentFilters() {
|
||||
const reportFilters = this.reportDetail?.filters || {}
|
||||
const reportDays = this.reportDetail?.days
|
||||
const fallbackDays = this.days || reportDays || 7
|
||||
return {
|
||||
range_preset: this.$route.query.range_preset || reportFilters.range_preset || '',
|
||||
start: this.$route.query.start || reportFilters.start || '',
|
||||
end: this.$route.query.end || reportFilters.end || '',
|
||||
filter_value: this.filterField ? (this.$route.query[this.filterField] || reportFilters[this.filterField] || '') : ''
|
||||
days: normalizeReportDays(this.$route.query.days || fallbackDays, '7')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildGetDataRouteKey(query = {}) {
|
||||
return JSON.stringify({
|
||||
path: this.$route.path,
|
||||
report_id: query.report_id || '',
|
||||
days: query.days || '',
|
||||
chart_key: query.chart_key || '',
|
||||
visible_charts: query.visible_charts || '',
|
||||
visible_tables: query.visible_tables || ''
|
||||
})
|
||||
},
|
||||
async fetchWithDedupe(url) {
|
||||
const now = Date.now()
|
||||
const cached = this.reportFetchCache[url]
|
||||
// Reuse recent same-url result to absorb rapid duplicate triggers.
|
||||
if (cached && (now - cached.ts) < 600) {
|
||||
reportDebugLog('mixin.fetch.cacheHit', { name: this.name, requestUrl: url })
|
||||
return cached.data
|
||||
}
|
||||
if (this.reportFetchInFlight[url]) {
|
||||
reportDebugLog('mixin.fetch.inFlightJoin', { name: this.name, requestUrl: url })
|
||||
return this.reportFetchInFlight[url]
|
||||
}
|
||||
const request = this.$axios.get(url)
|
||||
.then((res) => {
|
||||
this.reportFetchCache[url] = { ts: Date.now(), data: res }
|
||||
return res
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.reportFetchInFlight[url]
|
||||
})
|
||||
this.reportFetchInFlight[url] = request
|
||||
return request
|
||||
},
|
||||
async ensureReportDetail(reportId = this.reportId) {
|
||||
if (!reportId) {
|
||||
return null
|
||||
@@ -135,7 +177,7 @@ export default {
|
||||
if (this.reportDetail?.id === reportId) {
|
||||
return this.reportDetail
|
||||
}
|
||||
const data = await this.$axios.get(`/api/v1/reports/reports/${reportId}/`)
|
||||
const data = await fetchReportDetailShared(this.$axios, reportId)
|
||||
if (this.reportId !== reportId) {
|
||||
return data
|
||||
}
|
||||
@@ -147,7 +189,7 @@ export default {
|
||||
},
|
||||
buildTemplateUrl(baseUrl) {
|
||||
const query = pickReportQuery(this.$route.query)
|
||||
if (!query.start && !query.end && !query.range_preset && this.days) {
|
||||
if (!query.days && this.days) {
|
||||
query.days = this.days
|
||||
}
|
||||
return appendQuery(baseUrl, query)
|
||||
@@ -158,11 +200,32 @@ export default {
|
||||
if (reportId) {
|
||||
await this.ensureReportDetail(reportId)
|
||||
if (this.reportId !== reportId) {
|
||||
reportDebugLog('mixin.fetch.retry', {
|
||||
name: this.name,
|
||||
fromReportId: reportId,
|
||||
toReportId: this.reportId,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
return this.fetchReportData(baseUrl)
|
||||
}
|
||||
return this.$axios.get(appendQuery(`/api/v1/reports/reports/${reportId}/data/`, query))
|
||||
const requestUrl = appendQuery(`/api/v1/reports/reports/${reportId}/data/`, query)
|
||||
reportDebugLog('mixin.fetch.custom', {
|
||||
name: this.name,
|
||||
requestUrl,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
return this.fetchWithDedupe(requestUrl)
|
||||
}
|
||||
return this.$axios.get(this.buildTemplateUrl(baseUrl))
|
||||
const requestUrl = this.buildTemplateUrl(baseUrl)
|
||||
reportDebugLog('mixin.fetch.template', {
|
||||
name: this.name,
|
||||
requestUrl,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
return this.fetchWithDedupe(requestUrl)
|
||||
},
|
||||
async loadTableData(baseUrl) {
|
||||
const buildLabel = (k) => {
|
||||
@@ -294,61 +357,32 @@ export default {
|
||||
return
|
||||
}
|
||||
},
|
||||
handleToolbarFilterChange({ range_preset, start, end, filter_value }) {
|
||||
handleToolbarFilterChange({ days }) {
|
||||
const query = {}
|
||||
if (this.$route.query.chart_key) {
|
||||
query.chart_key = this.$route.query.chart_key
|
||||
}
|
||||
if (this.reportId) {
|
||||
query.report_id = this.reportId
|
||||
}
|
||||
if (range_preset && range_preset !== 'custom') {
|
||||
query.range_preset = range_preset
|
||||
if (days) {
|
||||
query.days = normalizeReportDays(days, '7')
|
||||
}
|
||||
if (range_preset === 'custom') {
|
||||
query.start = start
|
||||
query.end = end
|
||||
const visibleCharts = normalizeVisibleFilterList(this.$route.query.visible_charts || this.reportDetail?.filters?.visible_charts)
|
||||
const visibleTables = normalizeVisibleFilterList(this.$route.query.visible_tables || this.reportDetail?.filters?.visible_tables)
|
||||
if (visibleCharts.length) {
|
||||
query.visible_charts = visibleCharts.join(',')
|
||||
}
|
||||
if (this.filterField && filter_value) {
|
||||
query[this.filterField] = filter_value
|
||||
if (visibleTables.length) {
|
||||
query.visible_tables = visibleTables.join(',')
|
||||
}
|
||||
if (this.days !== undefined && days) {
|
||||
this.days = normalizeReportDays(days, '7')
|
||||
}
|
||||
if (this.buildGetDataRouteKey(this.$route.query) === this.buildGetDataRouteKey(query)) {
|
||||
return
|
||||
}
|
||||
this.$router.replace({ path: this.$route.path, query })
|
||||
},
|
||||
getFilterSelect() {
|
||||
if (this.filterField === 'user_id') {
|
||||
return {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/users/users/suggestions/',
|
||||
transformOption: (item) => ({ label: `${item.name}(${item.username})`, value: item.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.filterField === 'asset_id') {
|
||||
return {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/assets/assets/?fields_size=mini',
|
||||
transformOption: (item) => ({ label: item.name || item.address || item.hostname || item.id, value: item.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.filterField === 'account') {
|
||||
return {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/accounts/?fields_size=mini',
|
||||
transformOption: (item) => ({ label: item.asset ? `${item.username} @ ${item.asset.name}` : item.username, value: item.username })
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
async handleDeleteReport() {
|
||||
if (!this.reportId) {
|
||||
return
|
||||
}
|
||||
await this.$confirm(this.$t('ConfirmDeleteReport'), this.$t('Tip'), { type: 'warning' })
|
||||
await this.$axios.delete(`/api/v1/reports/reports/${this.reportId}/`)
|
||||
this.$message.success(this.$t('DeleteSuccessMsg'))
|
||||
this.$router.replace({ path: this.$route.path, query: {} })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export const REPORT_DEBUG_SWITCH_KEY = '__REPORT_DEBUG_SWITCH__'
|
||||
|
||||
export const REPORT_RANGE_PRESET_OPTIONS = [
|
||||
{ label: i18n.t('LastDay'), value: 'last_day', days: 1 },
|
||||
{ label: i18n.t('Last7Days'), value: 'last_week', days: 7 },
|
||||
{ label: i18n.t('Last30Days'), value: 'last_month', days: 30 },
|
||||
{ label: i18n.t('LastThreeMonths'), value: 'last_three_months', days: 90 },
|
||||
{ label: i18n.t('LastHalfYear'), value: 'last_half_year', days: 180 },
|
||||
{ label: i18n.t('LastYear'), value: 'last_year', days: 365 },
|
||||
{ label: i18n.t('Custom'), value: 'custom', days: null }
|
||||
{ label: i18n.t('Today'), value: '1', days: 1 },
|
||||
{ label: i18n.t('Last7Days'), value: '7', days: 7 },
|
||||
{ label: i18n.t('Last30Days'), value: '30', days: 30 }
|
||||
]
|
||||
|
||||
export const REPORT_ALLOWED_DAYS = REPORT_RANGE_PRESET_OPTIONS.map(item => String(item.value))
|
||||
|
||||
export const REPORT_FILTER_QUERY_KEYS = [
|
||||
'start',
|
||||
'end',
|
||||
'range_preset',
|
||||
'user_id',
|
||||
'asset_id',
|
||||
'account',
|
||||
'days',
|
||||
'report_id'
|
||||
]
|
||||
|
||||
@@ -57,4 +52,89 @@ export function appendQuery(url, query = {}) {
|
||||
export function getPresetLabel(value) {
|
||||
const preset = REPORT_RANGE_PRESET_OPTIONS.find(item => item.value === value)
|
||||
return preset ? preset.label : value
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeReportDays(value, fallback = '7') {
|
||||
const normalizedFallback = REPORT_ALLOWED_DAYS.includes(String(fallback)) ? String(fallback) : '7'
|
||||
const normalizedValue = String(value || '')
|
||||
if (REPORT_ALLOWED_DAYS.includes(normalizedValue)) {
|
||||
return normalizedValue
|
||||
}
|
||||
return normalizedFallback
|
||||
}
|
||||
|
||||
export function normalizeVisibleFilterList(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => String(item).trim()).filter(Boolean)
|
||||
}
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return []
|
||||
}
|
||||
return String(value)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function buildCustomReportRouteQuery(report = {}) {
|
||||
const normalizedDays = normalizeReportDays(report.days, '7')
|
||||
const query = {
|
||||
report_id: report.id,
|
||||
days: normalizedDays
|
||||
}
|
||||
const filters = report.filters || {}
|
||||
const visibleCharts = normalizeVisibleFilterList(filters.visible_charts)
|
||||
const visibleTables = normalizeVisibleFilterList(filters.visible_tables)
|
||||
if (visibleCharts.length) {
|
||||
query.visible_charts = visibleCharts.join(',')
|
||||
}
|
||||
if (visibleTables.length) {
|
||||
query.visible_tables = visibleTables.join(',')
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
export function isReportDebugEnabled() {
|
||||
try {
|
||||
const val = localStorage.getItem(REPORT_DEBUG_SWITCH_KEY)
|
||||
return val === '1' || val === 'true'
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function reportDebugLog(scope, payload = {}) {
|
||||
if (!isReportDebugEnabled()) {
|
||||
return
|
||||
}
|
||||
console.log(`[report-debug:${scope}]`, payload)
|
||||
}
|
||||
|
||||
// 模块级 report detail 请求去重缓存
|
||||
// 多个组件实例(reportPageMixin + RightAction)可能同时请求同一 reportId,
|
||||
// 通过此缓存保证同一 URL 在短时间内只发一次 HTTP 请求
|
||||
const _reportDetailInFlight = Object.create(null)
|
||||
const _reportDetailCache = Object.create(null)
|
||||
const REPORT_DETAIL_CACHE_TTL = 1500
|
||||
|
||||
export function fetchReportDetailShared(axios, reportId) {
|
||||
const url = `/api/v1/reports/reports/${reportId}/`
|
||||
const now = Date.now()
|
||||
const cached = _reportDetailCache[url]
|
||||
if (cached && (now - cached.ts) < REPORT_DETAIL_CACHE_TTL) {
|
||||
return Promise.resolve(cached.data)
|
||||
}
|
||||
if (_reportDetailInFlight[url]) {
|
||||
return _reportDetailInFlight[url]
|
||||
}
|
||||
const request = axios.get(url)
|
||||
.then((res) => {
|
||||
_reportDetailCache[url] = { ts: Date.now(), data: res }
|
||||
return res
|
||||
})
|
||||
.finally(() => {
|
||||
delete _reportDetailInFlight[url]
|
||||
})
|
||||
_reportDetailInFlight[url] = request
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
:title="title"
|
||||
:nav="nav"
|
||||
:name="name"
|
||||
:charts="charts"
|
||||
:tables="tables"
|
||||
:show-display-mode-toggle="true"
|
||||
:display-mode.sync="displayMode"
|
||||
v-bind="$attrs"
|
||||
@@ -20,11 +22,7 @@
|
||||
</div>
|
||||
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
@@ -70,16 +68,6 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<ReportToolbar
|
||||
v-if="tableData.length"
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<div v-for="(t, idx) in tableData.slice(1)" :key="t.name || idx" class="report-table-wrap chart-container full-width">
|
||||
<div v-if="t.name" class="chart-container-title">
|
||||
<div class="chart-container-title-text">{{ t.name }}</div>
|
||||
@@ -92,15 +80,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<el-table :data="tableData.rows" border>
|
||||
<el-table-column v-for="column in tableData.columns" :key="column.key" :label="column.label" :prop="column.key" min-width="140" />
|
||||
</el-table>
|
||||
@@ -140,6 +119,18 @@ export default {
|
||||
return {
|
||||
title: this.$t('UserChangePasswordReport'),
|
||||
name: 'UserChangePasswordReport',
|
||||
charts: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'UserModificationTrends', title: this.$t('UserModificationTrends') },
|
||||
{ name: 'ModifyTheTargetUserTopTank', title: this.$t('ModifyTheTargetUserTopTank') },
|
||||
{ name: 'TopRankOfOperateUsers', title: this.$t('TopRankOfOperateUsers') }
|
||||
],
|
||||
tables: [
|
||||
{ name: 'Overview', title: this.$t('Overview') },
|
||||
{ name: 'UserModificationTrends', title: this.$t('UserModificationTrends') },
|
||||
{ name: 'ModifyTheTargetUserTopTank', title: this.$t('ModifyTheTargetUserTopTank') },
|
||||
{ name: 'TopRankOfOperateUsers', title: this.$t('TopRankOfOperateUsers') }
|
||||
],
|
||||
days: localStorage.getItem(this.name) || '7',
|
||||
total_count_change_password: {
|
||||
'total': 0,
|
||||
@@ -317,18 +308,10 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
days() {
|
||||
this.getData()
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getData()
|
||||
},
|
||||
methods: {
|
||||
onChange(val) {
|
||||
this.days = val
|
||||
},
|
||||
async getData() {
|
||||
const data = await this.fetchReportData('/api/v1/reports/reports/user-change-password/')
|
||||
await this.loadTableData('/api/v1/reports/reports/user-change-password/')
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
<template #default>
|
||||
<div class="charts-grid">
|
||||
<div class="chart-container full-width" data-report-type="chart" data-report-name="UserLoginTrends">
|
||||
<div class="chart-container full-width" data-report-type="chart" data-report-name="Overview">
|
||||
<div class="chart-container-title">
|
||||
<div class="chart-container-title-text">{{ $t('Overview') }}</div>
|
||||
<SummaryCountCard
|
||||
@@ -20,11 +20,7 @@
|
||||
</div>
|
||||
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
@@ -95,16 +91,6 @@
|
||||
<el-table-column v-for="column in tableData[0].columns" :key="column.key" :label="column.label" :prop="column.key" min-width="140" />
|
||||
</el-table>
|
||||
</div>
|
||||
<ReportToolbar
|
||||
v-if="tableData.length"
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<div
|
||||
v-for="(t, idx) in tableData.slice(1)"
|
||||
:key="t.name || idx"
|
||||
@@ -121,15 +107,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ReportToolbar
|
||||
:filter-field="filterField"
|
||||
:filter-label="filterLabel"
|
||||
:filter-select="getFilterSelect()"
|
||||
:filters="currentFilters"
|
||||
:is-custom-report="isCustomReport"
|
||||
class="chart-container full-width report-toolbar-wrap"
|
||||
@filter-change="handleToolbarFilterChange"
|
||||
/>
|
||||
<el-table :data="tableData.rows" border>
|
||||
<el-table-column v-for="column in tableData.columns" :key="column.key" :label="column.label" :prop="column.key" min-width="140" />
|
||||
</el-table>
|
||||
@@ -168,6 +145,10 @@ export default {
|
||||
title: this.$t('UserLoginReport'),
|
||||
name: 'UserLoginReport',
|
||||
charts: [
|
||||
{
|
||||
name: 'Overview',
|
||||
title: this.$t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'UserLoginTrends',
|
||||
title: this.$t('UserLoginTrends')
|
||||
@@ -190,23 +171,22 @@ export default {
|
||||
name: 'Overview',
|
||||
title: this.$t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'LoginSource',
|
||||
title: this.$t('LoginSource')
|
||||
},
|
||||
{
|
||||
name: 'UserLoginTrends',
|
||||
title: this.$t('UserLoginTrends')
|
||||
},
|
||||
{
|
||||
name: 'LoginMethodStatistics',
|
||||
title: this.$t('LoginMethodStatistics')
|
||||
name: 'LoginSource',
|
||||
title: this.$t('LoginSource')
|
||||
},
|
||||
{
|
||||
name: 'VisitTimeDistribution',
|
||||
title: this.$t('VisitTimeDistribution')
|
||||
},
|
||||
{
|
||||
name: 'LoginMethodStatistics',
|
||||
title: this.$t('LoginMethodStatistics')
|
||||
}
|
||||
|
||||
],
|
||||
days: localStorage.getItem(this.name) || '7',
|
||||
user_stats: {
|
||||
@@ -510,19 +490,10 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
days() {
|
||||
this.getData()
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getData()
|
||||
},
|
||||
methods: {
|
||||
onChange(val) {
|
||||
this.days = val
|
||||
localStorage.setItem('reportDays', val)
|
||||
},
|
||||
async getData() {
|
||||
const data = await this.fetchReportData('/api/v1/reports/reports/users/')
|
||||
await this.loadTableData('/api/v1/reports/reports/users/')
|
||||
|
||||
@@ -38,20 +38,25 @@
|
||||
|
||||
<script>
|
||||
import Page from '@/layout/components/Page'
|
||||
import { resolveRoute } from '@/utils/vue/index'
|
||||
import { appendQuery, isSameReportQuery } from '@/views/reports/base/reportUtils'
|
||||
import UserActivity from '@/views/reports/users/UserActivity.vue'
|
||||
import ChangePassword from '@/views/reports/users/ChangePassword.vue'
|
||||
import { appendQuery, buildCustomReportRouteQuery, reportDebugLog } from '@/views/reports/base/reportUtils'
|
||||
|
||||
const TEMPLATE_ROUTE_MAP = {
|
||||
UserLoginReport: {
|
||||
name: 'UserReport',
|
||||
key: 'UserReport',
|
||||
titleKey: 'UserLoginReport',
|
||||
icon: 'fa fa-sign-in',
|
||||
component: UserActivity,
|
||||
path: '/reports/users/user-activity',
|
||||
perm: 'rbac.view_userloginreport'
|
||||
},
|
||||
UserChangePasswordReport: {
|
||||
name: 'ChangePassword',
|
||||
key: 'ChangePassword',
|
||||
titleKey: 'UserChangePasswordReport',
|
||||
icon: 'fa fa-key',
|
||||
component: ChangePassword,
|
||||
path: '/reports/users/change-password',
|
||||
perm: 'rbac.view_userchangepasswordreport'
|
||||
}
|
||||
}
|
||||
@@ -68,33 +73,68 @@ export default {
|
||||
component: '',
|
||||
componentKey: '',
|
||||
selectedChartKey: '',
|
||||
chartItems: []
|
||||
chartItems: [],
|
||||
catalogLoaded: false,
|
||||
lastSyncQueryKey: '',
|
||||
isPageActive: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.fullPath'() {
|
||||
if (!this.isPageActive) return
|
||||
const routeKey = this.buildRouteSyncKey(this.$route.query)
|
||||
if (routeKey === this.lastSyncQueryKey) {
|
||||
return
|
||||
}
|
||||
this.lastSyncQueryKey = routeKey
|
||||
reportDebugLog('users.index.route.fullPath', {
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query,
|
||||
selectedChartKey: this.selectedChartKey
|
||||
})
|
||||
this.syncSelectedFromRoute()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadCatalog()
|
||||
},
|
||||
activated() {
|
||||
this.isPageActive = true
|
||||
this.syncSelectedFromRoute()
|
||||
},
|
||||
deactivated() {
|
||||
this.isPageActive = false
|
||||
},
|
||||
methods: {
|
||||
buildRouteSyncKey(query = {}) {
|
||||
return JSON.stringify({
|
||||
report_id: query.report_id || '',
|
||||
chart_key: query.chart_key || '',
|
||||
days: query.days || '',
|
||||
visible_charts: query.visible_charts || '',
|
||||
visible_tables: query.visible_tables || ''
|
||||
})
|
||||
},
|
||||
getBuiltInTemplates() {
|
||||
return Object.entries(TEMPLATE_ROUTE_MAP)
|
||||
.filter(([, item]) => this.$hasPerm(item.perm))
|
||||
.map(([reportType, item]) => ({
|
||||
key: reportType,
|
||||
key: item.key,
|
||||
reportType,
|
||||
title: this.$t(item.titleKey),
|
||||
routeName: item.name,
|
||||
component: item.component,
|
||||
path: item.path,
|
||||
icon: item.icon,
|
||||
isCustom: false,
|
||||
query: {},
|
||||
query: {
|
||||
chart_key: item.key
|
||||
},
|
||||
children: []
|
||||
}))
|
||||
},
|
||||
async loadCatalog() {
|
||||
reportDebugLog('users.index.loadCatalog.start', { routePath: this.$route.path, query: this.$route.query })
|
||||
this.catalogLoaded = false
|
||||
const templates = this.getBuiltInTemplates()
|
||||
const chartMap = templates.reduce((acc, item) => {
|
||||
acc[item.reportType] = item
|
||||
@@ -110,16 +150,24 @@ export default {
|
||||
target.children = (group.children || []).map(child => ({
|
||||
key: `report-${child.id}`,
|
||||
title: child.name,
|
||||
routeName: target.routeName,
|
||||
reportId: child.id,
|
||||
component: target.component,
|
||||
path: target.path,
|
||||
reportId: String(child.id),
|
||||
isCustom: true,
|
||||
query: { report_id: child.id }
|
||||
query: {
|
||||
chart_key: target.key,
|
||||
...buildCustomReportRouteQuery(child)
|
||||
}
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('load report catalog failed', error)
|
||||
}
|
||||
this.chartItems = templates
|
||||
this.catalogLoaded = true
|
||||
reportDebugLog('users.index.loadCatalog.done', {
|
||||
items: templates.map(item => ({ key: item.key, childCount: (item.children || []).length }))
|
||||
})
|
||||
this.syncSelectedFromRoute()
|
||||
},
|
||||
syncSelectedFromRoute() {
|
||||
@@ -129,32 +177,74 @@ export default {
|
||||
if (reportId) {
|
||||
target = this.chartItems
|
||||
.flatMap(item => item.children || [])
|
||||
.find(item => item.reportId === reportId)
|
||||
.find(item => String(item.reportId) === String(reportId))
|
||||
if (!target) {
|
||||
this.loadCatalog()
|
||||
if (!this.catalogLoaded) {
|
||||
return
|
||||
}
|
||||
const nextQuery = { ...(this.$route.query || {}) }
|
||||
delete nextQuery.report_id
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery })
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
target = this.chartItems[0]
|
||||
const chartKey = this.$route.query.chart_key
|
||||
target = this.chartItems.find(item => item.key === chartKey)
|
||||
|| this.chartItems.find(item => item.key === this.selectedChartKey)
|
||||
|| this.chartItems[0]
|
||||
}
|
||||
if (target) {
|
||||
if (target && (this.selectedChartKey !== target.key || !this.component)) {
|
||||
this.applyChart(target)
|
||||
}
|
||||
reportDebugLog('users.index.syncSelectedFromRoute', {
|
||||
reportId,
|
||||
selectedChartKey: this.selectedChartKey,
|
||||
targetKey: target?.key || ''
|
||||
})
|
||||
},
|
||||
isActive(item) {
|
||||
return this.selectedChartKey === item.key
|
||||
},
|
||||
applyChart(chart) {
|
||||
this.selectedChartKey = chart.key
|
||||
const route = resolveRoute({ name: chart.routeName }, this.$router)
|
||||
this.component = route.components.default
|
||||
this.componentKey = `${chart.key}-${this.$route.fullPath}`
|
||||
this.url = appendQuery('/ui/#' + route.path, chart.query || {})
|
||||
if (!chart.component || !chart.path) {
|
||||
return
|
||||
}
|
||||
const nextUrl = appendQuery('/ui/#' + chart.path, chart.query || {})
|
||||
if (this.component === chart.component && this.componentKey === chart.key && this.url === nextUrl) {
|
||||
return
|
||||
}
|
||||
this.component = chart.component
|
||||
this.componentKey = chart.key
|
||||
this.url = nextUrl
|
||||
reportDebugLog('users.index.applyChart', {
|
||||
chartKey: chart.key,
|
||||
reportId: chart.reportId || '',
|
||||
url: this.url,
|
||||
routePath: this.$route.path,
|
||||
query: this.$route.query
|
||||
})
|
||||
},
|
||||
handleChangeChart(chart) {
|
||||
const nextQuery = chart.query || {}
|
||||
if (isSameReportQuery(this.$route.query, nextQuery)) {
|
||||
const nextQuery = {
|
||||
...(this.$route.query.days ? { days: this.$route.query.days } : {}),
|
||||
...(chart.query || {})
|
||||
}
|
||||
reportDebugLog('users.index.handleChangeChart', {
|
||||
chartKey: chart.key,
|
||||
reportId: chart.reportId || '',
|
||||
nextQuery,
|
||||
currentQuery: this.$route.query
|
||||
})
|
||||
const normalize = (query = {}) => ({
|
||||
days: query.days || '',
|
||||
report_id: query.report_id || '',
|
||||
chart_key: query.chart_key || '',
|
||||
visible_charts: query.visible_charts || '',
|
||||
visible_tables: query.visible_tables || ''
|
||||
})
|
||||
if (JSON.stringify(normalize(this.$route.query)) === JSON.stringify(normalize(nextQuery))) {
|
||||
this.applyChart(chart)
|
||||
return
|
||||
}
|
||||
@@ -225,7 +315,7 @@ h5 {
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list li.active .menu-link {
|
||||
.folder-list li.active > .menu-link {
|
||||
color: var(--color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,11 @@ export default {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasDatePicker: true
|
||||
hasReportExport: true,
|
||||
hasDatePicker: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import DetailCard from '@/components/Cards/DetailCard/index'
|
||||
import { QuickActions } from '@/components'
|
||||
import { terminateSession } from '@/api/sessions'
|
||||
import { addBasePath } from '@/utils/common'
|
||||
import { toSafeLocalDateStr } from '@/utils/common/time'
|
||||
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
|
||||
|
||||
@@ -70,7 +71,7 @@ export default {
|
||||
click: function() {
|
||||
// 跳转到luna页面
|
||||
const joinUrl = '/luna/monitor/' + vm.session.id
|
||||
window.open(joinUrl, 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
|
||||
window.open(addBasePath(joinUrl), 'height=600, width=800, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +90,7 @@ export default {
|
||||
callbacks: {
|
||||
click: function() {
|
||||
const replayUrl = '/luna/replay/' + vm.session.id
|
||||
window.open(replayUrl)
|
||||
window.open(addBasePath(replayUrl))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasReportExport: true,
|
||||
hasDatePicker: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: false,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script>
|
||||
import BaseList from './BaseList'
|
||||
import { download } from '@/utils/common/index'
|
||||
import { addBasePath, download } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
name: 'OfflineList',
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
callback: function({ row, tableData }) {
|
||||
// 跳转到luna页面
|
||||
const replayUrl = '/luna/replay/' + row.id
|
||||
window.open(replayUrl)
|
||||
window.open(addBasePath(replayUrl))
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<script>
|
||||
import BaseList from './BaseList'
|
||||
import { terminateSession, toggleLockSession } from '@/api/sessions'
|
||||
import { addBasePath } from '@/utils/common/index'
|
||||
import { IsSupportPauseSessionType } from '@/utils/jms/index'
|
||||
|
||||
export default {
|
||||
@@ -97,7 +98,7 @@ export default {
|
||||
},
|
||||
callback: function({ row, tableData }) {
|
||||
const monitorUrl = '/luna/monitor/' + row.id
|
||||
window.open(monitorUrl, '_blank', 'height=600, width=850, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
|
||||
window.open(addBasePath(monitorUrl), '_blank', 'height=600, width=850, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
[
|
||||
this.$t('Basic'),
|
||||
[
|
||||
'SECURITY_PASSWORD_EXPIRATION_TIME_ADMIN',
|
||||
'SECURITY_PASSWORD_EXPIRATION_TIME',
|
||||
'OLD_PASSWORD_HISTORY_LIMIT_COUNT',
|
||||
'SECURITY_EXPIRED_TOKEN_RECORD_KEEP_DAYS'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { GenericCreateUpdatePage } from '@/layout/components'
|
||||
import { STORAGE_TYPE_META_MAP } from '@/views/sessions/const'
|
||||
import { UploadSecret } from '@/components/Form/FormFields'
|
||||
import { encryptPassword } from '@/utils/secure'
|
||||
import { encryptPassword } from '@/utils/session-encrypt'
|
||||
|
||||
export default {
|
||||
name: 'ReplayStorageUpdate',
|
||||
@@ -51,19 +51,19 @@ export default {
|
||||
fields: storageTypeMeta.meta,
|
||||
fieldsMeta: {
|
||||
SFTP_PASSWORD: {
|
||||
hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'password'
|
||||
hidden: formValue => formValue.STP_SECRET_TYPE !== 'password'
|
||||
},
|
||||
STP_PRIVATE_KEY: {
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
hidden: formValue => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
},
|
||||
STP_PASSPHRASE: {
|
||||
hidden: (formValue) => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
hidden: formValue => formValue.STP_SECRET_TYPE !== 'ssh_key'
|
||||
}
|
||||
}
|
||||
},
|
||||
is_default: {
|
||||
hidden: (formValue) => formValue.type === 'sftp'
|
||||
hidden: formValue => formValue.type === 'sftp'
|
||||
},
|
||||
comment: {
|
||||
component: 'el-input',
|
||||
@@ -102,6 +102,4 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
<script type="text/jsx">
|
||||
import { ChoicesFormatter, DetailFormatter, SwitchFormatter } from '@/components/Table/TableFormatters'
|
||||
import { BASE_URL } from '@/utils/common/index'
|
||||
import { DrawerListTable as ListTable } from '@/components'
|
||||
|
||||
export default {
|
||||
@@ -141,7 +140,7 @@ export default {
|
||||
type: 'primary',
|
||||
can: this.$hasPerm('ops.view_taskmonitor'),
|
||||
callback: () => {
|
||||
window.open(`${BASE_URL}/core/flower/?_=${Date.now()}`,)
|
||||
window.open(`/core/flower/?_=${Date.now()}`,)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -193,6 +193,7 @@ export default {
|
||||
},
|
||||
defaultTicketActions: {
|
||||
hasImport: false,
|
||||
hasReportExport: true,
|
||||
hasMoreActions: false,
|
||||
hasLeftActions: true,
|
||||
canCreate: this.$hasPerm('tickets.view_ticket'),
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<script>
|
||||
import IBox from '@/components/Common/IBox'
|
||||
import { addBasePath } from '@/utils/common/index'
|
||||
import { IsSupportPauseSessionType } from '@/utils/jms/index'
|
||||
|
||||
export default {
|
||||
@@ -138,7 +139,7 @@ export default {
|
||||
},
|
||||
onMonitor() {
|
||||
const joinUrl = `/luna/monitor/${this.session.id}?ticket_id=${this.object.id}`
|
||||
window.open(joinUrl, '_blank', 'height=600, width=850, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
|
||||
window.open(addBasePath(joinUrl), '_blank', 'height=600, width=850, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
|
||||
},
|
||||
onToggleLock() {
|
||||
const url = '/api/v1/terminal/tasks/toggle-lock-session-for-ticket/'
|
||||
|
||||
@@ -16,7 +16,7 @@ import GrantedAssets from '@/components/Apps/GrantedAssets/index.vue'
|
||||
import Page from '@/layout/components/Page/index.vue'
|
||||
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
|
||||
import { getPreference } from '@/api/settings'
|
||||
import { openNewWindow } from '@/utils/common/index'
|
||||
import { addBasePath, openNewWindow } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -49,7 +49,7 @@ export default {
|
||||
openNewWindow(url)
|
||||
} else {
|
||||
const url = `/luna/?login_to=${row.id}${oid ? `&oid=${oid}` : ''}`
|
||||
window.open(url, '_blank')
|
||||
window.open(addBasePath(url), '_blank')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<script>
|
||||
import HomeCard from './HomeCard.vue'
|
||||
import { getPreference } from '@/api/settings'
|
||||
import { openNewWindow } from '@/utils/common/index'
|
||||
import { addBasePath, openNewWindow } from '@/utils/common/index'
|
||||
|
||||
export default {
|
||||
name: 'Announcement',
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
if (this.preference?.basic?.connect_default_open_method === 'new') {
|
||||
openNewWindow(`/luna/connect?login_to=${row.asset_id}&login_account=${row.account_id}`)
|
||||
} else {
|
||||
window.open(`/luna/?login_to=${row.asset_id}&login_account=${row.account_id}`, '_blank')
|
||||
window.open(addBasePath(`/luna/?login_to=${row.asset_id}&login_account=${row.account_id}`), '_blank')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -8577,6 +8577,11 @@ js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.7.0, js-yaml@^3.9.
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
jsbn@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
|
||||
integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
@@ -12849,6 +12854,13 @@ slice-ansi@^4.0.0:
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
sm-crypto@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.4.0.tgz#13917f5b41e8428d764e9e6934840fdede6a4022"
|
||||
integrity sha512-OexH2V1EqmhXuOIPGoCl55OjMF0wwPUM/zhUjT0Q6vHBeopSRvTNRy76/1eRoFs3VBKt39hdFnxwpFmooHYa2A==
|
||||
dependencies:
|
||||
jsbn "^1.1.0"
|
||||
|
||||
smart-buffer@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
||||
|
||||
Reference in New Issue
Block a user