From 461549c117c9760392debae54bb8c7a1ed66b807 Mon Sep 17 00:00:00 2001 From: Udo Walter Date: Sat, 23 Feb 2019 08:35:27 +0000 Subject: [groups] add iprange functionality to groups; add refresh button --- server/api/groups.js | 40 +++++++- server/migrations/20190223034423-create-iprange.js | 31 +++++++ server/models/group.js | 1 + server/models/iprange.js | 19 ++++ webapp/src/components/GroupModule.vue | 13 ++- webapp/src/components/GroupModuleClientView.vue | 32 ++++--- webapp/src/components/GroupModuleGroupView.vue | 103 ++++++++++++++++++--- webapp/src/store/groups.js | 34 +++++-- 8 files changed, 232 insertions(+), 41 deletions(-) create mode 100644 server/migrations/20190223034423-create-iprange.js create mode 100644 server/models/iprange.js diff --git a/server/api/groups.js b/server/api/groups.js index 40d2591..c869800 100644 --- a/server/api/groups.js +++ b/server/api/groups.js @@ -9,6 +9,12 @@ var router = decorateApp(express.Router()) // ############################################################################ // ########################### GET requests ################################# +router.getAsync('/test', async (req, res) => { + const group = await db.group.create({ name: 'yxcv' }) + console.log(group.ipranges) + res.send(group) +}) + router.getAsync('', async (req, res) => { const groups = await db.group.findAll({ order: [['name', 'ASC']] }) res.send(groups) @@ -17,7 +23,7 @@ router.getAsync('', async (req, res) => { router.getAsync('/:id', async (req, res) => { const all = req.query.all !== undefined && req.query.all !== 'false' if (req.params.id > 0) { - const group = await db.group.findOne({ where: { id: req.params.id }, include: ['parents', 'subgroups', 'clients'] }) + const group = await db.group.findOne({ where: { id: req.params.id }, include: ['parents', 'ipranges', 'subgroups', 'clients'] }) if (group) { if (all) res.status(200).send({ ...group.get({ plain: true }), ...await groupUtil.getAllChildren([group]) }) else res.status(200).send(group) @@ -44,11 +50,39 @@ router.postAsync(['', '/:id'], async (req, res) => { let group if (req.params.id === undefined) group = await db.group.create(req.body.data) else { - group = await db.group.findOne({ where: { id: req.params.id } }) + group = await db.group.findOne({ where: { id: req.params.id }, include: ['ipranges'] }) if (group) await group.update(req.body.data) } if (group) { - await group.setParents(req.body.parentIds) + const promises = [] + // Set group parents + if (Array.isArray(req.body.parentIds)) promises.push(group.setParents(req.body.parentIds)) + // Update, create or destroy ipranges + if (Array.isArray(req.body.ipranges)) { + // Get the ipranges of the group an create a id -> iprange map of them + const iprangeIdMap = {} + if (group.ipranges) { + group.ipranges.forEach(iprange => { + iprangeIdMap[iprange.id] = iprange + }) + } + // Update existing ipranges and create the new ones + req.body.ipranges.forEach(iprange => { + if (iprange.id) { + if (iprangeIdMap[iprange.id]) { + promises.push(iprangeIdMap[iprange.id].update(iprange)) + delete iprangeIdMap[iprange.id] + } + } else { + promises.push(group.createIprange(iprange)) + } + }) + // Destroy the deleted ipranges + for (let id in iprangeIdMap) { + promises.push(iprangeIdMap[id].destroy()) + } + } + await Promise.all(promises) res.status(200).send({ id: group.id }) } else { res.status(404).end() diff --git a/server/migrations/20190223034423-create-iprange.js b/server/migrations/20190223034423-create-iprange.js new file mode 100644 index 0000000..75d6d6f --- /dev/null +++ b/server/migrations/20190223034423-create-iprange.js @@ -0,0 +1,31 @@ +'use strict' +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('ipranges', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + startIp: { + type: Sequelize.STRING + }, + endIp: { + type: Sequelize.STRING(2048) + }, + groupId: { + allowNull: false, + type: Sequelize.INTEGER, + onDelete: 'cascade', + references: { + model: 'groups', + key: 'id' + } + } + }) + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('ipranges') + } +} diff --git a/server/models/group.js b/server/models/group.js index 472ecf2..30fc78d 100644 --- a/server/models/group.js +++ b/server/models/group.js @@ -19,6 +19,7 @@ module.exports = (sequelize, DataTypes) => { group.belongsToMany(group, { as: 'subgroups', through: GroupXGroup, foreignKey: 'parentId', otherKey: 'childId' }) group.belongsToMany(models.client, { as: 'clients', through: GroupXClient, foreignKey: 'groupId', otherKey: 'clientId' }) group.belongsTo(models.config) + group.hasMany(models.iprange, { as: 'ipranges' }) } return group } diff --git a/server/models/iprange.js b/server/models/iprange.js new file mode 100644 index 0000000..c0975e4 --- /dev/null +++ b/server/models/iprange.js @@ -0,0 +1,19 @@ +'use strict' +module.exports = (sequelize, DataTypes) => { + var iprange = sequelize.define('iprange', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + startIp: DataTypes.STRING, + endIp: DataTypes.STRING + }, { + timestamps: false + }) + iprange.associate = function (models) { + iprange.belongsTo(models.group) + } + return iprange +} \ No newline at end of file diff --git a/webapp/src/components/GroupModule.vue b/webapp/src/components/GroupModule.vue index cd487b0..a02fccf 100644 --- a/webapp/src/components/GroupModule.vue +++ b/webapp/src/components/GroupModule.vue @@ -11,8 +11,8 @@ - - + + + refresh @@ -39,7 +40,7 @@ import GroupModuleGroupView from '@/components/GroupModuleGroupView' import GroupModuleClientView from '@/components/GroupModuleClientView' import GroupModuleDialog from '@/components/GroupModuleDialog' -import { mapState, mapGetters, mapMutations } from 'vuex' +import { mapState, mapGetters, mapMutations, mapActions } from 'vuex' export default { name: 'GroupModule', @@ -60,7 +61,10 @@ export default { }, computed: { ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor']), - ...mapState('groups', ['tabChain', 'activeTab', 'groupList', 'clientList']) + ...mapState('groups', ['tabChain', 'activeTab', 'groupList', 'clientList']), + reloading () { + return this.tabChain.some(tab => tab.loading) + } }, watch: { activeTab (index) { @@ -75,6 +79,7 @@ export default { }, methods: { ...mapMutations('groups', ['setActiveTab', 'setTab']), + ...mapActions('groups', ['reload']), loadItem (routeName, id) { const type = routeName.replace('GroupModule.', '') if (id === 'create') { diff --git a/webapp/src/components/GroupModuleClientView.vue b/webapp/src/components/GroupModuleClientView.vue index bc7ea10..b78b2ed 100644 --- a/webapp/src/components/GroupModuleClientView.vue +++ b/webapp/src/components/GroupModuleClientView.vue @@ -27,7 +27,7 @@ - + @@ -42,13 +42,14 @@ v-if="editMode" class="info-input" :items="$store.state.groups.groupList" - v-model="groupIds" + v-model="groups" :menu-props="{ offsetY: '' }" :label="$t('groups')" color="primary" multiple item-value="id" item-text="name" + return-object small-chips deletable-chips > @@ -74,7 +75,7 @@ - +
description{{ $t('description') }}
@@ -96,21 +97,21 @@ - +
language{{ $t('ip') }}
{{ client.ip || '-' }}
- +
memory{{ $t('mac') }}
{{ client.mac || '-' }}
- +
fingerprint{{ $t('uuid') }}
@@ -138,7 +139,7 @@ export default { mac: '', uuid: '' }, - groupIds: [] + groups: [] } }, computed: { @@ -156,9 +157,6 @@ export default { } }, methods: { - removeGroup (id) { - this.groupIds.splice(this.groupIds.indexOf(id), 1) - }, editInfo () { this.editMode = true this.info.name = this.client.name @@ -167,7 +165,7 @@ export default { this.info.ip = this.client.ip this.info.mac = this.client.mac this.info.uuid = this.client.uuid - this.groupIds = this.client.groups ? this.client.groups.map(x => x.id) : [] + this.groups = this.client.groups || [] }, cancelEdit () { this.editMode = false @@ -181,7 +179,7 @@ export default { this.$store.dispatch('groups/saveClient', { id: this.client.id, data: this.info, - groupIds: this.groupIds, + groups: this.groups, tabIndex: this.tabIndex, callback: this.updateUrl }) @@ -205,17 +203,25 @@ export default { .info-buttons { margin: 0; } + .info-input { - margin: 20px; + padding: 20px; +} + +.info-input >>> input, .info-input >>> textarea { + font-family: 'Roboto Mono'; } + .info-heading { display: flex; align-items: center; margin-bottom: 10px; } + .info-heading > span { margin-left: 10px; } + .info-text { margin-left: 34px; font-family: 'Roboto Mono'; diff --git a/webapp/src/components/GroupModuleGroupView.vue b/webapp/src/components/GroupModuleGroupView.vue index 7418928..5403b61 100644 --- a/webapp/src/components/GroupModuleGroupView.vue +++ b/webapp/src/components/GroupModuleGroupView.vue @@ -8,8 +8,11 @@ "clients": "Clients", "name": "Name", "description": "Description", + "ipranges": "IP Ranges", "config": "iPXE Config", - "parents": "Parents" + "parents": "Parents", + "startIp": "Start IP", + "endIp": "End IP" }, "de": { "info": "Info", @@ -19,8 +22,11 @@ "clients": "Clienten", "name": "Name", "description": "Beschreibung", + "ipranges": "IP Bereiche", "config": "iPXE Konfiguration", - "parents": "Übergruppen" + "parents": "Übergruppen", + "startIp": "Start IP", + "endIp": "End IP" } } @@ -31,7 +37,7 @@ - + @@ -46,13 +52,14 @@ v-if="editMode" class="info-input" :items="$store.state.groups.groupList" - v-model="parentIds" + v-model="parents" :menu-props="{ offsetY: '' }" :label="$t('parents')" color="primary" multiple item-value="id" item-text="name" + return-object small-chips deletable-chips > @@ -78,11 +85,39 @@ - +
description{{ $t('description') }}
-
{{ group.description || '-' }}
+
{{ group.description || '-' }}
+
+ +
+
+ settings_ethernet{{ $t('ipranges') }} + add +
+
+
+ remove + + - + +
+
+
+
+
settings_ethernet{{ $t('ipranges') }}
+
+ + + + + + +
{{ iprange.startIp }}-{{ iprange.endIp }}
+
-
+
@@ -114,9 +149,9 @@ >
{{ group.id > 0 ? $t('subgroups') : $t('groups') }} - + {{ $t('clients') }} - +
@@ -140,7 +175,8 @@ export default { description: '', configId: null }, - parentIds: [] + parents: [], + ipranges: [{ startIp: '', endIp: '' }] } }, computed: { @@ -162,15 +198,19 @@ export default { this.$store.commit('groups/setShowAll', { index: this.tabIndex, value }) this.$store.dispatch('groups/loadGroup', { id: this.group.id, tabIndex: this.tabIndex, reload: true }) }, - removeParent (id) { - this.parentIds.splice(this.parentIds.indexOf(id), 1) + removeIprange (index) { + this.ipranges.splice(index, 1) + }, + addIprange () { + this.ipranges.push({ startIp: '', endIp: '' }) }, editInfo () { this.editMode = true this.info.name = this.group.name this.info.description = this.group.description this.info.configId = this.group.configId - this.parentIds = this.group.parents ? this.group.parents.map(x => x.id) : [] + this.parents = this.group.parents.slice(0) || [] + this.ipranges = this.group.ipranges.slice(0) || [{ startIp: '', endIp: '' }] }, cancelEdit () { this.editMode = false @@ -184,7 +224,8 @@ export default { this.$store.dispatch('groups/saveGroup', { id: this.group.id, data: this.info, - parentIds: this.parentIds, + parents: this.parents, + ipranges: this.ipranges, tabIndex: this.tabIndex, callback: this.updateUrl }) @@ -208,21 +249,55 @@ export default { .info-buttons { margin: 0; } + +.iprange { + display: flex; + align-items: center; +} + +.ipranges-nonedit { + overflow-x: auto; +} + +.ipranges-nonedit .ip-seperator { + padding: 0 10px; +} + +.iprange > .remove-iprange { + margin: 0 8px 0 -2px; +} + +.iprange >>> input { + font-size: 14px; +} + +.iprange > span { + margin: 0 10px; +} + .info-input { - margin: 20px; + padding: 10px 20px; } + +.info-input >>> input, .info-input >>> textarea { + font-family: 'Roboto Mono'; +} + .info-heading { display: flex; align-items: center; margin-bottom: 10px; } + .info-heading > span { margin-left: 10px; } + .info-text { margin-left: 34px; font-family: 'Roboto Mono'; } + .show-toggle { margin-top: 20px; margin-right: 20px; diff --git a/webapp/src/store/groups.js b/webapp/src/store/groups.js index 8f50978..8315dd0 100644 --- a/webapp/src/store/groups.js +++ b/webapp/src/store/groups.js @@ -55,7 +55,7 @@ export default { context.commit('setClientList', res[1].data.map(x => ({ id: x.id, name: x.name || x.id }))) }) }, - loadGroup (context, { id, name, tabIndex, switchTab, reload, placeholderName }) { + loadGroup (context, { id, name, tabIndex, switchTab, reload, save, placeholderName }) { if (!reload && context.state.tabChain.length > tabIndex && context.state.tabChain[tabIndex].id === id) { if (switchTab) context.commit('setActiveTab', tabIndex) } else { @@ -65,7 +65,7 @@ export default { if (context.state.tabChain.length <= tabIndex || context.state.tabChain[tabIndex].id !== id) { context.commit('setTab', { index: tabIndex, item: { id, name, tabType: 'group', tabShowAll: showAll, subgroups: [], clients: [] } }) } - context.commit('setTabLoading', tabIndex) + if (!save) context.commit('setTabLoading', tabIndex) if (switchTab) context.commit('setActiveTab', tabIndex) axios.get('/api/groups/' + id + (showAll ? '?all' : '')).then(res => { res.data.tabType = 'group' @@ -99,16 +99,27 @@ export default { reload (context) { context.dispatch('loadLists') context.state.tabChain.forEach((item, index) => { + if (item.id === 'create') return if (item.tabType === 'group') context.dispatch('loadGroup', { id: item.id, tabIndex: index, reload: true, tabShowAll: item.tabShowAll }) else if (item.tabType === 'client') context.dispatch('loadClient', { id: item.id, tabIndex: index, reload: true }) }) }, - saveGroup (context, { id, data, parentIds, tabIndex, callback }) { + saveGroup (context, { id, data, parents, ipranges, tabIndex, callback }) { + const parentIds = parents.map(x => x.id) const url = id === 'create' ? '/api/groups' : '/api/groups/' + id - axios.post(url, { data, parentIds }).then(res => { + axios.post(url, { data, parentIds, ipranges }).then(res => { if (res.data.id) { if (callback) callback(res.data.id) - context.commit('setTab', { index: tabIndex, item: { id: res.data.id, name: data.name, tabType: 'group', subgroups: [], clients: [] } }) + context.commit('setTab', { + index: tabIndex, + item: { + ...context.state.tabChain[tabIndex], + ...data, + id: res.data.id, + parents, + ipranges + } + }) if (parentIds && tabIndex > 1 && !parentIds.includes(context.state.tabChain[tabIndex - 1].id)) { context.commit('deleteFromTabChain', { index: 1, count: tabIndex - 1 }) context.commit('setActiveTab', 1) @@ -117,12 +128,21 @@ export default { } }) }, - saveClient (context, { id, data, groupIds, tabIndex, callback }) { + saveClient (context, { id, data, groups, tabIndex, callback }) { + const groupIds = groups.map(x => x.id) const url = id === 'create' ? '/api/clients' : '/api/clients/' + id axios.post(url, { data, groupIds }).then(res => { if (res.data.id) { if (callback) callback(res.data.id) - context.commit('setTab', { index: tabIndex, item: { id: res.data.id, name: data.name, tabType: 'client' } }) + context.commit('setTab', { + index: tabIndex, + item: { + ...context.state.tabChain[tabIndex], + ...data, + id: res.data.id, + groups + } + }) if (groupIds && tabIndex > 1 && !groupIds.includes(context.state.tabChain[tabIndex - 1].id)) { context.commit('deleteFromTabChain', { index: 1, count: tabIndex - 1 }) context.commit('setActiveTab', 1) -- cgit v1.2.3-55-g7522