summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/.sequelizerc5
-rw-r--r--server/api/backends.js34
-rw-r--r--server/api/clients.js2
-rw-r--r--server/api/configurator.js2
-rw-r--r--server/api/groups.js37
-rw-r--r--server/api/registrations.js97
-rw-r--r--server/lib/external-backends/backendhelper.js58
-rw-r--r--server/lib/external-backends/backends/idoit-backend.js273
-rw-r--r--server/lib/external-backends/index.js28
-rw-r--r--server/migrations/20181008151633-create-registrationhook.js7
-rw-r--r--server/models/registrationhook.js8
-rw-r--r--webapp/package-lock.json50
-rw-r--r--webapp/package.json14
-rw-r--r--webapp/src/components/BackendModule.vue2
-rw-r--r--webapp/src/components/ComponentSearchTable.vue5
-rw-r--r--webapp/src/components/ConfiguratorModuleConfig.vue2
-rw-r--r--webapp/src/components/GroupModuleClientList.vue5
-rw-r--r--webapp/src/components/GroupModuleGroupList.vue5
-rw-r--r--webapp/src/components/PermissionModule.vue2
-rw-r--r--webapp/src/components/RegistrationModule.vue137
-rw-r--r--webapp/src/components/RegistrationModuleDelete.vue63
-rw-r--r--webapp/src/components/RegistrationModuleEdit.vue120
-rw-r--r--webapp/src/components/SettingsModule.vue2
-rw-r--r--webapp/src/config/dashboard.js2
-rw-r--r--webapp/src/config/i18n.js2
-rw-r--r--webapp/src/config/store.js2
-rw-r--r--webapp/src/store/groups.js5
-rw-r--r--webapp/src/store/registration.js40
28 files changed, 935 insertions, 74 deletions
diff --git a/server/.sequelizerc b/server/.sequelizerc
new file mode 100644
index 0000000..3a02529
--- /dev/null
+++ b/server/.sequelizerc
@@ -0,0 +1,5 @@
+const path = require('path');
+
+module.exports = {
+ 'config': path.resolve('config', 'database.json')
+} \ No newline at end of file
diff --git a/server/api/backends.js b/server/api/backends.js
index 77506ba..7729588 100644
--- a/server/api/backends.js
+++ b/server/api/backends.js
@@ -4,6 +4,7 @@ const ExternalBackends = require(path.join(__appdir, 'lib', 'external-backends')
var db = require(path.join(__appdir, 'lib', 'sequelize'))
var express = require('express')
var router = express.Router()
+var noAuthRouter = express.Router()
// GET requests.
@@ -119,7 +120,38 @@ router.get('/:id/synctypes', (req, res) => {
})
})
-// POST requests
+// vvvv TEST MEHTOD !!!! <----------------------------------------------------------
+noAuthRouter.get('/:id/test/getObjectTree', (req, res) => {
+ const id = req.params.id
+ db.backend.findOne({ where: { id: id } }).then(backend => {
+ if (backend) {
+ const ba = new ExternalBackends()
+ const instance = ba.getInstance(backend.type)
+
+ // Get the backend with all the mapped groups. ! Only groups can have childs !
+ db.backend.findOne({ where: { id: backend.id }, include: ['mappedGroups'] }).then(b => {
+ var objectData = []
+ // Put all groups in the array to make a one session call which returns all needed informations.
+ b.mappedGroups.forEach(mGroup => {
+ const mG = mGroup.backend_x_group
+ const eid = mG.externalId
+ const gid = mGroup.id
+ objectData.push({ eid: eid, gid: gid })
+ })
+ // Get all the information needed from the backend instance. (For all object ids in the array)
+ var promise = new Promise(function (resolve) {
+ // resolve(instance.getDataTree(backend.credentials, objectData.slice(0, 200)))
+ resolve(instance.getDataTree(backend.credentials, objectData))
+ })
+ promise.then(data => {
+ res.send({ length: data.length, result: data })
+ })
+ })
+ } else res.send({ error: 'error' })
+ })
+})
+module.exports.noAuthRouter = noAuthRouter
+// ^^^^ TEST MEHTOD !!!! <----------------------------------------------------------
/*
* id: <BACKEND_ID>
diff --git a/server/api/clients.js b/server/api/clients.js
index 3e257e2..0fbb7af 100644
--- a/server/api/clients.js
+++ b/server/api/clients.js
@@ -1,6 +1,7 @@
/* global __appdir */
var path = require('path')
var db = require(path.join(__appdir, 'lib', 'sequelize'))
+const backendHelper = require(path.join(__appdir, 'lib', 'external-backends', 'backendhelper'))
// GET Requests
module.exports.get = {
@@ -42,5 +43,6 @@ module.exports.post = {
// delete clients
delete: function (req, res) {
db.client.destroy({ where: { id: req.body.ids } }).then(count => { res.send({ count }) })
+ backendHelper.deleteClients(req.body.ids)
}
}
diff --git a/server/api/configurator.js b/server/api/configurator.js
index 3c5bab8..250471b 100644
--- a/server/api/configurator.js
+++ b/server/api/configurator.js
@@ -60,7 +60,7 @@ router.post(['/entries', '/entries/:id'], async (req, res) => {
entry = await db.entry.findOne({ where: { id: req.params.id } })
if (entry) await entry.update(item)
} else {
- await db.entry.create(item)
+ entry = await db.entry.create(item)
}
if (entry) {
res.send({ id: entry.id })
diff --git a/server/api/groups.js b/server/api/groups.js
index 0ea45fb..ddaed18 100644
--- a/server/api/groups.js
+++ b/server/api/groups.js
@@ -2,25 +2,20 @@
var path = require('path')
var db = require(path.join(__appdir, 'lib', 'sequelize'))
-function includeAllChilds (group, groupIdChain = []) {
- const newIdChain = [...groupIdChain, group.id]
- return new Promise((resolve, reject) => {
- group.getSubgroups({ include: ['subgroups', 'clients'] }).then(subgroups => {
- const subgroupPromises = []
- subgroups.forEach(subgroup => {
- if (!groupIdChain.includes(subgroup.id)) {
- subgroupPromises.push(includeAllChilds(subgroup, newIdChain))
- }
- })
- Promise.all(subgroupPromises).then(result => {
- result.forEach(resolvedSubgroup => {
- resolvedSubgroup.subgroups.forEach(x => { if (!group.subgroups.includes(x)) group.subgroups.push(x) })
- resolvedSubgroup.clients.forEach(x => { if (!group.clients.includes(x)) group.clients.push(x) })
- })
- resolve(group)
- })
- })
- })
+async function getAllChildren (groups, knownGroupIds = []) {
+ groups = groups.filter(subgroup => !knownGroupIds.includes(subgroup.id))
+ var groupIds = groups.map(g => g.id)
+ knownGroupIds = [...knownGroupIds, ...groupIds]
+ var [subgroups, clients] = await Promise.all([
+ db.group.findAll({ where: { '$parents.id$': groupIds }, include: ['parents'] }),
+ db.client.findAll({ where: { '$groups.id$': groupIds }, include: ['groups'] })
+ ])
+ if (subgroups.length > 0) {
+ var subChildren = await getAllChildren(subgroups, knownGroupIds)
+ subgroups = [...subgroups, ...subChildren.subgroups]
+ clients = [...clients, ...subChildren.clients]
+ }
+ return { subgroups, clients }
}
// GET Requests
@@ -36,9 +31,9 @@ module.exports.get = {
getGroup: function (req, res) {
const all = req.query.all === 'true'
if (req.query.id > 0) {
- db.group.findOne({ where: { id: req.query.id }, include: ['parents', 'subgroups', 'clients'] }).then(group => {
+ db.group.findOne({ where: { id: req.query.id }, include: ['parents', 'subgroups', 'clients'] }).then(async group => {
if (group) {
- if (all) includeAllChilds(group).then(x => res.send(x))
+ if (all) res.send({ ...group.get({ plain: true }), ...await getAllChildren([group]) })
else res.send(group)
} else {
res.status(404).end()
diff --git a/server/api/registrations.js b/server/api/registrations.js
index eb9a7a0..f6a8f87 100644
--- a/server/api/registrations.js
+++ b/server/api/registrations.js
@@ -5,6 +5,7 @@ var router = express.Router()
var noAuthRouter = express.Router()
var db = require(path.join(__appdir, 'lib', 'sequelize'))
const ExternalBackends = require(path.join(__appdir, 'lib', 'external-backends'))
+const backendHelper = require(path.join(__appdir, 'lib', 'external-backends', 'backendhelper'))
// GET requests.
@@ -21,6 +22,65 @@ router.get('/', (req, res) => {
})
})
+/*
+ * Returns all registration hooks sorted by sortValue.
+ *
+ * @return: List of registration hooks
+ */
+router.get('/hooks', (req, res) => {
+ db.registrationhook.findAll({ order: [ ['sortValue', 'ASC'] ], include: [{ model: db.group, as: 'groups', attributes: ['id'] }] }).then(hooks => {
+ res.send(hooks)
+ })
+})
+
+// POST requests.
+
+/*
+ * Reorders the registration hooks based on an array of hook ids.
+ */
+router.post('/hookorder', async (req, res) => {
+ var idSortvalueMap = {}
+ req.body.ids.forEach((id, index) => {
+ idSortvalueMap[id] = index
+ })
+ var hooks = await db.registrationhook.findAll()
+ var promises = []
+ hooks.forEach(hook => {
+ promises.push(hook.update({ sortvalue: idSortvalueMap[hook.id] }))
+ })
+ await Promise.all(promises)
+ res.end()
+})
+
+router.post(['/hooks', '/hooks/:id'], async (req, res) => {
+ var item = {
+ name: req.body.name,
+ description: req.body.description,
+ type: req.body.type,
+ script: req.body.script
+ }
+ var hook = null
+ if (req.params.id > 0) {
+ hook = await db.registrationhook.findOne({ where: { id: req.params.id } })
+ if (hook) await hook.update(item)
+ } else {
+ var maxSortvalue = await db.registrationhook.max('sortvalue')
+ item.sortvalue = maxSortvalue ? maxSortvalue + 1 : 1
+ hook = await db.registrationhook.create(item)
+ }
+ if (hook) {
+ hook.setGroups(req.body.groups)
+ res.send({ id: hook.id })
+ }
+ res.end()
+})
+
+// DELETE requests.
+
+router.delete(['/hooks', '/hooks/:id'], (req, res) => {
+ db.registrationhook.destroy({ where: { id: req.params.id || req.body.ids } }).then(count => { res.send({ count }) })
+})
+
module.exports.router = router
// GET requests.
@@ -56,7 +116,7 @@ noAuthRouter.post('/group', (req, res) => {
})
/*
- * Adds the client in the database and set parents if a parent was selected.
+ * Adds the client to the database and set parents if a parent was selected. Calls addClient for all external-backends.
*/
noAuthRouter.post('/add', (req, res) => {
const mac = req.body.mac
@@ -70,10 +130,27 @@ noAuthRouter.post('/add', (req, res) => {
var groupids = []
if (parentId) groupids = [parentId]
getNextHookScript(groupids, 0).then(resId => {
- db.client.create({ name: name, ip: ip, mac: mac, uuid: uuid, registrationState: resId }).then(client => {
+ db.client.create({ name: name, ip: ip, mac: mac, uuid: uuid, registrationState: resId }).then(newClient => {
if (parentId) {
- client.addGroup(parentId)
+ newClient.addGroup(parentId)
}
+
+ // Add the client to the backends.
+ const c = { network: { mac: mac, ip: ip } }
+ if (parentId) c.parentId = parentId
+ if (name) c.title = name
+ else c.title = 'Client_' + uuid
+ backendHelper.addClient(c).then(result => {
+ result.forEach(response => {
+ // If the object was created we need to make the objectid / external id mapping.
+ if (response.success && response.create) {
+ db.backend.findOne({ where: { id: response.backendId }, include: ['mappedClients'] }).then(backend => {
+ backend.addMappedClients(newClient, { through: { externalId: response.id, externalType: response.type } })
+ })
+ }
+ })
+ })
+
res.send('#!ipxe\r\nreboot')
})
})
@@ -82,6 +159,20 @@ noAuthRouter.post('/add', (req, res) => {
})
/*
+ * Adds additional information for the backends of the client in the firstregistration.
+ */
+noAuthRouter.post('/addInfo', (req, res) => {
+ const id = req.body.id
+ const systemModel = req.body.sysmodel
+ const systemManufacturer = req.body.sysmanu
+ const systemSerial = req.body.sysserial
+
+ // Add the client to the backends.
+ backendHelper.addClient({ id: id, system: { model: systemModel, manufacturer: systemManufacturer, serialnumber: systemSerial } })
+ res.send({ status: 'success' })
+})
+
+/*
* Open api method for setting the registration state of a given uuid.
*/
noAuthRouter.post('/:uuid/success', (req, res) => {
diff --git a/server/lib/external-backends/backendhelper.js b/server/lib/external-backends/backendhelper.js
new file mode 100644
index 0000000..0905c48
--- /dev/null
+++ b/server/lib/external-backends/backendhelper.js
@@ -0,0 +1,58 @@
+/* global __appdir */
+const path = require('path')
+const ExternalBackends = require(path.join(__appdir, 'lib', 'external-backends'))
+var db = require(path.join(__appdir, 'lib', 'sequelize'))
+
+module.exports = {
+ addClient: function (client) {
+ // Get all backends and call addClient for each instance.
+ return db.backend.findAll({ include: ['mappedGroups', 'mappedClients'] }).then(backends => {
+ var promises = []
+ var backendids = []
+ backends.forEach(backend => {
+ const ba = new ExternalBackends()
+ const instance = ba.getInstance(backend.type)
+ var tmpClient = JSON.parse(JSON.stringify(client))
+
+ // If the client id is set we need to get the external id.
+ if (client.id) {
+ var exid = backend.mappedClients.find(y => y.id === parseInt(client.id))
+ if (exid) tmpClient.id = exid.backend_x_client.externalId
+ }
+
+ // Convert the parent group id to the external backend parentId.
+ if (client.parentId) {
+ var element = backend.mappedGroups.find(x => x.id === parseInt(client.parentId))
+ if (element) tmpClient['parentId'] = element.backend_x_group.externalId
+ }
+
+ // TODO: Stuff to do if client already exists
+ backendids.push(backend.id)
+ promises.push(instance.addClient(backend.credentials, tmpClient))
+ })
+ return Promise.all(promises).then(result => {
+ result.forEach(object => {
+ object.backendId = backendids.shift()
+ })
+ return result
+ })
+ })
+ },
+
+ deleteClients: function (objectIds) {
+ // Get all backends and call deleteClient for each instance.
+ db.backend.findAll({ include: ['mappedClients'] }).then(backends => {
+ backends.forEach(backend => {
+ const ba = new ExternalBackends()
+ const instance = ba.getInstance(backend.type)
+ var objectsToDelete = []
+ objectIds.forEach(oid => {
+ var element = backend.mappedClients.find(x => x.id === parseInt(oid))
+ if (element) objectsToDelete.push(element.backend_x_client.externalId)
+ })
+ // If there are objects to delete -> delete them.
+ if (objectsToDelete.length > 0) instance.deleteObjects(backend.credentials, objectsToDelete)
+ })
+ })
+ }
+}
diff --git a/server/lib/external-backends/backends/idoit-backend.js b/server/lib/external-backends/backends/idoit-backend.js
index a48c740..fc3bbca 100644
--- a/server/lib/external-backends/backends/idoit-backend.js
+++ b/server/lib/external-backends/backends/idoit-backend.js
@@ -184,8 +184,42 @@ class IdoitBackend extends ExternalBackends {
return result
}
+ async deleteObjects (credentials, objectIds) {
+ var c = this.mapCredentials(credentials)
+ var login = await this.getSession(c)
+ var sid = login.data.result['session-id']
+
+ var config = {
+ timeout: 180000,
+ headers: {
+ 'X-RPC-Auth-Session': sid,
+ 'Content-Type': 'application/json'
+ }
+ }
+
+ var bodies = []
+
+ objectIds.forEach(oid => {
+ // Body
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.object.delete',
+ 'params': {
+ 'id': oid,
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': oid
+ })
+ })
+
+ var requestDelete = await axios.post(c.url, bodies, config)
+
+ return { success: requestDelete.success, data: requestDelete.data }
+ }
+
// Function to use the same session for multiple requests
- async getDataTree (credentials, array) {
+ async getDataTree (credentials, objects) {
var c = this.mapCredentials(credentials)
// LOGIN
@@ -224,9 +258,23 @@ class IdoitBackend extends ExternalBackends {
// Go through the objects and get all the childs.
var promises = []
+
+ var counter = 0
+ var index = 0
+ var gids = {}
+
+ // Prepare all the batch request bodies.
var e
- for (e in array) {
- var element = array[e]
+ for (e in objects) {
+ // Pack 400 requests in one batch request to reduce api calls.
+ if (counter >= 400) {
+ counter = 0
+ index++
+ }
+ if (counter === 0) promises[index] = []
+ counter++
+
+ var element = objects[e]
const bod = {
'version': '2.0',
'method': 'cmdb.location_tree',
@@ -235,16 +283,223 @@ class IdoitBackend extends ExternalBackends {
'apikey': c.apikey,
'language': 'en'
},
- 'id': 1
+ 'id': element.eid
}
+ promises[index].push(bod)
+ gids[element.eid] = element.gid
+ }
- var tmp = await axios.post(c.url, bod, config)
- promises.push({ gid: element.gid, childs: tmp.data.result })
+ // Send all the batch request and post proccess the result into one array.
+ var result = []
+ var p
+ var counter2 = 1
+ for (p in promises) {
+ // TODO: Remove
+ // Counter for getting an overview, how far the requests are.
+ console.log(counter2 + '/' + promises.length + ' requests send')
+ counter2++
+
+ // Send the request.
+ var requestResult = await axios.post(c.url, promises[p], config)
+ const args = requestResult.data
+
+ // Post process the data.
+ var a
+ for (a in args) {
+ result.push({ gid: gids[args[a].id], childs: args[a].result })
+ }
+ }
+ return result
+ }
- // Add a delay, so that idoit can handle it.
- // await new Promise(resolve => { setTimeout(() => { resolve() }, 500) })
+ /*
+ * Adds the client to the backend.
+ *
+ * credentials: <BACKEND_CREDENTIALS>
+ * The client parameters are all optional. If the client has an id the object is not created but the categories of the object.
+ * client: {
+ * id: <CLIENT_ID>, title: <CLIENT_TITLE>, parentId: <PARENT_ID>,
+ * system: { model: <SYSTEM_MODEL>, manufacturer: <SYSTEM_MANUFACTURER>, serialnumber: <SYSTEM_SERIALNUMBER> },
+ * cpu: { model: <CPU_MODEL>, manufacturer: <CPU_MANUFACTURER>, type: <CPU_TYPE>, frequency: <CPU_FREQUENCY>, cores: <CPU_CORES> },
+ * ram: [{ model: <RAM_MODEL>, manufacturer: <RAM_MANUFACTURER>, type: <RAM_TYPE>, capacity: <RAM_CAPACITY>, unit: <RAM_UNIT> }, ...],
+ * storage: {},
+ * network: { mac: <MAC_ADDRESS>, ip: <IP_ADDRESS> }
+ * }
+ */
+ async addClient (credentials, client) {
+ var c = this.mapCredentials(credentials)
+ var login = await this.getSession(c)
+ var sid = login.data.result['session-id']
+
+ var config = {
+ timeout: 180000,
+ headers: {
+ 'X-RPC-Auth-Session': sid,
+ 'Content-Type': 'application/json'
+ }
}
- return promises
+
+ var clientid
+ if (!client.id) {
+ // Create the object in idoIT.
+ var params = {
+ 'type': 'C__OBJTYPE__CLIENT',
+ 'title': client.title,
+ 'apikey': c.apikey,
+ 'language': 'en'
+ }
+
+ var requestCreate = await this.axiosRequest(c.url, 'cmdb.object.create', params, config.headers)
+
+ clientid = requestCreate.data.result.id
+ } else {
+ clientid = client.id
+ }
+
+ var bodies = []
+ if (client.network) {
+ // Update the object. MAC-Address
+ if (client.network.mac) {
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.category.create',
+ 'params': {
+ 'objID': clientid,
+ 'category': 'C__CATG__NETWORK_PORT',
+ 'data': {
+ 'category_id': 1,
+ 'mac': client.network.mac
+ },
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': 'create_mac'
+ })
+ }
+
+ // Update the object. IP-Address
+ if (client.network.ip) {
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.category.create',
+ 'params': {
+ 'objID': clientid,
+ 'category': 'C__CATG__IP',
+ 'data': {
+ 'category_id': 1,
+ 'ipv4_address': client.network.ip
+ },
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': 'create_ip'
+ })
+ }
+ }
+
+ // Update the object. Location
+ if (client.parentId) {
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.category.update',
+ 'params': {
+ 'objID': clientid,
+ 'category': 'C__CATG__LOCATION',
+ 'data': {
+ 'parent': client.parentId
+ },
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': 'update_parent'
+ })
+ }
+
+ if (client.system) {
+ // Update the object.
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.category.update',
+ 'params': {
+ 'objID': clientid,
+ 'category': 'C__CATG__MODEL',
+ 'data': {
+ 'manufacturer': client.system.manufacturer,
+ 'title': client.system.model,
+ 'serial': client.system.serialnumber
+ },
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': 'update_model'
+ })
+ }
+
+ if (client.cpu) {
+ // Update the object.
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.category.create',
+ 'params': {
+ 'objID': clientid,
+ 'category': 'C__CATG__CPU',
+ 'data': {
+ 'category_id': 1,
+ 'manufacturer': client.cpu.manufacturer,
+ 'title': client.cpu.model,
+ 'type': client.cpu.type,
+ 'frequency': client.cpu.frequency,
+ 'frequency_unit': 1,
+ 'cores': client.cpu.cores
+ },
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': 'update_cpu'
+ })
+ }
+
+ if (client.ram) {
+ var counter = 1
+ var mem
+ for (mem in client.ram) {
+ var id = 'create_memory_' + counter
+ // Update the object.
+ bodies.push({
+ 'version': '2.0',
+ 'method': 'cmdb.category.create',
+ 'params': {
+ 'objID': clientid,
+ 'category': 'C__CATG__MEMORY',
+ 'data': {
+ 'title': mem.model,
+ 'manufacturer': mem.manufacturer,
+ 'type': mem.type,
+ 'capacity': mem.capacity,
+ 'unit': mem.unit
+ },
+ 'apikey': c.apikey,
+ 'language': 'en'
+ },
+ 'id': id
+ })
+ counter++
+ }
+ }
+
+ var requestUpdate = await axios.post(c.url, bodies, config)
+
+ var result = {
+ success: true,
+ id: clientid,
+ type: 'C__OBJTYPE__CLIENT',
+ create: requestCreate ? requestCreate.success : false,
+ update: requestUpdate ? requestUpdate.success : false,
+ createData: requestCreate ? requestCreate.data : false,
+ updateData: requestUpdate ? requestUpdate.data : false
+ }
+
+ return result
}
// ############################################################################
diff --git a/server/lib/external-backends/index.js b/server/lib/external-backends/index.js
index 9f99153..00d0da3 100644
--- a/server/lib/external-backends/index.js
+++ b/server/lib/external-backends/index.js
@@ -84,6 +84,16 @@ class ExternalBackends {
}
/*
+ * credendtials: <BACKEND_CREDENTIALS>
+ * objectIds: [<OBJECT_ID>, <OBJECT_ID>, ...]
+ *
+ * Deletes the objecs from the backend.
+ */
+ async deleteObjects (credentials, objectIds) {
+ return { status: 'NOT_IMPLEMENTED_EXCEPTION', error: 'The provided backend does not have a deleteObject method' }
+ }
+
+ /*
* credentials: <BACKEND_CREDENTIALS>
* objects: [{ eid: <EXTERNAL_ID>, gid: <GROUP_ID> }, ...]
* EXTERNAL_ID is the id of the objects in the external backend requestet.
@@ -110,6 +120,24 @@ class ExternalBackends {
async getObjectTypes (credentials) {
return []
}
+
+ /*
+ * Adds the client to the backend.
+ *
+ * credentials: <BACKEND_CREDENTIALS>
+ * The client parameters are all optional. If the client has an id the object is not created but the categories of the object.
+ * client: {
+ * id: <CLIENT_ID>, title: <CLIENT_TITLE>, parentId: <PARENT_ID>,
+ * system: { model: <SYSTEM_MODEL>, manufacturer: <SYSTEM_MANUFACTURER>, serialnumber: <SYSTEM_SERIALNUMBER> },
+ * cpu: { model: <CPU_MODEL>, manufacturer: <CPU_MANUFACTURER>, type: <CPU_TYPE>, frequency: <CPU_FREQUENCY>, cores: <CPU_CORES> },
+ * ram: [{ model: <RAM_MODEL>, manufacturer: <RAM_MANUFACTURER>, type: <RAM_TYPE>, capacity: <RAM_CAPACITY>, unit: <RAM_UNIT> }, ...],
+ * storage: {},
+ * network: { mac: <MAC_ADDRESS>, ip: <IP_ADDRESS> }
+ * }
+ */
+ async addClient (credentials, client) {
+ return { success: false, status: 'NOT_IMPLEMENTED_EXCEPTION', error: 'The provided backend does not have an addClient method' }
+ }
}
module.exports = ExternalBackends
diff --git a/server/migrations/20181008151633-create-registrationhook.js b/server/migrations/20181008151633-create-registrationhook.js
index 59692c5..5914188 100644
--- a/server/migrations/20181008151633-create-registrationhook.js
+++ b/server/migrations/20181008151633-create-registrationhook.js
@@ -4,9 +4,16 @@ module.exports = {
return queryInterface.createTable('registrationhooks', {
id: {
primaryKey: true,
+ autoIncrement: true,
allowNull: false,
type: Sequelize.INTEGER,
},
+ name: {
+ type: Sequelize.STRING
+ },
+ description: {
+ type: Sequelize.STRING(2048)
+ },
sortvalue: Sequelize.INTEGER,
type: Sequelize.STRING,
script: {
diff --git a/server/models/registrationhook.js b/server/models/registrationhook.js
index 1e74288..0d1ea6e 100644
--- a/server/models/registrationhook.js
+++ b/server/models/registrationhook.js
@@ -7,11 +7,17 @@ module.exports = (sequelize, DataTypes) => {
primaryKey: true,
type: DataTypes.INTEGER
},
+ name: DataTypes.STRING,
+ description: DataTypes.STRING(2048),
sortvalue: DataTypes.INTEGER,
type: DataTypes.STRING,
script: {
allowNull: true,
- type: DataTypes.BLOB
+ type: DataTypes.BLOB,
+ get() {
+ var blob = this.getDataValue('script')
+ return blob ? blob.toString() : '';
+ }
}
}, {
timestamps: false
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index b97dd0d..4d73910 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -2221,9 +2221,9 @@
"dev": true
},
"copy-webpack-plugin": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.5.2.tgz",
- "integrity": "sha512-zmC33E8FFSq3AbflTvqvPvBo621H36Afsxlui91d+QyZxPIuXghfnTsa1CuqiAaCPgJoSUWfTFbKJnadZpKEbQ==",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz",
+ "integrity": "sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==",
"dev": true,
"requires": {
"cacache": "^10.0.4",
@@ -6503,13 +6503,13 @@
}
},
"node-notifier": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz",
- "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz",
+ "integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==",
"dev": true,
"requires": {
"growly": "^1.3.0",
- "semver": "^5.4.1",
+ "semver": "^5.5.0",
"shellwords": "^0.1.1",
"which": "^1.3.0"
}
@@ -7005,9 +7005,9 @@
"dev": true
},
"portfinder": {
- "version": "1.0.17",
- "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.17.tgz",
- "integrity": "sha512-syFcRIRzVI1BoEFOCaAiizwDolh1S1YXSodsVhncbhjzjZQulhczNRbqnUl9N31Q4dKGOXsNDqxC2BWBgSMqeQ==",
+ "version": "1.0.19",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.19.tgz",
+ "integrity": "sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw==",
"dev": true,
"requires": {
"async": "^1.5.2",
@@ -9969,9 +9969,9 @@
}
},
"semver": {
- "version": "5.5.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
- "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
"dev": true
},
"send": {
@@ -11046,9 +11046,9 @@
}
},
"url-loader": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.1.tgz",
- "integrity": "sha512-vugEeXjyYFBCUOpX+ZuaunbK3QXMKaQ3zUnRfIpRBlGkY7QizCnzyyn2ASfcxsvyU3ef+CJppVywnl3Kgf13Gg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz",
+ "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==",
"dev": true,
"requires": {
"loader-utils": "^1.1.0",
@@ -11057,9 +11057,9 @@
},
"dependencies": {
"ajv": {
- "version": "6.5.3",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz",
- "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==",
+ "version": "6.5.5",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
+ "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
"dev": true,
"requires": {
"fast-deep-equal": "^2.0.1",
@@ -11323,9 +11323,9 @@
}
},
"vue2-hammer": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/vue2-hammer/-/vue2-hammer-1.0.6.tgz",
- "integrity": "sha512-MfRDkMdQoEng/BRe7moIDbiMgcPqgclVD3WqNfnQ6JlKtTOyMV/X5Z8/IEySa8QGHDVDOp9fI3WmwEoKxnZQ3g==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/vue2-hammer/-/vue2-hammer-1.0.7.tgz",
+ "integrity": "sha512-ClD5i3MEDhITE5Jmc1ZtTx6q8wJeymv13FWXlj9iDxORfajF0N4DT7j5Q4dIwEwT4lXzYtNh739cIUUCw5UjKA==",
"requires": {
"hammerjs": "^2.0.8"
}
@@ -11339,9 +11339,9 @@
}
},
"vuetify": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.2.3.tgz",
- "integrity": "sha512-ssMsLVwth62T4usPAFbn8yk69qqBDp4VWPmlY4XiYZ7aGMPoa/sQ08BlAeG8J81y+K60bY88FZ2gquQKlGqlsg=="
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.3.7.tgz",
+ "integrity": "sha512-EXKQggY+SPTBlmzmcFcNI6h/xdZLv6vr9VBcos8+kUumftIG7HDqqtFdRVNCco4EPrQgiG5sWDm5raM07NDvBg=="
},
"vuex": {
"version": "3.0.1",
diff --git a/webapp/package.json b/webapp/package.json
index a1a3ff9..bc4513b 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -16,9 +16,9 @@
"vue-i18n": "^7.8.1",
"vue-router": "^3.0.1",
"vue-touch": "^2.0.0-beta.4",
- "vue2-hammer": "^1.0.6",
+ "vue2-hammer": "^1.0.7",
"vuedraggable": "^2.16.0",
- "vuetify": "^1.2.3",
+ "vuetify": "^1.3.7",
"vuex": "^3.0.1"
},
"devDependencies": {
@@ -34,7 +34,7 @@
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
- "copy-webpack-plugin": "^4.5.2",
+ "copy-webpack-plugin": "^4.6.0",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
@@ -49,18 +49,18 @@
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
- "node-notifier": "^5.1.2",
+ "node-notifier": "^5.3.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
- "portfinder": "^1.0.17",
+ "portfinder": "^1.0.19",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.1.6",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
- "semver": "^5.5.1",
+ "semver": "^5.6.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.3.0",
- "url-loader": "^1.1.1",
+ "url-loader": "^1.1.2",
"vue-loader": "^13.7.3",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.17",
diff --git a/webapp/src/components/BackendModule.vue b/webapp/src/components/BackendModule.vue
index c21ac42..874110c 100644
--- a/webapp/src/components/BackendModule.vue
+++ b/webapp/src/components/BackendModule.vue
@@ -83,7 +83,7 @@ export default {
data () {
return {
component: 'BackendModuleTable',
- tab: ''
+ tab: 0
}
},
methods: {
diff --git a/webapp/src/components/ComponentSearchTable.vue b/webapp/src/components/ComponentSearchTable.vue
index 30c3600..23aa071 100644
--- a/webapp/src/components/ComponentSearchTable.vue
+++ b/webapp/src/components/ComponentSearchTable.vue
@@ -61,6 +61,7 @@
:items="items"
:select-all="selectAll"
:item-key="itemKey"
+ :loading="loading"
v-bind="dataTableProps"
:value="value"
@input="$emit('input', $event)"
@@ -102,6 +103,10 @@ export default {
type: String,
default: () => 'id'
},
+ loading: {
+ type: Boolean,
+ default: () => false
+ },
dataTableProps: {
type: Object,
default: () => {}
diff --git a/webapp/src/components/ConfiguratorModuleConfig.vue b/webapp/src/components/ConfiguratorModuleConfig.vue
index 3e6abb4..5253086 100644
--- a/webapp/src/components/ConfiguratorModuleConfig.vue
+++ b/webapp/src/components/ConfiguratorModuleConfig.vue
@@ -20,7 +20,7 @@
"description": "Beschreibung",
"timeout": "Timeout",
"expertMode": "Expertenmodus",
- "script": "iPXE Script",
+ "script": "iPXE Skript",
"titleNew": "Konfiguration erstellen",
"titleExisting": "Konfiguration bearbeiten",
"entries": "Einträge",
diff --git a/webapp/src/components/GroupModuleClientList.vue b/webapp/src/components/GroupModuleClientList.vue
index f64563c..35819d5 100644
--- a/webapp/src/components/GroupModuleClientList.vue
+++ b/webapp/src/components/GroupModuleClientList.vue
@@ -28,7 +28,7 @@
<template>
<div>
<v-card>
- <component-search-table v-model="selected" :headers="headers" :items="clients" select-all>
+ <component-search-table v-model="selected" :headers="headers" :items="clients" select-all :loading="loading">
<template slot="items" slot-scope="row">
<tr :style="row.color" @click="row.data.selected = !row.data.selected" @dblclick="loadClient(row.data.item)">
<td class="narrow-td">
@@ -90,6 +90,9 @@ export default {
{ text: this.$t('uuid'), value: 'uuid' },
{ sortable: false }
]
+ },
+ loading () {
+ return this.$store.state.groups.tabChain[this.tabIndex].loading
}
},
watch: {
diff --git a/webapp/src/components/GroupModuleGroupList.vue b/webapp/src/components/GroupModuleGroupList.vue
index 1984ef8..896eeaa 100644
--- a/webapp/src/components/GroupModuleGroupList.vue
+++ b/webapp/src/components/GroupModuleGroupList.vue
@@ -24,7 +24,7 @@
<template>
<div>
<v-card>
- <component-search-table v-model="selected" :headers="headers" :items="groups" select-all>
+ <component-search-table v-model="selected" :headers="headers" :items="groups" select-all :loading="loading">
<template slot="items" slot-scope="row">
<tr :style="row.color" @click="row.data.selected = !row.data.selected" @dblclick="loadGroup(row.data.item)">
<td class="narrow-td">
@@ -82,6 +82,9 @@ export default {
{ text: this.$t('description'), value: 'description' },
{ sortable: false }
]
+ },
+ loading () {
+ return this.$store.state.groups.tabChain[this.tabIndex].loading
}
},
watch: {
diff --git a/webapp/src/components/PermissionModule.vue b/webapp/src/components/PermissionModule.vue
index 5abe7d6..26d4792 100644
--- a/webapp/src/components/PermissionModule.vue
+++ b/webapp/src/components/PermissionModule.vue
@@ -95,7 +95,7 @@ export default {
data () {
return {
components: ['PermissionModuleRoleList', 'PermissionModuleUserList'],
- tab: ''
+ tab: 0
}
},
computed: {
diff --git a/webapp/src/components/RegistrationModule.vue b/webapp/src/components/RegistrationModule.vue
new file mode 100644
index 0000000..f9b857f
--- /dev/null
+++ b/webapp/src/components/RegistrationModule.vue
@@ -0,0 +1,137 @@
+<i18n>
+{
+ "en": {
+ "hooks": "Registration hooks",
+ "createHook": "Create hook"
+ },
+ "de": {
+ "hooks": "Registrierungs Hooks ",
+ "createHook": "Hook erstellen"
+ }
+}
+</i18n>
+
+<template>
+ <v-container fill-height>
+ <v-layout>
+ <v-flex class="tabs-wrapper" xl10 offset-xl1 lg12>
+ <v-card>
+ <v-tabs v-model="tabs" centered :dark="tabsDark" :color="tabsColor" :slider-color="tabsSliderColor">
+ <v-tab>{{ $t('hooks') }}</v-tab>
+ </v-tabs>
+ </v-card>
+ <v-tabs-items v-model="tabs" style="padding-bottom: 20px">
+ <v-tab-item>
+ <v-subheader>{{ $t('hooks') }}</v-subheader>
+ <v-card v-if="hooks.length > 0">
+ <v-list two-line>
+ <draggable :value="hooks" @input="setHooks($event)" :options="{ handle:'.handle' }">
+ <v-list-tile v-for="hook in hooks" :key="hook.id" @click.stop @dblclick="editHook(hook)">
+ <v-list-tile-action class="handle">
+ <v-icon>drag_handle</v-icon>
+ </v-list-tile-action>
+ <v-list-tile-content>
+ <v-list-tile-title>{{ hook.name }}<small class="type">{{ hook.type }}</small></v-list-tile-title>
+ <v-list-tile-sub-title>{{ hook.description }}</v-list-tile-sub-title>
+ </v-list-tile-content>
+ <v-icon v-if="hook.groups.length > 0">device_hub</v-icon>
+ <v-list-tile-action>
+ <v-btn @click="editHook(hook)" icon><v-icon color="primary">edit</v-icon></v-btn>
+ </v-list-tile-action>
+ <v-list-tile-action class="delete">
+ <v-btn @click="deleteHook(hook)" icon><v-icon color="error">delete</v-icon></v-btn>
+ </v-list-tile-action>
+ </v-list-tile>
+ </draggable>
+ </v-list>
+ </v-card>
+ <div class="text-xs-right">
+ <v-btn flat color="success" @click="createHook"><v-icon left>create</v-icon>{{ $t('createHook') }}</v-btn>
+ </div>
+ </v-tab-item>
+ </v-tabs-items>
+ </v-flex>
+ </v-layout>
+ <v-dialog
+ :value="dialog.show"
+ @input="setDialog({ show: $event })"
+ :max-width="dialog.type === 'delete' ? '500px' : '1000px'"
+ scrollable
+ :persistent="dialog.type !== 'delete'"
+ >
+ <registration-module-delete v-if="dialog.type === 'delete'" />
+ <registration-module-edit v-else-if="dialog.type === 'edit'"/>
+ </v-dialog>
+ </v-container>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+import RegistrationModuleDelete from '@/components/RegistrationModuleDelete'
+import RegistrationModuleEdit from '@/components/RegistrationModuleEdit'
+import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
+
+export default {
+ name: 'RegistrationModule',
+ components: {
+ draggable,
+ RegistrationModuleDelete,
+ RegistrationModuleEdit
+ },
+ data () {
+ return {
+ tabs: 0
+ }
+ },
+ computed: {
+ ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor']),
+ ...mapState('registration', ['hooks', 'dialog'])
+ },
+ methods: {
+ ...mapMutations('registration', ['setDialog']),
+ ...mapActions('registration', ['setHooks']),
+ deleteHook (hook) {
+ this.setDialog({ show: true, type: 'delete', info: hook })
+ },
+ editHook (hook) {
+ this.setDialog({ show: true, type: 'edit', info: hook })
+ },
+ createHook () {
+ this.setDialog({ show: true, type: 'edit', info: {} })
+ }
+ },
+ created () {
+ this.$store.dispatch('registration/loadGroupList')
+ this.$store.dispatch('registration/loadHooks')
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.handle {
+ margin-left: 12px;
+}
+.delete {
+ margin-right: 12px;
+}
+.type {
+ margin-left: 24px;
+}
+.hook-info {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+}
+.hook-info > .hook-type {
+ min-width: 60px;
+}
+.hook-info > .hook-name {
+ white-space: nowrap;
+}
+.hook-info > .hook-description {
+ margin-left: 40px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+</style>
diff --git a/webapp/src/components/RegistrationModuleDelete.vue b/webapp/src/components/RegistrationModuleDelete.vue
new file mode 100644
index 0000000..002e629
--- /dev/null
+++ b/webapp/src/components/RegistrationModuleDelete.vue
@@ -0,0 +1,63 @@
+<i18n>
+{
+ "en": {
+ "title": "Delete this hook?"
+ },
+ "de": {
+ "title": "Diesen Hook löschen?"
+ }
+}
+</i18n>
+
+<template>
+ <v-card>
+ <v-card-title primary-title class="dialog-title elevation-3">
+ <div class="headline">{{ $t('title') }}</div>
+ </v-card-title>
+ <v-card-text>
+ {{ dialog.info.type }} {{ dialog.info.name }}
+ </v-card-text>
+ <v-divider></v-divider>
+ <v-card-actions>
+ <v-spacer></v-spacer>
+ <v-btn flat="flat" @click="setDialog({ show: false })">{{ $t('cancel') }}</v-btn>
+ <v-btn color="error" @click="deleteItems">{{ $t('delete') }}</v-btn>
+ </v-card-actions>
+ </v-card>
+</template>
+
+<script>
+import axios from 'axios'
+import { mapState } from 'vuex'
+
+export default {
+ name: 'RegistrationModuleDelete',
+ data () {
+ return {
+ }
+ },
+ computed: {
+ ...mapState('registration', ['dialog'])
+ },
+ methods: {
+ setDialog (data) {
+ this.$store.commit('registration/setDialog', data)
+ },
+ async deleteItems () {
+ await axios.delete('/api/registrations/hooks/' + this.dialog.info.id)
+ this.$store.dispatch('registration/loadHooks')
+ this.setDialog({ show: false })
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.dialog-title {
+ z-index: 1;
+}
+.selected-list {
+ padding: 30px;
+}
+</style>
diff --git a/webapp/src/components/RegistrationModuleEdit.vue b/webapp/src/components/RegistrationModuleEdit.vue
new file mode 100644
index 0000000..302cc2e
--- /dev/null
+++ b/webapp/src/components/RegistrationModuleEdit.vue
@@ -0,0 +1,120 @@
+<i18n>
+{
+ "en": {
+ "name": "Name",
+ "description": "Description",
+ "type": "Script type",
+ "groups": "Groups",
+ "script": "Script",
+ "titleNew": "Create hook",
+ "titleExisting": "Edit hook"
+ },
+ "de": {
+ "name": "Name",
+ "description": "Beschreibung",
+ "type": "Skript Art",
+ "groups": "Gruppen",
+ "script": "Skript",
+ "titleNew": "Hook erstellen",
+ "titleExisting": "Hook bearbeiten"
+ }
+}
+</i18n>
+
+<template>
+ <v-card>
+ <v-card-title primary-title class="dialog-title elevation-3">
+ <div class="headline">{{ dialog.info.id ? $t('titleExisting') : $t('titleNew') }}</div>
+ </v-card-title>
+ <v-card-text>
+ <v-layout row wrap>
+ <v-flex xs12 sm9>
+ <v-text-field prepend-icon="label" :label="$t('name')" color="primary" v-model="name"></v-text-field>
+ </v-flex>
+ <v-flex xs12 sm2 offset-sm1>
+ <v-select prepend-icon="label" color="primary" :items="types" :label="$t('type')" v-model="type" menu-props="offsetY"></v-select>
+ </v-flex>
+ </v-layout>
+ <v-autocomplete
+ prepend-icon="device_hub"
+ :items="groupList"
+ v-model="groups"
+ :label="$t('groups')"
+ color="primary"
+ multiple
+ item-value="id"
+ item-text="name"
+ small-chips
+ deletable-chips
+ ></v-autocomplete>
+ <v-textarea prepend-icon="description" rows="3" :label="$t('description')" color="primary" v-model="description"></v-textarea>
+ <v-textarea prepend-icon="code" rows="20" :label="$t('script')" color="primary" v-model="script"></v-textarea>
+ </v-card-text>
+ <v-divider></v-divider>
+ <v-card-actions>
+ <v-spacer></v-spacer>
+ <v-btn flat="flat" @click="setDialog({ show: false })">{{ $t('cancel') }}</v-btn>
+ <v-btn :color="dialog.info.id ? 'primary' : 'success'" @click="saveHook">{{ dialog.info.id ? $t('save') : $t('create') }}</v-btn>
+ </v-card-actions>
+ </v-card>
+</template>
+
+<script>
+import axios from 'axios'
+import { mapState } from 'vuex'
+
+export default {
+ name: 'RegistrationModuleEdit',
+ data () {
+ return {
+ name: '',
+ description: '',
+ type: '',
+ groups: [],
+ script: '',
+ types: ['BASH', 'IPXE']
+ }
+ },
+ computed: {
+ ...mapState('registration', ['dialog', 'groupList'])
+ },
+ watch: {
+ dialog: {
+ immediate: true,
+ deep: true,
+ handler (value) {
+ if (value.type === 'edit' && value.show) {
+ this.name = value.info.name
+ this.description = value.info.description
+ this.type = value.info.type || 'BASH'
+ this.groups = value.info.groups ? value.info.groups.map(x => x.id) : []
+ this.script = value.info.script
+ }
+ }
+ }
+ },
+ methods: {
+ setDialog (data) {
+ this.$store.commit('registration/setDialog', data)
+ },
+ async saveHook () {
+ await axios.post('/api/registrations/hooks/' + this.dialog.info.id, {
+ name: this.name,
+ description: this.description,
+ type: this.type,
+ groups: this.groups,
+ script: this.script
+ })
+ this.$store.dispatch('registration/loadHooks')
+ this.setDialog({ show: false })
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.dialog-title {
+ z-index: 1;
+}
+</style>
diff --git a/webapp/src/components/SettingsModule.vue b/webapp/src/components/SettingsModule.vue
index ed23cc9..642e52a 100644
--- a/webapp/src/components/SettingsModule.vue
+++ b/webapp/src/components/SettingsModule.vue
@@ -74,7 +74,7 @@ export default {
data () {
return {
langChoices: [ { text: 'Deutsch', value: 'de' }, { text: 'English', value: 'en' } ],
- tab: ''
+ tab: 0
}
},
computed: {
diff --git a/webapp/src/config/dashboard.js b/webapp/src/config/dashboard.js
index 44c894f..a092cdc 100644
--- a/webapp/src/config/dashboard.js
+++ b/webapp/src/config/dashboard.js
@@ -1,11 +1,13 @@
import GroupModule from '@/components/GroupModule'
import ConfiguratorModule from '@/components/ConfiguratorModule'
+import RegistrationModule from '@/components/RegistrationModule'
import BackendModule from '@/components/BackendModule'
import PermissionModule from '@/components/PermissionModule'
export default [
{ path: 'groups', component: GroupModule, icon: 'category' },
{ path: 'configurator', component: ConfiguratorModule, icon: 'list' },
+ { path: 'registration', component: RegistrationModule, icon: 'assignment' },
{ path: 'backends', component: BackendModule, icon: 'cloud' },
{ path: 'permissions', component: PermissionModule, icon: 'lock_open' }
]
diff --git a/webapp/src/config/i18n.js b/webapp/src/config/i18n.js
index 91f8a79..818f57c 100644
--- a/webapp/src/config/i18n.js
+++ b/webapp/src/config/i18n.js
@@ -25,6 +25,7 @@ export default {
'$dashboardModules': {
'GroupModule': 'Groups / Clients',
'ConfiguratorModule': 'iPXE Configurator',
+ 'RegistrationModule': 'Client Registration',
'BackendModule': 'External Backends',
'PermissionModule': 'Permission Manager'
}
@@ -55,6 +56,7 @@ export default {
'$dashboardModules': {
'GroupModule': 'Gruppen / Clienten',
'ConfiguratorModule': 'iPXE Konfigurator',
+ 'RegistrationModule': 'Client Registrierung',
'BackendModule': 'Externe Backends',
'PermissionModule': 'Rechteverwaltung'
}
diff --git a/webapp/src/config/store.js b/webapp/src/config/store.js
index ca6b01f..710a07b 100644
--- a/webapp/src/config/store.js
+++ b/webapp/src/config/store.js
@@ -1,11 +1,13 @@
import groups from '@/store/groups'
import configurator from '@/store/configurator'
+import registration from '@/store/registration'
import backends from '@/store/backends'
import permissions from '@/store/permissions'
export default {
groups,
configurator,
+ registration,
backends,
permissions
}
diff --git a/webapp/src/store/groups.js b/webapp/src/store/groups.js
index 12eea4f..1bbded9 100644
--- a/webapp/src/store/groups.js
+++ b/webapp/src/store/groups.js
@@ -1,3 +1,4 @@
+import Vue from 'vue'
import axios from 'axios'
export default {
@@ -34,6 +35,9 @@ export default {
}
state.tabChain.splice(index, 1, item)
},
+ setTabLoading (state, index) {
+ if (state.tabChain.length > index) Vue.set(state.tabChain[index], 'loading', true)
+ },
setDialog (state, { show, info }) {
if (info !== undefined) state.dialog.info = info
if (show !== undefined) state.dialog.show = show
@@ -61,6 +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 (switchTab) context.commit('setActiveTab', tabIndex)
axios.get('/api/groups/getGroup?id=' + id + (showAll ? '&all=true' : '')).then(res => {
res.data.tabType = 'group'
diff --git a/webapp/src/store/registration.js b/webapp/src/store/registration.js
new file mode 100644
index 0000000..388558d
--- /dev/null
+++ b/webapp/src/store/registration.js
@@ -0,0 +1,40 @@
+import axios from 'axios'
+
+export default {
+ namespaced: true,
+ state: {
+ hooks: [],
+ groupList: [],
+ dialog: {
+ show: false,
+ type: null,
+ info: {}
+ }
+ },
+ mutations: {
+ setHooks (state, hooks) { state.hooks = hooks },
+ setGroupList (state, groupList) { state.groupList = groupList },
+ setDialog (state, { show, type, info }) {
+ if (info !== undefined) state.dialog.info = info
+ if (type !== undefined) state.dialog.type = type
+ if (show !== undefined) state.dialog.show = show
+ }
+ },
+ actions: {
+ loadHooks (context) {
+ axios.get('/api/registrations/hooks').then(result => {
+ context.commit('setHooks', result.data)
+ })
+ },
+ loadGroupList (context) {
+ axios.get('/api/groups/getList').then(result => {
+ context.commit('setGroupList', result.data)
+ })
+ },
+ setHooks (context, hooks) {
+ axios.post('/api/registrations/hookorder', { ids: hooks.map(x => x.id) }).then(result => {
+ context.commit('setHooks', hooks)
+ })
+ }
+ }
+}