Merge pull request #3101 from jumpserver/pr@dev@perf_m2m_json_field

feat: ACL 中选择可以根据属性进行选择
This commit is contained in:
老广
2023-05-24 15:21:28 +08:00
committed by GitHub
35 changed files with 1338 additions and 245 deletions

View File

@@ -136,6 +136,8 @@ export default {
const form = this.$refs['form']
const values = form.getFormValue()
callback(values, form, button)
},
getFormValue() {
}
}
}

View File

@@ -3,9 +3,9 @@
ref="table"
class="el-data-table"
v-bind="tableConfig"
@sizeChange="handleSizeChange"
@update="onUpdate"
v-on="iListeners"
@sizeChange="handleSizeChange"
/>
</template>
@@ -168,57 +168,4 @@ export default {
</script>
<style lang="scss" scoped>
.el-data-table > > > .el-table {
.table {
margin-top: 15px;
}
.el-table__row {
&.selected-row {
background-color: #f5f7fa;
}
& > td {
line-height: 1.5;
padding: 6px 0;
font-size: 13px;
border-right: none;
* {
vertical-align: middle;
}
.el-checkbox {
vertical-align: super;
}
& > div > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
.el-table__header > thead > tr > th {
padding: 6px 0;
background-color: #ffffff;
font-size: 13px;
line-height: 1.5;
border-right: none;
.cell {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
border-right: 2px solid #EBEEF5;
}
}
}
}
.el-data-table >>> .el-table .el-table__header > thead > tr .is-sortable {
padding: 5px 0;
.cell {
padding-top: 3px!important;
}
}
</style>

View File

@@ -12,8 +12,8 @@
<slot />
<div slot="footer" class="dialog-footer">
<slot name="footer">
<el-button v-if="showCancel" @click="onCancel">{{ cancelTitle }}</el-button>
<el-button v-if="showConfirm" :loading="loadingStatus" type="primary" @click="onConfirm">
<el-button v-if="showCancel && showButtons" @click="onCancel">{{ cancelTitle }}</el-button>
<el-button v-if="showConfirm && showButtons" :loading="loadingStatus" type="primary" @click="onConfirm">
{{ confirmTitle }}
</el-button>
</slot>
@@ -29,16 +29,6 @@ export default {
type: String,
default: 'Title'
},
showCancel: {
type: Boolean,
default: true
},
cancelTitle: {
type: String,
default() {
return this.$t('common.Cancel')
}
},
top: {
type: String,
default: '3vh'
@@ -51,16 +41,30 @@ export default {
type: Boolean,
default: true
},
loadingStatus: {
type: Boolean,
default: false
},
confirmTitle: {
type: String,
default() {
return this.$t('common.Confirm')
}
},
showCancel: {
type: Boolean,
default: true
},
cancelTitle: {
type: String,
default() {
return this.$t('common.Cancel')
}
},
showButtons: {
type: Boolean,
default: true
},
loadingStatus: {
type: Boolean,
default: false
},
maxWidth: {
type: String,
default: '1200px'
@@ -90,6 +94,10 @@ export default {
border-radius: 0.3em;
max-width: 1500px;
.el-icon-circle-check {
display: none;
}
&__header {
box-sizing: border-box;
padding: 15px 22px;

View File

@@ -0,0 +1,148 @@
<template>
<Dialog
:destroy-on-close="true"
:show-buttons="false"
:title="$tc('common.SelectAttrs')"
v-bind="$attrs"
v-on="$listeners"
>
<div v-if="!loading">
<DataForm
:form="form"
class="attr-form"
v-bind="formConfig"
@submit="onAttrDialogConfirm"
/>
</div>
</Dialog>
</template>
<script>
import DataForm from '@/components/DataForm/index.vue'
import Dialog from '@/components/Dialog/index.vue'
import ValueField from '@/components/FormFields/JSONManyToManySelect/ValueField.vue'
import { attrMatchOptions, typeMatchMapper } from './const'
export default {
name: 'AttrFormDialog',
components: { Dialog, DataForm },
props: {
attrs: {
type: Array,
default: () => ([])
},
attrsAdded: {
type: Array,
default: () => ([])
},
form: {
type: Object,
default: () => ({})
}
},
data() {
return {
loading: true,
formConfig: {
// 为了方便更新,避免去取 fields 的索引
hasSaveContinue: false,
fields: [
{
id: 'name',
label: this.$t('common.AttrName'),
type: 'select',
options: this.attrs.map(attr => {
const disabled = this.attrsAdded.includes(attr.name) && this.form.name !== attr.name
return { label: attr.label, value: attr.name, disabled: disabled }
}),
on: {
change: ([val], updateForm) => {
const attr = this.attrs.find(attr => attr.name === val)
if (!attr) return
this.formConfig.fields[2].el.attr = attr
const attrType = attr.type || 'str'
const matchSupports = typeMatchMapper[attrType]
attrMatchOptions.forEach((option) => {
option.hidden = !matchSupports.includes(option.value)
})
let defaultValue = ''
if (['m2m', 'select'].includes(attrType)) {
defaultValue = []
} else if (['bool'].includes(attrType)) {
defaultValue = false
}
setTimeout(() => {
updateForm({ match: matchSupports[0], value: defaultValue })
}, 0.1)
}
}
},
{
id: 'match',
label: this.$t('common.Match'),
type: 'select',
options: attrMatchOptions,
on: {
change: ([value], updateForm) => {
let defaultValue = ''
if (['in', 'ip_in'].includes(value)) {
defaultValue = []
}
updateForm({ value: defaultValue })
this.formConfig.fields[2].el.match = value
}
}
},
{
id: 'value',
label: this.$t('common.AttrValue'),
component: ValueField,
el: {
match: attrMatchOptions[0].value,
attr: this.attrs[0]
}
}
]
}
}
},
mounted() {
if (this.form.index === undefined || this.form.index === -1) {
Object.assign(this.form, this.getDefaultAttrForm())
}
this.formConfig.fields[2].el.attr = this.attrs.find(attr => attr.name === this.form.name)
this.formConfig.fields[2].el.match = this.form.match
this.$log.debug('Form config: ', this.formConfig)
this.loading = false
},
methods: {
getDefaultAttrForm() {
const attrKeys = this.attrs.map(attr => attr.name)
const diff = attrKeys.filter(attr => !this.attrsAdded.includes(attr))
let name = this.attrs[0].name
if (diff.length > 0) {
name = diff[0]
}
return {
name: name,
match: 'exact',
value: '',
rel: 'and'
}
},
onAttrDialogConfirm(form) {
this.$emit('confirm', form)
}
}
}
</script>
<style lang="scss" scoped>
.attr-form {
>>> .el-select {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<Dialog
:destroy-on-close="true"
:show-buttons="false"
:title="$tc('common.MatchResult')"
:v-bind="$attrs"
:v-on="$listeners"
:visible.sync="iVisible"
>
<ListTable v-bind="attrMatchTableConfig" />
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import ListTable from '@/components/ListTable/index.vue'
export default {
name: 'AttrMatchResultDialog',
components: { ListTable, Dialog },
props: {
url: {
type: String,
default: ''
},
attrs: {
type: Array,
default: () => ([])
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
attrMatchTableConfig: {
headerActions: {
hasCreate: false,
hasImport: false,
hasExport: false,
hasMoreActions: false
},
tableConfig: {
url: this.url,
columns: this.attrs.filter(item => item.inTable).map(item => {
return {
prop: item.name,
label: item.label,
formatter: item.formatter
}
})
}
}
}
},
computed: {
iVisible: {
set(val) {
this.$emit('update:visible', val)
},
get() {
return this.visible
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div v-if="!loading">
<TagInput v-if="type === 'array'" :value="value" @input="handleInput" />
<Select2 v-else-if="type === 'select'" :value="value" v-bind="attr.el" @change="handleInput" @input="handleInput" />
<Switcher v-else-if="type === 'bool'" :value="value" @change="handleInput" @input="handleInput" />
<el-input v-else :value="value" @input="handleInput" />
</div>
</template>
<script>
import TagInput from '@/components/FormFields/TagInput.vue'
import Select2 from '@/components/FormFields/Select2.vue'
import Switcher from '@/components/FormFields/Switcher.vue'
export default {
name: 'ValueField',
components: { Switcher, TagInput, Select2 },
props: {
value: {
type: [String, Number, Boolean, Array, Object],
default: () => ''
},
match: {
type: String,
default: 'exact'
},
attr: {
type: Object,
default: () => ({})
}
},
data() {
return {
loading: true,
type: 'string'
}
},
watch: {
match() {
this.setTypeAndValue()
},
attr: {
handler() {
this.setTypeAndValue()
},
deep: true
}
},
mounted() {
this.setTypeAndValue()
},
methods: {
handleInput(value) {
this.$emit('input', value)
},
setTypeAndValue() {
this.loading = false
this.type = this.getType()
this.$log.debug('ValueField: Type: ', this.type, 'Value: ', this.value)
if (['select', 'array'].includes(this.type) && typeof this.value === 'string') {
const value = this.value ? this.value.split(',') : []
this.handleInput(value)
} else if (this.type === 'bool') {
const value = !!this.value
this.handleInput(value)
}
this.$nextTick(() => {
this.loading = false
})
},
getType() {
const attrType = this.attr.type || 'str'
this.$log.debug('Value field attr type: ', attrType, this.attr, this.match)
if (attrType === 'm2m') {
return 'select'
} else if (attrType === 'bool') {
return 'bool'
} else if (attrType === 'select') {
return 'select'
}
if (['in', 'ip_in'].includes(this.match)) {
return 'array'
} else {
return 'string'
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,79 @@
<template>
<span v-if="attr.type === 'bool'">
<i v-if="value" class="fa fa-check text-primary" />
<i v-else class="fa fa-times text-danger" />
</span>
<span v-else :title="value">{{ value }}</span>
</template>
<script>
import BaseFormatter from '@/components/TableFormatters/base.vue'
import { setUrlParam } from '@/utils/common'
export default {
name: 'ValueFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
attrs: {}
}
}
}
},
data() {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
return {
formatterArgs: formatterArgs,
loading: true,
attr: {},
value: ''
}
},
computed: {
},
watch: {
cellValue: {
handler(val) {
this.getValue()
},
immediate: true,
deep: true
}
},
mounted() {
this.getValue()
},
methods: {
async getValue() {
this.attr = this.formatterArgs.attrs.find(attr => attr.name === this.row.name)
this.match = this.row.match
this.$log.debug('ValueFormatter: ', this.attr, this.row.name)
if (this.attr.type === 'm2m') {
const url = setUrlParam(this.attr.el.url, 'ids', this.cellValue.join(','))
const data = await this.$axios.get(url)
if (data.length > 0) {
const displayField = this.attr.el.displayField || 'name'
this.value = data.map(item => item[displayField]).join(', ')
}
} else if (this.attr.type === 'select') {
this.value = this.attr.el.options
.filter(item => this.cellValue.includes(item.value))
.map(item => item.label).join(',')
} else if (['in', 'ip_in'].includes(this.match)) {
this.value = this.cellValue.join(', ')
} else {
this.value = this.cellValue
}
this.loading = false
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,25 @@
import i18n from '@/i18n/i18n'
export const strMatchValues = ['exact', 'not', 'in', 'contains', 'startswith', 'endswith', 'regex']
export const typeMatchMapper = {
str: strMatchValues,
bool: ['exact', 'not'],
m2m: ['m2m'],
ip: strMatchValues + ['ip_in'],
int: strMatchValues + ['gte', 'lte'],
select: ['in']
}
export const attrMatchOptions = [
{ label: i18n.t('common.Equal'), value: 'exact' },
{ label: i18n.t('common.NotEqual'), value: 'not' },
{ label: i18n.t('common.MatchIn'), value: 'in' },
{ label: i18n.t('common.Contains'), value: 'contains' },
{ label: i18n.t('common.Startswith'), value: 'startswith' },
{ label: i18n.t('common.Endswith'), value: 'endswith' },
{ label: i18n.t('common.Regex'), value: 'regex' },
{ label: i18n.t('common.BelongTo'), value: 'm2m' },
{ label: i18n.t('common.IPMatch'), value: 'ip_in' },
{ label: i18n.t('common.GreatEqualThan'), value: 'gte' },
{ label: i18n.t('common.LessEqualThan'), value: 'lte' }
]

View File

@@ -0,0 +1,240 @@
<template>
<div>
<el-radio-group v-model="iValue.type" @input="handleTypeChange">
<el-radio v-for="tp of types" :key="tp.name" :label="tp.name">
{{ tp.label }}
</el-radio>
</el-radio-group>
<Select2 v-if="iValue.type === 'ids'" v-model="ids" v-bind="select2" @change="onChangeEmit" />
<div v-if="iValue.type === 'attrs'">
<DataTable :config="tableConfig" class="attr-list" />
<div class="actions">
<el-button size="mini" type="primary" @click="handleAttrAdd">
{{ $t('common.Add') }}
</el-button>
<span style="padding-left: 10px; font-size: 13px">
<span class="help-tips; ">{{ $t('common.MatchedCount') }}:</span>
<a class="text-link" style="padding: 0 5px;" @click="showAttrMatchTable">{{ attrMatchCount }}</a>
</span>
</div>
</div>
<AttrFormDialog
v-if="attrFormVisible"
:attrs="attrs"
:attrs-added="attrsAdded"
:form="attrForm"
:visible.sync="attrFormVisible"
@confirm="handleAttrDialogConfirm"
/>
<AttrMatchResultDialog
v-if="attrMatchTableVisible"
:attrs="attrs"
:url="attrMatchTableUrl"
:visible.sync="attrMatchTableVisible"
/>
</div>
</template>
<script>
import Select2 from '../Select2.vue'
import DataTable from '@/components/DataTable/index.vue'
import ValueFormatter from './ValueFormatter.vue'
import AttrFormDialog from './AttrFormDialog.vue'
import AttrMatchResultDialog from './AttrMatchResultDialog.vue'
import { setUrlParam } from '@/utils/common'
import { attrMatchOptions } from './const'
import { toM2MJsonParams } from '@/utils/jms'
export default {
name: 'JSONManyToManySelect',
components: { AttrFormDialog, DataTable, Select2, AttrMatchResultDialog },
props: {
value: {
type: Object,
default: () => {
return {
type: 'all'
}
}
},
select2: {
type: Object,
required: true
},
attrs: {
type: Array,
default: () => ([])
},
resource: {
type: String,
default: ''
},
attrTableColumns: {
type: Array,
default: () => (['name'])
}
},
data() {
const tableFormatter = (colName) => {
return (row, col, cellValue) => {
const value = cellValue
switch (colName) {
case 'name':
return this.attrs.find(attr => attr.name === value)?.label || value
case 'match':
return attrMatchOptions.find(opt => opt.value === value).label || value
case 'value':
return Array.isArray(value) ? value.join(', ') : value
default:
return value
}
}
}
return {
iValue: Object.assign({ type: 'all' }, this.value),
attrFormVisible: false,
attrForm: {},
attrMatchCount: 0,
attrMatchTableVisible: false,
attrMatchTableUrl: '',
ids: this.value.ids || [],
editIndex: -1,
types: [
{ name: 'all', label: this.$t('common.All') + this.resource },
{ name: 'ids', label: this.$t('common.Spec') + this.resource },
{ name: 'attrs', label: this.$t('common.SelectByAttr') }
],
tableConfig: {
columns: [
{ prop: 'name', label: this.$t('common.AttrName'), formatter: tableFormatter('name') },
{ prop: 'match', label: this.$t('common.Match'), formatter: tableFormatter('match') },
{ prop: 'value', label: this.$t('common.AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs }},
{ prop: 'action', label: this.$t('common.Action'), align: 'center', width: '120px', formatter: (row, col, cellValue, index) => {
return (
<div className='input-button'>
<el-button
icon='el-icon-edit'
size='mini'
style={{ 'flexShrink': 0 }}
type='primary'
onClick={this.handleAttrEdit({ row, col, cellValue, index })}
/>
<el-button
icon='el-icon-minus'
size='mini'
style={{ 'flexShrink': 0 }}
type='danger'
onClick={this.handleAttrDelete({ row, col, cellValue, index })}
/>
</div>
)
} }
],
totalData: this.value.attrs || [],
hasPagination: false
}
}
},
computed: {
attrsAdded() {
return this.tableConfig.totalData.map(item => item.name)
}
},
watch: {
attrFormVisible(val) {
if (!val) {
this.getAttrsCount()
}
}
},
mounted() {
if (this.value.type === 'attrs') {
this.getAttrsCount()
}
this.$emit('input', this.iValue)
},
methods: {
showAttrMatchTable() {
const [key, value] = this.getAttrFilterKey()
this.attrMatchTableUrl = setUrlParam(this.select2.url, key, value)
this.attrMatchTableVisible = true
},
getAttrFilterKey() {
if (this.tableConfig.totalData.length === 0) return ''
let attrFilter = { type: 'attrs', attrs: this.tableConfig.totalData }
attrFilter = toM2MJsonParams(attrFilter)
return attrFilter
},
getAttrsCount() {
const attrFilter = this.getAttrFilterKey()
if (!attrFilter) {
this.attrMatchCount = 0
return
}
const [key, value] = attrFilter
let url = setUrlParam(this.select2.url, key, value)
url = setUrlParam(url, 'limit', 1)
return this.$axios.get(url).then(res => {
this.attrMatchCount = res.count
})
},
handleAttrEdit({ row, index }) {
return () => {
this.attrForm = Object.assign({ index }, row)
this.editIndex = index
this.attrFormVisible = true
}
},
handleAttrDelete({ index }) {
return () => {
this.tableConfig.totalData.splice(index, 1)
this.getAttrsCount()
}
},
handleAttrAdd() {
this.attrForm = {}
this.editIndex = -1
this.attrFormVisible = true
},
onChangeEmit() {
const tp = this.iValue.type
this.handleTypeChange(tp)
},
handleTypeChange(val) {
switch (val) {
case 'ids':
this.$emit('input', { type: 'ids', ids: this.ids })
break
case 'attrs':
this.$emit('input', { type: 'attrs', attrs: this.tableConfig.totalData })
break
default:
this.$emit('input', { type: 'all' })
break
}
},
handleAttrDialogConfirm(form) {
if (this.editIndex > -1) {
this.tableConfig.totalData.splice(this.editIndex, 1)
}
const allAttrs = this.tableConfig.totalData
// 因为可能 attr 的 name 会重复,所以需要先删除再添加
const setIndex = allAttrs.findIndex(attr => attr.name === form.name)
if (setIndex === -1) {
allAttrs.push(Object.assign({}, form))
} else {
allAttrs.splice(setIndex, 1, Object.assign({}, form))
}
this.attrFormVisible = false
this.onChangeEmit()
}
}
}
</script>
<style lang="scss" scoped>
.attr-list {
width: 99%;
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<el-switch
v-model="iValue"
inactive-color="#dcdfe6"
:class="type"
inactive-color="#dcdfe6"
v-bind="$attrs"
v-on="$listeners"
/>
@@ -17,7 +17,7 @@ export default {
default: 'primary'
},
value: {
type: Boolean,
type: [Boolean, String],
default: true
}
},
@@ -31,9 +31,14 @@ export default {
this.$emit('input', newValue)
},
get: function() {
return this.value
return !!this.value
}
}
},
watch: {
value(val) {
this.$log.debug('Switcher Value changed: ', val)
}
}
}
</script>

View File

@@ -15,6 +15,7 @@ import UploadSecret from './UploadSecret'
import WeekCronSelect from './WeekCronSelect'
import NestedObjectSelect2 from './NestedObjectSelect2'
import DatetimeRangePicker from './DatetimeRangePicker'
import JSONManyToManySelect from './JSONManyToManySelect/index.vue'
export default {
Text,
@@ -33,7 +34,8 @@ export default {
UploadSecret,
WeekCronSelect,
NestedObjectSelect2,
DatetimeRangePicker
DatetimeRangePicker,
JSONManyToManySelect
}
export {
@@ -53,5 +55,6 @@ export {
UploadSecret,
WeekCronSelect,
NestedObjectSelect2,
DatetimeRangePicker
DatetimeRangePicker,
JSONManyToManySelect
}

View File

@@ -59,7 +59,8 @@ export default {
hasTree: true,
columnsExclude: ['spec_info'],
columnShow: {
min: ['name', 'address', 'accounts']
min: ['name', 'address', 'accounts'],
default: ['name', 'address', 'accounts', 'actions']
},
columnsMeta: {
name: {

View File

@@ -0,0 +1,53 @@
<template>
<el-row :gutter="24">
<el-col :md="20" :sm="22">
<ListTable v-bind="config" />
</el-col>
</el-row>
</template>
<script>
import ListTable from '@/components/ListTable/index.vue'
import { toM2MJsonParams } from '@/utils/jms'
export default {
name: 'AssetJsonTab',
components: {
ListTable
},
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
const [key, value] = toM2MJsonParams(this.object.assets)
return {
config: {
headerActions: {
hasLeftActions: false,
hasImport: false,
hasExport: false
},
tableConfig: {
url: `/api/v1/assets/assets/?${key}=${value}`,
columns: ['name', 'address'],
columnsShow: {
min: ['id']
}
}
}
}
},
computed: {
iUrl() {
return `/api/v1/users/users/`
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,53 @@
<template>
<el-row :gutter="24">
<el-col :md="20" :sm="22">
<ListTable v-bind="config" />
</el-col>
</el-row>
</template>
<script>
import ListTable from '@/components/ListTable/index.vue'
import { toM2MJsonParams } from '@/utils/jms'
export default {
name: 'User',
components: {
ListTable
},
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
const [key, value] = toM2MJsonParams(this.object.users)
return {
config: {
headerActions: {
hasLeftActions: false,
hasImport: false,
hasExport: false
},
tableConfig: {
url: `/api/v1/users/users/?${key}=${value}`,
columns: ['name', 'username'],
columnsShow: {
min: ['id']
}
}
}
}
},
computed: {
iUrl() {
return `/api/v1/users/users/`
}
}
}
</script>
<style scoped>
</style>

View File

@@ -53,7 +53,6 @@ export default {
iGetTag() {
let tag = this.formatterArgs.getTag({ row: this.row, cellValue: this.cellValue })
if (tag) return tag
console.log('Tag: ', tag)
tag = {
size: this.formatterArgs.getTagSize({ row: this.row, cellValue: this.cellValue }),
type: this.formatterArgs.getTagType({ row: this.row, cellValue: this.cellValue }),

View File

@@ -38,7 +38,6 @@ export default {
},
methods: {
handleNodeClick(data) {
console.log(data)
}
}
}

View File

@@ -3,6 +3,8 @@
"accounts": {
"GenerateSuccessMsg": "Accounts generated successfully",
"GenerateAccounts": "Regenerate accounts",
"Accounts": "Accounts",
"SelectAccount": "Select account",
"UpdateSecret": "Update secret",
"AccountPolicy": "Account policy",
"BulkCreateStrategy": "When creating accounts that do not meet the requirements, such as key type non-compliance and unique key constraints, the above policies can be selected.",
@@ -436,7 +438,11 @@
"AssetTree": "Asset tree",
"SSHPort": "SSH Port",
"PrimaryProtocol": "The primary protocol, the most basic and commonly used protocol for assets, can only and must be set up with one.",
"Primary": "Primary"
"Primary": "Primary",
"CreateCustom": "Create Custom",
"CustomType": "Custom Type",
"CustomHelpMessage": "The assets of custom types require applet support. Please ensure that the corresponding applet is installed.",
"CustomFields": "Custom Fields"
},
"audits": {
"ChangeField": "Change field",
@@ -461,6 +467,7 @@
"ReLoginErr": "Login time has exceeded 5 minutes, please login again"
},
"common": {
"BatchProcessing": "Select {Number} items",
"Generate": "Generate",
"BatchProcessing": "Batch processing(select {Number} items)",
"ServerError": "Server Error",
@@ -811,7 +818,25 @@
"Error": "Error",
"Created": "Created",
"Skipped": "Skipped",
"Updated": "Updated"
"Updated": "Updated",
"NotEqual": "Not Equal",
"Startswith": "Starts With",
"AttrName": "Attribute Name",
"SelectByAttr": "Select By Attribute",
"IPMatch": "IP Match",
"Regex": "Regex",
"AttrValue": "Attribute Value",
"Contains": "Contains",
"Match": "Match",
"Spec": "Specific",
"All": "All",
"Endswith": "Ends With",
"RelAnd": "And",
"Equal": "Equal",
"MatchIn": "In ...",
"RelNot": "Not",
"RelOr": "Or",
"Relation": "Relation"
},
"dashboard": {
"ActiveAsset": "Asset active",

View File

@@ -1,6 +1,8 @@
{
"": "",
"accounts": {
"Accounts": "アカウント",
"SelectAccount": "アカウントを選択",
"GenerateSuccessMsg": "アカウントの生成に成功しました",
"GenerateAccounts": "アカウントを再生成する",
"UpdateSecret": "機密の更新",
@@ -436,7 +438,11 @@
"Category": "カテゴリー",
"SSHPort": "SSH ポート",
"PrimaryProtocol": "主要協議は、資産にとって最も基本的で最も一般的に使用されるプロトコルであり、1つのみ設定でき、必ず設定する必要があります",
"Primary": "主要な"
"Primary": "主要な",
"CreateCustom": "カスタムアセットを作成する",
"CustomType": "カスタムタイプ",
"CustomHelpMessage": "カスタムタイプのアセットにはアプレットのサポートが必要です。対応するアプレットがインストールされていることを確認してください。",
"CustomFields": "カスタム属性"
},
"audits": {
"ChangeField": "フィールドを変更します",
@@ -461,6 +467,7 @@
"ReLoginErr": "ログイン時間が 5 分を超えました。もう一度ログインしてください"
},
"common": {
"BatchProcessing": "選択 {Number} 項目",
"Generate": "生成",
"BatchProcessing": "一括処理(選択 {Number} 項目)",
"ServerError": "サーバーエラー",
@@ -810,7 +817,25 @@
"Product": "产品",
"Created": "已创建",
"Skipped": "已跳过",
"Updated": "已更新"
"Updated": "已更新",
"NotEqual": "等しくない",
"Startswith": "で始まる",
"AttrName": "属性名",
"SelectByAttr": "属性で選択",
"IPMatch": "IPアドレスが一致する",
"Regex": "正規表現",
"AttrValue": "属性値",
"Contains": "含む",
"Match": "一致する",
"Spec": "指定する",
"All": "すべて",
"Endswith": "で終わる",
"RelAnd": "かつ",
"Equal": "等しい",
"MatchIn": "以下のいずれかに一致する",
"RelNot": "でない",
"RelOr": "または",
"Relation": "関係"
},
"dashboard": {
"TotalJobLog": "ジョブ実行総数",

View File

@@ -1,6 +1,8 @@
{
"": "",
"accounts": {
"Accounts": "账号",
"SelectAccount": "选择账号",
"GenerateSuccessMsg": "账号生成成功",
"GenerateAccounts": "重新生成账号",
"UpdateSecret": "更新密文",
@@ -459,6 +461,33 @@
"ReLoginErr": "登录时长已超过 5 分钟,请重新登录"
},
"common": {
"MatchedCount": "匹配结果",
"SelectAttrs": "选择属性",
"MatchResult": "匹配结果",
"GreatEqualThan": "大于等于",
"LessEqualThan": "小于等于",
"BelongTo": "所属",
"Email": "邮箱",
"IsActive": "激活",
"All": "所有",
"Spec": "指定",
"SelectByAttr": "属性筛选",
"AttrName": "属性名",
"AttrValue": "属性值",
"Match": "匹配",
"Relation": "关系",
"Equal": "等于",
"NotEqual": "不等于",
"MatchIn": "在...中",
"Contains": "包含",
"Startswith": "以...开头",
"Endswith": "以...结尾",
"Regex": "正则表达式",
"IPMatch": "IP 匹配",
"RelAnd": "与",
"RelOr": "或",
"RelNot": "非",
"BatchProcessing": "选中 {Number} 项",
"Generate": "生成",
"BatchProcessing": "批量处理(选中 {Number} 项)",
"Created": "已创建",

View File

@@ -40,7 +40,7 @@ export default [
]
},
{
path: 'host-acls',
path: 'login-asset-acls',
component: empty,
redirect: '',
meta: {

View File

@@ -218,3 +218,66 @@ input[type=file] {
width: 100vw!important;
}
}
.el-data-table .el-table {
.table {
margin-top: 15px;
}
.el-table__row {
&.selected-row {
background-color: #f5f7fa;
}
& > td {
line-height: 1.5;
padding: 6px 0;
font-size: 13px;
border-right: none;
&:last-child {
border-right: solid 1px #EBEEF5 !important;
}
* {
vertical-align: middle;
}
.el-checkbox {
vertical-align: super;
}
& > div > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
.el-table__header > thead > tr > th {
padding: 6px 0;
background-color: #ffffff;
font-size: 13px;
line-height: 1.5;
border-right: none;
&:last-child {
border-right: solid 1px #EBEEF5 !important;
}
.cell {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
border-right: 2px solid #EBEEF5;
}
}
}
}
.el-data-table >>> .el-table .el-table__header > thead > tr .is-sortable {
padding: 5px 0;
.cell {
padding-top: 3px!important;
}
}

View File

@@ -144,3 +144,7 @@ export function getConstRouteName() {
addRoutes(names, constRoutes)
return names
}
export function toM2MJsonParams(attrFilter) {
return ['attr_rules', encodeURIComponent(btoa(JSON.stringify(attrFilter)))]
}

View File

@@ -5,7 +5,9 @@
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
import rules from '@/components/DataForm/rules'
import { cleanFormValueForHandleUserAssetAccount } from '../common'
import { userJSONSelectMeta } from '@/views/users/const'
import { assetJSONSelectMeta } from '@/views/assets/const'
import AccountFormatter from '@/views/perms/AssetPermission/components/AccountFormatter.vue'
export default {
name: 'AclCreateUpdate',
@@ -14,28 +16,30 @@ export default {
},
data() {
return {
initial: {},
initial: {
accounts: ['@ALL']
},
fields: [
[this.$t('common.Basic'), ['name', 'priority']],
[this.$t('acl.users'), ['users']],
[this.$t('acl.host'), ['assets']],
[this.$t('acl.account'), ['accounts']],
[this.$t('acl.action'), ['action', 'reviewers']],
[this.$t('common.Other'), ['is_active', 'comment']]
[this.$t('common.Basic'), ['name']],
[this.$t('users.Users'), ['users']],
[this.$t('assets.Asset'), ['assets']],
[this.$t('accounts.Accounts'), ['accounts']],
[this.$t('common.Action'), ['action', 'reviewers']],
[this.$t('common.Other'), ['priority', 'is_active', 'comment']]
],
fieldsMeta: {
priority: {
rules: [rules.Required]
},
assets: {
fields: ['name_group', 'address_group']
},
users: {
fields: ['username_group'],
fieldsMeta: {}
},
assets: assetJSONSelectMeta(this),
users: userJSONSelectMeta(this),
accounts: {
fields: ['username_group']
component: AccountFormatter,
el: {
showAddTemplate: false,
showVirtualAccount: false,
value: ['@ALL']
}
},
reviewers: {
hidden: (item) => item.action !== 'review',
@@ -51,8 +55,7 @@ export default {
}
}
},
url: '/api/v1/acls/login-asset-acls/',
cleanFormValue: cleanFormValueForHandleUserAssetAccount
url: '/api/v1/acls/login-asset-acls/'
}
},
methods: {}

View File

@@ -1,7 +1,7 @@
<template>
<el-row :gutter="20">
<el-col :md="14" :sm="24">
<AutoDetailCard :url="url" :fields="detailFields" :object="object" />
<AutoDetailCard :fields="detailFields" :object="object" :url="url" />
</el-col>
</el-row>
</template>
@@ -23,21 +23,9 @@ export default {
},
data() {
return {
url: `/api/v1/acls/login-asset-acls/${this.object.id}`,
url: `/api/v1/acls/login-asset-acls/${this.object.id}/`,
detailFields: [
'name',
{
key: this.$t('acl.UserUsername'),
value: this.object.users.username_group.toString()
},
{
key: this.$t('acl.AssetName'),
value: this.object.assets.name_group.toString()
},
{
key: this.$t('acl.AssetAddress'),
value: this.object.accounts.username_group.toString()
},
{
key: this.$t('acl.action'),
value: this.object.action.label

View File

@@ -1,5 +1,5 @@
<template>
<GenericDetailPage :object.sync="TaskDetail" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<GenericDetailPage :active-menu.sync="config.activeMenu" :object.sync="TaskDetail" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="TaskDetail" />
</keep-alive>
@@ -9,10 +9,15 @@
<script>
import { GenericDetailPage } from '@/layout/components'
import Detail from './Detail.vue'
import UserJsonTab from '@/components/ManyJsonTabs/UserJsonTab.vue'
import AssetJsonTab from '@/components/ManyJsonTabs/AssetJsonTab.vue'
export default {
components: {
GenericDetailPage,
Detail
Detail,
UserJsonTab,
AssetJsonTab
},
data() {
return {
@@ -23,6 +28,14 @@ export default {
{
title: this.$t('acl.RuleDetail'),
name: 'Detail'
},
{
title: this.$t('users.Users'),
name: 'UserJsonTab'
},
{
title: this.$t('assets.Assets'),
name: 'AssetJsonTab'
}
],
hasRightSide: true,

View File

@@ -1,8 +1,8 @@
<template>
<GenericCreateUpdatePage
:fields="fields"
:initial="initial"
:fields-meta="fieldsMeta"
:initial="initial"
:url="url"
v-bind="$data"
/>
@@ -10,10 +10,10 @@
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import AccountFormatter from '@/views/perms/AssetPermission/components/AccountFormatter.vue'
import rules from '@/components/DataForm/rules'
import {
cleanFormValueForHandleUserAssetAccount
} from '../../common'
import { userJSONSelectMeta } from '@/views/users/const'
import { assetJSONSelectMeta } from '@/views/assets/const'
export default {
name: 'CommandFilterAclCreateUpdate',
@@ -23,7 +23,8 @@ export default {
data() {
return {
initial: {
is_active: true
is_active: true,
accounts: ['@ALL']
},
fields: [
[this.$t('common.Basic'), ['name']],
@@ -37,17 +38,16 @@ export default {
url: '/api/v1/acls/command-filter-acls/',
createSuccessNextRoute: { name: 'CommandFilterAclList' },
fieldsMeta: {
users: {
fields: ['username_group']
},
assets: {
fields: ['name_group', 'address_group']
},
users: userJSONSelectMeta(this),
assets: assetJSONSelectMeta(this),
accounts: {
fields: ['username_group']
},
action: {
component: AccountFormatter,
el: {
showAddTemplate: false,
showVirtualAccount: false
}
},
action: {},
command_groups: {
el: {
value: [],
@@ -75,8 +75,7 @@ export default {
is_active: {
type: 'checkbox'
}
},
cleanFormValue: cleanFormValueForHandleUserAssetAccount
}
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<el-row :gutter="20">
<el-col :md="14" :sm="24">
<AutoDetailCard :url="url" :fields="detailFields" :object="object" />
<AutoDetailCard :fields="detailFields" :object="object" :url="url" />
</el-col>
</el-row>
</template>
@@ -25,22 +25,6 @@ export default {
url: `/api/v1/acls/command-filter-acls/${this.object.id}/`,
detailFields: [
'name',
{
key: this.$t('acl.UserUsername'),
value: this.object.users.username_group.toString()
},
{
key: this.$t('acl.AssetName'),
value: this.object.assets.name_group.toString()
},
{
key: this.$t('acl.AssetAddress'),
value: this.object.assets.address_group.toString()
},
{
key: this.$t('acl.AccountUsername'),
value: this.object.accounts.username_group.toString()
},
{
key: this.$t('acl.CommandGroup'),
value: this.object.command_groups.map((item) => item.name).join(', ')

View File

@@ -1,7 +1,7 @@
<template>
<GenericDetailPage
:object.sync="CommandFilterAcl"
:active-menu.sync="config.activeMenu"
:object.sync="CommandFilterAcl"
v-bind="config"
v-on="$listeners"
>
@@ -13,13 +13,17 @@
<script>
import { GenericDetailPage, TabPage } from '@/layout/components'
import UserJsonTab from '@/components/ManyJsonTabs/UserJsonTab.vue'
import AssetJsonTab from '@/components/ManyJsonTabs/AssetJsonTab.vue'
import Detail from './Detail.vue'
export default {
components: {
GenericDetailPage,
TabPage,
Detail
Detail,
UserJsonTab,
AssetJsonTab
},
data() {
return {
@@ -31,6 +35,14 @@ export default {
{
title: this.$t('common.BasicInfo'),
name: 'Detail'
},
{
title: this.$t('users.Users'),
name: 'UserJsonTab'
},
{
title: this.$t('assets.Assets'),
name: 'AssetJsonTab'
}
],
actions: {

View File

@@ -1,5 +1,5 @@
<template>
<ListTable :table-config="tableConfig" :header-actions="headerActions" />
<ListTable :header-actions="headerActions" :table-config="tableConfig" />
</template>
<script>
@@ -22,7 +22,8 @@ export default {
columnsShow: {
min: ['name', 'actions'],
default: [
'name', 'command_groups_amount', 'priority', 'is_active', 'comment', 'actions'
'name', 'command_groups_amount', 'priority',
'is_active', 'comment', 'actions'
]
},
columnsMeta: {
@@ -40,7 +41,6 @@ export default {
hasImport: false,
hasRefresh: true,
hasSearch: true,
hasMoreActions: false,
createRoute: 'CommandFilterAclCreate',
canCreate: () => {
return this.$hasPerm('acls.add_commandfilteracl') && !this.$store.getters.currentOrgIsRoot

View File

@@ -1,42 +1 @@
export const UserAssetAccountFieldInitial = {
users: {
username_group: '*'
},
assets: {
name_group: '*',
address_group: '*'
},
accounts: {
username_group: '*'
}
}
export function afterGetFormValueForHandleUserAssetAccount(formValue) {
// users
formValue.users.username_group = formValue.users.username_group.toString()
// assets
formValue.assets.name_group = formValue.assets.name_group.toString()
formValue.assets.address_group = formValue.assets.address_group.toString()
// accounts
formValue.accounts.username_group = formValue.accounts.username_group.toString()
return formValue
}
export function cleanFormValueForHandleUserAssetAccount(value) {
// users
if (!Array.isArray(value.users.username_group)) {
value.users.username_group = value.users.username_group ? value.users.username_group.split(',') : []
}
// assets
if (!Array.isArray(value.assets.name_group)) {
value.assets.name_group = value.assets.name_group ? value.assets.name_group.split(',') : []
}
if (!Array.isArray(value.assets.address_group)) {
value.assets.address_group = value.assets.address_group ? value.assets.address_group.split(',') : []
}
// accounts
if (!Array.isArray(value.accounts.username_group)) {
value.accounts.username_group = value.accounts.username_group ? value.accounts.username_group.split(',') : []
}
return value
}

View File

@@ -3,21 +3,23 @@
<el-link v-if="isUpdate(this)" :underline="false" type="default" @click="goToAssetAccountsPage()">
{{ $t('assets.InAssetDetail') }}
</el-link>
<div v-else class="accounts">
<el-table :data="accounts" style="width: 100%">
<div v-else class="accounts el-data-table">
<el-table :data="accounts" class="el-table--fit el-table--border">
<el-table-column :label="$tc('assets.Name')" prop="name" />
<el-table-column :label="$tc('assets.Username')" prop="username" />
<el-table-column :label="$tc('assets.Privileged')" prop="privileged">
<template v-slot="scope">
<i :class="scope.row['privileged'] ? 'fa-check' : ''" class="fa text-primary" />
<i v-if="scope.row['privileged']" class="fa fa-check text-primary" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$tc('common.TemplateAdd')" prop="template">
<template v-slot="scope">
<i :class="scope.row['template'] ? 'fa-check' : ''" class="fa text-primary" />
<i v-if="scope.row['template']" class="fa fa-check text-primary" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$tc('common.Actions')" align="right" class-name="buttons" fixed="right" width="135">
<el-table-column :label="$tc('common.Actions')" align="center" class-name="buttons" fixed="right" width="135">
<template v-slot="scope">
<el-button icon="el-icon-minus" size="mini" type="danger" @click="removeAccount(scope.row)" />
<el-button :disabled="scope.row.template" icon="el-icon-edit" size="mini" type="primary" @click="onEditClick(scope.row)" />
@@ -74,8 +76,9 @@ export default {
}
},
data() {
const accounts = this.value || []
return {
accounts: this.value || [],
accounts: accounts,
account: {},
initial: false,
addAccountDialogVisible: false,
@@ -152,7 +155,57 @@ export default {
</script>
<style lang="scss" scoped>
.accounts >>> .buttons .cell {
padding-right: 2px;
.el-data-table >>> .el-table {
.table {
margin-top: 15px;
}
.el-table__row {
&.selected-row {
background-color: #f5f7fa;
}
& > td {
line-height: 1.5;
padding: 6px 0;
font-size: 13px;
border-right: none;
* {
vertical-align: middle;
}
.el-checkbox {
vertical-align: super;
}
& > div > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
.el-table__header > thead > tr > th {
padding: 6px 0;
background-color: #ffffff;
font-size: 13px;
line-height: 1.5;
border-right: none;
.cell {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
border-right: 2px solid #EBEEF5;
}
}
}
}
.el-data-table >>> .el-table .el-table__header > thead > tr .is-sortable {
padding: 5px 0;
.cell {
padding-top: 3px!important;
}
}
</style>

View File

@@ -2,7 +2,7 @@ import i18n from '@/i18n/i18n'
import ProtocolSelector from '@/components/FormFields/ProtocolSelector'
import AssetAccounts from '@/views/assets/Asset/AssetCreateUpdate/components/AssetAccounts'
import rules from '@/components/DataForm/rules'
import { Select2 } from '@/components/FormFields'
import { JSONManyToManySelect, Select2 } from '@/components/FormFields'
import { message } from '@/utils/message'
export const filterSelectValues = (values) => {
@@ -138,3 +138,127 @@ export const assetFieldsMeta = (vm) => {
}
}
}
export const assetJSONSelectMeta = (vm) => {
const categories = []
const types = []
const protocols = []
vm.$axios.get('/api/v1/assets/categories/').then((res) => {
const _types = []
const _protocols = []
for (const category of res) {
categories.push({ value: category.value, label: category.label })
_types.push(...category.types.map(item => ({ value: item.value, label: item.label })))
for (const type of category.types) {
_protocols.push(...type.constraints.protocols?.map(item => ({ value: item.name, label: item.name.toUpperCase() })))
}
}
types.push(..._.uniqBy(_types, 'value'))
protocols.push(..._.uniqBy(_protocols, 'value'))
})
return {
component: JSONManyToManySelect,
el: {
value: [],
resource: vm.$t('assets.Asset'),
select2: {
url: '/api/v1/assets/assets/',
ajax: {
transformOption: (item) => {
return { label: item.name + '(' + item.address + ')', value: item.id }
}
}
},
attrs: [
{
name: 'name',
label: vm.$t('common.Name'),
inTable: true
},
{
name: 'address',
label: vm.$t('assets.Address'),
type: 'ip',
inTable: true
},
{
name: 'nodes',
label: vm.$t('assets.Node'),
type: 'm2m',
el: {
url: '/api/v1/assets/nodes/',
ajax: {
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
}
}
},
{
name: 'platform',
label: vm.$t('assets.Platform'),
type: 'm2m',
el: {
multiple: false,
url: '/api/v1/assets/platforms/'
}
},
{
name: 'category',
label: vm.$t('assets.Category'),
type: 'select',
inTable: true,
formatter: (row, column, cellValue) => cellValue.label,
el: {
options: categories
}
},
{
name: 'type',
label: vm.$t('assets.Type'),
type: 'select',
inTable: true,
formatter: (row, column, cellValue) => cellValue.label,
el: {
options: types
}
},
{
name: 'protocols',
label: vm.$t('assets.Protocols'),
type: 'select',
el: {
options: protocols
}
},
{
name: 'labels',
label: vm.$t('assets.Label'),
type: 'm2m',
el: {
multiple: true,
url: '/api/v1/assets/labels/'
}
}
]
}
}
}
export function getAssetSelect2Meta() {
return {
component: Select2,
el: {
value: [],
select2: {
ajax: {
url: '/api/v1/assets/assets/?fields_size=mini',
transformOption: (item) => {
return { label: item.name + '(' + item.address + ')', value: item.id }
}
}
}
}
}
}

View File

@@ -14,10 +14,8 @@
</el-tooltip>
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<div v-if="showSpecAccounts" class="spec-accounts">
<el-form-item label="选择账号">
<div v-if="showSpecAccounts" class="spec-accounts">
<TagInput
:autocomplete="autocomplete"
:tag-type="getTagType"
@@ -25,20 +23,20 @@
@change="handleTagChange"
/>
<span v-if="showAddTemplate">
<el-button size="small" type="primary" style="margin-left: 10px" @click="showAccountTemplateDialog=true">
<el-button size="small" style="margin-left: 10px" type="primary" @click="showTemplateDialog=true">
{{ $t('common.TemplateAdd') }}
</el-button>
{{ addTemplateHelpText }}
</span>
</el-form-item>
</div>
</div>
</el-form-item>
<Dialog
v-if="showAccountTemplateDialog"
v-if="showTemplateDialog"
:title="$tc('accounts.AccountTemplate')"
:visible.sync="showAccountTemplateDialog"
@confirm="handleAccountTemplateConfirm"
:visible.sync="showTemplateDialog"
@cancel="handleAccountTemplateCancel"
@confirm="handleAccountTemplateConfirm"
>
<ListTable ref="templateTable" v-bind="accountTemplateTable" />
</Dialog>
@@ -59,7 +57,7 @@ export default {
},
props: {
value: {
type: [Array],
type: [Array, String],
default: () => []
},
assets: {
@@ -78,6 +76,10 @@ export default {
type: Boolean,
default: true
},
showVirtualAccount: {
type: Boolean,
default: true
},
addTemplateHelpText: {
type: String,
default() {
@@ -112,10 +114,12 @@ export default {
return {
ALL: AllAccount,
SPEC: SpecAccount,
showAccountTemplateDialog: false,
choices: choices,
choicesSelected: [],
defaultChoices: [this.ALL],
showTemplateDialog: false,
choices: choices.filter(i => {
const isVirtualAccount = [SameAccount, ManualAccount].includes(i.value)
return !(isVirtualAccount && !this.showVirtualAccount)
}),
choicesSelected: [this.ALL],
specAccountsInput: [],
specAccountsTemplate: [],
showSpecAccounts: false,
@@ -156,19 +160,14 @@ export default {
username: query,
assets: this.assets.slice(0, 20).join(','),
nodes: this.nodes.slice(0, 20).map(item => {
if (typeof item === 'object') {
return item.pk
} else {
return item
}
return typeof item === 'object' ? item.pk : item
}).join(','),
oid: this.oid
}
}).then(res => {
if (!res) {
res = []
}
const data = res.filter(item => vm.value.indexOf(item) === -1)
if (!res) res = []
const data = res
.filter(item => vm.value.indexOf(item) === -1)
.map(v => ({ value: v, label: v }))
cb(data)
})
@@ -176,10 +175,18 @@ export default {
}
},
mounted() {
this.init()
this.initDefaultChoice()
setTimeout(() => {
console.log('Account Value: ', this.value)
if (this.value === '') {
this.$emit('input', ['@ALL'])
} else {
this.$emit('input', this.value)
}
})
},
methods: {
init() {
initDefaultChoice() {
const choicesSelected = this.value.filter(i => i.startsWith('@'))
const specAccountsInput = this.value.filter(i => !i.startsWith('@'))
if (specAccountsInput.length > 0 && !choicesSelected.includes(this.ALL)) {
@@ -189,11 +196,14 @@ export default {
if (this.value.indexOf(this.SPEC) > -1) {
this.showSpecAccounts = true
}
if (choicesSelected.length === 0) {
choicesSelected.push(this.ALL)
}
this.choicesSelected = choicesSelected
this.specAccountsInput = specAccountsInput
},
handleAccountTemplateCancel() {
this.showAccountTemplateDialog = false
this.showTemplateDialog = false
},
handleAccountTemplateConfirm() {
this.specAccountsTemplate = this.$refs.templateTable.selectedRows
@@ -201,7 +211,7 @@ export default {
this.specAccountsInput = this.specAccountsInput.filter(i => !added.includes(i)).concat(added)
this.outputValue()
setTimeout(() => {
this.showAccountTemplateDialog = false
this.showTemplateDialog = false
this.outputValue()
}, 100)
},
@@ -242,19 +252,8 @@ export default {
}
.spec-accounts {
border: solid 1px #f3f3f4;
padding: 10px 10px 0;
&>>> .el-form-item {
display: flex;
}
&>>> .el-form-item__content {
width: 80% !important;
flex: 1;
}
&>>> .filter-field {
width: calc(100% - 94px);
display: inline-block;
>>> .el-select {
width: 100%;
}
}

View File

@@ -61,6 +61,11 @@ export default {
source: {
width: '120px'
},
username: {
formatter: (row) => {
return row['username'].replace(' ', '*')
}
},
system_roles: {
width: '100px',
label: this.$t('users.SystemRoles'),

81
src/views/users/const.js Normal file
View File

@@ -0,0 +1,81 @@
import { JSONManyToManySelect } from '@/components/FormFields'
export const userJSONSelectMeta = (vm) => {
return {
component: JSONManyToManySelect,
el: {
value: [],
resource: vm.$t('users.Users'),
select2: {
url: '/api/v1/users/users/',
ajax: {
transformOption: (item) => {
return { label: item.name + '(' + item.username + ')', value: item.id }
}
}
},
attrs: [
{
name: 'name',
label: vm.$t('common.Name'),
inTable: true
},
{
name: 'username',
label: vm.$t('common.Username'),
inTable: true
},
{
name: 'email',
label: vm.$t('common.Email'),
inTable: true
},
{
name: 'comment',
label: vm.$t('common.Comment')
},
{
name: 'is_active',
label: vm.$t('common.IsActive'),
type: 'bool'
},
{
name: 'system_roles',
label: vm.$t('users.SystemRoles'),
type: 'm2m',
el: {
url: '/api/v1/rbac/system-roles/?fields_size=mini',
ajax: {
transformOption: (item) => {
return { label: item.display_name, value: item.id }
}
},
displayField: 'display_name'
}
},
{
name: 'org_roles',
label: vm.$t('users.OrgRoles'),
type: 'm2m',
el: {
url: '/api/v1/rbac/org-roles/',
ajax: {
transformOption: (item) => {
return { label: item.display_name, value: item.id }
}
},
displayField: 'display_name'
}
},
{
name: 'groups',
label: vm.$t('users.UserGroups'),
type: 'm2m',
el: {
url: '/api/v1/users/groups/?fields_size=mini'
}
}
]
}
}
}