summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/api/groups.js40
-rw-r--r--server/migrations/20190223034423-create-iprange.js31
-rw-r--r--server/models/group.js1
-rw-r--r--server/models/iprange.js19
-rw-r--r--webapp/src/components/GroupModule.vue13
-rw-r--r--webapp/src/components/GroupModuleClientView.vue32
-rw-r--r--webapp/src/components/GroupModuleGroupView.vue103
-rw-r--r--webapp/src/store/groups.js34
8 files changed, 232 insertions, 41 deletions
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 @@
<v-container fill-height>
<v-layout>
<v-flex class="tabs-wrapper" xl10 offset-xl1 lg12>
- <v-card class="tabbar-card">
- <v-tabs :value="activeTab" @change="setActiveTab" :dark="tabsDark" :color="tabsColor" :slider-color="tabsSliderColor">
+ <v-card class="tabbar-card" style="display: flex; justify-content: space-between">
+ <v-tabs :value="activeTab" @change="setActiveTab" :dark="tabsDark" :color="tabsColor" :slider-color="tabsSliderColor" style="overflow-x: hidden">
<template v-for="(item, index) in tabChain">
<v-icon v-if="item.id > 0 || item.id === 'create'" :key="'arrow' + index">keyboard_arrow_right</v-icon>
<v-tab ripple :key="'tab' + index">
@@ -22,6 +22,7 @@
</v-tab>
</template>
</v-tabs>
+ <v-btn icon :loading="reloading" @click="reload"><v-icon>refresh</v-icon></v-btn>
</v-card>
<v-tabs-items :value="activeTab" @input="setActiveTab" touchless style="padding-bottom: 20px">
<v-tab-item v-for="(item, index) in tabChain" :key="index">
@@ -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 @@
<v-card>
<v-card-text>
<v-layout wrap>
- <v-flex lg4 md6 xs12 order-lg1 order-xs2>
+ <v-flex lg4 sm6 xs12 order-lg1 order-xs2>
<v-layout column>
<v-flex>
<v-text-field v-if="editMode" prepend-icon="label" class="info-input" :label="$t('name')" color="primary" v-model="info.name"></v-text-field>
@@ -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 @@
</v-flex>
</v-layout>
</v-flex>
- <v-flex lg4 md6 xs12 order-lg2 order-xs3>
+ <v-flex lg4 sm6 xs12 order-lg2 order-xs3>
<v-textarea prepend-icon="description" v-if="editMode" rows="1" auto-grow class="info-input" :label="$t('description')" color="primary" v-model="info.description"></v-textarea>
<div v-else class="info-input">
<div class="body-2 info-heading"><v-icon>description</v-icon><span>{{ $t('description') }}</span></div>
@@ -96,21 +97,21 @@
</v-flex>
</v-layout>
<v-layout wrap>
- <v-flex lg4 md6 xs12>
+ <v-flex lg4 sm6 xs12>
<v-text-field prepend-icon="language" v-if="editMode" class="info-input" :label="$t('ip')" color="primary" v-model="info.ip"></v-text-field>
<div v-else class="info-input">
<div class="body-2 info-heading"><v-icon>language</v-icon><span>{{ $t('ip') }}</span></div>
<div class="info-text">{{ client.ip || '-' }}</div>
</div>
</v-flex>
- <v-flex lg4 md6 xs12>
+ <v-flex lg4 sm6 xs12>
<v-text-field prepend-icon="memory" v-if="editMode" class="info-input" :label="$t('mac')" color="primary" v-model="info.mac"></v-text-field>
<div v-else class="info-input">
<div class="body-2 info-heading"><v-icon>memory</v-icon><span>{{ $t('mac') }}</span></div>
<div class="info-text">{{ client.mac || '-' }}</div>
</div>
</v-flex>
- <v-flex lg4 md6 xs12>
+ <v-flex lg4 sm6 xs12>
<v-text-field prepend-icon="fingerprint" v-if="editMode" class="info-input" :label="$t('uuid')" color="primary" v-model="info.uuid"></v-text-field>
<div v-else class="info-input">
<div class="body-2 info-heading"><v-icon>fingerprint</v-icon><span>{{ $t('uuid') }}</span></div>
@@ -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"
}
}
</i18n>
@@ -31,7 +37,7 @@
<v-card v-if="group.id !== 0">
<v-card-text>
<v-layout wrap>
- <v-flex lg4 md6 xs12 order-lg1 order-xs2>
+ <v-flex lg4 sm6 xs12 order-lg1 order-xs2>
<v-layout column>
<v-flex>
<v-text-field prepend-icon="label" v-if="editMode" class="info-input" :label="$t('name')" color="primary" v-model="info.name"></v-text-field>
@@ -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 @@
</v-flex>
</v-layout>
</v-flex>
- <v-flex lg4 md6 xs12 order-lg2 order-xs3>
+ <v-flex lg4 sm6 xs12 order-lg2 order-xs3>
<v-textarea prepend-icon="description" v-if="editMode" rows="1" auto-grow class="info-input" :label="$t('description')" color="primary" v-model="info.description"></v-textarea>
<div v-else class="info-input">
<div class="body-2 info-heading"><v-icon>description</v-icon><span>{{ $t('description') }}</span></div>
- <div class="info-text">{{ group.description || '-' }}</div>
+ <pre class="info-text">{{ group.description || '-' }}</pre>
+ </div>
+
+ <div v-if="editMode" class="info-input">
+ <div class="body-2 info-heading">
+ <v-icon>settings_ethernet</v-icon><span>{{ $t('ipranges') }}</span>
+ <v-btn small icon @click="addIprange"><v-icon>add</v-icon></v-btn>
+ </div>
+ <div>
+ <div v-for="(iprange, index) in ipranges" class="iprange" :key="index">
+ <v-btn small icon class="remove-iprange" @click="removeIprange(index)"><v-icon>remove</v-icon></v-btn>
+ <v-text-field :label="$t('startIp')" color="primary" v-model="iprange.startIp"></v-text-field>
+ <span>-</span>
+ <v-text-field :label="$t('endIp')" color="primary" v-model="iprange.endIp"></v-text-field>
+ </div>
+ </div>
+ </div>
+ <div v-else class="info-input">
+ <div class="body-2 info-heading"><v-icon>settings_ethernet</v-icon><span>{{ $t('ipranges') }}</span></div>
+ <div class="info-text ipranges-nonedit">
+ <table>
+ <tr v-for="(iprange, index) in group.ipranges" :key="index">
+ <td class="text-xs-right">{{ iprange.startIp }}</td>
+ <td class="ip-seperator">-</td>
+ <td>{{ iprange.endIp }}</td>
+ </tr>
+ </table>
+ <div v-if="group.ipranges.length === 0">-</div>
+ </div>
</div>
</v-flex>
<v-flex lg4 xs12 order-lg3 order-xs1 class="text-xs-right">
@@ -114,9 +149,9 @@
></v-switch></div>
</v-layout>
<v-subheader>{{ group.id > 0 ? $t('subgroups') : $t('groups') }}</v-subheader>
- <group-module-group-list :tabIndex="tabIndex" :groupId="group.id" :groups="group.subgroups" />
+ <group-module-group-list :tabIndex="tabIndex" :groupId="group.id" :groups="group.subgroups || []" />
<v-subheader>{{ $t('clients') }}</v-subheader>
- <group-module-client-list :tabIndex="tabIndex" :groupId="group.id" :clients="group.clients" />
+ <group-module-client-list :tabIndex="tabIndex" :groupId="group.id" :clients="group.clients || []" />
</template>
</div>
</template>
@@ -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)