Compare commits

..

3 Commits

216 changed files with 2324 additions and 4815 deletions

BIN
dump.rdb

Binary file not shown.

View File

@@ -15,9 +15,7 @@
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a"
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
@@ -31,7 +29,6 @@
"install": "^0.13.0",
"jquery": "^3.5.0",
"js-cookie": "2.2.0",
"krry-transfer": "^1.7.3",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.15",
@@ -46,7 +43,6 @@
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"moment": "^2.29.1",
"moment-parseformat": "^3.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -8,6 +8,7 @@
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>

View File

@@ -57,26 +57,6 @@ export function TestReplayStorage(id) {
})
}
function SetToDefaultStorage(url) {
return request({
url: url,
method: 'patch',
data: { 'is_default': true }
})
}
export function SetToDefaultCommandStorage(id) {
return SetToDefaultStorage(
`/api/v1/terminal/command-storages/${id}/`,
)
}
export function SetToDefaultReplayStorage(id) {
return SetToDefaultStorage(
`/api/v1/terminal/replay-storages/${id}/`,
)
}
export function getReplayStorage(id) {
return request({
url: `/api/v1/terminal/replay-storages/${id}/`,

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
src/assets/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,83 +0,0 @@
<template>
<div>
<MFAVerifyDialog
@MFAVerifyDone="getAuthInfo"
@MFAVerifyCancel="exit"
/>
<Dialog
:title="dialogTitle"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
:width="'50'"
:visible.sync="showAuthInfo"
v-bind="$attrs"
v-on="$listeners"
>
<div>
<el-form label-position="right" label-width="80px" :model="authInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="account.hostname" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" show-password />
</el-form-item>
<el-form-item :label="this.$t('assets.SSHKey')">
<el-input v-model="authInfo['private_key']" type="password" show-password />
</el-form-item>
</el-form>
</div>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
export default {
name: 'ShowSecretInfo',
components: {
Dialog,
MFAVerifyDialog
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
dialogTitle: this.$t('common.ViewSecret'),
authInfo: {},
showAuthInfo: false
}
},
mounted() {
this.getAuthInfo()
},
methods: {
getAuthInfo() {
const url = `/api/v1/assets/account-secrets/${this.account.id}/`
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
this.authInfo = resp
this.showAuthInfo = true
})
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,91 +0,0 @@
<template>
<Dialog
width="50"
:title="this.$t('assets.UpdateAssetUserToken')"
:destroy-on-close="true"
v-bind="$attrs"
@confirm="handleConfirm()"
@cancel="handleCancel()"
v-on="$listeners"
>
<el-form label-position="right" label-width="80px">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="account.hostname" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.SSHKey')">
<input type="file" @change="onPrivateKeyLoaded">
</el-form-item>
</el-form>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
export default {
name: 'UpdateSecretInfo',
components: {
Dialog
},
props: {
account: {
type: Object,
default: () => ({})
}
},
data() {
return {
authInfo: {
password: '',
private_key: ''
}
}
},
methods: {
handleConfirm() {
const data = {}
if (this.authInfo.password !== '') {
data.password = this.authInfo.password
}
if (this.authInfo.private_key !== '') {
data.private_key = this.authInfo.private_key
}
this.$axios.patch(
`/api/v1/assets/accounts/${this.account.id}/`,
data
).then(res => {
this.authInfo = { password: '', private_key: '' }
this.$message.success(this.$tc('common.updateSuccessMsg'))
this.$emit('updateAuthDone', res)
this.$emit('update:visible', false)
}).catch(err => {
this.$message.error(this.$tc('common.updateErrorMsg' + ' ' + err))
this.$emit('update:visible', false)
})
},
handleCancel() {
this.$emit('update:visible', false)
},
onPrivateKeyLoaded(e) {
const vm = this
// TODO 校验文件类型
const reader = new FileReader()
reader.onload = function() {
vm.authInfo.private_key = this.result
}
reader.readAsText(
e.target.files[0]
)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,31 +0,0 @@
import { ChoicesFormatter } from '@/components/TableFormatters'
import { toSafeLocalDateStr } from '@/utils/common'
import i18n from '@/i18n/i18n'
export const connectivityMeta = {
label: i18n.t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
ok: 'fa-check text-primary',
failed: 'fa-times text-danger',
unknown: 'fa-circle text-warning'
},
hasTips: true,
getTips: ({ row, cellValue }) => {
const mapper = {
'ok': i18n.tc('assets.Reachable'),
'failed': i18n.tc('assets.Unreachable'),
'unknown': i18n.tc('assets.Unknown')
}
let tips = mapper[cellValue]
if (row['date_verified']) {
const datetime = toSafeLocalDateStr(row['date_verified'])
tips += '<br> ' + datetime
}
return tips
}
},
width: '90px',
align: 'center'
}

View File

@@ -1,180 +0,0 @@
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
<UpdateSecretInfo :visible.sync="showUpdateSecretDialog" :account="account" @updateAuthDone="onUpdateAuthDone" />
</div>
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { ActionsFormatter, DetailFormatter, DisplayFormatter } from '@/components/TableFormatters'
import ShowSecretInfo from './ShowSecretInfo'
import UpdateSecretInfo from './UpdateSecretInfo'
import { connectivityMeta } from './const'
export default {
name: 'Detail',
components: {
ListTable,
UpdateSecretInfo,
ShowSecretInfo
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/assets/accounts/', '/assets/account-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: true
}
},
data() {
return {
showViewSecretDialog: false,
showUpdateSecretDialog: false,
account: {},
tableConfig: {
url: this.url,
columns: [
'hostname', 'ip', 'username', 'version', 'connectivity',
'systemuser', 'date_created', 'date_updated', 'actions'
],
columnsShow: {
min: ['username', 'actions'],
default: ['hostname', 'ip', 'username', 'version', 'actions']
},
columnsMeta: {
hostname: {
prop: 'hostname',
label: this.$t('assets.Hostname'),
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.asset }
}
}
}
},
ip: {
width: '120px'
},
username: {
showOverflowTooltip: true
},
systemuser: {
formatter: DisplayFormatter
},
version: {
width: '70px'
},
connectivity: connectivityMeta,
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function({ row }) {
this.account = row
this.showViewSecretDialog = true
}.bind(this)
},
{
name: 'Delete',
title: this.$t('common.Delete'),
type: 'primary',
callback: ({ row }) => {
this.$axios.delete(`/api/v1/assets/accounts/${row.id}/`).then(() => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
}
},
{
name: 'Test',
title: this.$t('common.Test'),
callback: ({ row }) => {
this.$axios.post(
`/api/v1/assets/accounts/${row.id}/verify/`,
{ action: 'test' }
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
})
}
},
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function({ row }) {
this.account = row
this.showUpdateSecretDialog = true
}.bind(this)
}
]
}
}
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasImport: this.hasImport,
hasExport: this.hasExport,
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
hasSearch: true
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
onUpdateAuthDone(account) {
Object.assign(this.account, account)
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -23,8 +23,8 @@
<script>
import TreeTable from '@/components/TreeTable'
import { DetailFormatter } from '@/components/TableFormatters'
import Select2 from '@/components/FormFields/Select2'
import { DetailFormatter } from '@/components/ListTable/formatters'
import Select2 from '@/components/Select2'
import Dialog from '@/components/Dialog'
export default {

View File

@@ -0,0 +1,371 @@
<template>
<div>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="MFAInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="MFAInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
<el-form label-position="right" label-width="80px" :model="dialogInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="dialogInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="dialogInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="dialogInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.sshkey')">
<input type="file" @change="Onchange">
</el-form-item>
</el-form>
</Dialog>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import ListTable from '@/components/ListTable/index'
import Dialog from '@/components/Dialog'
import { ActionsFormatter, DateFormatter } from '@/components/ListTable/formatters'
export default {
name: 'Detail',
components: {
ListTable,
Dialog
},
props: {
url: {
type: String,
required: true
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
handleExport: {
type: Function,
default: null
},
handleImport: {
type: Function,
default: null
},
hasImport: {
type: Boolean,
default: true
},
hasExport: {
type: Boolean,
default: true
},
hasClone: {
type: Boolean,
default: true
}
},
data() {
return {
MFAConfirmed: false,
MFAInput: '',
MFAInfo: {
asset: '',
username: '',
hostname: '',
password: ''
},
showDialog: false,
showMFADialog: false,
dialogInfo: {
asset: '',
username: '',
hostname: '',
password: '',
private_key: ''
},
tableConfig: {
url: this.url,
columns: [
{
prop: 'hostname',
label: this.$t('assets.Hostname'),
showOverflowTooltip: true
},
{
prop: 'ip',
label: this.$t('assets.ip'),
width: '120px'
},
{
prop: 'username',
label: this.$t('assets.Username'),
showOverflowTooltip: true
},
{
prop: 'version',
label: this.$t('assets.Version'),
width: '70px'
},
{
prop: 'date_created',
label: this.$t('assets.date_joined'),
formatter: DateFormatter
},
{
prop: 'id',
label: this.$t('common.Action'),
align: 'center',
width: 150,
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function(val) {
this.MFAInfo.asset = val.row.id
if (!this.needMFAVerify) {
this.showMFADialog = true
this.MFAConfirmed = true
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.hostname = res.hostname
this.MFAInfo.password = res.password
this.MFAInfo.username = res.username
})
} else {
this.showMFADialog = true
}
}.bind(this)
},
{
name: 'Delete',
title: this.$t('common.Delete'),
type: 'primary',
callback: (val) => {
this.$axios.delete(`/api/v1/assets/asset-users/${val.row.id}/`).then(() => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
}
},
{
name: 'Test',
title: this.$t('common.Test'),
callback: (val) => {
this.$axios.post(
`/api/v1/assets/asset-users/tasks/?id=${val.row.id}`,
{ action: 'test' }
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
})
}
},
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function(val) {
this.showDialog = true
this.dialogInfo.asset = val.row.asset
this.dialogInfo.hostname = val.row.hostname
this.dialogInfo.username = val.row.username
}.bind(this)
}
]
}
}
],
extraQuery: {
latest: 1
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasImport: this.hasImport,
hasExport: this.hasExport,
hasSearch: true,
searchConfig: {
options: [
{
label: this.$t('assets.OnlyLatestVersion'),
value: 'latest',
children: [
{
label: this.$t('common.Yes'),
value: 1
},
{
label: this.$t('common.No'),
value: 0
}
]
}
]
}
}
}
},
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
return false
}
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
created() {
if (this.handleExport) {
this.headerActions.handleExport = this.handleExport
}
if (this.handleImport) {
this.headerActions.handleImport = this.handleImport
}
},
methods: {
MFAConfirm() {
if (this.MFAInput.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAInput
}
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.hostname = res.hostname
this.MFAInfo.password = res.password
this.MFAInfo.username = res.username
})
}
)
},
handleMFAConfirm() {
this.MFAInfo = {
asset: '',
username: '',
hostname: '',
password: ''
}
this.MFAInput = ''
this.showMFADialog = false
this.MFAConfirmed = false
},
handleCancel() {
this.dialogInfo = {
asset: '',
username: '',
hostname: '',
password: '',
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
},
Onchange(e) {
const vm = this
// TODO 校验文件类型
const reader = new FileReader()
reader.onload = function() {
vm.dialogInfo.private_key = this.result
}
reader.readAsText(
e.target.files[0]
)
},
handleConfirm() {
const data = {
asset: this.dialogInfo.asset,
username: this.dialogInfo.username
}
if (this.dialogInfo.password !== '') {
data.password = this.dialogInfo.password
}
if (this.dialogInfo.private_key !== '') {
data.private_key = this.dialogInfo.private_key
}
this.$axios.post(
`/api/v1/assets/asset-users/`,
data
).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(err => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
})
this.dialogInfo = {
asset: '',
username: '',
hostname: '',
password: '',
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -9,7 +9,7 @@
</template>
<script>
import DataForm from '@/components/DataForm'
import { DataForm } from '@/components'
export default {
name: 'NestedField',

View File

@@ -1,5 +1,5 @@
import Vue from 'vue'
import Select2 from '@/components/FormFields/Select2'
import Select2 from '@/components/Select2'
import NestedField from '@/components/AutoDataForm/components/NestedField'
import rules from '@/components/DataForm/rules'
import { assignIfNot } from '@/utils/common'
@@ -124,7 +124,7 @@ export class FormFieldGenerator {
field.el = el
field.rules = rules
_.set(field, 'attrs.error', '')
// Vue.$log.debug('Generate field: ', name, field)
Vue.$log.debug('Generate field: ', name, field)
return field
}
generateFieldGroup(field, fieldsMeta, remoteFieldsMeta) {

View File

@@ -58,10 +58,6 @@ export default {
minColumns: {
type: Array,
default: () => []
},
url: {
type: String,
default: ''
}
},
data() {
@@ -71,17 +67,15 @@ export default {
}
},
mounted() {
this.$eventBus.$on('showColumnSettingPopover', ({ url }) => {
if (url === this.url) {
this.showColumnSettingPopover = true
this.iCurrentColumns = this.currentColumns
}
this.$eventBus.$on('showColumnSettingPopover', () => {
this.showColumnSettingPopover = true
this.iCurrentColumns = this.currentColumns
})
},
methods: {
handleColumnConfirm() {
this.showColumnSettingPopover = false
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })
this.$emit('columnsUpdate', this.iCurrentColumns)
}
}
}

View File

@@ -5,7 +5,6 @@
:current-columns="popoverColumns.currentCols"
:total-columns-list="popoverColumns.totalColumnsList"
:min-columns="popoverColumns.minCols"
:url="config.url"
@columnsUpdate="handlePopoverColumnsChange"
/>
</div>
@@ -13,10 +12,9 @@
<script type="text/jsx">
import DataTable from '../DataTable'
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/TableFormatters'
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/ListTable/formatters'
import i18n from '@/i18n/i18n'
import ColumnSettingPopover from './components/ColumnSettingPopover'
import { newURL } from '@/utils/common'
export default {
name: 'AutoDataTable',
components: {
@@ -170,12 +168,12 @@ export default {
col.filters = column.choices.map(item => {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { text: item['display_name'], value: 'True' }
return { text: item.display_name, value: 'True' }
} else {
return { text: item['display_name'], value: 'False' }
return { text: item.display_name, value: 'False' }
}
}
return { text: item['display_name'], value: item.value }
return { text: item.display_name, value: item.value }
})
col.sortable = false
col['column-key'] = col.prop
@@ -223,15 +221,14 @@ export default {
defaultColumnsNames = totalColumnsNames.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 最小列
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['actions', 'id'])
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['action', 'id'])
.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 应该显示的列
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
const tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
const configShowColumnsNames = _.get(_tableConfig[tableName], 'showColumns', null)
const configShowColumnsNames = _.get(_tableConfig[this.$route.name], 'showColumns', null)
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
if (showColumnsNames.length === 0) {
showColumnsNames = totalColumnsNames
@@ -251,7 +248,7 @@ export default {
min: minColumnsNames,
configShow: configShowColumnsNames
}
this.$log.debug('Cleaned columns show: ', this.cleanedColumnsShow)
this.$log.debug('Cleaned colums show: ', this.cleanedColumnsShow)
},
filterShowColumns() {
this.cleanColumnsShow()
@@ -267,14 +264,13 @@ export default {
this.popoverColumns.minCols = this.cleanedColumnsShow.min
this.$log.debug('Popover cols: ', this.popoverColumns)
},
handlePopoverColumnsChange({ columns, url }) {
this.$log.debug('Columns change: ', columns)
handlePopoverColumnsChange(columns) {
// this.$log.debug('Columns change: ', columns)
this.popoverColumns.currentCols = columns
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
const tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
_tableConfig[tableName] = {
_tableConfig[this.$route.name] = {
'showColumns': columns
}
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))

View File

@@ -37,13 +37,15 @@
class="action-item"
@click="handleClick(action)"
>
<el-tooltip :disabled="!action.tip" :content="action.tip" placement="top">
<span>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
<el-tooltip v-if="action.tip" effect="dark" :content="action.tip" placement="top">
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</el-tooltip>
<span v-else>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-button>
</template>
</div>
</template>

View File

@@ -19,16 +19,3 @@ export default {
RequiredChange,
EmailCheck
}
export const JsonRequired = {
required: true,
trigger: 'change',
validator: (rule, value, callback) => {
try {
JSON.parse(value)
callback()
} catch (e) {
callback(new Error(i18n.t('common.InvalidJson')))
}
}
}

View File

@@ -426,6 +426,7 @@ export default {
onEdit: {
type: Function,
default(row) {
// console.log('On delete row')
}
},
/**
@@ -861,9 +862,6 @@ export default {
}
}
}
if (this.totalData) {
this.getList()
}
},
methods: {
getQuery() {
@@ -927,9 +925,6 @@ export default {
if (!this.hasPagination) {
this.data = this.totalData
this.loading = false
if (this.isTree) {
this.data = this.tree2Array(this.data, this.expandAll)
}
return this.data
}
// page
@@ -940,7 +935,6 @@ export default {
this.$log.debug(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
this.data = this.totalData.slice(start, end)
this.loading = false
this.data = this.tree2Array(this.data, this.expandAll)
return this.data
},
/**

View File

@@ -74,7 +74,7 @@ export default {
}
.dialog-footer {
padding-right: 20px;
padding-right: 50px;
}
</style>

View File

@@ -1,30 +0,0 @@
<template>
<el-link @click="onClick">
{{ title }}
</el-link>
</template>
<script>
export default {
name: 'Link',
props: {
href: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
methods: {
onClick() {
window.open(this.href)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,250 +0,0 @@
<template>
<div>
<div class="hours-container">
<div v-for="(item, index) in hours" :key="index" class="hours-item">
<div class="hours-item-header">{{ compItem(item) }}</div>
<div class="hours-item-value">
<div
:class="compClass(2 * item)"
@click="handleClick(2 * item)"
@mouseover="handleHover(2 * item)"
/>
</div>
</div>
</div>
<div class="tips">{{ tips }}</div>
</div>
</template>
<script>
export default {
model: {
prop: 'sendTimeList'
},
props: {
sendTimeList: {
type: Object,
required: true,
default: () => []
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
hours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], // 选项
selectStart: false, // 开始
startIndex: '', // 开始下标
timeRangeList: [], // 选择的时间段
timeRangeListIndex: [], // 选中的下标
tempRangeIndex: [], // 预选下标
tips: '向右选中,向左取消选择'
}
},
computed: {
},
watch: {
timeRangeList: function(value) {
this.$emit('change', value)
this.$parent.$emit('el.form.change')// 触发父组件的校验规则
},
sendTimeList: {
handler() {
this.transformedIndex()
},
deep: true
}
},
mounted() {
this.transformedIndex()
},
methods: {
// 时间区间转换成下标区间
transformedIndex() {
this.timeRangeListIndex = []
this.timeRangeList = this.sendTimeList
this.timeRangeList.forEach(element => {
const [startTime, endTime] = element.match(/\d+\:\d+/g)
if (startTime && endTime) {
const [startHour, startMin] = startTime.split(':')
const [endHour, endMin] = endTime.split(':')
if (startHour && startMin && endHour && endMin) {
let startNum, endNum
if (startMin === '00') {
startNum = 2 * parseInt(startHour)
} else {
startNum = 2 * parseInt(startHour) + 1
}
if (endMin === '00') {
endNum = 2 * parseInt(endHour) - 1
} else {
endNum = 2 * parseInt(endHour)
}
while (endNum >= startNum) {
this.timeRangeListIndex.push(startNum)
startNum++
}
} else {
this.$message.error('时间段格式不正确')
}
} else {
this.$message.error('没有拿到开始时间或结束时间或者时间段格式不对')
}
})
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
},
// 下标区间转换成时间区间
transformedSection() {
this.timeRangeList = []
let startTime = ''; let endTime = ''; const len = this.hours.length
for (let index = this.hours[0] * 2; index < 2 * (len + 1); index++) {
if (this.timeRangeListIndex.indexOf(index) > -1) {
if (startTime) { // 如果有开始时间,直接确定结束时间
const endHour = Math.floor((index + 1) / 2)
const endMin = (index + 1) % 2 === 0 ? '00' : '30'
endTime = `${endHour < 10 ? '0' + endHour : endHour}:${endMin}`
} else { // 没有开始时间,确定当前点为开始时间
const startHour = Math.floor(index / 2)
const startMin = index % 2 === 0 ? '00' : '30'
startTime = `${startHour < 10 ? '0' + startHour : startHour}:${startMin}`
}
if (index === 2 * this.hours.length + 1) { // 如果是最后一格,直接结束
endTime = `${Math.floor((index + 1) / 2)}:00`
this.timeRangeList.push(`${startTime || '23:30'}-${endTime}`)
startTime = ''
endTime = ''
}
} else { // 若这个点不在选择区间,确定一个时间段
if (startTime && endTime) {
this.timeRangeList.push(`${startTime}-${endTime}`)
startTime = ''
endTime = ''
} else if (startTime && !endTime) { // 这里可能只选半个小时
const endHour = Math.floor(index / 2)
const endMin = index % 2 === 0 ? '00' : '30'
endTime = `${endHour < 10 ? '0' + endHour : endHour}:${endMin}`
this.timeRangeList.push(`${startTime}-${endTime}`)
startTime = ''
endTime = ''
}
}
}
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
},
// 点击事件
handleClick(index) {
if (this.selectStart) {
if (index === this.startIndex) { // 双击取反
if (this.timeRangeListIndex.indexOf(index) > -1) {
this.timeRangeListIndex.splice(this.timeRangeListIndex.indexOf(index), 1)
} else {
this.timeRangeListIndex.push(this.startIndex)
}
} else if (index > this.startIndex) { // 选取数据--向右添加,向左取消
while (index >= this.startIndex) {
this.timeRangeListIndex.push(this.startIndex)
this.startIndex++
}
this.timeRangeListIndex = Array.from(new Set(this.timeRangeListIndex))
} else { // 删除数据
while (this.startIndex >= index) {
if (this.timeRangeListIndex.indexOf(index) > -1) {
this.timeRangeListIndex.splice(this.timeRangeListIndex.indexOf(index), 1)
}
index++
}
}
this.startIndex = ''
this.tempRangeIndex = []
this.transformedSection()
} else {
this.startIndex = index
}
this.selectStart = !this.selectStart
},
// 预选区间
handleHover(index) {
if (this.selectStart) {
this.tempRangeIndex = []
if (index > this.startIndex) { // 选取数据--向右添加,向左取消
while (index >= this.startIndex) {
this.tempRangeIndex.push(index)
index--
}
} else { // 删除数据
while (this.startIndex >= index) {
this.tempRangeIndex.push(index)
index++
}
}
}
},
// 是否选中计算className
compClass(index) {
if (index === this.startIndex) {
return 'hours-item-left preSelected'
}
if (index >= this.startIndex) {
if (this.tempRangeIndex.indexOf(index) > -1) {
return 'hours-item-left preSelected'
}
} else {
if (this.tempRangeIndex.indexOf(index) > -1) {
return 'hours-item-left unSelected'
}
}
return this.timeRangeListIndex.indexOf(index) > -1 ? 'hours-item-left selected' : 'hours-item-left'
},
compItem(item) { // 不足10前面补0
return item < 10 ? `0${item}` : item
}
}
}
</script>
<style lang='scss' scoped>
.hours-container {
display: flex;
cursor: pointer;
.hours-item {
width: 30px;
height: 60px;
border: 1px solid #c2d0f3;
border-right: none;
text-align: center;
&:last-child {
border-right: 1px solid #c2d0f3;
}
.hours-item-header {
width: 100%;
height: 30px;
line-height: 30px;
border-bottom: 1px solid #c2d0f3;
}
.hours-item-value {
width: 100%;
height: 30px;
box-sizing: border-box;
display: flex;
}
.selected {
background-color: #4e84fe;
border-bottom: 1px solid #c2d0f3;
}
.preSelected {
background-color: #8eaffc;
border-bottom: 1px solid #c2d0f3;
}
.unSelected {
background-color: #ffffff;
border-bottom: 1px solid #c2d0f3;
}
}
}
.tips {
width: 100%;
line-height: 30px;
}
</style>

View File

@@ -1,33 +0,0 @@
import DatetimeRangePicker from './DatetimeRangePicker'
import Link from './Link'
import PasswordInput from './PasswordInput'
import Select2 from './Select2'
import Swicher from './Swicher'
import UploadField from './UploadField'
import UploadKey from './UploadKey'
import UserPassword from './UserPassword'
import WeekCronSelect from './WeekCronSelect'
export default {
DatetimeRangePicker,
Link,
PasswordInput,
Select2,
Swicher,
UploadKey,
UploadField,
UserPassword,
WeekCronSelect
}
export {
DatetimeRangePicker,
Link,
PasswordInput,
Select2,
Swicher,
UploadKey,
UploadField,
UserPassword,
WeekCronSelect
}

View File

@@ -3,7 +3,7 @@
</template>
<script type="text/jsx">
import { DetailFormatter, SystemUserFormatter } from '@/components/TableFormatters'
import { DetailFormatter, SystemUserFormatter } from '@/components/ListTable/formatters'
import TreeTable from '../TreeTable'
export default {

View File

@@ -1,45 +1,29 @@
<template>
<div>
<MFAVerifyDialog
v-if="mfaDialogShow"
@MFAVerifyDone="showExportDialog"
@MFAVerifyCancel="handleExportCancel"
/>
<Dialog
v-if="exportDialogShow"
:title="$t('common.Export')"
:visible.sync="exportDialogShow"
:destroy-on-close="true"
@confirm="handleExportConfirm()"
@cancel="handleExportCancel()"
>
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</Dialog>
</div>
<Dialog v-if="showExportDialog" :title="$t('common.Export')" :visible.sync="showExportDialog" :destroy-on-close="true" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
export default {
name: 'ExportDialog',
components: {
Dialog,
MFAVerifyDialog
Dialog
},
props: {
selectedRows: {
@@ -48,15 +32,7 @@ export default {
},
url: {
type: String,
default: ''
},
beforeExport: {
type: Function,
default: () => {}
},
mfaVerifyRequired: {
type: Boolean,
default: false
default: () => ''
},
performExport: {
type: Function,
@@ -79,12 +55,10 @@ export default {
},
data() {
return {
exportDialogShow: false,
showExportDialog: false,
exportOption: 'all',
exportTypeOption: 'csv',
meta: {},
mfaVerified: false,
mfaDialogShow: false
meta: {}
}
},
computed: {
@@ -141,26 +115,11 @@ export default {
}
},
mounted() {
this.$eventBus.$on('showExportDialog', ({ selectedRows, url, name }) => {
// Todo: 没有时间了,只能先这么处理了
if (url === this.url || url.indexOf('account') > -1) {
this.showExportDialog()
}
this.$eventBus.$on('showExportDialog', (row) => {
this.showExportDialog = true
})
},
methods: {
showExportDialog() {
if (!this.mfaVerifyRequired) {
this.exportDialogShow = true
return
}
// 这是需要校验 MFA 的
if (!this.mfaDialogShow) {
this.mfaDialogShow = true
} else {
this.exportDialogShow = true
}
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
@@ -178,6 +137,11 @@ export default {
}
const spm = await createSourceIdCache(resources)
query['spm'] = spm.spm
} else if (exportOption === 'filtered') {
// console.log(listTableRef)
// console.log(listTableRef.dataTable)
// delete query['limit']
// delete query['offset']
}
query['format'] = this.exportTypeOption
const queryStr =
@@ -190,17 +154,14 @@ export default {
const query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
await this.beforeExport()
return this.performExport(this.selectedRows, this.exportOption, query)
},
async handleExportConfirm() {
await this.handleExport()
this.exportDialogShow = false
this.mfaDialogShow = false
this.showExportDialog = false
},
handleExportCancel() {
this.exportDialogShow = false
this.mfaDialogShow = false
this.showExportDialog = false
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<ExportDialog :selected-rows="selectedRows" v-bind="exportOptions" v-on="$listeners" />
<ImportDialog :selected-rows="selectedRows" v-bind="importOptions" v-on="$listeners" />
<ExportDialog :selected-rows="selectedRows" :url="url" v-bind="$attrs" v-on="$listeners" />
<ImportDialog :selected-rows="selectedRows" :url="url" v-bind="$attrs" v-on="$listeners" />
</div>
</template>
@@ -10,7 +10,7 @@ import ExportDialog from './ExportDialog'
import ImportDialog from './ImportDialog'
export default {
name: 'ImExportDialog',
name: 'DialogAction',
components: {
ExportDialog,
ImportDialog
@@ -20,13 +20,9 @@ export default {
type: Array,
default: () => []
},
exportOptions: {
type: Object,
default: () => ({})
},
importOptions: {
type: Object,
default: () => ({})
url: {
type: String,
default: () => ''
}
},
data() {

View File

@@ -7,6 +7,7 @@
:loading-status="loadStatus"
width="80%"
class="importDialog"
:confirm-title="confirmTitle"
:show-cancel="false"
:show-confirm="false"
@close="handleImportCancel"
@@ -117,6 +118,9 @@ export default {
} else {
return this.$t('common.Import') + this.$t('common.Update')
}
},
confirmTitle() {
return '导入'
}
},
watch: {
@@ -125,10 +129,8 @@ export default {
}
},
mounted() {
this.$eventBus.$on('showImportDialog', ({ url }) => {
if (url === this.url) {
this.showImportDialog = true
}
this.$eventBus.$on('showImportDialog', (row) => {
this.showImportDialog = true
})
},
methods: {

View File

@@ -33,8 +33,8 @@
<script>
import DataTable from '@/components/DataTable'
import { sleep, getUpdateObjURL } from '@/utils/common'
import { EditableInputFormatter, StatusFormatter } from '@/components/TableFormatters'
import { sleep } from '@/utils/common'
import { EditableInputFormatter, StatusFormatter } from '@/components/ListTable/formatters'
export default {
name: 'ImportTable',
components: {
@@ -207,7 +207,7 @@ export default {
if (!d) {
return 0
}
if (typeof itemColData !== 'number' && (!itemColData || !itemColData.length)) {
if (!itemColData || !itemColData.length) {
return 0
}
return itemColData.length
@@ -350,7 +350,7 @@ export default {
}
},
async performUpdateObject(item) {
const updateUrl = getUpdateObjURL(this.url, item.id)
const updateUrl = `${this.url}${item.id}/`
return this.$axios.put(
updateUrl,
item,

View File

@@ -32,7 +32,7 @@ export default {
default: ''
},
createRoute: {
type: [String, Object, Function],
type: [String, Object],
default: function() {
return this.$route.name.replace('List', 'Create')
}
@@ -149,13 +149,10 @@ export default {
},
methods: {
handleCreate() {
let route
let route = {}
if (typeof this.createRoute === 'string') {
route = { name: this.createRoute }
route.name = this.createRoute
} else if (typeof this.createRoute === 'function') {
route = this.createRoute()
} else if (typeof this.createRoute === 'object') {
} else {
route = this.createRoute
}
this.$router.push(route)

View File

@@ -1,12 +1,7 @@
<template>
<div>
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
<ImExportDialog
:selected-rows="selectedRows"
:export-options="iExportOptions"
:import-options="iImportOptions"
v-bind="$attrs"
/>
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" v-bind="$attrs" />
</div>
</template>
@@ -14,7 +9,6 @@
import ActionsGroup from '@/components/ActionsGroup'
import ImExportDialog from './ImExportDialog'
import { cleanActions } from './utils'
import { assignIfNot } from '@/utils/common'
const defaultTrue = { type: Boolean, default: true }
@@ -30,32 +24,24 @@ export default {
default: ''
},
hasExport: defaultTrue,
exportOptions: {
type: Object,
default: () => ({})
},
handleExportClick: {
handleExport: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showExportDialog', { selectedRows, url: this.tableUrl, name: this.name })
this.$eventBus.$emit('showExportDialog', { selectedRows })
}
},
hasImport: defaultTrue,
importOptions: {
type: Object,
default: () => ({})
},
handleImportClick: {
handleImport: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showImportDialog', { selectedRows, url: this.tableUrl, name: this.name })
this.$eventBus.$emit('showImportDialog', { selectedRows })
}
},
hasColumnSetting: defaultTrue,
handleTableSettingClick: {
handleColumnConfig: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl, row: selectedRows, name: this.name })
default: function() {
this.$eventBus.$emit('showColumnSettingPopover')
}
},
hasRefresh: defaultTrue,
@@ -75,12 +61,13 @@ export default {
data() {
return {
defaultRightSideActions: [
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleTableSettingClick.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExportClick.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImportClick.bind(this) },
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleColumnConfig.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExport.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImport.bind(this) },
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefresh }
],
dialogExportVisible: false
dialogExportVisible: false,
exportValue: 2
}
},
computed: {
@@ -94,19 +81,18 @@ export default {
},
hasSelectedRows() {
return this.selectedRows.length > 0
},
iImportOptions() {
return assignIfNot(this.importOptions, { url: this.tableUrl })
},
iExportOptions() {
const options = assignIfNot(this.exportOptions, { url: this.tableUrl })
return options
}
},
methods: {
handleTagSearch(val) {
this.searchTable(val)
},
// handleExport({ selectedRows }) {
// this.$eventBus.$emit('showExportDialog', { selectedRows })
// },
// handleImport({ selectedRows }) {
// this.$eventBus.$emit('showImportDialog', { selectedRows })
// },
handleRefresh() {
this.reloadTable()
}

View File

@@ -20,7 +20,7 @@
<script>
import AutoDataSearch from '@/components/AutoDataSearch'
import LeftSide from './LeftSide'
import DatetimeRangePicker from '@/components/FormFields/DatetimeRangePicker'
import DatetimeRangePicker from '@/components/DatetimeRangePicker'
import RightSide from './RightSide'
const defaultTrue = { type: Boolean, default: true }

View File

@@ -1,19 +1,16 @@
<template>
<ActionsGroup v-loading="loadingStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
<ActionsGroup :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup'
import ActionsGroup from '@/components/ActionsGroup/index'
import BaseFormatter from './base'
const defaultPerformDelete = function({ row, col }) {
const id = row.id
const url = new URL(this.url, location.origin)
url.pathname += `${id}/`
const deleteUrl = url.href
return this.$axios.delete(deleteUrl)
const url = `${this.url}${id}/`
return this.$axios.delete(url)
}
const defaultUpdateCallback = function({ row, col }) {
const id = row.id
let route = { params: { id: id }}
@@ -147,12 +144,10 @@ export default {
let actions = [...this.defaultActions, ...this.extraActions]
actions = _.cloneDeep(actions)
actions = actions.map((v) => {
v.has = this.cleanBoolean(v, 'has', true)
v.can = this.cleanBoolean(v, 'can', true)
v.callback = this.cleanCallback(v, 'callback')
v.fa = this.cleanValue(v, 'fa')
v.has = this.cleanBoolean(v, 'has')
v.can = this.cleanBoolean(v, 'can')
v.callback = this.cleanCallback(v)
v.order = v.order || 100
v.tip = this.cleanValue(v, 'tip')
return v
})
actions = actions.filter((v) => v.has)
@@ -170,21 +165,18 @@ export default {
return []
}
return this.cleanedActions.slice(1, this.cleanedActions.length)
},
loadingStatus() {
return this.col.formatterArgs.loading
}
},
methods: {
cleanBoolean(item, attr, defaults) {
cleanBoolean(item, attr) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? defaults : ok
return ok === undefined ? true : ok
}
return this.cleanValue(item, attr)
return ok(this.row, this.cellValue)
},
cleanCallback(item, attr) {
const callback = item[attr]
cleanCallback(item) {
const callback = item.callback
const attrs = {
reload: this.reload,
row: this.row,
@@ -193,20 +185,6 @@ export default {
tableData: this.tableData
}
return () => { return callback.bind(this)(attrs) }
},
cleanValue(item, attr) {
const value = item[attr]
if (!value || typeof value !== 'function') {
return value
}
const attrs = {
reload: this.reload,
row: this.row,
col: this.col,
cellValue: this.cellValue,
tableData: this.tableData
}
return value(attrs)
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
<div slot="content" v-html="tips" />
<div slot="content">{{ tipStatus }}<br>{{ tipTime }}</div>
<i :class="'fa ' + iconClass" />
</el-tooltip>
<i v-else :class="'fa ' + iconClass" />
@@ -10,6 +10,7 @@
<script>
import BaseFormatter from './base'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'ChoicesFormatter',
extends: BaseFormatter,
@@ -22,12 +23,21 @@ export default {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
},
getIconKey({ row, cellValue }) {
return cellValue
typeChange(val) {
return !!val
},
hasTips: false,
getTips: ({ row, cellValue }) => {
return cellValue
tipStatus(val, vm) {
if (!val) {
return vm.$t('assets.Unknown')
}
if (val.status === 0) {
return vm.$t('assets.Unreachable')
} else if (val.status === 1) {
return vm.$t('assets.Reachable')
} else if (val.status === 2) {
return vm.$t('assets.Unknown')
}
}
}
}
@@ -40,11 +50,18 @@ export default {
},
computed: {
iconClass() {
const key = this.formatterArgs.getIconKey({ row: this.row, cellValue: this.cellValue })
const key = this.formatterArgs.typeChange(this.cellValue)
return this.formatterArgs.iconChoices[key]
},
tips() {
return this.formatterArgs.getTips({ cellValue: this.cellValue, row: this.row })
tipStatus() {
const vm = this
return this.formatterArgs.tipStatus(this.cellValue, vm)
},
tipTime() {
if (!this.cellValue) {
return ''
}
return toSafeLocalDateStr(this.cellValue.datetime)
}
}
}

View File

@@ -20,7 +20,7 @@ export default {
defaultOnDelete(col, row, cellValue, reload) {
const url = col.deleteUrl + cellValue
this.$axios.delete(url).then(res => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$message.success(this.$t('common.deleteSuccessMsg'))
reload()
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)

View File

@@ -0,0 +1,116 @@
<template>
<ActionsGroup v-loading="loadStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" />
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index'
import BaseFormatter from './base'
export default {
name: 'LoadingActionsFormatter',
components: { ActionsGroup },
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default: function() {
return {
hasUpdate: true, // can set function(row, value)
canUpdate: true, // can set function(row, value)
hasDelete: true, // can set function(row, value)
canDelete: true,
updateRoute: this.$route.name.replace('List', 'Update'),
extraActions: [] // format see defaultActions
}
}
}
},
data() {
const colActions = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
const defaultActions = [
{
name: 'update',
title: this.$t('common.Update'),
type: 'primary',
has: colActions.hasUpdate,
can: colActions.canUpdate,
callback: colActions.onUpdate
},
{
name: 'delete',
title: this.$t('common.Delete'),
type: 'danger',
has: colActions.hasDelete,
can: colActions.canDelete,
callback: colActions.onDelete
}
]
return {
colActions: colActions,
defaultActions: defaultActions,
extraActions: colActions.extraActions
}
},
computed: {
cleanedActions() {
let actions = [...this.defaultActions, ...this.extraActions]
actions = _.cloneDeep(actions)
actions = actions.map((v) => {
v.has = this.cleanBoolean(v, 'has')
v.can = this.cleanBoolean(v, 'can')
v.fa = this.cleanFa(v, 'fa')
v.callback = this.cleanCallback(v)
return v
})
actions = actions.filter((v) => v.has)
return actions
},
actions() {
if (this.cleanedActions.length <= 2) {
return this.cleanedActions
}
return this.cleanedActions.slice(0, 1)
},
moreActions() {
if (this.cleanedActions.length <= 2) {
return []
}
return this.cleanedActions.slice(1, this.cleanedActions.length)
},
loadStatus() {
return this.col.formatterArgs.loading
}
},
methods: {
cleanBoolean(item, attr) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? true : ok
}
return ok(this.row, this.cellValue)
},
cleanCallback(item) {
const callback = item.callback
const attrs = {
reload: this.reload,
row: this.row,
col: this.col,
cellValue: this.cellValue,
tableData: this.tableData
}
return () => { return callback.bind(this)(attrs) }
},
cleanFa(item, attr) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? false : ok
}
return ok(this.row, this.cellValue)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -7,8 +7,9 @@ import ActionsFormatter from './ActionsFormatter'
import DeleteActionFormatter from './DeleteActionFormatter'
import DateFormatter from './DateFormatter'
import SystemUserFormatter from './GrantedSystemUsersShowFormatter'
import ShowKeyFormatter from '@/components/TableFormatters/ShowKeyFormatter'
import ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
import DialogDetailFormatter from './DialogDetailFormatter'
import LoadingActionsFormatter from './LoadingActionsFormatter'
import EditableInputFormatter from './EditableInputFormatter'
import StatusFormatter from './StatusFormatter'
@@ -23,6 +24,7 @@ export default {
SystemUserFormatter,
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter,
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
@@ -39,6 +41,7 @@ export {
SystemUserFormatter,
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter,
ArrayFormatter,
EditableInputFormatter,
StatusFormatter

View File

@@ -113,6 +113,15 @@ export default {
& >>> .el-table__header thead > tr > th {
background-color: white;
}
/*& >>> .el-table--striped .el-table__body tr.el-table__row--striped td {*/
/*background: white;*/
/*}*/
/*& >>> .el-table th, .el-table tr {*/
/*background-color: red;*/
/*!*background-color: #FAFAFA;*!*/
/*}*/
}
//修改颜色

View File

@@ -1,77 +0,0 @@
<template>
<Dialog
:title="$t('common.MFAVerify')"
:width="'50'"
:show-confirm="false"
:show-cancel="false"
:visible.sync="visible"
:destroy-on-close="true"
v-bind="$attrs"
v-on="$listeners"
>
<el-row :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAToken" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="verifyMFA">
{{ this.$t('common.Confirm') }}
</el-button>
</el-col>
</el-row>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
export default {
name: 'MFAVerifyDialog',
components: {
Dialog
},
data() {
return {
MFAToken: '',
visible: false
}
},
watch: {
visible(val) {
if (!val) {
this.$emit('MFAVerifyCancel', true)
}
}
},
mounted() {
this.$axios.get('/api/v1/authentication/otp/verify/', { disableFlashErrorMsg: true }).then(() => {
this.$emit('MFAVerifyDone', true)
}).catch(err => {
this.$log.debug('Verify otp code error: ', err)
this.visible = true
})
},
methods: {
verifyMFA() {
if (this.MFAToken.length !== 6) {
return this.$message.error(this.$tc('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAToken
}
).then(res => {
this.$emit('MFAVerifyDone', true)
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -10,7 +10,7 @@
</template>
<script>
import Switcher from '../FormFields/Swicher'
import Switcher from '../Swicher'
export default {
name: 'ActionItem',
components: {

View File

@@ -41,7 +41,7 @@
</template>
<script>
import Select2 from '../FormFields/Select2'
import Select2 from '../Select2'
import IBox from '../IBox'
import { createSourceIdCache } from '@/api/common'
import { mapGetters } from 'vuex'

View File

@@ -4,16 +4,13 @@
<span class="header-title">{{ title }}</span>
<span class="pull-right right-side">
<slot name="header-right">
<el-tag :type="rightSideLabel.type || 'success'" effect="dark" size="mini">
{{ rightSideLabel.title }}
</el-tag>
<el-tag :type="rightSideLabel.type || 'success'" effect="dark" size="mini">{{ rightSideLabel.title }}</el-tag>
</slot>
</span>
</div>
<slot>
<h1 class="no-margins">
<span v-if="body.disabled" class="disabled-link">{{ body.count }}</span>
<router-link v-else :to="body.route">
<router-link :to="body.route">
<span>{{ body.count }}</span>
</router-link>
</h1>
@@ -77,8 +74,4 @@ export default {
.no-margins {
margin: 0 !important;
}
.disabled-link {
color: #428bca;
}
</style>

View File

@@ -6,6 +6,7 @@
</template>
<script>
export default {
props: {
value: {

View File

@@ -7,7 +7,7 @@
</template>
<script>
import PasswordInput from './PasswordInput'
import PasswordInput from '../PasswordInput'
import { mapGetters } from 'vuex'
import store from '@/store'
import i18n from '@/i18n/i18n'

View File

@@ -12,16 +12,14 @@ export { default as FormGroupHeader } from './FormGroupHeader'
export { default as Hamburger } from './Hamburger'
export { default as ListTable } from './ListTable'
export { default as RelationCard } from './RelationCard'
export { default as Select2 } from './FormFields/Select2'
export { default as UploadKey } from './FormFields/UploadKey.vue'
export { default as Select2 } from './Select2'
export { default as AssetSelect } from './AssetSelect'
export { default as SvgIcon } from './SvgIcon'
export { default as TreeTable } from './TreeTable'
export { default as IBox } from './IBox'
export { default as QuickActions } from './QuickActions'
export { default as Switcher } from './FormFields/Swicher'
export { default as Switcher } from './Swicher'
export { default as SummaryCard } from './SummaryCard'
export { default as UploadField } from './FormFields/UploadField'
export { default as AccountListTable } from './AccountListTable/index'
export { default as UploadField } from './UploadField'
export { default as AssetUserTable } from './AssetUserTable'
export { default as AssetRelationCard } from './AssetRelationCard'
export { default as MFAVerifyDialog } from './MFAVerifyDialog'

View File

@@ -1,10 +1,5 @@
{
"": "",
"accounts": {
"PleaseClickLeftAssetToViewAssetAccount": "资产账号列表,点击左侧资产进行查看",
"PleaseClickLeftApplicationToViewApplicationAccount": "应用账号列表,点击左侧应用进行查看",
"PleaseClickLeftAssetToViewGatheredUser": "收集用户列表,点击左侧资产进行查看"
},
"acl": {
"name": "名称",
"username": "用户名",
@@ -83,13 +78,11 @@
"DBInfo": "数据库信息"
},
"assets": {
"AssociateAssets": "关联资产",
"AssociateNodes": "关联节点",
"Action": "动作",
"ActiveSelected": "激活所选",
"AdminUser": "特权用户",
"AdminUser": "管理用户",
"AdminUserDetail": "管理用户详情",
"AdminUserListHelpMessage": "<b>特权用户</b> 是资产已存在的, 并且拥有 高级权限 的系统用户, 如 root拥有 `NOPASSWD: ALL` sudo 权限的用户 JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。",
"AdminUserListHelpMessage": "管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户 JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。\n",
"Asset": "资产",
"HardwareInfo": "硬件信息",
"AssetDetail": "资产详情",
@@ -100,11 +93,11 @@
"TestGatewayHelpMessage": "如果使用了nat端口映射请设置为ssh真实监听的端口",
"SshPort": "SSH 端口",
"AssetNumber": "资产编号",
"AssetUserList": "账号列表",
"AssetUserList": "资产用户列表",
"Assets": "资产",
"Auth": "认证",
"AccountList": "账号列表",
"AutoGenerateKey": "自动生成",
"AutoGenerateKey": "自动生成密钥",
"AutoPush": "自动推送",
"BasePlatform": "基础平台",
"Basic": "基本",
@@ -117,7 +110,6 @@
"CommandFilterRules": "命令过滤器规则",
"Comment": "备注",
"Cpu": "CPU",
"CommonUser": "普通用户",
"CreatedBy": "创建者",
"Database": "数据库",
"DateJoined": "创建日期",
@@ -159,7 +151,6 @@
"PriorityHelpMessage": "1-100, 1最低优先级100最高优先级。授权多个用户时高优先级的系统用户将会作为默认登录用户",
"Protocol": "协议",
"Protocols": "协议组",
"LoginOption": "登录选项",
"PublicIp": "公网IP",
"Push": "推送",
"PushSystemUserNow": "推送系统用户",
@@ -182,8 +173,7 @@
"PasswordHelpMessage": "密码或密钥密码",
"SystemUser": "系统用户",
"SystemUserDetail": "系统用户详情",
"SystemUserListHelpMessage": "<b>系统用户</b> 是JumpServer 登录资产时使用的账号,如 root `ssh root@host`,而不是使用该用户名登录资产ssh admin@host)`;<br><b>特权用户</b> 是资产已存在的, 并且拥有 高级权限 的系统用户, JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等;</br><b>普通用户</b> 可以在资产上预先存在,也可以由 特权用户 来自动创建。",
"DynamicUsername": "动态用户名",
"SystemUserListHelpMessage": "系统用户是 JumpServer 跳转登录资产时使用的用户,可以理解为登录资产用户,如 websadba`ssh web@some-host`,而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host` 简单来说是用户使用自己的用户名登录 JumpServerJumpServer 使用系统用户登录资产。 系统用户创建时,如果选择了自动推送,JumpServer 使用 Ansible 自动推送系统用户到资产中,如果资产(交换机)不支持 Ansible请手动填写账号密码。\n",
"SystemUsers": "系统用户",
"Test": "测试",
"TestAssetsConnective": "测试资产可连接性",
@@ -191,7 +181,7 @@
"Type": "类型",
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
"UnselectedNodes": "未选择节点",
"UpdateAssetUserToken": "更新账号认证信息",
"UpdateAssetUserToken": "更新资产用户认证信息",
"Username": "用户名",
"UsernameHelpMessage": "用户名是动态的,登录资产时使用当前用户的用户名登录",
"Value": "值",
@@ -202,7 +192,6 @@
"sshKeyFingerprint": "SSH 指纹",
"ip": "IP",
"sshkey": "sshkey",
"SSHKey": "SSH 密钥",
"GroupsHelpMessage": "请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)",
"HomeHelpMessage": "默认家目录 /home/系统用户名: /home/username",
"Home": "家目录",
@@ -210,25 +199,20 @@
"ipDomain": "IP(域名)",
"HostProtocol": "主机协议",
"DatabaseProtocol": "数据库协议",
"OtherProtocol": "其它协议",
"PasswordOrToken": "密码 / 令牌"
"OtherProtocol": "其它协议"
},
"audits": {
"Hosts": "主机",
"RunUser": "运行用户",
"User": "用户",
"Username": "用户名",
"View": "查看",
"SystemUserName": "系统用户名"
"View": "查看"
},
"auth": {
"LoginRequiredMsg": "账号已退出,请重新登录",
"ReLogin": "重新登录"
},
"common": {
"MFAVerify": "验证 MFA",
"ViewSecret": "查看密码",
"ConnectWebSocketError": "连接 WebSocket 失败",
"Action": "动作",
"RequestTickets": "申请工单",
"Actions": "操作",
@@ -243,8 +227,6 @@
"UpdateAssetDetail": "配置更多信息",
"AddSuccessMsg": "添加成功",
"Auth": "认证",
"bind": "绑定",
"unbind": "解绑",
"PushSelected":"推送所选",
"BadRequestErrorMsg": "请求错误,请检查填写内容",
"BadRoleErrorMsg": "请求错误,无该操作权限",
@@ -284,8 +266,6 @@
"Info": "提示",
"MFAConfirm": "MFA 认证",
"MFARequireForSecurity": "为了安全请输入MFA",
"PasswordConfirm": "密码认证",
"PasswordRequireForSecurity": "为了安全请输入密码",
"Members": "成员",
"More": "更多",
"Message": "消息",
@@ -319,7 +299,6 @@
"View": "查看",
"Yes": "是",
"action": "动作",
"User": "用户",
"activateSelected": "激活所选",
"bulkDeleteErrorMsg": "批量删除失败: ",
"bulkDeleteSuccessMsg": "批量删除成功",
@@ -351,7 +330,6 @@
"Pending": "等待",
"Status": "状态",
"InputEmailAddress": "请输入正确的邮箱地址",
"Receivers": "接收人",
"imExport": {
"ExportAll": "导出所有",
"ExportOnlyFiltered": "仅导出搜索结果",
@@ -402,8 +380,7 @@
"SPECIAL_CHAR_REQUIRED": "须包含特殊字符",
"MIN_LENGTH_ERROR": "密码最小长度 {0} 位"
},
"lastCannotBeDeleteMsg": "最后一项,不能被删除",
"InvalidJson": "不是合法 JSON"
"lastCannotBeDeleteMsg": "最后一项,不能被删除"
},
"dashboard": {
"ActiveAsset": "近期被登录过",
@@ -540,11 +517,7 @@
},
"route": {
"": "",
"Accounts": "账号管理",
"AssetAccount": "资产账号",
"ApplicationAccount": "应用账号",
"Ticket":"工单",
"CommandConfirm": "命令复核",
"AdminUserCreate": "创建管理用户",
"AdminUserDetail": "管理用户详情",
"AdminUserList": "管理用户",
@@ -646,7 +619,6 @@
"SystemUserDetail": "系统用户详情",
"SystemUserList": "系统用户",
"SystemUserUpdate": "更新系统用户",
"AssetUserList": "资产用户",
"TaskDetail": "任务详情",
"TaskList": "任务列表",
"TaskMonitor": "任务监控",
@@ -667,15 +639,9 @@
"UserUpdate": "更新用户",
"Users": "用户管理",
"WebFTP": "文件管理",
"WebTerminal": "Web终端",
"Notifications": "通知",
"SiteMessageList": "站内信"
"WebTerminal": "Web终端"
},
"sessions": {
"SetToDefaultStorage": "设置为默认存储",
"SetToDefault": "设为默认",
"SetSuccess": "设置成功",
"SetFailed": "设置失败",
"StorageConfiguration": "存储配置",
"accountKey": "账户密钥",
"accountName": "账户名称",
@@ -736,7 +702,6 @@
"common": "普通"
},
"Monitor": "监控",
"XRDPNotSupport": "RDP 客户端会话, 暂不支持监控",
"sessionMonitor": "监控",
"TerminateTaskSendSuccessMsg": "终断任务已下发,请稍后刷新查看",
"helpText": {
@@ -748,7 +713,6 @@
}
},
"setting": {
"InsecureCommandNotifyToSubscription": "危险命令通知已升级到消息订阅中,支持更多通知方式",
"ApiKeyList": "API Key 列表",
"AssetCount": "资产数量",
"Basic": "基本设置",
@@ -767,8 +731,6 @@
"PasswordCheckRule": "密码校验规则",
"Security": "安全设置",
"SecuritySetting": "安全设置",
"SystemMessageSubscription": "系统消息订阅",
"insecureCommandEmailUpdate": "点我设置",
"SubscriptionID": "订阅授权ID",
"Terminal": "终端设置",
"all": "全部",
@@ -882,15 +844,12 @@
"refreshLdapCache":"刷新Ldap缓存请稍后",
"LicenseExpired": "许可证已经过期",
"LicenseWillBe": "许可证即将在 ",
"Expire": " 过期",
"WeCom": "企业微信",
"DingTalk": "钉钉",
"dingTalkTest": "测试",
"weComTest": "测试",
"Expire": " 过期"
},
"settings": {
"setting": "设置"
},
"tickets": {
"PermissionName": "授权规则名称",
"Accept": "同意",
"AssignedMe": "待我审批",
"Assignee": "处理人",
@@ -932,13 +891,7 @@
"ips": "请输入逗号分割的IP地址组",
"fuzzySearch": "支持模糊搜索",
"application": "请输入逗号分割的应用名称组"
},
"ApplyRunUser": "申请运行的用户",
"ApplyRunAsset": "申请运行的资产",
"ApplyRunSystemUser": "申请运行的系统用户",
"ApplyRunCommand": "申请运行的命令",
"ApplyFromSession": "会话",
"ApplyFromCMDFilterRule": "命令过滤规则"
}
},
"tree": {
"AddAssetToNode": "添加资产到节点",
@@ -954,7 +907,6 @@
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
},
"users": {
"UserName": "姓名",
"Account": "账户",
"Authentication": "认证",
"Comment": "备注",
@@ -975,8 +927,6 @@
"Invite": "邀请",
"InviteUserInOrg": "邀请用户加入此组织",
"Guide": "向导",
"setWeCom": "设置企业微信认证",
"setDingTalk": "设置钉钉认证",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "启用多因子认证,使账号更加安全。<br/> 启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速修改->更改多因子设置)中直接绑定!",
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)",
@@ -1024,14 +974,7 @@
"resetSSHKey": "重置SSH密钥",
"resetSSHKeySuccessMsg": "发送邮件任务已提交, 用户稍后会收到重置密钥邮件",
"resetSSHKeyWarningMsg": "你确定要发送重置用户的SSH Key的邮件吗?",
"resetWechat": "解绑企业微信",
"resetWechatLoginWarningMsg": "你确定要解绑用户的 企业微信 吗?",
"resetWechatLoginSuccessMsg": "重置成功, 用户可以重新绑定企业微信了",
"resetDingTalk": "解绑钉钉",
"resetDingTalkLoginWarningMsg": "你确定要解绑用户的 钉钉 吗?",
"resetDingTalkLoginSuccessMsg": "重置成功, 用户可以重新绑定钉钉了",
"send": "发送",
"unbind": "解绑",
"unblock": "解锁",
"unblockSuccessMsg": "解锁成功",
"unblockUser": "解锁用户"
@@ -1050,27 +993,12 @@
"remoteAppPermissionRules": "远程应用授权规则"
},
"dateLastLogin": "最后登录日期",
"needUpdatePasswordNextLogin": "下次登录须修改密码",
"UpdatePassword": "更新密码",
"SetPublicKey": "设置SSH公钥",
"passwordExpired": "密码过期了",
"passwordWillExpiredPrefixMsg": "密码即将在 ",
"passwordWillExpiredSuffixMsg": "天 后过期,请尽快修改您的密码。"
},
"notifications": {
"MessageType": "消息类型",
"Receivers": "接收人",
"Subscription": "消息订阅",
"ChangeReceiver": "修改消息接收人",
"Subject": "主题",
"Message": "消息",
"DeliveryTime": "发送时间",
"HasRead": "是否已读",
"Sender": "发送人",
"MarkAsRead": "标记已读",
"NoUnreadMsg": "暂无未读消息",
"SiteMessage": "站内信"
},
"xpack": {
"Admin": "管理员",
"Asset": "资产",
@@ -1120,8 +1048,6 @@
"Cloud": {
"Aliyun": "阿里云",
"Qcloud": "腾讯云",
"QingyunPrivatecloud": "青云私有云",
"HuaweiPrivatecloud": "华为私有云",
"AWS_China": "AWS(中国)",
"AWS_Int": "AWS(国际)",
"HuaweiCloud": "华为云",
@@ -1149,7 +1075,7 @@
"Name": "名称",
"Account":"账户",
"Node": "节点",
"AdminUser":"特权用户",
"AdminUser":"管理用户",
"Periodic":"执行周期",
"PeriodicPerform":"定时执行",
"RegularlyPerform": "定期执行",
@@ -1179,9 +1105,7 @@
"GatherUserList": "收集用户",
"GatherUserTaskCreate": "创建任务",
"GatherUserTaskList": "任务列表",
"GatherUserTaskUpdate": "更新任务",
"GatherUserTaskDetail": "任务详情",
"GatherUserTaskExecutionList": "任务执行列表"
"GatherUserTaskUpdate": "更新任务"
},
"Import": "导入",
"ImportLicense": "导入许可证",
@@ -1211,7 +1135,7 @@
"users_amount": "用户数量",
"groups_amount": "用户组数量",
"assets_amount": "资产数量",
"admin_users_amount": "特权用户数量",
"admin_users_amount": "管理用户数量",
"system_users_amount": "系统用户数量",
"applications_amount": "应用数量",
"asset_perms_amount": "资产授权数量",

View File

@@ -1,10 +1,5 @@
{
"": "",
"accounts": {
"PleaseClickLeftAssetToViewAssetAccount": "Asset account list, please click on the assets on the left to view",
"PleaseClickLeftApplicationToViewApplicationAccount": "Application account list, please click on the application on the left to view",
"PleaseClickLeftAssetToViewGatheredUser": "Gathered user list, please click on the assets on the left to view"
},
"acl": {
"name": "Name",
"username": "Username",
@@ -82,20 +77,16 @@
"DBInfo": "Database Info"
},
"assets": {
"AssociateAssets": "Associate assets",
"AssociateNodes": "Associate nodes",
"Action": "Action",
"ActiveSelected": "Active selected",
"AdminUser": "Admin user",
"ReplaceNodeAssetsAdminUser":"Replace node assets admin user with this",
"AdminUserDetail": "Admin user detail",
"DynamicUsername": "Dynamic username",
"AdminUserListHelpMessage": "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, JumpServer users of the system using the user to `push system user`, `get assets hardware information`, etc.\n",
"Asset": "Asset",
"HardwareInfo": "Hardware info",
"Hardware": "Hardware",
"AccountList": "Account list",
"LoginOption": "Login option",
"AssetDetail": "Asset detail",
"AssetList": "Asset list",
"AssetListHelpMessage": "The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node\n",
@@ -119,7 +110,6 @@
"CommandFilterRules": "Command filter rules",
"Comment": "Comment",
"Cpu": "Cpu",
"CommonUser": "Common user",
"CreatedBy": "Created by",
"Database": "Database",
"DateJoined": "Date joined",
@@ -178,11 +168,11 @@
"Rules": "Rules",
"SFTPHelpMessage": "SFTP root dir, tmp, home or custom",
"SerialNumber": "Serial number",
"SudoHelpMessage": "Use comma split multi command, ex: /bin/whoami, /bin/ifconfig",
"SudoHelpMessage": "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig",
"PasswordHelpMessage": "Password or private key password",
"SystemUser": "System user",
"SystemUserDetail": "System user detail",
"SystemUserListHelpMessage": "<b>System user</b> is the account JumpServer used to log into the asset, such as using root `ssh root@host`, rather than the current user usernamessh admin@host)`;<br><b>Admin user</b> is the account that already exists on an asset, and have privileged permissions, JumpServer using this create common system user, and gather hardware Etc;</br><b>Common user</b> can pre-exist assets or created automatically by the admin user.",
"SystemUserListHelpMessage": "System user is JumpServer jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); In simple terms, users log into JumpServer using their own username, and JumpServer uses system users to log into assets. When system users are created, if you choose auto push JumpServer to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.\n",
"SystemUsers": "System users",
"Test": "Test",
"TestAssetsConnective": "Test assets connective",
@@ -208,15 +198,12 @@
"ipDomain": "IP(Domain)",
"HostProtocol": "Host Protocol",
"DatabaseProtocol": "Database Protocol",
"OtherProtocol": "Other Protocol",
"PasswordOrToken": "Password / Token"
"Other Protocol": "Database Protocol"
},
"audits": {
"Hosts": "Host",
"RunUser": "Run user",
"User": "User",
"Username": "Username",
"SystemUserName": "System username",
"View": "View"
},
"auth": {
@@ -224,9 +211,6 @@
"ReLogin": "Re-Login"
},
"common": {
"MFAVerify": "Verify MFA",
"ViewSecret": "View secret",
"ConnectWebSocketError": "Connect Websocket failed",
"Nothing": "Nothing",
"Action": "Action",
"CustomCol":"Custom table col",
@@ -257,7 +241,7 @@
"Confirm": "Confirm",
"Create": "Create",
"CreatedBy": "Created by",
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set, give priority to Regularly perform",
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set,give priority to Regularly perform",
"DateEnd": "End date",
"Resource": "Resource",
"DateLast24Hours": "Last 24 hours",
@@ -281,8 +265,6 @@
"Info": "Info",
"MFAConfirm": "MFA Confirm",
"MFARequireForSecurity": "MFA required for security",
"PasswordConfirm": "Password Confirm",
"PasswordRequireForSecurity": "Password required for security",
"Members": "Members",
"More": "More",
"Message": "Message",
@@ -294,12 +276,11 @@
"Other": "Other",
"Others": "Others",
"Push": "Push",
"Receivers": "Receivers",
"QuickUpdate": "Quick update",
"RemoveSuccessMsg": "Remove success",
"Reset": "Reset",
"Search": "Search",
"MFAErrorMsg": "MFA Error, please check",
"MFAErrorMsg": "MFA Errorplease check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select",
"SelectFile": "Select file",
@@ -310,8 +291,6 @@
"TestSuccessMsg": "Test Success",
"To": "To",
"Update": "Update",
"bind": "Bind",
"unbind": "Unbind",
"Upload": "Upload",
"Clone": "Clone",
"Username": "Username",
@@ -399,8 +378,7 @@
"SPECIAL_CHAR_REQUIRED": "Special char required",
"MIN_LENGTH_ERROR": "Password minimum length {}"
},
"lastCannotBeDeleteMsg": "The last one can't be delete",
"InvalidJson": "Not a valid json format"
"lastCannotBeDeleteMsg": "The last one can't be delete"
},
"dashboard": {
"ActiveAsset": "Asset active",
@@ -537,33 +515,29 @@
},
"route": {
"": "",
"Accounts": "Accounts",
"AssetAccount": "Asset account",
"ApplicationAccount": "Application account",
"Ticket": "Tickets",
"CommandConfirm": "Command confirm",
"AdminUserCreate": "Admin user create",
"AdminUserDetail": "Admin user detail",
"AdminUserList": "Admin Users",
"AdminUserList": "Admin users",
"AdminUserUpdate": "Admin user update",
"Applications": "Applications",
"AssetCreate": "Asset create",
"AssetDetail": "Asset detail",
"AssetList": "Assets",
"AssetPermission": "Asset Permissions",
"AssetPermission": "Asset permissions",
"AssetPermissionCreate": "Asset permissions create",
"AssetPermissionDetail": "Asset permissions detail",
"AssetPermissionUpdate": "Asset permissions update",
"AssetUpdate": "Asset update",
"Assets": "Assets",
"Audits": "Audits",
"BatchCommand": "Batch Command",
"BatchCommandLog": "Batch Command Log",
"BatchCommand": "Batch command",
"BatchCommandLog": "Batch command log",
"CeleryTaskLog": "Celery task log",
"CommandExecutions": "CommandExecutions ",
"CommandFilterCreate": "Command filter create",
"CommandFilterDetail": "Command filter detail",
"CommandFilterList": "Command Filters",
"CommandFilterList": "Command filters",
"CommandFilterRulesCreate": "Command filter rules create",
"CommandFilterRulesUpdate": "Command filter rules update",
"CommandFilterUpdate": "Command filter update",
@@ -572,7 +546,7 @@
"CreateCommandStorage": "Create command storage",
"CreateReplayStorage": "Create replay storage",
"Dashboard": "Dashboard",
"DatabaseApp": "Database Apps",
"DatabaseApp": "Database apps",
"DatabaseAppCreate": "Database app create",
"DatabaseAppDetail": "Database app detail",
"DatabaseAppPermission": "Databases permissions",
@@ -580,7 +554,7 @@
"DatabaseAppPermissionDetail": "Databases permissions detail",
"DatabaseAppPermissionUpdate": "Databases permissions update",
"DatabaseAppUpdate": "Database app update",
"KubernetesApp": "Kubernetes Apps",
"KubernetesApp": "Kubernetes apps",
"KubernetesAppCreate": "Kubernetes app create",
"KubernetesAppDetail": "Kubernetes app detail",
"KubernetesAppPermission": "Kubernetes permissions",
@@ -588,13 +562,13 @@
"KubernetesAppPermissionDetail": "Kubernetes permissions detail",
"KubernetesAppPermissionUpdate": "Kubernetes permissions update",
"KubernetesAppUpdate": "Kubernetes app update",
"Acl": "Access Control",
"Acl": "Access control",
"UserAclList": "User acl list",
"UserAclCreate": "User acl create",
"UserAclUpdate": "User acl update",
"UserAclLists": "User acl lists",
"UserAclDetail": "User acl detail",
"AssetAclList": "Asset Acl",
"AssetAclList": "Asset acl list",
"AssetAclCreate": "Asset acl create",
"AssetAclUpdate": "Asset acl update",
"AssetAclDetail": "Asset acl detail",
@@ -602,29 +576,29 @@
"DomainDetail": "Domain detail",
"DomainList": "Domains",
"DomainUpdate": "Domain update",
"FileManager": "File Manager",
"FtpLog": "FTP Logs",
"FileManager": "File manager",
"FtpLog": "FTP logs",
"GatewayCreate": "Gateway create",
"GatewayUpdate": "Gateway update",
"JobCenter": "Jobcenter",
"LabelCreate": "Label create",
"LabelList": "Labels",
"LabelUpdate": "Label update",
"LoginLog": "Login Logs",
"MyApps": "My Apps",
"MyAssets": "My Assets",
"OperateLog": "Operation Logs",
"PasswordChangeLog": "Password Update Logs",
"LoginLog": "Login logs",
"MyApps": "My apps",
"MyAssets": "My assets",
"OperateLog": "Operation logs",
"PasswordChangeLog": "Password update logs",
"Perms": "Permissions",
"PersonalInformationImprovement": "PersonalInformationImprovement",
"PlatformCreate": "Platform create",
"PlatformDetail": "Platform detail",
"PlatformList": "Platforms",
"PlatformUpdate": "Platform update",
"RemoteApp": "Remote Apps",
"RemoteApp": "Remote apps",
"RemoteAppDetail": "Remote app detail",
"RemoteAppPermission": "Remote apps permissions",
"ApplicationPermission": "Application Permissions",
"ApplicationPermission": "Application permissions",
"RemoteAppPermissionCreate": "Remote apps permission create",
"RemoteAppPermissionDetail": "Remote apps permissions detail",
"RemoteAppPermissionUpdate": "Remote app permission update",
@@ -641,11 +615,11 @@
"Settings": "Settings",
"SystemUserCreate": "System user create",
"SystemUserDetail": "System user detail",
"SystemUserList": "System Users",
"SystemUserList": "System users",
"SystemUserUpdate": "System user update",
"TaskDetail": "Tasks detail",
"TaskList": "Tasks",
"TaskMonitor": "Task Monitor",
"TaskMonitor": "Task monitor",
"Terminal": "Terminal",
"TicketDetail": "Ticket detail",
"TicketCreate": "Ticket create",
@@ -655,7 +629,7 @@
"UserFirstLogin": "UserFirstLogin",
"UserGroupCreate": "User group create",
"UserGroupDetail": "User group detail",
"UserGroupList": "User Groups",
"UserGroupList": "User groups",
"UserGroupUpdate": "User group update",
"UserGuide": "UserGuide",
"UserList": "Users",
@@ -663,15 +637,9 @@
"UserUpdate": "User update",
"Users": "Users",
"WebFTP": "WebFTP",
"WebTerminal": "Web Terminal",
"Notifications": "Notifications",
"SiteMessageList": "Site message"
"WebTerminal": "Web terminal"
},
"sessions": {
"SetToDefaultStorage": "Set to default storage",
"SetToDefault": "Set to default",
"SetSuccess": "Set success",
"SetFailed": "Set failed",
"StorageConfiguration": "Storage configuration",
"accountKey": "Account key",
"accountName": "Account name",
@@ -732,11 +700,10 @@
"common": "common"
},
"Monitor": "Monitor",
"XRDPNotSupport": "RDP Client session not support now",
"sessionMonitor": "Session Monitor",
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later",
"helpText": {
"esUrl": "Tip: If you have multiple hosts, use comma (, ) to split (eg: http://www.jumpserver.a.com, http://www.jumpserver.b.com)",
"esUrl": "Tip: If you have multiple hosts, use comma (,) to split (eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com)",
"esIndex":"Es provides the default index: jumpserver",
"esDocType": "Es provides the default document type: command",
"s3Endpoint": "S3: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn<br>Example: http://s3.cn-north-1.amazonaws.com.cn",
@@ -744,7 +711,6 @@
}
},
"setting": {
"InsecureCommandNotifyToSubscription": "Insecure command notification setting, change to system message subscription, support more notify method",
"ApiKeyList": "Api key list",
"AssetCount": "Asset count",
"Basic": "Basic setting",
@@ -764,8 +730,6 @@
"Security": "Security setting",
"SecuritySetting": "Security setting",
"SubscriptionID": "Subscription ID",
"SystemMessageSubscription": "System messages",
"insecureCommandEmailUpdate": "Setting",
"Terminal": "Terminal setting",
"all": "All",
"authLdap": "Enable LDAP auth",
@@ -803,7 +767,7 @@
"ApiKeyList": "The API key is used to sign the request header. The header of each request is different. Please refer to the usage documentation",
"authLdapSearchFilter": "Choice may be (cn|uid|sAMAccountName)=%(user)s)",
"authLdapSearchOu": "Use | split User OUs",
"authLdapUserAttrMap": "User attr map present how to map LDAP user attr to jumpserver, username, name, email is jumpserver attr",
"authLdapUserAttrMap": "User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr",
"emailCustomUserCreatedBody": "Tips:When creating a user, send the content of the email",
"emailCustomUserCreatedHonorific": "Tips: When creating a user, send the honorific of the email (eg:Hello)",
"emailCustomUserCreatedSignature": "Tips: Email signature (eg:jumpserver)",
@@ -875,16 +839,12 @@
"refreshLdapCache":"Refreshing Ldap cache ",
"LicenseExpired": "License expired",
"LicenseWillBe": "License will expire at ",
"Expire": "",
"WeCom": "WeCom",
"DingTalk": "DingTalk",
"dingTalkTest": "Test",
"weComTest": "Test",
"Expire": ""
},
"settings": {
"setting": "Setting"
},
"tickets": {
"PermissionName": "Permission name",
"Accept": "Accept",
"AssignedMe": "Assigned me",
"Assignee": "Assignee",
@@ -926,13 +886,7 @@
"ips": "Enter the IP address group, separated by commas",
"fuzzySearch": "Support for fuzzy search",
"application": "Enter the application group, separated by commas"
},
"ApplyRunUser": "Apply run user",
"ApplyRunAsset": "Apply run asset",
"ApplyRunSystemUser": "Apply run system user",
"ApplyRunCommand": "Apply run command",
"ApplyFromSession": "Session",
"ApplyFromCMDFilterRule": "Command filter rule"
}
},
"tree": {
"AddAssetToNode": "Add asset to node",
@@ -948,7 +902,6 @@
"UpdateNodeAssetHardwareInfo": "Update node asset hardware information"
},
"users": {
"UserName": "Name",
"Account": "Account",
"Existing":"Existing",
"Authentication": "Account",
@@ -958,8 +911,6 @@
"DateJoined": "Date joined",
"DateLastLogin": "Date last login",
"DatePasswordLastUpdated": "Date password last updated",
"setWeCom": "Set wecom login",
"setDingTalk": "Set dingtalk login",
"DatePasswordUpdated": "Date password updated",
"DescribeOfGuide": "Welcome to JumpServer. Click here for more information",
"Email": "Email",
@@ -1019,11 +970,7 @@
"resetSSHKey": "Reset SSH key",
"resetSSHKeySuccessMsg": "An e-mail has been sent to the user`s mailbox",
"resetSSHKeyWarningMsg": "This will reset the user public key and send a reset mail",
"resetWechat": "Reset Wechat",
"resetWechatLoginWarningMsg": "This will reset the user Wechat setting, user can reset it",
"resetWechatLoginSuccessMsg": "Reset Wechat success",
"send": "Send",
"unbind": "Unbind",
"unblock": "Unblock",
"unblockSuccessMsg": "Account has unblocked",
"unblockUser": "Unblock login"
@@ -1041,28 +988,12 @@
"ApplicationPermissionRules": "Application permission rules",
"remoteAppPermissionRules": "Remote app permission rules"
},
"needUpdatePasswordNextLogin": "Update password next login",
"UpdatePassword": "Update password",
"SetPublicKey": "Set public key",
"UpdatePassword": "",
"UpdatePublicKey": "",
"passwordExpired": "Password expired",
"passwordWillExpiredPrefixMsg": "The password will expire in ",
"passwordWillExpiredSuffixMsg": " days.Please change your password as soon as possible."
},
"notifications": {
"MessageType": "Message Type",
"Receivers": "Receivers",
"Subscription": "Subscription",
"ChangeReceiver": "Change Receivers",
"Subject": "Subject",
"Message": "Message",
"DeliveryTime": "Delivery time",
"HasRead": "Has read",
"Sender": "Sender",
"MarkAsRead": "Mark as read",
"NoUnreadMsg": "No unread messages",
"SiteMessage": "Site messages"
},
"xpack": {
"Admin": "Admin",
"Asset": "Asset",
@@ -1074,7 +1005,7 @@
"Asset": "Asset",
"AssetAmount": "Asset",
"AssetAndNode": "Asset and Node",
"ChangeAuthPlan": "Change Auth Plan",
"ChangeAuthPlan": "Change auth plan",
"ChangeAuthPlanCreate": "Create change auth plan",
"ChangeAuthPlanUpdate": "Update change auth plan",
"CyclePerform": "Cycle perform",
@@ -1112,8 +1043,6 @@
"Cloud": {
"Aliyun": "Ali Cloud",
"Qcloud": "Tencent Cloud",
"QingyunPrivatecloud": "Qingyun Private Cloud",
"HuaweiPrivatecloud": "Huawei Private Cloud",
"AWS_China": "AWS(China)",
"AWS_Int": "AWS(International)",
"HuaweiCloud": "Huawei Cloud",
@@ -1126,7 +1055,7 @@
"AccountUpdate": "Update account",
"AccountDetail": "Account detail",
"Cloud": "Cloud center",
"CloudCenter": "Cloud Center",
"CloudCenter": "Cloud center",
"Provider": "Provider",
"Validity": "Validity",
"IsAlwaysUpdateHelpTips": "Whether the asset information, including Hostname, IP, Platform, and AdminUser, is updated synchronously each time a synchronization task is performed",
@@ -1167,18 +1096,16 @@
"Execute": "Execute",
"Expired": "Expired",
"GatherUser": {
"GatherUser": "Gather User",
"GatherUser": "Gather user",
"GatherUserList": "Gather user",
"GatherUserTaskCreate": "Create gather user task",
"GatherUserTaskList": "Gather user task list",
"GatherUserTaskUpdate": "Update gather user task",
"GatherUserTaskDetail": "Gather user detail",
"GatherUserTaskExecutionList": "Gather user task execution list"
"GatherUserTaskUpdate": "Update gather user task"
},
"Import": "Import",
"ImportLicense": "Import license",
"ImportLicenseTip": "Please Import License",
"InterfaceSettings": "Interface Setting",
"InterfaceSettings": "Interface setting",
"License": "License",
"LicenseDetail": "License detail",
"SystemMonitor": "System Monitor",

View File

@@ -15,7 +15,6 @@
</template>
<script>
import AutoDataForm from '@/components/AutoDataForm'
import { getUpdateObjURL } from '@/utils/common'
export default {
name: 'GenericCreateUpdateForm',
components: {
@@ -130,7 +129,7 @@ export default {
const params = this.$route.params
let url = this.url
if (params.id) {
url = getUpdateObjURL(url, params.id)
url = `${url}${params.id}/`
}
return url
}
@@ -283,7 +282,6 @@ export default {
if (object) {
object = _.cloneDeep(object)
this.$emit('update:object', object)
this.$emit('getObjectDone', object)
}
return object
},

View File

@@ -1,5 +1,5 @@
<template>
<ListTable ref="ListTable" v-bind="$attrs" v-on="$listeners" />
<ListTable ref="ListTable" v-bind="iAttrs" v-on="$listeners" />
</template>
<script>
@@ -11,15 +11,16 @@ export default {
ListTable
},
computed: {
...mapGetters(['currentOrgIsRoot'])
},
created() {
const headerActions = this.$attrs['header-actions'] || {}
if (headerActions.canCreate === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.canCreate', false)
}
if (headerActions.hasImport === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.hasImport', false)
...mapGetters(['currentOrgIsRoot']),
iAttrs() {
const attrs = _.cloneDeep(this.$attrs)
const canCreate = _.get(attrs, 'header-actions.canCreate', null)
this.$log.debug('Can create: ', canCreate)
if (canCreate === null && this.currentOrgIsRoot) {
_.set(attrs, 'header-actions.canCreate', false)
}
this.$log.debug('List table Attrs: ', attrs)
return attrs
}
}
}

View File

@@ -1,8 +1,11 @@
<template>
<Page v-bind="$attrs">
<Page>
<el-alert v-if="helpMessage" type="success"> {{ helpMessage }} </el-alert>
<TreeTable
ref="TreeTable"
v-bind="$attrs"
:table-config="tableConfig"
:header-actions="iHeaderActions"
:tree-setting="treeSetting"
v-on="$listeners"
>
<template #table>
@@ -24,16 +27,23 @@ export default {
components: {
Page, TreeTable
},
computed: {
...mapGetters(['currentOrgIsRoot'])
},
created() {
const headerActions = this.$attrs['header-actions'] || {}
if (headerActions.canCreate === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.canCreate', false)
props: {
...TreeTable.props,
helpMessage: {
type: String,
default: null
}
if (headerActions.hasImport === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.hasImport', false)
},
computed: {
...mapGetters(['currentOrg']),
iHeaderActions() {
const attrs = _.cloneDeep(this.headerActions)
const canCreate = _.get(attrs, 'canCreate', null)
// this.$log.debug('Current org: ', this.currentOrg)
if (canCreate === null && this.currentOrg && this.currentOrg.is_root) {
_.set(attrs, 'canCreate', false)
}
return attrs
}
},
methods: {

View File

@@ -2,10 +2,13 @@
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img :src="logoSrc" class="sidebar-logo">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }}</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img :src="logoTextSrc" class="sidebar-logo-text">
<img :src="logoSrc" class="sidebar-logo-text">
<!-- <img v-else-if="logoText" :src="logoText" class="sidebar-logo-text">-->
<!-- <h1 class="sidebar-title">{{ title }}</h1>-->
</router-link>
</transition>
</div>
@@ -23,6 +26,10 @@ export default {
},
data() {
return {
title: 'JumpServer',
logoText: require('@/assets/img/logo-text.png'),
logo: require('@/assets/img/logo.png'),
xpackData: {}
}
},
computed: {
@@ -30,14 +37,16 @@ export default {
'publicSettings'
]),
// eslint-disable-next-line vue/return-in-computed-property
logoTextSrc() {
return this.publicSettings.LOGO_URLS.logo_index
},
logoSrc() {
return this.publicSettings.LOGO_URLS.logo_logout
if (this.publicSettings.LOGO_URLS.logo_index !== '/static/img/logo_text.png') {
return this.publicSettings.LOGO_URLS.logo_index
} else {
return this.logoText
}
}
},
created() {
}
}
</script>

View File

@@ -15,7 +15,7 @@
<el-option
v-for="item in userAdminOrgList"
:key="item.id"
:selected="item.id === currentOrg.id"
selected="item.id == currentOrg.id"
:label="item.name"
:value="item.id"
/>
@@ -48,8 +48,10 @@ export default {
},
methods: {
needShow() {
const hasValidLicense = this.$store.getters.hasValidLicense
return !this.isCollapse && this.inAdminPage && hasValidLicense
const otherOrgs = this.userAdminOrgList.filter(org => {
return !org.is_root && !org.is_default
})
return !this.isCollapse && otherOrgs.length > 0 && this.inAdminPage
},
changeOrg(orgId) {
orgUtil.changeOrg(orgId)

View File

@@ -12,7 +12,7 @@
<script>
import Dialog from '@/components/Dialog'
import ListTable from '@/components/ListTable'
import { DateFormatter, ShowKeyFormatter } from '@/components/TableFormatters'
import { DateFormatter, ShowKeyFormatter } from '@/components/ListTable/formatters'
export default {
name: 'ApiKey',
components: {

View File

@@ -10,6 +10,7 @@
</template>
<script>
export default {
name: 'Language',
data() {
@@ -46,22 +47,11 @@ export default {
}
},
mounted() {
this.changeLang()
this.changeMomentLang()
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
},
methods: {
changeLang() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
},
changeMomentLang() {
if (this.currentLang.code.indexOf('en') > -1) {
this.$moment.locale('en')
} else {
this.$moment.locale('zh-cn')
}
},
changeLangTo(item) {
this.$i18n.locale = item.code
localStorage.setItem('lang', item.code)

View File

@@ -1,274 +0,0 @@
<template>
<div>
<el-badge :value="unreadMsgCount" :hidden="unreadMsgCount === 0" :max="99" size="mini" type="primary">
<a style="color: #606266 !important; width: 30px" @click="toggleDrawer">
<i class="el-icon-message" style="font-size: 18px" />
</a>
</el-badge>
<el-drawer
:visible.sync="show"
:before-close="handleClose"
:modal="false"
:title="$t('notifications.SiteMessage')"
custom-class="site-msg"
size="25%"
@open="getMessages"
>
<div v-if="unreadMsgCount !== 0" class="msg-list">
<div
v-for="msg of messages"
:key="msg.id"
class="msg-item"
:class="msg.has_read ? 'msg-read' : 'msg-unread'"
@mouseover="hoverMsgId = msg.id"
@mouseleave="hoverMsgId = ''"
@click="showMsgDetail(msg)"
>
<div class="msg-item-head">
<span class="msg-item-head-type">
<i :class="msg.has_read ? 'fa-envelope-open-o' : 'fa-envelope'" class="fa msg-icon" />
{{ msg.subject }}
</span>
<span v-if="hoverMsgId !== msg.id || msg.has_read" class="msg-item-head-time">
{{ formatDate(msg.date_created) }}
</span>
<div v-else class="msg-item-read-btn" @click.stop="markAsRead(msg)">
<a>{{ $t('notifications.MarkAsRead') }}</a>
</div>
</div>
<div class="msg-item-txt">
<span v-html="msg.message" />
</div>
</div>
</div>
<div v-else class="no-msg">
{{ $t('notifications.NoUnreadMsg') }}
</div>
</el-drawer>
<Dialog
v-if="msgDetailVisible"
:visible.sync="msgDetailVisible"
:title="''"
:close-on-click-modal="false"
:confirm-title="$t('notifications.MarkAsRead')"
@confirm="markAsRead(currentMsg)"
@cancel="cancelRead"
>
<div class="msg-detail">
<div class="msg-detail-head">
<h3>{{ currentMsg.subject }}</h3>
<h5>
<span class="msg-detail-time">{{ formatDate(currentMsg.date_created) }}</span>
</h5>
</div>
<div class="msg-detail-txt">
<span v-html="currentMsg.message" />
</div>
</div>
</Dialog>
</div>
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import Dialog from '@/components/Dialog'
export default {
name: 'SiteMessages',
components: { Dialog },
data() {
return {
show: false,
messages: [],
hoverMsgId: '',
msgDetailVisible: false,
currentMsg: null,
unreadMsgCount: 0
}
},
mounted() {
this.enablePullMsgCount()
},
methods: {
handleClose() {
this.show = false
},
toggleDrawer() {
this.show = !this.show
},
showMsgDetail(msg) {
this.currentMsg = msg
this.msgDetailVisible = true
},
getMessages() {
const url = '/api/v1/notifications/site-message/?offset=0&limit=15&has_read=false'
this.$axios.get(url).then(resp => {
this.messages = [...resp.results]
this.unreadMsgCount = resp.count
})
},
formatDate(s) {
if (!s) {
return ''
}
const d = new Date(s)
const now = new Date()
if (now.getTime() - d.getTime() > (3600 * 24 * 7) * 1000) {
return toSafeLocalDateStr(s)
} else {
return this.$moment(d).fromNow()
}
},
markAsRead(msg) {
const url = `/api/v1/notifications/site-message/mark-as-read/`
this.$axios.patch(url, { ids: [msg.id] }).then(res => {
this.msgDetailVisible = false
this.getMessages()
}).catch(err => {
this.$message(err.detail)
})
},
cancelRead() {
this.msgDetailVisible = false
},
enablePullMsgCount() {
const scheme = document.location.protocol === 'https:' ? 'wss' : 'ws'
const port = document.location.port ? ':' + document.location.port : ''
const url = '/ws/notifications/site-msg/'
const wsURL = scheme + '://' + document.location.hostname + port + url
const ws = new WebSocket(wsURL)
ws.onopen = (event) => {
this.$log.debug('Websocket connected: ', event)
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.$log.debug('Data: ', data)
const unreadCount = data['unread_count']
if (unreadCount !== undefined) {
this.unreadMsgCount = unreadCount
}
} catch (e) {
this.$log.debug('Recv site message error')
}
}
ws.onerror = (error) => {
this.$message.error(this.$t('common.ConnectWebSocketError'))
this.$log.debug('site message ws error: ', error)
}
}
}
}
</script>
<style lang="scss" scoped>
.el-badge ::v-deep .el-badge__content.is-fixed{
top:10px;
}
.msg-list {
padding: 0 25px 20px;
}
>>> .site-msg {
.el-drawer__header {
border-bottom: solid 1px rgb(231, 234, 239);
margin-bottom: 0;
padding-top: 10px;
font-size: 16px;
}
.el-drawer__body {
overflow-y: auto;
}
}
.msg-item {
border-bottom: solid 1px rgb(231, 234, 239);
padding: 15px 0 10px;
position: relative;
border-bottom: 1px solid #ddd;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
padding: 15px 20px 10px;
margin: 0 -20px;
border-bottom: 1px solid #fff;
}
.msg-icon {
font-size: 13px;
line-height: 13px;
}
&.msg-unread {
.msg-item-txt {
font-weight: bolder;
}
}
}
.msg-item-head {
line-height: 20px;
color: #888;
font-size: 12px;
&:after {
clear: both;
content: ".";
display: block;
height: 0;
overflow: hidden;
}
.msg-item-head-type {
float: left;
width: 240px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.msg-item-head-time {
float: right;
}
.msg-item-read-btn {
float: right;
}
}
.msg-item-txt {
overflow: hidden;
color: #000;
padding: 4px 0 0;
line-height: 21px;
max-height: 21px;
display: -webkit-box;
font-size: 12px;
display: block;
}
.msg-detail {
padding-left: 20px;
.msg-detail-time {
font-weight: 400;
font-size: 12px;
line-height: 1.1;
}
.msg-detail-txt {
margin-bottom: 20px;
line-height: 25px;
}
}
.no-msg {
padding-top: 20px;
text-align: center;
}
>>> :focus{ outline:0; }
</style>

View File

@@ -3,26 +3,30 @@
<div class="navbar-header">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
</div>
<ul class="navbar-right">
<li class="header-item header-icon">
<SiteMessages />
</li>
<li class="header-item" style="margin-left: 10px">
<div class="navbar-right">
<div class="header-item">
<Help />
</li>
<li class="header-item">
</div>
<div class="header-item">
<Language />
</li>
<li v-if="showTickets" class="header-item">
</div>
<div
v-if="
publicSettings.TICKETS_ENABLED
&& publicSettings.XPACK_LICENSE_IS_VALID
&& !isOrgAuditor
"
class="header-item"
>
<Tickets />
</li>
<li class="header-item">
</div>
<div class="header-item">
<WebTerminal />
</li>
<li class="header-item header-profile">
</div>
<div class="header-item header-profile">
<AccountDropdown />
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -30,7 +34,6 @@
import { mapGetters } from 'vuex'
import Hamburger from '@/components/Hamburger'
import AccountDropdown from './AccountDropdown'
import SiteMessages from './SiteMessages'
import Help from './Help'
import Language from './Language'
import WebTerminal from './WebTerminal'
@@ -45,8 +48,7 @@ export default {
Language,
Help,
Tickets,
WebTerminal,
SiteMessages
WebTerminal
},
data() {
return {
@@ -58,17 +60,13 @@ export default {
]),
isOrgAuditor() {
return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor')
},
showTickets() {
return this.publicSettings.TICKETS_ENABLED &&
this.publicSettings.XPACK_LICENSE_IS_VALID &&
!this.isOrgAuditor
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
}
}
}
</script>
@@ -93,19 +91,12 @@ export default {
.navbar-right {
float: right;
margin-right: 10px;
}
.header-item {
line-height: 50px;
display: inline-block;
padding-right: 10px;
padding-left: 10px;
}
.header-icon {
&:hover {
background-color: #e6e6e6;
}
}
.header-item {
line-height: 50px;
display: inline-block;
padding-right: 20px;
}
.breadcrumb-container {
@@ -117,9 +108,5 @@ export default {
.el-header {
background-color: #ffffff;
}
ul {
margin: 0;
}
</style>

View File

@@ -10,7 +10,7 @@
<div>
<el-tabs v-if="submenu.length > 0" slot="submenu" v-model="iActiveMenu" class="page-submenu" @tab-click="handleTabClick">
<template v-for="item in submenu">
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name" :disabled="item.disabled">
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name">
<span slot="label">
{{ item.title }}
<slot name="badge" :tab="item.name" />
@@ -80,23 +80,22 @@ export default {
},
getPropActiveTab() {
let activeTab = ''
let tabObj = null
const preActiveTabs = [
const activeTabs = [
this.$route.query[ACTIVE_TAB_KEY],
this.$cookie.get(ACTIVE_TAB_KEY),
this.activeMenu
]
for (const preTab of preActiveTabs) {
for (const tabName in this.tabIndices) {
if (preTab && tabName && preTab.toLowerCase() === tabName.toLowerCase()) {
return tabName
}
for (activeTab of activeTabs) {
tabObj = this.tabIndices[activeTab]
if (tabObj !== undefined) {
return activeTab
}
}
activeTab = this.submenu[0].name
return activeTab
return this.submenu[0].name
}
}
}

View File

@@ -7,7 +7,6 @@ export { default as GenericDetailPage } from './GenericDetailPage'
export { default as TabPage } from './TabPage'
export { default as Footer } from './Footer'
export { default as GenericListPage } from './GenericListPage'
export { default as GenericListTable } from './GenericListTable'
export { default as GenericCreateUpdatePage } from './GenericCreateUpdatePage'
export { default as GenericCreateUpdateForm } from './GenericCreateUpdateForm'
export { default as GenericUpdateFormDialog } from './GenericUpdateFormDialog'

View File

@@ -40,13 +40,8 @@ Vue.config.productionTip = false
import VueCookie from 'vue-cookie'
Vue.use(VueCookie)
window.$cookie = VueCookie
const moment = require('moment')
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
moment
})
import VueMoment from 'vue-moment'
Vue.use(VueMoment)
// logger
import VueLogger from 'vuejs-logger'
import loggerOptions from './utils/logger'

View File

@@ -1,120 +0,0 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'asset-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'AssetAccountList',
component: () => import('@/views/accounts/AssetAccount/AssetAccountList'),
meta: { title: i18n.t('route.AssetAccount') }
}
]
},
{
path: 'application-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'ApplicationAccountList',
component: () => import('@/views/accounts/ApplicationAccount/ApplicationAccountList'),
meta: { title: i18n.t('route.ApplicationAccount') }
}
]
},
{
path: 'gathered-user',
component: empty,
redirect: '',
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
children: [
{
path: '',
component: () => import('@/views/accounts/GatheredUser/index'),
name: 'GatherUserListIndex',
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/accounts/gathered-user' }
},
{
path: '',
component: () => import('@/views/accounts/GatheredUser/GatheredUserList'),
name: 'GatherUserList',
hidden: true,
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/accounts/gathered-user' }
},
{
path: 'tasks',
component: () => import('@/views/accounts/GatheredUser/TaskList'),
name: 'GatherUserTaskList',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id',
component: () => import('@/views/accounts/GatheredUser/TaskDetail/index'),
name: 'GatherUserTaskDetail',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/create',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskCreate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id/update',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskUpdate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/accounts/gathered-user' },
hidden: true
}
]
},
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
children: [
{
path: 'plan',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' }
},
{
path: 'plan/create',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'create' },
hidden: true
},
{
path: 'plan/:id/update',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'update' },
hidden: true
},
{
path: 'plan/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'ChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
}
]
}
]

View File

@@ -101,6 +101,41 @@ export default [
}
]
},
{
path: 'admin-users',
component: empty,
redirect: '',
meta: { title: i18n.t('route.AdminUserList') },
children: [
{
path: '',
name: 'AdminUserList',
component: () => import('@/views/assets/AdminUser/AdminUserList'),
meta: { title: i18n.t('route.AdminUserList'), activeMenu: '/assets/admin-users' }
},
{
path: 'create',
component: () => import('@/views/assets/AdminUser/AdminUserCreateUpdate.vue'), // Parent router-view
name: 'AdminUserCreate',
meta: { title: i18n.t('route.AdminUserCreate'), activeMenu: '/assets/admin-users' },
hidden: true
},
{
path: ':id/update',
component: () => import('@/views/assets/AdminUser/AdminUserCreateUpdate.vue'), // Parent router-view
name: 'AdminUserUpdate',
meta: { title: i18n.t('route.AdminUserUpdate'), activeMenu: '/assets/admin-users' },
hidden: true
},
{
path: ':id',
component: () => import('@/views/assets/AdminUser/AdminUserDetail/index.vue'), // Parent router-view
name: 'AdminUserDetail',
meta: { title: i18n.t('route.AdminUserDetail'), activeMenu: '/assets/admin-users' },
hidden: true
}
]
},
{
path: 'system-users',
component: empty,
@@ -110,20 +145,20 @@ export default [
{
path: '',
name: 'SystemUserList',
component: () => import('@/views/assets/SystemUser/SystemUserList/index.vue'),
component: () => import('@/views/assets/SystemUser/SystemUserList.vue'),
meta: { title: i18n.t('route.SystemUserList'), activeMenu: '/assets/system-users' }
},
{
path: 'create',
name: 'SystemUserCreate',
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate/index.vue'),
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate.vue'),
meta: { title: i18n.t('route.SystemUserCreate'), activeMenu: '/assets/system-users' },
hidden: true
},
{
path: ':id/update',
name: 'SystemUserUpdate',
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate/index.vue'),
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate.vue'),
meta: { title: i18n.t('route.SystemUserUpdate'), activeMenu: '/assets/system-users' },
hidden: true
},

View File

@@ -37,7 +37,6 @@ import TicketsRoutes from './tickets'
import AuditsRoutes from './audits'
import commonRoutes from './common'
import aclRoutes from './acl'
import AccountRoutes from './accounts'
/**
* constantRoutes
@@ -111,18 +110,6 @@ export const allRoleRoutes = [
meta: { title: i18n.t('route.Applications'), icon: 'th' },
children: ApplicationsRoute
},
{
path: '/accounts',
component: Layout,
redirect: '/accounts/asset-accounts/',
name: 'Accounts',
meta: {
licenseRequired: true,
title: i18n.t('route.Accounts'),
icon: 'address-book'
},
children: AccountRoutes
},
{
path: '/perms/',
component: Layout,

View File

@@ -48,12 +48,5 @@ export default [
component: () => import('@/views/tickets/RequestApplicationPerm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'tickets/command-confirm/:id',
name: 'CommandConfirmDetail',
component: () => import('@/views/tickets/CommandConfirm/Detail/index'),
meta: { title: i18n.t('route.CommandConfirm'), activeMenu: '/tickets/tickets' },
hidden: true
}
]

View File

@@ -65,7 +65,7 @@ export default [
path: '',
name: 'CommandExecutions',
component: () => import('@/views/ops/CommandExecution'),
meta: { title: i18n.t('route.BatchCommand'), icon: 'terminal', permissions: [rolec.PERM_USE] }
meta: { title: i18n.t('route.CommandExecutions'), icon: 'terminal', permissions: [rolec.PERM_USE] }
}
]
},
@@ -124,13 +124,6 @@ export default [
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_USE] },
hidden: true
},
{
path: 'tickets/command-confirm/:id',
name: 'CommandConfirmDetail',
component: () => import('@/views/tickets/CommandConfirm/Detail/index'),
meta: { title: i18n.t('route.CommandConfirm'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_USE] },
hidden: true
},
{
path: 'tickets/:id',
name: 'TicketDetail',
@@ -162,7 +155,7 @@ export default [
children: [
{
path: `${BASE_URL}/koko/elfinder/sftp/`,
meta: { title: i18n.t('route.FileManager'), icon: 'file', activeMenu: '/assets', permissions: [rolec.PERM_USE] }
meta: { title: i18n.t('route.WebFTP'), icon: 'file', activeMenu: '/assets', permissions: [rolec.PERM_USE] }
}
]
}

View File

@@ -1,5 +1,6 @@
module.exports = {
title: '.',
title: 'JumpServer',
/**
* @type {boolean} true | false

View File

@@ -35,18 +35,15 @@ const actions = {
getPublicSettings({ commit, state }) {
return new Promise((resolve, reject) => {
getPublicSettings().then(response => {
const faviconURL = response.data.LOGO_URLS.favicon
let link = document.querySelector("link[rel*='icon']")
if (!link) {
link = document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
document.getElementsByTagName('head')[0].appendChild(link)
}
link.href = faviconURL
const link = document.querySelector("link[rel*='icon']") || document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
link.href = response.data.LOGO_URLS.favicon
document.getElementsByTagName('head')[0].appendChild(link)
// 动态修改Title
document.title = response.data.LOGIN_TITLE
if (response.data.LOGIN_TITLE) { document.title = response.data.LOGIN_TITLE }
commit('SET_PUBLIC_SETTINGS', response.data)
resolve(response)
}).catch(error => {

View File

@@ -20,9 +20,7 @@ const getDefaultState = () => {
orgs: [],
perms: 0b00000000,
MFAVerifyAt: null,
isSuperAdmin: false,
hasAdminPerm: false,
hasAuditPerm: false
isSuperAdmin: false
}
}
@@ -55,15 +53,12 @@ const mutations = {
},
SET_ROLES(state, roles) {
state.roles = roles
// rolec.PERM_ADMIN &
},
SET_SYS_ROLE(state, role) {
state.sysRole = role
},
SET_PERMS(state, perms) {
state.perms = perms
state.hasAdmin = (perms & rolec.PERM_ADMIN) === rolec.PERM_ADMIN
state.hasAudit = (perms & rolec.PERM_AUDIT) === rolec.PERM_AUDIT
},
SET_CURRENT_ORG(state, org) {
saveCurrentOrgToCookie(org)

View File

@@ -8,7 +8,7 @@
<script>
import ListTable from '@/components/ListTable/index'
import Page from '@/layout/components/Page/index'
import { ActionsFormatter } from '@/components/TableFormatters'
import { ActionsFormatter } from '@/components/ListTable/formatters'
export default {
name: 'DatabaseApp',

View File

@@ -8,7 +8,7 @@
<script>
import ListTable from '@/components/ListTable/index'
import Page from '@/layout/components/Page/index'
import { ActionsFormatter } from '@/components/TableFormatters'
import { ActionsFormatter } from '@/components/ListTable/formatters'
export default {
name: 'KubernetesApp',

View File

@@ -7,7 +7,7 @@
<script>
import ListTable from '@/components/ListTable/index'
import Page from '@/layout/components/Page/index'
import { ActionsFormatter } from '@/components/TableFormatters'
import { ActionsFormatter } from '@/components/ListTable/formatters'
export default {
name: 'RemoteApp',

View File

@@ -4,7 +4,7 @@
<script>
import GenericTreeListPage from '@/layout/components/GenericTreeListPage/index'
import { ActionsFormatter, SystemUserFormatter, DialogDetailFormatter } from '@/components/TableFormatters'
import { LoadingActionsFormatter, SystemUserFormatter, DialogDetailFormatter } from '@/components/ListTable/formatters'
export default {
components: {
GenericTreeListPage
@@ -104,7 +104,7 @@ export default {
{
prop: 'id',
align: 'center',
formatter: ActionsFormatter,
formatter: LoadingActionsFormatter,
width: '100px',
label: this.$t('common.action'),
formatterArgs: {
@@ -117,7 +117,7 @@ export default {
name: 'connect',
fa: 'fa-terminal',
type: 'primary',
can: function({ row, cellValue }) {
can: (row, cellValue) => {
return row.is_active
},
callback: function({ row, col, cellValue, reload }) {
@@ -127,7 +127,7 @@ export default {
{
name: 'favor',
type: 'info',
fa: function({ row, cellValue }) {
fa: function(row, cellValue) {
if (this.checkFavorite(row.id)) {
return 'fa-star'
}
@@ -160,25 +160,20 @@ export default {
},
methods: {
refreshAllFavorites() {
const actionsIndex = this.tableConfig.columns.length - 1
this.tableConfig.columns[actionsIndex].formatterArgs.loading = true
this.tableConfig.columns[this.tableConfig.columns.length - 1].formatterArgs.loading = true
this.$axios.get('/api/v1/assets/favorite-assets/').then(resp => {
this.allFavorites = resp
this.tableConfig.columns[actionsIndex].formatterArgs.loading = false
this.tableConfig.columns[this.tableConfig.columns.length - 1].formatterArgs.loading = false
})
},
addOrDeleteFavorite(assetId) {
if (this.checkFavorite(assetId)) {
this.$axios.delete(`/api/v1/assets/favorite-assets/?asset=${assetId}`).then(
res => this.removeFavorite(assetId)
)
this.$axios.delete(`/api/v1/assets/favorite-assets/?asset=${assetId}`).then(res => this.removeFavorite(assetId))
} else {
const data = {
asset: assetId
}
this.$axios.post('/api/v1/assets/favorite-assets/', data).then(
res => this.addFavorite(assetId)
)
this.$axios.post('/api/v1/assets/favorite-assets/', data).then(res => this.addFavorite(assetId))
}
},
checkFavorite(assetId) {

View File

@@ -14,9 +14,8 @@
<script>
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
import UserPassword from '@/components/FormFields/UserPassword'
import UserPassword from '@/components/UserPassword'
import { IBox } from '@/components'
import rules from '@/components/DataForm/rules'
export default {
name: 'PasswordUpdate',
@@ -43,8 +42,8 @@ export default {
},
new_password: {
label: this.$t('users.NewPassword'),
rules: [rules.RequiredChange],
component: UserPassword
component: UserPassword,
rules: []
},
new_password_again: {
label: this.$t('users.ConfirmPassword'),

View File

@@ -1,49 +1,23 @@
<template>
<div>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<QuickActions type="primary" :actions="quickActions" />
</el-col>
</el-row>
<Dialog
width="50"
top="20vh"
:title="this.$t('common.PasswordConfirm')"
:visible.sync="showPasswordDialog"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
>
<el-row :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">{{ $t('assets.Password') }}</div>
</el-col>
<el-col :span="14">
<el-input v-model="passwordInput" type="password" />
<span class="help-tips help-block">{{ $t('common.PasswordRequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="passConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
</div>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<QuickActions type="primary" :actions="quickActions" />
</el-col>
</el-row>
</template>
<script type="text/jsx">
import DetailCard from '@/components/DetailCard'
import QuickActions from '@/components/QuickActions'
import Dialog from '@/components/Dialog'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'ProfileInfo',
components: {
DetailCard,
QuickActions,
Dialog
QuickActions
},
props: {
object: {
@@ -54,40 +28,7 @@ export default {
data() {
return {
url: `/api/v1/users/profile/`,
showPasswordDialog: false,
passwordInput: '',
currentEdit: '',
quickActions: [
{
title: this.$t('users.setWeCom'),
attrs: {
type: 'primary',
label: this.$store.state.users.profile.is_wecom_bound ? this.$t('common.unbind') : this.$t('common.bind'),
disabled: this.$store.state.users.profile.source !== 'local'
},
has: this.$store.getters.publicSettings.AUTH_WECOM,
callbacks: {
click: function() {
this.currentEdit = 'wecom'
this.showPasswordDialog = true
}.bind(this)
}
},
{
title: this.$t('users.setDingTalk'),
attrs: {
type: 'primary',
label: this.$store.state.users.profile.is_dingtalk_bound ? this.$t('common.unbind') : this.$t('common.bind'),
disabled: this.$store.state.users.profile.source !== 'local'
},
has: this.$store.getters.publicSettings.AUTH_DINGTALK,
callbacks: {
click: function() {
this.currentEdit = 'dingtalk'
this.showPasswordDialog = true
}.bind(this)
}
},
{
title: this.$t('users.SetMFA'),
attrs: {
@@ -138,7 +79,7 @@ export default {
attrs: {
type: 'primary',
label: this.$t('common.Update'),
disabled: !this.$store.state.users.profile.can_public_key_auth
disabled: this.$store.state.users.profile.source !== 'local'
},
callbacks: {
click: function() {
@@ -150,8 +91,7 @@ export default {
title: this.$t('users.ResetPublicKeyAndDownload'),
attrs: {
type: 'primary',
label: this.$t('common.Reset'),
disabled: !this.$store.state.users.profile.can_public_key_auth
label: this.$t('common.Reset')
},
callbacks: {
click: function() {
@@ -230,24 +170,6 @@ export default {
}
},
methods: {
passConfirm() {
this.$axios.post(
`/api/v1/authentication/password/verify/`, {
password: this.passwordInput
}
).then(res => {
if (!this.object[`is_${this.currentEdit}_bound`]) {
window.location.href = `/core/auth/${this.currentEdit}/qr/bind/?redirect_url=${this.$route.fullPath}`
} else {
this.$axios.post(`/api/v1/authentication/${this.currentEdit}/qr/unbind/`).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
this.$store.dispatch('users/getProfile')
})
}
})
this.passwordInput = ''
this.showPasswordDialog = false
}
}
}
</script>

View File

@@ -38,7 +38,7 @@ export default {
],
fieldsMeta: {
public_key_comment: {
label: this.$t('common.Name'),
label: this.$t('users.Name'),
disabled: true
},
public_key_hash_md5: {

View File

@@ -37,7 +37,7 @@ export default {
},
methods: {
getSubmenu() {
return [
let submenu = [
{
title: this.$t('common.BasicInfo'),
name: 'ProfileInfo'
@@ -45,18 +45,21 @@ export default {
{
title: this.$t('users.ProfileSetting'),
name: 'ProfileUpdate'
},
{
title: this.$t('users.LoginPasswordSetting'),
name: 'PasswordUpdate',
disabled: this.$store.state.users.profile.source !== 'local'
},
{
title: this.$t('users.SSHKeySetting'),
name: 'SSHUpdate',
disabled: !this.$store.state.users.profile.can_public_key_auth
}
]
if (this.$store.state.users.profile.source === 'local') {
submenu = submenu.concat([
{
title: this.$t('users.LoginPasswordSetting'),
name: 'PasswordUpdate'
},
{
title: this.$t('users.SSHKeySetting'),
name: 'SSHUpdate'
}
])
}
return submenu
},
handleUpdate(value) {
this.config.activeMenu = value

View File

@@ -88,14 +88,11 @@ export function toSafeLocalDateStr(d) {
}
export function getApiPath(that) {
let pagePath = that.$route.path
const pagePathArray = pagePath.split('/')
if (pagePathArray.indexOf('orgs') !== -1) {
pagePathArray[pagePathArray.indexOf('xpack')] = 'orgs'
} else if (pagePathArray.indexOf('gathered-user') !== -1 || pagePathArray.indexOf('change-auth-plan') !== -1) {
pagePathArray[pagePathArray.indexOf('accounts')] = 'xpack'
const pagePath = that.$route.path
const isOrgPath = pagePath.split('/').indexOf('orgs') !== -1
if (isOrgPath) {
return `/api/v1/orgs/orgs/${pagePath.split('/').pop()}/`
}
pagePath = pagePathArray.join('/')
return `/api/v1${pagePath}/`
}
@@ -209,27 +206,6 @@ function customizer(objValue, srcValue) {
return _.isUndefined(objValue) ? srcValue : objValue
}
export function newURL(url) {
let obj
if (url.indexOf('//') > -1) {
obj = new URL(url)
} else {
obj = new URL(url, location.origin)
}
return obj
}
export function getUpdateObjURL(url, objId) {
const urlObj = new URL(url, location.origin)
let pathname = urlObj.pathname
if (!pathname.endsWith('/')) {
pathname += '/'
}
pathname += `${objId}/`
urlObj.pathname = pathname
return urlObj.href
}
export const assignIfNot = _.partialRight(_.assignInWith, customizer)
const scheme = document.location.protocol

View File

@@ -80,11 +80,6 @@ export function hasUserPagePerm(userPerm) {
}
export function hasPerm(source, target) {
if (target === null) {
return true
} else if (source === null) {
return false
}
if (typeof source !== 'object') {
source = [source]
}

View File

@@ -1,4 +1,4 @@
// import getPageTitle from '@/utils/get-page-title'
import getPageTitle from '@/utils/get-page-title'
import store from '@/store'
import router from '@/router'
import { Message } from 'element-ui'
@@ -15,9 +15,9 @@ function reject(msg) {
return new Promise((resolve, reject) => reject(msg))
}
// function setHeadTitle({ to, from, next }) {
// document.title = getPageTitle(to.meta.title)
// }
function setHeadTitle({ to, from, next }) {
document.title = getPageTitle(to.meta.title)
}
async function checkLogin({ to, from, next }) {
if (whiteList.indexOf(to.path) !== -1) {
@@ -145,7 +145,7 @@ export async function startup({ to, from, next }) {
// set page title
await getPublicSetting({ to, from, next })
// await setHeadTitle({ to, from, next })
await setHeadTitle({ to, from, next })
await checkLogin({ to, from, next })
await changeCurrentOrgIfNeed({ to, from, next })
await changeCurrentRoleIfNeed({ to, from, next })

View File

@@ -1,401 +0,0 @@
<template>
<Page>
<el-row>
<el-col :span="11">
<GenericListTable ref="LeftTable" class="application-table" :header-actions="leftTable.headerActions" :table-config="leftTable.tableConfig" @row-click="leftTable.tableConfig.rowClick" />
</el-col>
<el-col :span="13">
<GenericListTable v-if="!isInit" ref="RightTable" class="application-user-table" :header-actions="rightTable.headerActions" :table-config="rightTable.tableConfig" />
<div v-else class="noDataR">
<div class="hintWrap">
<div>{{ $t('accounts.PleaseClickLeftApplicationToViewApplicationAccount') }}</div>
</div>
</div>
</el-col>
</el-row>
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<el-form-item :label="this.$t('assets.Applications')">
<el-input v-model="MFAInfo.application" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.PasswordOrToken')">
<el-input v-model="MFAInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog :title="$t('common.Export')" :visible.sync="showExportDialog" :destroy-on-close="true" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</Dialog>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
import Dialog from '@/components/Dialog'
import { mapGetters } from 'vuex'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
export default {
name: 'AssetAccountList',
components: {
GenericListTable, Page, Dialog
},
data() {
const vm = this
return {
isInit: true,
showMFADialog: false,
MFAConfirmed: false,
MFAInput: '',
MFAInfo: {
systemUser: '',
application: '',
username: '',
password: ''
},
selectedRows: '',
showExportDialog: false,
dialogStatus: '',
exportOption: 'all',
exportTypeOption: 'csv',
clickedRow: {},
leftTable: {
tableConfig: {
url: '/api/v1/applications/applications/',
columns: [
'name', 'category_display', 'type_display', 'comment', 'org_name'
],
columnsShow: {
min: ['name'],
default: ['name', 'category_display', 'type_display']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row, col, cellValue }) {
return {
'db': 'DatabaseAppDetail', 'remote_app': 'RemoteAppDetail', 'cloud': 'KubernetesAppDetail'
}[row.category]
}
},
showOverflowTooltip: true,
sortable: false
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/applications/application-users/?application_id=${row.id}`
vm.rightTable.tableConfig.extraQuery.application_id = row.id
vm.clickedRow = row
vm.MFAInfo.application = row.name
vm.isInit = false
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/applications/application-users/?application_id=`,
extraQuery: {
application_id: ''
},
columns: [
'name', 'username', 'username_same_with_user', 'protocol', 'login_mode', 'priority', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'username', 'actions'],
default: ['name', 'username', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'SystemUserDetail'
},
showOverflowTooltip: true,
sortable: false
},
protocol: {
sortable: false
},
login_mode: {
sortable: false
},
actions: {
label: this.$t('common.Action'),
align: 'center',
width: 150,
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: false,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function(val) {
this.dialogStatus = 'viewAuthInfo'
this.MFAInfo.systemUser = val.row
if (!this.needMFAVerify) {
this.showMFADialog = true
this.MFAConfirmed = true
this.$axios.get(`/api/v1/assets/system-users/${this.MFAInfo.systemUser.id}/auth-info/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.username = res.username
if (res.protocol === 'k8s') {
this.MFAInfo.password = res.token
} else {
this.MFAInfo.password = res.password
}
})
} else {
this.showMFADialog = true
}
}.bind(this)
}
]
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
}
},
headerActions: {
hasLeftActions: false,
hasImport: false,
handleExport({ selectedRows }) {
vm.selectedRows = selectedRows
vm.dialogStatus = 'export'
if (!vm.needMFAVerify) {
vm.showMFADialog = false
vm.showExportDialog = true
} else {
vm.showMFADialog = true
}
}
}
}
}
},
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
return false
}
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
exportOptions() {
return [
{
label: this.$t('common.imExport.ExportAll'),
value: 'all',
can: true
},
{
label: this.$t('common.imExport.ExportOnlySelectedItems'),
value: 'selected',
can: this.selectedRows.length > 0
}
]
},
exportTypeOptions() {
return [
{
label: 'CSV',
value: 'csv',
can: true
},
{
label: 'Excel',
value: 'xlsx',
can: true
}
]
}
},
methods: {
MFAConfirm() {
if (this.MFAInput.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAInput
}
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
if (this.dialogStatus === 'export') {
this.showMFADialog = false
this.showExportDialog = true
} else {
this.$axios.get(`/api/v1/assets/system-users/${this.MFAInfo.systemUser.id}/auth-info/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.username = res.username
this.MFAInfo.password = res.password
})
}
}
)
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
},
async performExport(selectRows, exportOption, q) {
const url = `/api/v1/applications/application-user-auth-infos/`
const query = Object.assign({}, q)
if (exportOption === 'selected') {
const resources = []
const data = selectRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
query['spm'] = spm.spm
} else if (exportOption === 'filtered') {
// console.log(listTableRef)
// console.log(listTableRef.dataTable)
// delete query['limit']
// delete query['offset']
}
query['format'] = this.exportTypeOption
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
return this.downloadCsv(url + queryStr)
},
async performExportConfirm() {
const listTableRef = this.$refs.RightTable.$refs.ListTable
const query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
return this.performExport(this.selectedRows, this.exportOption, query)
},
async handleExportConfirm() {
await this.performExportConfirm()
this.showExportDialog = false
},
handleExportCancel() {
this.showExportDialog = false
}
}
}
</script>
<style lang="scss" scoped>
.application-table ::v-deep .row-clicked, .application-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.application-table {
&:hover {
cursor: pointer;
}
& ::v-deep .el-table__row{
height: 40px;
& > td{
padding: 0;
}
}
}
.export-item {
width: 100%;
display: block;
padding: 10px 20px;
}
.export-form >>> .el-form-item__label {
line-height: 2
}
.application-user-table{
padding-left:20px ;
}
.noDataR {
width: 100%;
height: 40vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-direction: column;
.hintWrap {
color: #D4D6E6;
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
}
}
</style>

View File

@@ -1,185 +0,0 @@
<template>
<Page>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?9:11">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" />
<i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
v-bind="leftTable"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?11:13">
<AccountListTable
ref="RightTable"
class="asset-user-table"
v-bind="rightTable"
/>
</el-col>
</el-row>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { AccountListTable } from '@/components'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable, Page, AccountListTable
},
data() {
const vm = this
return {
isInit: true,
clickedRow: null,
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
vm.isInit = true
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'protocols', 'platform', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip'],
default: ['hostname', 'ip', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
ip: {
showOverflowTooltip: true
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.url = `/api/v1/assets/accounts/?asset=${row.id}`
vm.rightTable.extraQuery.asset = row.id
vm.clickedRow = row
vm.isInit = false
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasColumnSetting: true,
hasRefresh: true,
hasBulkUpdate: false
}
},
rightTable: {
url: `/api/v1/assets/accounts/`,
name: 'AssetAccountListTable',
extraQuery: {
latest: 1
},
hasLeftActions: false,
searchExclude: ['hostname', 'id', 'ip']
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.asset-table {
&:hover {
cursor: pointer;
}
& >>> .table-content {
margin-left: 21px;
}
& ::v-deep .el-table__row{
height: 40px;
& > td{
padding: 0;
}
}
}
.mini-button{
width: 12px;
float: left;
margin-right: 10px;
text-align: center;
padding: 9px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 5px;
line-height: 1.428;
cursor:pointer;
}
.noDataR{
width: 100%;
height: 40vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-direction: column;
.hintWrap{
color: #D4D6E6;
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
}
}
.asset-user-table{
padding-left: 20px;
}
</style>

View File

@@ -1,249 +0,0 @@
<template>
<div>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?9:11">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" />
<i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?11:13">
<GenericListTable
v-if="!isInit"
ref="RightTable"
class="asset-user-table"
:header-actions="rightTable.headerActions"
:table-config="rightTable.tableConfig"
/>
<div v-else class="noDataR">
<div class="hintWrap">
<div>{{ $t('accounts.PleaseClickLeftAssetToViewGatheredUser') }}</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { ChoicesFormatter, DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable
},
data() {
const vm = this
return {
isInit: true,
clickedRow: {},
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
vm.isInit = true
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'public_ip', 'admin_user_display',
'protocols', 'platform', 'connectivity',
'created_by', 'date_created', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip', 'platform'],
default: ['hostname', 'ip', 'connectivity', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',
1: 'fa-check text-primary',
2: 'fa-circle text-warning'
},
typeChange: function(val) {
if (!val) {
return 2
}
return val.status
},
hasTips: true
},
width: '90px',
align: 'center'
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/assets/gathered-users/?asset_id=${row.id}`
vm.clickedRow = row
vm.isInit = false
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/assets/gathered-users/?asset__hostname=ShowFirstAssetRelated`,
columns: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated', 'org_name'
],
columnsShow: {
min: ['username'],
default: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated'
]
},
columnsMeta: {
username: {
showOverflowTooltip: true
},
present: {
width: 80
},
ip_last_login: {
width: 120
}
},
tableAttrs: {
stripe: true, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: true,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false,
searchConfig: {
exclude: ['asset']
}
}
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.asset-table {
&:hover {
cursor: pointer;
}
& >>> .table-content {
margin-left: 21px;
}
& ::v-deep .el-table__row{
height: 40px;
& > td{
padding: 0;
}
}
}
.mini-button{
width: 12px;
float: left;
margin-right: 10px;
text-align: center;
padding: 9px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 5px;
line-height: 1.428;
cursor:pointer;
}
.noDataR{
width: 100%;
height: 40vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-direction: column;
.hintWrap{
color: #D4D6E6;
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
}
}
.asset-user-table{
padding-left: 20px;
& ::v-deep .el-table__header-wrapper thead tr{
height: 40px;
& > th{
padding: 0;
}
}
}
</style>

View File

@@ -1,70 +0,0 @@
<template>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
</el-row>
</template>
<script>
import DetailCard from '@/components/DetailCard'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'Detail',
components: {
DetailCard
},
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
return {
}
},
computed: {
detailCardItems() {
return [
{
key: this.$t('common.Name'),
value: this.object.name
},
{
key: this.$t('xpack.Cloud.PeriodicPerform'),
value: this.object.is_periodic ? (this.$t('xpack.Cloud.True')) : (this.$t('xpack.Cloud.False'))
},
{
key: this.$t('xpack.Cloud.Periodic'),
value: this.object.periodic_display
},
{
key: this.$t('xpack.Cloud.DateLastSync'),
value: this.object.date_last_sync ? toSafeLocalDateStr(this.object.date_created) : ''
},
{
key: this.$t('xpack.Cloud.DateCreated'),
value: this.object.date_created ? toSafeLocalDateStr(this.object.date_created) : ''
},
{
key: this.$t('common.Comment'),
value: this.object.comment
}
]
}
},
mounted() {
},
methods: {
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,64 +0,0 @@
<template>
<GenericListTable ref="GenericListTable" :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable/index'
import { BooleanFormatter, DateFormatter } from '@/components/TableFormatters'
export default {
name: 'TaskExecutionList',
components: { GenericListTable },
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
return {
headerActions: {
hasLeftActions: false,
hasBulkDelete: false,
hasImport: false,
hasExport: false,
hasSearch: true
},
tableConfig: {
url: `/api/v1/xpack/gathered-user/task-executions/?task=${this.object.id}`,
columns: [
{
prop: 'timedelta',
label: this.$t('ops.timeDelta'),
formatter: function(row) {
return row.timedelta.toFixed(2) + 's'
}
},
{
prop: 'date_start',
label: this.$t('common.DateStart'),
formatter: DateFormatter
},
{
prop: 'success',
label: this.$t('common.Success'),
width: '90px',
align: 'center',
formatter: BooleanFormatter,
formatterArgs: {
iconChoices: {
false: 'fa-times text-danger',
true: 'fa-check text-primary'
}
}
}
]
}
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -13,17 +13,10 @@ export default {
return {
tableConfig: {
url: '/api/v1/acls/login-asset-acls/',
columns: [
'name', 'user_username_group', 'hostname_group', 'ip_group', 'name_group',
'protocol_group', 'systemuser_username_group', 'reviewers', 'priority',
'is_active', 'comment', 'actions'
],
columns: ['name', 'user_username_group', 'hostname_group', 'ip_group', 'name_group', 'protocol_group', 'systemuser_username_group', 'reviewers', 'priority', 'is_active', 'comment', 'actions'],
columnsShow: {
min: ['name', 'actions'],
default: [
'name', 'user_username_group', 'hostname_group', 'ip_group', 'reviewers',
'priority', 'is_active', 'comment', 'actions'
]
default: ['name', 'user_username_group', 'hostname_group', 'ip_group', 'reviewers', 'priority', 'is_active', 'comment', 'actions']
},
columnsMeta: {
user_username_group: {

Some files were not shown because too many files have changed in this diff Show More