UI: relation type define [done]

This commit is contained in:
pycook 2019-11-25 19:23:51 +08:00
parent ba80ec4403
commit 0a563deb11
18 changed files with 814 additions and 29 deletions

View File

@ -119,11 +119,12 @@ class PreferenceManager(object):
id2type = dict() id2type = dict()
for view_name in result: for view_name in result:
result[view_name] = toposort.toposort_flatten(
{i['child_id']: {i['parent_id']} for i in result[view_name]})
for i in result[view_name]: for i in result[view_name]:
id2type[i['parent_id']] = None id2type[i['parent_id']] = None
id2type[i['child']] = None id2type[i['child_id']] = None
result[view_name] = toposort.toposort_flatten(
{i['child_id']: {i['parent_id']} for i in result[view_name]})
for type_id in id2type: for type_id in id2type:
id2type[type_id] = CITypeCache.get(type_id).to_dict() id2type[type_id] = CITypeCache.get(type_id).to_dict()

View File

@ -15,14 +15,6 @@ export function getCITypeParent (CITypeID) {
}) })
} }
export function getRelationTypes (CITypeID, parameter) {
return axios({
url: '/v0.1/relation_types',
method: 'get',
params: parameter
})
}
export function createRelation (parentId, childrenId, relationTypeId) { export function createRelation (parentId, childrenId, relationTypeId) {
return axios({ return axios({
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`, url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,

View File

@ -0,0 +1,31 @@
import { axios } from '@/utils/request'
export function getRelationTypes () {
return axios({
url: '/v0.1/relation_types',
method: 'GET'
})
}
export function addRelationType (payload) {
return axios({
url: `/v0.1/relation_types`,
method: 'POST',
data: payload
})
}
export function updateRelationType (rtId, payload) {
return axios({
url: `/v0.1/relation_types/${rtId}`,
method: 'PUT',
data: payload
})
}
export function deleteRelationType (rtId) {
return axios({
url: `/v0.1/relation_types/${rtId}`,
method: 'DELETE'
})
}

View File

@ -12,7 +12,23 @@ const cmdbRouter = [
name: 'cmdb_preference', name: 'cmdb_preference',
meta: { title: '我的订阅', icon: 'book', keepAlive: true } meta: { title: '我的订阅', icon: 'book', keepAlive: true }
}, },
// views // relation views
{
path: '/relation_views',
component: () => import('@/views/cmdb/relation_views'),
name: 'cmdb_relation_views',
meta: { title: '关系视图', icon: 'link', keepAlive: true },
hideChildrenInMenu: true,
children: [
{
path: '/relation_views/:id',
name: 'cmdb_relation_views_item',
component: () => import('@/views/cmdb/relation_views'),
meta: { title: '关系视图', keepAlive: true },
hidden: true
}]
},
// tree views
{ {
path: '/tree_views', path: '/tree_views',
component: () => import('@/views/cmdb/tree_views'), component: () => import('@/views/cmdb/tree_views'),
@ -36,33 +52,47 @@ const cmdbRouter = [
meta: { 'title': '批量导入', icon: 'upload', keepAlive: true } meta: { 'title': '批量导入', icon: 'upload', keepAlive: true }
}, },
{ {
path: '/ci_types', path: '/config//ci_types',
name: 'cmdb_ci_type', name: 'cmdb_ci_type',
component: RouteView, component: RouteView,
redirect: '/ci_type', redirect: '/ci_types',
meta: { title: '模型配置', icon: 'setting', permission: ['admin'] }, meta: { title: '模型配置', icon: 'setting', permission: ['admin'] },
children: [ children: [
{ {
path: '/ci_types', path: '/config/ci_types',
name: 'ci_type', name: 'ci_type',
hideChildrenInMenu: true, // 强制显示 MenuItem 而不是 SubMenu hideChildrenInMenu: true,
component: () => import('@/views/cmdb/ci_type/list'), component: () => import('@/views/cmdb/model_config/ci_type/list'),
meta: { title: '模型定义', keepAlive: true } meta: { title: '模型管理', keepAlive: true }
}, },
{ {
path: '/ci_types/:CITypeName/detail/:CITypeId', path: '/config/ci_types/:CITypeName/detail/:CITypeId',
name: 'ci_type_detail', name: 'ci_type_detail',
hideChildrenInMenu: true, // 强制显示 MenuItem 而不是 SubMenu hideChildrenInMenu: true,
component: () => import('@/views/cmdb/ci_type/detail'), component: () => import('@/views/cmdb/model_config/ci_type/detail'),
meta: { title: '模型配置', keepAlive: true, hidden: true }, meta: { title: '模型管理', keepAlive: true, hidden: true },
hidden: true hidden: true
}, },
{ {
path: '/attributes', path: '/config/attributes',
name: 'attributes', name: 'attributes',
hideChildrenInMenu: true, // 强制显示 MenuItem 而不是 SubMenu hideChildrenInMenu: true,
component: () => import('@/views/cmdb/attributes/index'), component: () => import('@/views/cmdb/model_config/attributes/index'),
meta: { title: '属性库', keepAlive: true } meta: { title: '属性库', keepAlive: true }
},
{
path: '/config/relation_type',
name: 'relation_type',
hideChildrenInMenu: true,
component: () => import('@/views/cmdb/model_config/relation_type/index'),
meta: { title: '关系类型', keepAlive: true }
},
{
path: '/config/preference_relation',
name: 'preference_relation',
hideChildrenInMenu: true,
component: () => import('@/views/cmdb/model_config/preference_relation/index'),
meta: { title: '关系视图配置', keepAlive: true }
} }
] ]
}, },
@ -128,7 +158,7 @@ export const generatorDynamicRouter = () => {
component: () => import(`@/views/cmdb/ci/index`), component: () => import(`@/views/cmdb/ci/index`),
name: `cmdb_${item.id}`, name: `cmdb_${item.id}`,
meta: { title: item.alias, icon: 'table', keepAlive: true, typeId: item.id }, meta: { title: item.alias, icon: 'table', keepAlive: true, typeId: item.id },
hideChildrenInMenu: true // 强制显示 MenuItem 而不是 SubMenu hideChildrenInMenu: true
}) })
} }

View File

@ -143,8 +143,8 @@ import {
} from '@/api/cmdb/CITypeAttr' } from '@/api/cmdb/CITypeAttr'
import { STable } from '@/components' import { STable } from '@/components'
import { mixin, mixinDevice } from '@/utils/mixin' import { mixin, mixinDevice } from '@/utils/mixin'
import AttributeForm from '@/views/cmdb/attributes/module/attributeForm' import AttributeForm from '@/views/cmdb/model_config/attributes/module/attributeForm'
import { valueTypeMap } from '@/views/cmdb/attributes/module/const' import { valueTypeMap } from '@/views/cmdb/model_config/attributes/module/const'
export default { export default {
name: 'AttributesTable', name: 'AttributesTable',

View File

@ -103,7 +103,8 @@
</template> </template>
<script> <script>
import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/api/cmdb/CITypeRelation' import { createRelation, deleteRelation, getCITypeChildren } from '@/api/cmdb/CITypeRelation'
import { getRelationTypes } from '@/api/cmdb/relationType'
import { getCITypes } from '@/api/cmdb/CIType' import { getCITypes } from '@/api/cmdb/CIType'
import { STable } from '@/components' import { STable } from '@/components'

View File

@ -0,0 +1,6 @@
<template>
<div></div>
</template>
<script>
export default {}
</script>

View File

@ -0,0 +1,280 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
</div>
<s-table
:alert="options.alert"
:columns="columns"
:data="loadData"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
:showPagination="false"
:pageSize="25"
:rowKey="record=>record.id"
:rowSelection="options.rowSelection"
:scroll="scroll"
ref="table"
size="middle"
>
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
<a-input
v-ant-ref="c => searchInput = c"
:placeholder="` ${column.title}`"
:value="selectedKeys[0]"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
style="width: 188px; margin-bottom: 8px; display: block;"
/>
<a-button
type="primary"
@click="() => handleSearch(selectedKeys, confirm, column)"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
>搜索</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>重置</a-button>
</div>
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
<template slot="nameSearchRender" slot-scope="text">
<span v-if="columnSearchText.name">
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
<template v-else>{{ fragment }}</template>
</template>
</span>
<template v-else>{{ text }}</template>
</template>
<template slot="description" slot-scope="text">{{ text }}</template>
<span slot="action" slot-scope="text, record">
<template>
<a @click="handleEdit(record)">编辑</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确认删除?"
@confirm="handleDelete(record)"
@cancel="cancel"
okText=""
cancelText=""
>
<a>删除</a>
</a-popconfirm>
</template>
</span>
</s-table>
<relation-type-form ref="relationTypeForm" :handleOk="handleOk"> </relation-type-form>
</a-card>
</template>
<script>
import { STable } from '@/components'
import RelationTypeForm from './module/relationTypeForm'
import { getRelationTypes, deleteRelationType } from '@/api/cmdb/relationType'
export default {
name: 'Index',
components: {
STable,
RelationTypeForm
},
data () {
return {
scroll: { x: 1000, y: 500 },
btnName: '新增关系类型',
formLayout: 'vertical',
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
name: ''
},
columns: [
{
width: 150,
title: '类型名',
dataIndex: 'name',
sorter: false,
scopedSlots: {
customRender: 'nameSearchRender',
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon'
},
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: (visible) => {
if (visible) {
setTimeout(() => {
this.searchInput.focus()
}, 0)
}
}
},
{
width: 150,
title: '操作',
key: 'operation',
scopedSlots: { customRender: 'action' }
}
],
loadData: parameter => {
return getRelationTypes()
.then(res => {
const result = {}
result.data = res
console.log('loadData.res', result)
return result
})
},
mdl: {},
// 高级搜索 展开/关闭
advanced: false,
// 查询参数
queryParam: {},
// 表头
selectedRowKeys: [],
selectedRows: [],
// custom table alert & rowSelection
options: {
alert: false,
rowSelection: null
},
optionAlertShow: false
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
computed: {
formItemLayout () {
const { formLayout } = this
return formLayout === 'horizontal' ? {
labelCol: { span: 4 },
wrapperCol: { span: 14 }
} : {}
},
horizontalFormItemLayout () {
return {
labelCol: { span: 5 },
wrapperCol: { span: 12 }
}
},
buttonItemLayout () {
const { formLayout } = this
return formLayout === 'horizontal' ? {
wrapperCol: { span: 14, offset: 4 }
} : {}
}
},
mounted () {
this.setScrollY()
},
inject: ['reload'],
methods: {
handleSearch (selectedKeys, confirm, column) {
confirm()
this.columnSearchText[column.dataIndex] = selectedKeys[0]
this.queryParam[column.dataIndex] = selectedKeys[0]
},
handleReset (clearFilters, column) {
clearFilters()
this.columnSearchText[column.dataIndex] = ''
this.queryParam[column.dataIndex] = ''
},
setScrollY () {
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
},
handleEdit (record) {
console.log(record)
this.$refs.relationTypeForm.handleEdit(record)
},
handleDelete (record) {
this.deleteRelationType(record.id)
},
handleOk () {
this.$refs.table.refresh()
},
handleCreate () {
this.$refs.relationTypeForm.handleCreate()
},
deleteRelationType (id) {
deleteRelationType(id)
.then(res => {
this.$message.success(`删除成功`)
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
this.$message.error(`${msg}`)
},
cancel () {
}
},
watch: {}
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
// .fold {
// width: calc(100% - 216px);
// display: inline-block
// }
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
.custom-filter-dropdown {
padding: 8px;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
}
.highlight {
background-color: rgb(255, 192, 105);
padding: 0px;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<a-drawer
:closable="false"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="30%"
>
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
label="类型名"
>
<a-input
name="name"
placeholder=""
v-decorator="['name', {rules: [{ required: true, message: '请输入资源名'}]} ]"
/>
</a-form-item>
<a-form-item>
<a-input
name="id"
type="hidden"
v-decorator="['id', {rules: []} ]"
/>
</a-form-item>
<div
:style="{
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '0.8rem 1rem',
background: '#fff',
}"
>
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
<a-button @click="onClose">取消</a-button>
</div>
</a-form>
</a-drawer>
</template>
<script>
import { STable } from '@/components'
import { addRelationType, updateRelationType } from '@/api/cmdb/relationType'
export default {
name: 'RelationTypeForm',
components: {
STable
},
data () {
return {
drawerTitle: '新增关系类型',
drawerVisible: false,
formLayout: 'vertical'
}
},
beforeCreate () {
this.form = this.$form.createForm(this)
},
computed: {
formItemLayout () {
const { formLayout } = this
return formLayout === 'horizontal' ? {
labelCol: { span: 4 },
wrapperCol: { span: 14 }
} : {}
},
horizontalFormItemLayout () {
return {
labelCol: { span: 5 },
wrapperCol: { span: 12 }
}
},
buttonItemLayout () {
const { formLayout } = this
return formLayout === 'horizontal' ? {
wrapperCol: { span: 14, offset: 4 }
} : {}
}
},
mounted () {
},
methods: {
handleCreate () {
this.drawerVisible = true
},
onClose () {
this.form.resetFields()
this.drawerVisible = false
},
onChange (e) {
console.log(`checked = ${e}`)
},
handleEdit (record) {
this.drawerVisible = true
console.log(record)
this.$nextTick(() => {
this.form.setFieldsValue({
id: record.id,
name: record.name
})
})
},
handleSubmit (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
if (values.id) {
this.updateResourceType(values.id, values)
} else {
this.createResourceType(values)
}
}
})
},
updateResourceType (id, data) {
updateRelationType(id, data)
.then(res => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
createResourceType (data) {
addRelationType(data)
.then(res => {
this.$message.success(`添加成功`)
this.handleOk()
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
this.$message.error(`${msg}`)
}
},
watch: {},
props: {
handleOk: {
type: Function,
default: null
}
}
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,248 @@
<template>
<a-card :bordered="false">
<a-menu v-model="current" mode="horizontal" v-if="ciTypes.length">
<a-menu-item :key="ciType.id" v-for="ciType in ciTypes">
<router-link
:to="{name: 'cmdb_tree_views_item', params: {typeId: ciType.id}}"
>{{ ciType.alias || ciTypes.name }}</router-link>
</a-menu-item>
</a-menu>
<a-alert message="请先到 我的订阅 页面完成订阅!" banner v-else></a-alert>
<div style="clear: both; margin-top: 20px"></div>
<template>
<a-row :gutter="8">
<a-col :span="5">
<a-tree showLine :loadData="onLoadData" @select="onSelect" :treeData="treeData"></a-tree>
</a-col>
<a-col :span="19">
<s-table
v-if="ciTypes.length"
bordered
ref="table"
size="middle"
rowKey="ci_id"
:columns="columns"
:data="loadInstances"
:scroll="{ x: scrollX, y: scrollY }"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
:pageSize="25"
showPagination="auto"
></s-table>
</a-col>
</a-row>
</template>
</a-card>
</template>
<script>
import { STable } from '@/components'
import { getSubscribeTreeView, getSubscribeAttributes } from '@/api/cmdb/preference'
import { searchCI } from '@/api/cmdb/ci'
export default {
components: { STable },
data () {
return {
treeData: [],
triggerSelect: false,
treeNode: null,
ciTypes: [],
levels: [],
typeId: null,
current: [],
instanceList: [],
treeKeys: [],
columns: [],
pageSizeOptions: ['10', '25', '50', '100'],
loading: false,
scrollX: 0,
scrollY: 0,
loadInstances: parameter => {
const params = parameter || {}
// const params = Object.assign(parameter, this.$refs.search.queryParam)
let q = `q=_type:${this.typeId}`
Object.keys(params).forEach(key => {
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
if (typeof params[key] === 'object' && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:${params[key]}`
}
if (typeof params[key] === 'string') {
q += '*'
}
}
})
if (this.treeKeys.length > 0) {
this.treeKeys.forEach((item, idx) => {
q += `,${this.levels[idx].name}:${item}`
})
}
if (this.levels.length > this.treeKeys.length) {
q += `&facet=${this.levels[this.treeKeys.length].name}`
}
if ('pageNo' in params) {
q += `&page=${params['pageNo']}&count=${params['pageSize']}`
}
if ('sortField' in params) {
let order = ''
if (params['sortOrder'] !== 'ascend') {
order = '-'
}
q += `&sort=${order}${params['sortField']}`
}
return searchCI(q).then(res => {
const result = {}
result.pageNo = res.page
result.pageSize = res.total
result.totalCount = res.numfound
result.totalPage = Math.ceil(res.numfound / (params.pageSize || 25))
result.data = Object.assign([], res.result)
result.data.forEach((item, index) => (item.key = item.ci_id))
setTimeout(() => {
this.setColumnWidth()
}, 200)
if (Object.values(res.facet).length) {
this.wrapTreeData(res.facet)
}
return result
})
}
}
},
created () {
this.getCITypes()
},
inject: ['reload'],
watch: {
'$route.path': function (newPath, oldPath) {
this.typeId = this.$route.params.typeId
this.getCITypes()
this.reload()
}
},
methods: {
onSelect (keys) {
this.triggerSelect = true
if (keys.length) {
this.treeKeys = keys[0].split('-').filter(item => item !== '')
}
this.$refs.table.refresh(true)
},
wrapTreeData (facet) {
if (this.triggerSelect) {
return
}
const treeData = []
Object.values(facet)[0].forEach(item => {
treeData.push({
title: `${item[0]} (${item[1]})`,
key: this.treeKeys.join('-') + '-' + item[0],
isLeaf: this.levels.length - 1 === this.treeKeys.length
})
})
if (this.treeNode === null) {
this.treeData = treeData
} else {
this.treeNode.dataRef.children = treeData
this.treeData = [...this.treeData]
}
},
setColumnWidth () {
let rows = []
try {
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[2].childNodes[0].childNodes
} catch (e) {
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[1].childNodes[0].childNodes
}
let scrollX = 0
const columns = Object.assign([], this.columns)
for (let i = 0; i < rows.length; i++) {
columns[i].width = rows[i].offsetWidth < 80 ? 80 : rows[i].offsetWidth
scrollX += columns[i].width
}
this.columns = columns
this.scrollX = scrollX
this.scrollY = window.innerHeight - this.$refs.table.$el.offsetTop - 300
},
onLoadData (treeNode) {
this.triggerSelect = false
return new Promise(resolve => {
if (treeNode.dataRef.children) {
resolve()
return
}
this.treeKeys = treeNode.eventKey.split('-').filter(item => item !== '')
this.treeNode = treeNode
this.$refs.table.refresh(true)
resolve()
})
},
getCITypes () {
getSubscribeTreeView().then(res => {
this.ciTypes = res
if (this.ciTypes.length) {
this.typeId = this.$route.params.typeId || this.ciTypes[0].id
this.current = [this.typeId]
this.loadColumns()
this.levels = res.find(item => item.id === this.typeId).levels
this.$refs.table && this.$refs.table.refresh(true)
}
})
},
loadColumns () {
getSubscribeAttributes(this.typeId).then(res => {
const prefAttrList = res.attributes
const columns = []
prefAttrList.forEach((item, index) => {
const col = {}
col.title = item.alias
col.dataIndex = item.name
if (index !== prefAttrList.length - 1) {
col.width = 80
}
if (item.is_sortable) {
col.sorter = true
}
if (item.is_choice) {
const filters = []
item.choice_value.forEach(item => filters.push({ text: item, value: item }))
col.filters = filters
}
col.scopedSlots = { customRender: item.name }
columns.push(col)
})
this.columns = columns
})
}
}
}
</script>
<style scoped>
.ant-menu-horizontal {
border-bottom: 1px solid #ebedf0 !important;
}
.ant-menu-horizontal {
border-bottom: 1px solid #ebedf0 !important;
}
</style>