Merge pull request #537 from jumpserver/pr@dev@feat_custom_col_list

feat: 允许用户自定义表格列显示功能
This commit is contained in:
Orange 2021-01-12 02:10:29 +08:00 committed by GitHub
commit cec17bbef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 227 additions and 17 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div :class="grouped ? 'el-button-group' : ''"> <div :class="grouped ? 'el-button-group' : ''">
<el-button v-for="item in iActions" :key="item.name" :size="size" v-bind="item" @click="handleClick(item.name)"> <el-button v-for="item in iActions" :key="item.name" :size="size" v-bind="item" @click="handleClick(item.name)">
<el-tooltip v-if="['actionExport', 'actionImport', 'actionRefresh'].indexOf(item.name) !== -1" effect="dark" :content="item.tip" placement="top"> <el-tooltip v-if="['actionExport', 'actionImport', 'actionRefresh','actionColumnSetting'].indexOf(item.name) !== -1" effect="dark" :content="item.tip" placement="top">
<i v-if="item.fa" :class="'fa ' + item.fa" />{{ item.title }} <i v-if="item.fa" :class="'fa ' + item.fa" />{{ item.title }}
</el-tooltip> </el-tooltip>
<span v-else> <span v-else>

View File

@ -178,6 +178,7 @@ export default {
} }
config.columns = columns config.columns = columns
this.iConfig = config this.iConfig = config
this.$eventBus.$emit('columnsChange', config)
}, },
filterChange(filters) { filterChange(filters) {
const key = Object.keys(filters)[0] const key = Object.keys(filters)[0]

View File

@ -0,0 +1,97 @@
<template>
<Dialog
v-if="showColumnSettingPopover"
:title="$t('common.CustomCol')"
:visible.sync="showColumnSettingPopover"
:destroy-on-close="true"
:show-cancel="false"
width="35%"
top="10%"
@confirm="handleColumnConfirm()"
>
<el-alert type="success">
{{ this.$t('common.TableColSettingInfo') }}
</el-alert>
<el-checkbox-group
v-model="columnList"
>
<el-row>
<el-col
v-for="item in totalColumns"
:key="item.prop"
:span="8"
style="margin-top:5px;"
>
<el-checkbox
:label="item.prop"
:disabled="item.prop==='id' ||item.prop==='actions' "
>
{{ item.label }}
</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</Dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import Dialog from '@/components/Dialog/index'
export default {
name: 'ColumnSettingPopover',
components: {
Dialog
},
props: {
totalColumns: {
type: Array,
default: () => []
},
defaultColumn: {
type: Array,
default: () => []
}
},
data() {
return {
showColumnSettingPopover: false,
columnList: []
}
},
computed: {
...mapGetters([
'tableConfig'
])
},
mounted() {
this.$eventBus.$on('showColumnSettingPopover', () => {
this.showColumnSettingPopover = true
})
console.log(this.tableConfig, this.$route.name, this.defaultColumn)
this.columnList = _.get(this.tableConfig, this.$route.name, this.defaultColumn || [])
console.log(this.columnList)
console.log(this.totalColumns)
},
methods: {
handleColumnConfirm() {
const ACTIVE_COLUMN_KEY = this.$route.name
this.$store.commit('table/SET_TABLE_CONFIG',
{
key: ACTIVE_COLUMN_KEY,
value: this.columnList
}
)
this.showColumnSettingPopover = false
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@ -2,21 +2,25 @@
<div> <div>
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" /> <ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" v-bind="$attrs" /> <ImExportDialog :selected-rows="selectedRows" :url="tableUrl" v-bind="$attrs" />
<ColumnSettingPopover v-bind="$attrs" />
</div> </div>
</template> </template>
<script> <script>
import ActionsGroup from '@/components/ActionsGroup' import ActionsGroup from '@/components/ActionsGroup'
import ImExportDialog from './ImExportDialog' import ImExportDialog from './ImExportDialog'
import ColumnSettingPopover from './ColumnSettingPopover'
import { cleanActions } from './utils' import { cleanActions } from './utils'
const defaultTrue = { type: Boolean, default: true } const defaultTrue = { type: Boolean, default: true }
const defaultFalse = { type: Boolean, default: false }
export default { export default {
name: 'RightSide', name: 'RightSide',
components: { components: {
ActionsGroup, ActionsGroup,
ImExportDialog ImExportDialog,
ColumnSettingPopover
}, },
props: { props: {
tableUrl: { tableUrl: {
@ -37,6 +41,13 @@ export default {
this.$eventBus.$emit('showImportDialog', { selectedRows }) this.$eventBus.$emit('showImportDialog', { selectedRows })
} }
}, },
hasColumnSetting: defaultFalse,
handleColumnConfig: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showColumnSettingPopover')
}
},
hasRefresh: defaultTrue, hasRefresh: defaultTrue,
selectedRows: { selectedRows: {
type: Array, type: Array,
@ -54,6 +65,7 @@ export default {
data() { data() {
return { return {
defaultRightSideActions: [ defaultRightSideActions: [
{ 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: '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: '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 } { name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefresh }

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<TableAction :table-url="iTableConfig.url" :search-table="search" :date-pick="handleDateChange" v-bind="headerActions" :selected-rows="selectedRows" :reload-table="reloadTable" /> <TableAction :table-url="iTableConfig.url" :search-table="search" :date-pick="handleDateChange" v-bind="iHeaderActions" :selected-rows="selectedRows" :reload-table="reloadTable" />
<IBox class="table-content"> <IBox class="table-content">
<AutoDataTable ref="dataTable" :filter-table="filter" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" /> <AutoDataTable ref="dataTable" :filter-table="filter" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" />
</IBox> </IBox>
@ -13,6 +13,7 @@ import IBox from '../IBox'
import TableAction from './TableAction' import TableAction from './TableAction'
import Emitter from '@/mixins/emitter' import Emitter from '@/mixins/emitter'
import deepmerge from 'deepmerge' import deepmerge from 'deepmerge'
import { mapGetters } from 'vuex'
export default { export default {
name: 'ListTable', name: 'ListTable',
components: { components: {
@ -41,6 +42,7 @@ export default {
} }
}, },
computed: { computed: {
dataTable() { dataTable() {
return this.$refs.dataTable.$refs.dataTable return this.$refs.dataTable.$refs.dataTable
}, },
@ -65,13 +67,24 @@ export default {
} }
return false return false
}, },
iHeaderActions() {
const config = this.headerActions
const hasColumnSetting = _.get(this.headerActions, 'hasColumnSetting', false)
if (hasColumnSetting) {
config.totalColumns = this.tableConfig.columns
}
return config
},
iTableConfig() { iTableConfig() {
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery }) const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
this.$log.debug('Header actions', this.headerActions) this.$log.debug('Header actions', this.headerActions)
_.set(config, 'columnsMeta.actions.formatterArgs.hasClone', this.hasCloneAction) _.set(config, 'columnsMeta.actions.formatterArgs.hasClone', this.hasCloneAction)
this.$log.debug('ListTable: iTableConfig change', config) this.$log.debug('ListTable: iTableConfig change', config)
return config return this.getActiveColumns(config)
} },
...mapGetters({
tableColConfig: 'tableConfig'
})
}, },
watch: { watch: {
extraQuery: { extraQuery: {
@ -79,9 +92,31 @@ export default {
this.$log.debug('ListTable: found extraQuery change') this.$log.debug('ListTable: found extraQuery change')
}, },
deep: true deep: true
},
tableColConfig: {
handler() {
this.$log.debug('ListTable: found colConfig change')
},
deep: true
} }
}, },
methods: { methods: {
getActiveColumns(config) {
const ACTIVE_COLUMN_KEY = this.$route.name
const hasColumnSetting = _.get(this.headerActions, 'hasColumnSetting', false)
const defaultColumn = _.get(this.headerActions, 'defaultColumn', [])
if (hasColumnSetting) {
const currentColumnSetting = _.get(this.tableColConfig, ACTIVE_COLUMN_KEY, defaultColumn || [])
const currentColumn = []
config.columns.forEach((v, k) => {
if (currentColumnSetting.indexOf(v.prop) !== -1 || v.prop === 'id') {
currentColumn.push(config.columns[k])
}
})
config.columns = currentColumn
}
return config
},
handleSelectionChange(val) { handleSelectionChange(val) {
this.selectedRows = val this.selectedRows = val
}, },

View File

@ -58,6 +58,7 @@
"AdminUserDetail": "管理用户详情", "AdminUserDetail": "管理用户详情",
"AdminUserListHelpMessage": "管理用户是资产(被控服务器)上的 root或拥有 NOPASSWD: ALL sudo 权限的用户, JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。\n", "AdminUserListHelpMessage": "管理用户是资产(被控服务器)上的 root或拥有 NOPASSWD: ALL sudo 权限的用户, JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。\n",
"Asset": "资产", "Asset": "资产",
"HardwareInfo": "硬件信息",
"AssetDetail": "资产详情", "AssetDetail": "资产详情",
"AssetList": "资产列表", "AssetList": "资产列表",
"AssetListHelpMessage": "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产\n", "AssetListHelpMessage": "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产\n",
@ -184,11 +185,13 @@
"Action": "动作", "Action": "动作",
"RequestTickets": "申请工单", "RequestTickets": "申请工单",
"Actions": "操作", "Actions": "操作",
"CustomCol":"自定义列表字段",
"Activate": "激活", "Activate": "激活",
"NeedSpecifiedFile": "需上传指定格式文件", "NeedSpecifiedFile": "需上传指定格式文件",
"TestPortErrorMsg":"端口错误,请重新输入", "TestPortErrorMsg":"端口错误,请重新输入",
"Active": "激活中", "Active": "激活中",
"actionsTips":"剪切板权限控制目前仅支持 RDP/VNC 协议的连接", "actionsTips":"剪切板权限控制目前仅支持 RDP/VNC 协议的连接",
"TableColSettingInfo": "请选择您想显示的列表详细信息。",
"Add": "添加", "Add": "添加",
"UpdateAssetDetail": "配置更多信息", "UpdateAssetDetail": "配置更多信息",
"AddSuccessMsg": "添加成功", "AddSuccessMsg": "添加成功",

View File

@ -58,6 +58,7 @@
"AdminUserDetail": "Admin user detail", "AdminUserDetail": "Admin user detail",
"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", "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", "Asset": "Asset",
"HardwareInfo": "Hardware info",
"Hardware": "Hardware", "Hardware": "Hardware",
"AccountList": "Account list", "AccountList": "Account list",
"AssetDetail": "Asset detail", "AssetDetail": "Asset detail",
@ -182,6 +183,7 @@
"common": { "common": {
"Nothing": "Nothing", "Nothing": "Nothing",
"Action": "Action", "Action": "Action",
"CustomCol":"Custom table col",
"RequestTickets": "Request tickets", "RequestTickets": "Request tickets",
"Actions": "Actions", "Actions": "Actions",
"NeedSpecifiedFile": "Required to upload the specified format file", "NeedSpecifiedFile": "Required to upload the specified format file",
@ -189,6 +191,7 @@
"Activate": "Activate", "Activate": "Activate",
"actionsTips":"Clipboard's copy and paste control only support RDP/VNC protocol.", "actionsTips":"Clipboard's copy and paste control only support RDP/VNC protocol.",
"Active": "Active", "Active": "Active",
"TableColSettingInfo": "Please select the list details you want to display",
"Add": "Add", "Add": "Add",
"PleaseAgreeToTheTerms": "Please agree to the terms", "PleaseAgreeToTheTerms": "Please agree to the terms",
"PushSelected":"Push selected", "PushSelected":"Push selected",

View File

@ -13,6 +13,7 @@ const getters = {
currentOrgRoles: state => state.users.roles, currentOrgRoles: state => state.users.roles,
currentOrgPerms: state => state.users.perms, currentOrgPerms: state => state.users.perms,
MFAVerifyAt: state => state.users.MFAVerifyAt, MFAVerifyAt: state => state.users.MFAVerifyAt,
MFA_TTl: state => state.settings.publicSettings.SECURITY_MFA_VERIFY_TTL MFA_TTl: state => state.settings.publicSettings.SECURITY_MFA_VERIFY_TTL,
tableConfig: state => state.table.tableConfig
} }
export default getters export default getters

View File

@ -0,0 +1,29 @@
import VueCookie from 'vue-cookie'
import Vue from 'vue'
function getTableConfigfromCookie() {
console.log(VueCookie.get('tableConfig') ? JSON.parse(VueCookie.get('tableConfig')) : {})
return VueCookie.get('tableConfig') ? JSON.parse(VueCookie.get('tableConfig')) : {}
}
const state = {
tableConfig: getTableConfigfromCookie()
}
const mutations = {
SET_TABLE_CONFIG: (state, tableConfig) => {
Vue.set(state.tableConfig, tableConfig.key, tableConfig.value)
VueCookie.set('tableConfig', JSON.stringify(state.tableConfig), 14)
}
}
const actions = {
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -38,7 +38,6 @@ const mutations = {
state.orgs = orgs state.orgs = orgs
}, },
MODIFY_ORG: (state, org) => { MODIFY_ORG: (state, org) => {
// console.log(state.orgs)
state.orgs = state.orgs.map(oldOrg => { state.orgs = state.orgs.map(oldOrg => {
if (oldOrg.id === org.id) { if (oldOrg.id === org.id) {
oldOrg.name = org.name oldOrg.name = org.name

View File

@ -90,6 +90,11 @@ export default {
} }
} }
}, },
{
prop: 'platform',
label: this.$t('assets.Platform'),
width: '120px'
},
{ {
prop: 'comment', prop: 'comment',
label: this.$t('assets.Comment'), label: this.$t('assets.Comment'),
@ -146,7 +151,9 @@ export default {
hasExport: false, hasExport: false,
hasImport: false, hasImport: false,
hasLeftActions: false, hasLeftActions: false,
hasSearch: true hasSearch: true,
hasColumnSetting: true,
defaultColumn: ['hostname', 'ip', 'SystemUsers', 'comment', 'id']
} }
} }
}, },

View File

@ -98,24 +98,40 @@ export default {
url: '/api/v1/assets/assets/', url: '/api/v1/assets/assets/',
hasTree: true, hasTree: true,
columns: [ columns: [
'hostname', 'ip', 'hardware_info', 'connectivity', 'actions' {
], prop: 'hostname',
columnsMeta: {
hostname: {
formatter: DetailFormatter, formatter: DetailFormatter,
label: this.$t('assets.Hostname'),
formatterArgs: { formatterArgs: {
route: 'AssetDetail' route: 'AssetDetail'
}, },
showOverflowTooltip: true showOverflowTooltip: true
}, },
ip: { {
prop: 'ip',
sortable: 'custom', sortable: 'custom',
label: `IP`,
width: '140px' width: '140px'
}, },
hardware_info: { {
prop: 'domain_display',
label: this.$t('assets.Domain')
},
{
prop: 'hardware_info',
label: this.$t('assets.HardwareInfo'),
showOverflowTooltip: true showOverflowTooltip: true
}, },
connectivity: { {
prop: 'public_ip',
label: this.$t('assets.PublicIp')
},
{
prop: 'platform',
label: this.$t('assets.Platform')
},
{
prop: 'connectivity',
label: this.$t('assets.Reachable'), label: this.$t('assets.Reachable'),
formatter: BooleanFormatter, formatter: BooleanFormatter,
formatterArgs: { formatterArgs: {
@ -132,8 +148,11 @@ export default {
width: '90px', width: '90px',
align: 'center' align: 'center'
}, },
actions: { {
prop: 'actions',
formatter: ActionsFormatter, formatter: ActionsFormatter,
label: this.$t('common.Actions'),
width: '140px',
formatterArgs: { formatterArgs: {
performDelete: ({ row, col }) => { performDelete: ({ row, col }) => {
const id = row.id const id = row.id
@ -152,9 +171,11 @@ export default {
] ]
} }
} }
} ]
}, },
headerActions: { headerActions: {
hasColumnSetting: true,
defaultColumn: ['name', 'username', 'protocol', 'assets_amount', 'comment', 'actions'],
createRoute: { createRoute: {
name: 'AssetCreate', name: 'AssetCreate',
query: this.$route.query query: this.$route.query

View File

@ -31,6 +31,8 @@ export default {
}, },
headerActions: { headerActions: {
hasMoreActions: false, hasMoreActions: false,
hasColumnSetting: true,
defaultColumn: ['name', 'username', 'protocol', 'assets_amount', 'comment', 'id'],
createRoute: 'SystemUserCreate' createRoute: 'SystemUserCreate'
}, },
helpMessage: this.$t('assets.SystemUserListHelpMessage') helpMessage: this.$t('assets.SystemUserListHelpMessage')