mirror of
https://github.com/jumpserver/lina.git
synced 2025-09-12 13:23:41 +00:00
perf: stash json
This commit is contained in:
@@ -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'
|
||||
|
@@ -1,16 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<TagInput v-if="Array.isArray(value)" :value="value" @input="handleInput" />
|
||||
<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" />
|
||||
<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: { TagInput },
|
||||
components: { Switcher, TagInput, Select2 },
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Array, Object],
|
||||
@@ -18,11 +22,44 @@ export default {
|
||||
},
|
||||
match: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: 'exact'
|
||||
},
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
type() {
|
||||
const attrType = this.attr.type
|
||||
if (attrType === 'm2m') {
|
||||
return 'select'
|
||||
} else if (attrType === 'bool') {
|
||||
return 'bool'
|
||||
} else if (attrType === 'select') {
|
||||
return 'select'
|
||||
}
|
||||
if (this.match in ['in', 'ip_in']) {
|
||||
return 'array'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
attr: {
|
||||
handler() {
|
||||
this.loading = true
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<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,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
attr() {
|
||||
return this.formatterArgs.attrs.find(attr => attr.name === this.row.name) || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cellValue: {
|
||||
handler(val) {
|
||||
this.getValue()
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getValue()
|
||||
},
|
||||
methods: {
|
||||
async getValue() {
|
||||
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 {
|
||||
this.value = this.cellValue
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@@ -14,7 +14,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog v-if="visible" :show-cancel="false" :show-confirm="false" :visible.sync="visible" title="选择属性">
|
||||
<Dialog v-if="visible" :show-buttons="false" :visible.sync="visible" title="选择属性">
|
||||
<DataForm class="attr-form" v-bind="formConfig" @submit="onAttrDialogConfirm" />
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -25,6 +25,7 @@ import Select2 from '../Select2.vue'
|
||||
import DataTable from '@/components/DataTable/index.vue'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import DataForm from '@/components/DataForm/index.vue'
|
||||
import ValueFormatter from './ValueFormatter.vue'
|
||||
import ValueField from './ValueField.vue'
|
||||
|
||||
export default {
|
||||
@@ -61,14 +62,24 @@ export default {
|
||||
{ label: this.$t('common.Startswith'), value: 'startswith' },
|
||||
{ label: this.$t('common.Endswith'), value: 'endswith' },
|
||||
{ label: this.$t('common.Regex'), value: 'regex' },
|
||||
{ label: this.$t('common.IPMatch'), value: 'ip_in' }
|
||||
{ label: this.$t('common.BelongTo'), value: 'm2m' },
|
||||
{ label: this.$t('common.IPMatch'), value: 'ip_in' },
|
||||
{ label: this.$t('common.GreatEqualThan'), value: 'gte' },
|
||||
{ label: this.$t('common.LessEqualThan'), value: 'lte' }
|
||||
]
|
||||
const attrRelOptions = [
|
||||
{ label: this.$t('common.RelAnd'), value: 'and' },
|
||||
{ label: this.$t('common.RelOr'), value: 'or' },
|
||||
{ label: this.$t('common.RelNot'), value: 'not' }
|
||||
]
|
||||
const attrNameOptions = this.attrs.map(attr => ({ label: attr.label, value: attr.name }))
|
||||
|
||||
const strMatchValues = ['exact', 'not', 'in', 'contains', 'startswith', 'endswith', 'regex']
|
||||
const typeMatchMapper = {
|
||||
str: strMatchValues,
|
||||
bool: ['exact', 'not'],
|
||||
m2m: ['m2m'],
|
||||
ip: strMatchValues + ['ip_in'],
|
||||
int: strMatchValues + ['gte', 'lte'],
|
||||
select: ['in']
|
||||
}
|
||||
attrMatchOptions.forEach((option) => {
|
||||
option.hidden = !typeMatchMapper[this.attrs[0].type || 'str'].includes(option.value)
|
||||
})
|
||||
const tableFormatter = (colName) => {
|
||||
return (row, col, cellValue) => {
|
||||
const value = cellValue
|
||||
@@ -77,8 +88,6 @@ export default {
|
||||
return this.attrs.find(attr => attr.name === value)?.label || value
|
||||
case 'match':
|
||||
return attrMatchOptions.find(opt => opt.value === value).label || value
|
||||
case 'rel':
|
||||
return attrRelOptions.find(opt => opt.value === value)?.label || value
|
||||
case 'value':
|
||||
return Array.isArray(value) ? value.join(', ') : value
|
||||
default:
|
||||
@@ -98,8 +107,7 @@ export default {
|
||||
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: tableFormatter('value') },
|
||||
{ prop: 'rel', label: this.$t('common.Relation'), formatter: tableFormatter('rel') },
|
||||
{ prop: 'value', label: this.$t('common.AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs }},
|
||||
{ prop: 'action', label: this.$t('common.Action'), formatter: (row, col, cellValue, index) => {
|
||||
return (
|
||||
<div className='input-button'>
|
||||
@@ -126,46 +134,48 @@ export default {
|
||||
},
|
||||
formConfig: {
|
||||
// 为了方便更新,避免去取 fields 的索引
|
||||
attrNameOptions: attrNameOptions,
|
||||
hasSaveContinue: false,
|
||||
editRowIndex: -1,
|
||||
form: {},
|
||||
fields: [
|
||||
{
|
||||
id: 'name',
|
||||
label: '属性',
|
||||
label: this.$t('common.AttrName'),
|
||||
type: 'select',
|
||||
options: attrNameOptions
|
||||
options: this.attrs.map(attr => ({ label: attr.label, value: attr.name })),
|
||||
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)
|
||||
})
|
||||
setTimeout(() => updateForm({ match: matchSupports[0] }), 0.1)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'match',
|
||||
label: '匹配方式',
|
||||
label: this.$t('common.Match'),
|
||||
type: 'select',
|
||||
options: attrMatchOptions,
|
||||
on: {
|
||||
change: ([value], updateForm) => {
|
||||
this.formConfig.fields[2].el.match = value
|
||||
if (['in', 'ip_in'].includes(value)) {
|
||||
updateForm({ value: [] })
|
||||
} else {
|
||||
updateForm({ value: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
label: '值',
|
||||
label: this.$t('common.AttrValue'),
|
||||
component: ValueField,
|
||||
el: {
|
||||
match: 'exact'
|
||||
match: attrMatchOptions[0].value,
|
||||
attr: this.attrs[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rel',
|
||||
label: '关系',
|
||||
type: 'radio-group',
|
||||
options: attrRelOptions
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -209,9 +219,8 @@ export default {
|
||||
}
|
||||
},
|
||||
setAttrNameOptionUsed() {
|
||||
const options = this.formConfig.attrNameOptions
|
||||
const options = this.formConfig.fields[0].options
|
||||
const used = this.tableConfig.totalData.map(attr => attr.name)
|
||||
console.log('Used: ', used)
|
||||
options.forEach(opt => {
|
||||
if (used.includes(opt.value)) {
|
||||
opt.disabled = true
|
||||
@@ -219,11 +228,11 @@ export default {
|
||||
delete opt.disabled
|
||||
}
|
||||
})
|
||||
console.log('Options: ', options)
|
||||
},
|
||||
handleAttrEdit({ row, index }) {
|
||||
return () => {
|
||||
this.formConfig.editRowIndex = index
|
||||
this.formConfig.fields[2].el.attr = this.attrs.find(attr => attr.name === row.name)
|
||||
this.formConfig.form = Object.assign({ index }, row)
|
||||
this.setAttrNameOptionUsed()
|
||||
this.visible = true
|
||||
@@ -236,6 +245,7 @@ export default {
|
||||
},
|
||||
handleAttrAdd() {
|
||||
this.formConfig.form = this.getDefaultAttrForm()
|
||||
this.formConfig.fields[2].el.attr = this.attrs.find(attr => attr.name === this.formConfig.form.name)
|
||||
this.setAttrNameOptionUsed()
|
||||
this.visible = true
|
||||
},
|
||||
|
@@ -456,6 +456,11 @@
|
||||
"ReLoginErr": "登录时长已超过 5 分钟,请重新登录"
|
||||
},
|
||||
"common": {
|
||||
"GreatEqualThan": "大于等于",
|
||||
"LessEqualThan": "小于等于",
|
||||
"BelongTo": "所属",
|
||||
"Email": "邮箱",
|
||||
"IsActive": "激活",
|
||||
"All": "所有",
|
||||
"Spec": "指定",
|
||||
"SelectByAttr": "属性筛选",
|
||||
|
@@ -140,6 +140,23 @@ 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: {
|
||||
@@ -160,7 +177,63 @@ export const assetJSONSelectMeta = (vm) => {
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
label: vm.$t('assets.Address')
|
||||
label: vm.$t('assets.Address'),
|
||||
type: 'ip'
|
||||
},
|
||||
{
|
||||
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',
|
||||
el: {
|
||||
options: categories
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: vm.$t('assets.Type'),
|
||||
type: 'select',
|
||||
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/'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -176,6 +176,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.initDefaultChoice()
|
||||
this.$emit('input', this.value)
|
||||
},
|
||||
methods: {
|
||||
initDefaultChoice() {
|
||||
|
@@ -22,6 +22,55 @@ export const userJSONSelectMeta = (vm) => {
|
||||
{
|
||||
name: 'username',
|
||||
label: vm.$t('common.Username')
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: vm.$t('common.Email')
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user