summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUdo Walter2018-09-18 10:33:04 +0200
committerUdo Walter2018-09-18 10:33:04 +0200
commit1d07a975a2562b800fe183c6560f4a3b84ee307a (patch)
tree07f886f07047016af3d9a7565e4abedf497c4c2f
parent[groups] small bugfixes (diff)
downloadbas-1d07a975a2562b800fe183c6560f4a3b84ee307a.tar.gz
bas-1d07a975a2562b800fe183c6560f4a3b84ee307a.tar.xz
bas-1d07a975a2562b800fe183c6560f4a3b84ee307a.zip
[configurator] add ipxe configurator
-rw-r--r--server/api/configurator.js80
-rw-r--r--server/migrations/20180917202533-create-config_x_entry.js4
-rw-r--r--webapp/package-lock.json17
-rw-r--r--webapp/package.json1
-rw-r--r--webapp/src/components/ConfiguratorModule.vue188
-rw-r--r--webapp/src/components/ConfiguratorModuleConfig.vue193
-rw-r--r--webapp/src/components/ConfiguratorModuleDelete.vue65
-rw-r--r--webapp/src/components/ConfiguratorModuleEntry.vue84
-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/configurator.js33
-rw-r--r--webapp/src/store/groups.js3
13 files changed, 668 insertions, 6 deletions
diff --git a/server/api/configurator.js b/server/api/configurator.js
new file mode 100644
index 0000000..74f49bd
--- /dev/null
+++ b/server/api/configurator.js
@@ -0,0 +1,80 @@
+/* global __appdir */
+var path = require('path')
+var db = require(path.join(__appdir, 'lib', 'sequelize'))
+var express = require('express')
+var router = express.Router()
+
+router.get('/configs', (req, res) => {
+ db.config.findAll().then(configs => {
+ res.send(configs)
+ })
+})
+
+router.get('/entries', (req, res) => {
+ db.entry.findAll().then(entries => {
+ res.send(entries)
+ })
+})
+
+router.get('/configs/:id/entries', async (req, res) => {
+ var config = await db.config.findOne({ where: { id: req.params.id } })
+ var entries = await config.getEntries()
+ entries.sort((a, b) => a.config_x_entry.sortValue - b.config_x_entry.sortValue)
+ res.send(entries)
+})
+
+router.post(['/configs', '/configs/:id'], async (req, res) => {
+ var item = {
+ name: req.body.name,
+ description: req.body.description,
+ defaultEntry: req.body.defaultEntry,
+ timeout: req.body.timeout > 0 ? req.body.timeout : null,
+ script: req.body.script
+ }
+
+ var config = null
+ if (req.params.id > 0) {
+ config = await db.config.findOne({ where: { id: req.params.id } })
+ if (config) await config.update(item)
+ } else {
+ config = await db.config.create(item)
+ }
+
+ if (config) {
+ await config.setEntries([])
+ if (req.body.entries.length > 0) {
+ var promises = []
+ req.body.entries.forEach((entry, index) => {
+ promises.push(config.addEntry(entry.id, { through: { sortValue: index, customName: entry.customName, keyBind: entry.keyBind } }))
+ })
+ await Promise.all(promises)
+ }
+ res.send({ id: config.id })
+ }
+ res.end()
+})
+
+router.post(['/entries', '/entries/:id'], async (req, res) => {
+ var item = { name: req.body.name, script: req.body.script }
+ var entry = null
+ if (req.params.id > 0) {
+ entry = await db.entry.findOne({ where: { id: req.params.id } })
+ if (entry) await entry.update(item)
+ } else {
+ await db.entry.create(item)
+ }
+ if (entry) {
+ res.send({ id: entry.id })
+ }
+ res.end()
+})
+
+router.post('/delete/configs', (req, res) => {
+ db.config.destroy({ where: { id: req.body.ids } }).then(count => { res.send({ count }) })
+})
+
+router.post('/delete/entries', (req, res) => {
+ db.entry.destroy({ where: { id: req.body.ids } }).then(count => { res.send({ count }) })
+})
+
+module.exports.router = router
diff --git a/server/migrations/20180917202533-create-config_x_entry.js b/server/migrations/20180917202533-create-config_x_entry.js
index 4e32ea9..5b04cf7 100644
--- a/server/migrations/20180917202533-create-config_x_entry.js
+++ b/server/migrations/20180917202533-create-config_x_entry.js
@@ -2,7 +2,7 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('config_x_entry', {
- groupId: {
+ configId: {
primaryKey: true,
allowNull: false,
type: Sequelize.INTEGER,
@@ -12,7 +12,7 @@ module.exports = {
key: 'id'
}
},
- clientId: {
+ entryId: {
primaryKey: true,
allowNull: false,
type: Sequelize.INTEGER,
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index 87792b0..d14d0c9 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -10298,6 +10298,11 @@
"is-plain-obj": "^1.0.0"
}
},
+ "sortablejs": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.7.0.tgz",
+ "integrity": "sha1-gKKyNwq9Vo4c7IwnETHvMKkE+ig="
+ },
"source-list-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",
@@ -11325,6 +11330,14 @@
"hammerjs": "^2.0.8"
}
},
+ "vuedraggable": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.16.0.tgz",
+ "integrity": "sha512-fr9gcWKXMJlzbbtJcrQs4kU7qdOZqd4SEpAcx+r0nykbW8AygZN0aKVpadEtI53T8A2azhzCdXMvEqrLuKE2fA==",
+ "requires": {
+ "sortablejs": "^1.7.0"
+ }
+ },
"vuetify": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.2.3.tgz",
@@ -11645,7 +11658,7 @@
},
"os-locale": {
"version": "1.4.0",
- "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
@@ -11735,7 +11748,7 @@
},
"yargs": {
"version": "6.6.0",
- "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
"integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
"dev": true,
"requires": {
diff --git a/webapp/package.json b/webapp/package.json
index a92f489..a1a3ff9 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -17,6 +17,7 @@
"vue-router": "^3.0.1",
"vue-touch": "^2.0.0-beta.4",
"vue2-hammer": "^1.0.6",
+ "vuedraggable": "^2.16.0",
"vuetify": "^1.2.3",
"vuex": "^3.0.1"
},
diff --git a/webapp/src/components/ConfiguratorModule.vue b/webapp/src/components/ConfiguratorModule.vue
new file mode 100644
index 0000000..4cb278a
--- /dev/null
+++ b/webapp/src/components/ConfiguratorModule.vue
@@ -0,0 +1,188 @@
+<i18n>
+{
+ "en": {
+ "id": "ID",
+ "name": "Name",
+ "description": "Description",
+ "entries": "Entries",
+ "configs": "Configs",
+ "deleteConfigs": "Delete one config | Delete {0} configs",
+ "createConfig": "Create config",
+ "deleteEntries": "Delete one entry | Delete {0} entries",
+ "createEntry": "Create entry"
+ },
+ "de": {
+ "id": "ID",
+ "name": "Name",
+ "description": "Beschreibung",
+ "entries": "Einträge",
+ "configs": "Konfigurationen",
+ "deleteConfigs": "Lösche eine Konfiguration | Lösche {0} Konfigurationen",
+ "createConfig": "Konfiguration erstellen",
+ "deleteEntries": "Lösche einen Eintrag | Lösche {0} Einträge",
+ "createEntry": "Eintrag 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('configs') }}</v-tab>
+ <v-tab>{{ $t('entries') }}</v-tab>
+ </v-tabs>
+ </v-card>
+ <v-tabs-items v-model="tabs" style="padding-bottom: 20px">
+ <v-tab-item>
+ <v-subheader>{{ $t('configs') }}</v-subheader>
+ <v-card>
+ <component-search-table v-model="selectedConfigs" :headers="configHeaders" :items="configs" select-all>
+ <template slot="items" slot-scope="row">
+ <tr :style="row.color" @click="row.data.selected = !row.data.selected" @dblclick="editConfig(row.data.item)">
+ <td style="width: 10px">
+ <v-checkbox
+ color="primary"
+ v-model="row.data.selected"
+ hide-details
+ ></v-checkbox>
+ </td>
+ <td class="narrow-td">{{ row.data.item.id }}</td>
+ <td>{{ row.data.item.name }}</td>
+ <td>{{ row.data.item.description }}</td>
+ <td class="narrow-td">
+ <v-btn icon @click.stop="editConfig(row.data.item)"><v-icon color="primary">edit</v-icon></v-btn>
+ </td>
+ </tr>
+ </template>
+ </component-search-table>
+ </v-card>
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="deleteSelectedConfigs" :disabled="selectedConfigs.length === 0">
+ <v-icon left>delete</v-icon>{{ $tc('deleteConfigs', selectedConfigs.length, [selectedConfigs.length]) }}
+ </v-btn>
+ <v-btn flat color="success" @click="createConfig"><v-icon left>create</v-icon>{{ $t('createConfig') }}</v-btn>
+ </div>
+ </v-tab-item>
+ <v-tab-item>
+ <v-subheader>{{ $t('entries') }}</v-subheader>
+ <v-card>
+ <component-search-table v-model="selectedEntries" :headers="entryHeaders" :items="entries" select-all>
+ <template slot="items" slot-scope="row">
+ <tr :style="row.color" @click="row.data.selected = !row.data.selected" @dblclick="editEntry(row.data.item)">
+ <td style="width: 10px">
+ <v-checkbox
+ color="primary"
+ v-model="row.data.selected"
+ hide-details
+ ></v-checkbox>
+ </td>
+ <td class="narrow-td">{{ row.data.item.id }}</td>
+ <td>{{ row.data.item.name }}</td>
+ <td class="narrow-td">
+ <v-btn icon @click.stop="editEntry(row.data.item)"><v-icon color="primary">edit</v-icon></v-btn>
+ </td>
+ </tr>
+ </template>
+ </component-search-table>
+ </v-card>
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="deleteSelectedEntries" :disabled="selectedEntries.length === 0">
+ <v-icon left>delete</v-icon>{{ $tc('deleteEntries', selectedEntries.length, [selectedEntries.length]) }}
+ </v-btn>
+ <v-btn flat color="success" @click="createEntry"><v-icon left>create</v-icon>{{ $t('createEntry') }}</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'"
+ >
+ <configurator-module-delete v-if="dialog.type === 'delete'" />
+ <configurator-module-config v-else-if="dialog.type === 'config'"/>
+ <configurator-module-entry v-else-if="dialog.type === 'entry'" />
+ </v-dialog>
+ </v-container>
+</template>
+
+<script>
+import ComponentSearchTable from '@/components/ComponentSearchTable'
+import ConfiguratorModuleDelete from '@/components/ConfiguratorModuleDelete'
+import ConfiguratorModuleConfig from '@/components/ConfiguratorModuleConfig'
+import ConfiguratorModuleEntry from '@/components/ConfiguratorModuleEntry'
+import { mapState, mapGetters, mapMutations } from 'vuex'
+
+export default {
+ name: 'ConfiguratorModule',
+ components: {
+ ComponentSearchTable,
+ ConfiguratorModuleDelete,
+ ConfiguratorModuleConfig,
+ ConfiguratorModuleEntry
+ },
+ data () {
+ return {
+ tabs: 0,
+ selectedConfigs: [],
+ selectedEntries: []
+ }
+ },
+ computed: {
+ ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor']),
+ ...mapState('configurator', ['configs', 'entries', 'dialog']),
+ configHeaders () {
+ return [
+ { text: this.$t('id'), value: 'id' },
+ { text: this.$t('name'), value: 'name' },
+ { text: this.$t('description'), value: 'description' },
+ { sortable: false }
+ ]
+ },
+ entryHeaders () {
+ return [
+ { text: this.$t('id'), value: 'id' },
+ { text: this.$t('name'), value: 'name' },
+ { sortable: false }
+ ]
+ }
+ },
+ methods: {
+ ...mapMutations('configurator', ['setDialog']),
+ deleteSelectedConfigs () {
+ this.setDialog({ show: true, type: 'delete', info: { itemType: 'configs', selected: this.selectedConfigs } })
+ },
+ deleteSelectedEntries () {
+ this.setDialog({ show: true, type: 'delete', info: { itemType: 'entries', selected: this.selectedEntries } })
+ },
+ createConfig () {
+ this.setDialog({ show: true, type: 'config', info: {} })
+ },
+ createEntry () {
+ this.setDialog({ show: true, type: 'entry', info: {} })
+ },
+ editConfig (item) {
+ this.setDialog({ show: true, type: 'config', info: item })
+ },
+ editEntry (item) {
+ this.setDialog({ show: true, type: 'entry', info: item })
+ }
+ },
+ created () {
+ this.$store.dispatch('configurator/loadData')
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.narrow-td {
+ width: 10px;
+}
+</style>
diff --git a/webapp/src/components/ConfiguratorModuleConfig.vue b/webapp/src/components/ConfiguratorModuleConfig.vue
new file mode 100644
index 0000000..7413a90
--- /dev/null
+++ b/webapp/src/components/ConfiguratorModuleConfig.vue
@@ -0,0 +1,193 @@
+<i18n>
+{
+ "en": {
+ "name": "Name",
+ "description": "Description",
+ "timeout": "Timeout",
+ "expertMode": "Expert Mode",
+ "script": "iPXE Script",
+ "titleNew": "Create config",
+ "titleExisting": "Edit config",
+ "entries": "Entries",
+ "customName": "Custom Name",
+ "keyBind": "Key Bind",
+ "defaultEntry": "Mark this entry as default",
+ "entry": "Entry",
+ "spacer": "[Spacer]"
+ },
+ "de": {
+ "name": "Name",
+ "description": "Beschreibung",
+ "timeout": "Timeout",
+ "expertMode": "Expertenmodus",
+ "script": "iPXE Script",
+ "titleNew": "Konfiguration erstellen",
+ "titleExisting": "Konfiguration bearbeiten",
+ "entries": "Einträge",
+ "customName": "Alternativer Name",
+ "keyBind": "Tastenbelegung",
+ "defaultEntry": "Diesen Eintrag als Standard markieren",
+ "entry": "Eintrag",
+ "spacer": "[Abstandshalter]"
+ }
+}
+</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 wrap>
+ <v-flex>
+ <v-text-field prepend-icon="label" :label="$t('name')" color="primary" v-model="name"></v-text-field>
+ </v-flex>
+ <v-flex class="text-xs-center">
+ <div style="display: inline-block; max-width: 180px">
+ <v-text-field prepend-icon="timer" :label="$t('timeout')" color="primary" v-model="timeout" mask="######" suffix="ms"></v-text-field>
+ </div>
+ </v-flex>
+ <v-flex class="text-xs-center">
+ <div style="display: inline-block"><v-switch v-model="expertMode" color="primary" :label="$t('expertMode')"></v-switch></div>
+ </v-flex>
+ </v-layout>
+ <v-textarea prepend-icon="description" rows="1" :label="$t('description')" color="primary" v-model="description"></v-textarea>
+ <v-textarea v-if="expertMode" prepend-icon="code" rows="20" :label="$t('script')" color="primary" v-model="script"></v-textarea>
+ <div v-else class="text-xs-right">
+ <v-subheader>{{ $t('entries') }}</v-subheader>
+ <v-list>
+ <draggable v-model="items" :options="{handle:'.handle'}">
+ <v-list-tile v-for="item in items" :key="item.entry.id" @click.stop>
+ <v-list-tile-action class="handle">
+ <v-icon>drag_handle</v-icon>
+ </v-list-tile-action>
+ <v-list-tile-action>
+ <v-tooltip top open-delay="800">
+ <v-radio-group slot="activator" v-model="defaultEntry">
+ <v-radio v-if="item.entry.id" color="primary" :value="item.entry.id"></v-radio>
+ </v-radio-group>
+ <span>{{ $t('defaultEntry') }}</span></v-tooltip>
+ </v-list-tile-action>
+ <v-list-tile-content class="item-content">
+ <v-select return-object style="max-width: 280px" item-text="name" offset-y :label="$t('entry')" color="primary" v-model="item.entry"
+ :items="item.entry.id ? [item.entry, ...availableEntries] : availableEntries"></v-select>
+ <v-text-field v-if="item.entry.id" class="custom-name-input" :label="$t('customName')" color="primary" v-model="item.customName"></v-text-field>
+ <v-text-field v-if="item.entry.id" style="max-width: 60px" prepend-inner-icon="keyboard" color="primary" v-model="item.keyBind" maxlength="1"></v-text-field>
+ </v-list-tile-content>
+ <v-list-tile-action>
+ <v-btn @click="removeItem(item)" icon><v-icon>clear</v-icon></v-btn>
+ </v-list-tile-action>
+ </v-list-tile>
+ </draggable>
+ </v-list>
+ <v-btn @click="addItem" color="success" fab small><v-icon dark>add</v-icon></v-btn>
+ </div>
+ </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="saveConfig">{{ dialog.info.id ? $t('save') : $t('create') }}</v-btn>
+ </v-card-actions>
+ </v-card>
+</template>
+
+<script>
+import axios from 'axios'
+import draggable from 'vuedraggable'
+import ComponentSearchTable from '@/components/ComponentSearchTable'
+import { mapState } from 'vuex'
+
+export default {
+ name: 'ConfiguratorModuleConfig',
+ components: {
+ draggable,
+ ComponentSearchTable
+ },
+ data () {
+ return {
+ name: '',
+ description: '',
+ defaultEntry: null,
+ timeout: '',
+ script: '',
+ expertMode: false,
+ items: []
+ }
+ },
+ computed: {
+ ...mapState('configurator', ['dialog', 'entries']),
+ availableEntries () {
+ var selectedEntryIds = this.items.map(x => x.entry.id)
+ return [...this.entries.filter(x => !selectedEntryIds.includes(x.id)), {id: 0, name: this.$t('spacer')}]
+ }
+ },
+ watch: {
+ dialog: {
+ immediate: true,
+ deep: true,
+ async handler (value) {
+ if (value.type === 'config' && value.show) {
+ this.name = value.info.name
+ this.description = value.info.description
+ this.defaultEntry = value.info.defaultEntry
+ this.timeout = value.info.timeout
+ this.script = value.info.script
+ if (this.script) this.expertMode = true
+ this.items = []
+ var result = await axios.get('/api/configurator/configs/' + value.info.id + '/entries')
+ this.items = result.data.map(x => ({
+ entry: { id: x.id, name: x.name, script: x.script },
+ customName: x.config_x_entry.customName,
+ keyBind: x.config_x_entry.keyBind
+ }))
+ }
+ }
+ }
+ },
+ methods: {
+ setDialog (data) {
+ this.$store.commit('configurator/setDialog', data)
+ },
+ addItem () {
+ this.items.push({ entry: {} })
+ },
+ removeItem (item) {
+ this.items.splice(this.items.indexOf(item), 1)
+ },
+ async saveConfig () {
+ await axios.post('/api/configurator/configs/' + this.dialog.info.id, {
+ name: this.name,
+ description: this.description,
+ defaultEntry: this.defaultEntry,
+ timeout: this.timeout,
+ script: this.script,
+ entries: this.items.map(x => ({
+ id: x.entry.id,
+ customName: x.customName,
+ keyBind: x.keyBind
+ }))
+ })
+ this.$store.dispatch('configurator/loadData')
+ this.setDialog({ show: false })
+ }
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.dialog-title {
+ z-index: 1;
+}
+.item-content {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+}
+.custom-name-input {
+ margin-left: 20px;
+ margin-right: 20px;
+}
+</style>
diff --git a/webapp/src/components/ConfiguratorModuleDelete.vue b/webapp/src/components/ConfiguratorModuleDelete.vue
new file mode 100644
index 0000000..dc9fe72
--- /dev/null
+++ b/webapp/src/components/ConfiguratorModuleDelete.vue
@@ -0,0 +1,65 @@
+<i18n>
+{
+ "en": {
+ "title": "Delete this config? | Delete these {0} configs?"
+ },
+ "de": {
+ "title": "Diese Konfiguration löschen? | Diese {0} Konfigurationen löschen?"
+ }
+}
+</i18n>
+
+<template>
+ <v-card>
+ <v-card-title primary-title class="dialog-title elevation-3">
+ <div class="headline">{{ $tc('title', dialog.info.selected.length, [dialog.info.selected.length]) }}</div>
+ </v-card-title>
+ <v-card-text>
+ <div v-for="item in dialog.info.selected" class="grey--text" :key="item.id">[{{ item.id }}] {{ item.name }}</div>
+ </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: 'ConfiguratorModuleDelete',
+ data () {
+ return {
+ }
+ },
+ computed: {
+ ...mapState('configurator', ['dialog'])
+ },
+ methods: {
+ setDialog (data) {
+ this.$store.commit('configurator/setDialog', data)
+ },
+ async deleteItems () {
+ await axios.post('/api/configurator/delete/' + this.dialog.info.itemType, {
+ ids: this.dialog.info.selected.map(x => x.id)
+ })
+ this.$store.dispatch('configurator/loadData')
+ 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/ConfiguratorModuleEntry.vue b/webapp/src/components/ConfiguratorModuleEntry.vue
new file mode 100644
index 0000000..7907742
--- /dev/null
+++ b/webapp/src/components/ConfiguratorModuleEntry.vue
@@ -0,0 +1,84 @@
+<i18n>
+{
+ "en": {
+ "name": "Name",
+ "script": "iPXE Script",
+ "titleNew": "Create entry",
+ "titleExisting": "Edit entry"
+ },
+ "de": {
+ "name": "Name",
+ "script": "iPXE Script",
+ "titleNew": "Eintrag erstellen",
+ "titleExisting": "Eintrag 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-text-field prepend-icon="label" :label="$t('name')" color="primary" v-model="name"></v-text-field>
+ <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="saveEntry">{{ 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: 'ConfiguratorModuleEntry',
+ data () {
+ return {
+ name: '',
+ script: ''
+ }
+ },
+ computed: {
+ ...mapState('configurator', ['dialog'])
+ },
+ watch: {
+ dialog: {
+ immediate: true,
+ deep: true,
+ handler (value) {
+ if (value.type === 'entry' && value.show) {
+ this.name = value.info.name
+ this.script = value.info.script
+ }
+ }
+ }
+ },
+ methods: {
+ setDialog (data) {
+ this.$store.commit('configurator/setDialog', data)
+ },
+ async saveEntry () {
+ await axios.post('/api/configurator/entries/' + this.dialog.info.id, {
+ name: this.name,
+ script: this.script
+ })
+ this.$store.dispatch('configurator/loadData')
+ 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/config/dashboard.js b/webapp/src/config/dashboard.js
index c03c9a3..44c894f 100644
--- a/webapp/src/config/dashboard.js
+++ b/webapp/src/config/dashboard.js
@@ -1,9 +1,11 @@
import GroupModule from '@/components/GroupModule'
+import ConfiguratorModule from '@/components/ConfiguratorModule'
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: '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 078245d..91f8a79 100644
--- a/webapp/src/config/i18n.js
+++ b/webapp/src/config/i18n.js
@@ -24,6 +24,7 @@ export default {
},
'$dashboardModules': {
'GroupModule': 'Groups / Clients',
+ 'ConfiguratorModule': 'iPXE Configurator',
'BackendModule': 'External Backends',
'PermissionModule': 'Permission Manager'
}
@@ -53,6 +54,7 @@ export default {
},
'$dashboardModules': {
'GroupModule': 'Gruppen / Clienten',
+ 'ConfiguratorModule': 'iPXE Konfigurator',
'BackendModule': 'Externe Backends',
'PermissionModule': 'Rechteverwaltung'
}
diff --git a/webapp/src/config/store.js b/webapp/src/config/store.js
index 6975cee..ca6b01f 100644
--- a/webapp/src/config/store.js
+++ b/webapp/src/config/store.js
@@ -1,9 +1,11 @@
import groups from '@/store/groups'
+import configurator from '@/store/configurator'
import backends from '@/store/backends'
import permissions from '@/store/permissions'
export default {
groups,
+ configurator,
backends,
permissions
}
diff --git a/webapp/src/store/configurator.js b/webapp/src/store/configurator.js
new file mode 100644
index 0000000..5624c41
--- /dev/null
+++ b/webapp/src/store/configurator.js
@@ -0,0 +1,33 @@
+import axios from 'axios'
+
+export default {
+ namespaced: true,
+ state: {
+ configs: [],
+ entries: [],
+ dialog: {
+ show: false,
+ type: null,
+ info: {}
+ }
+ },
+ mutations: {
+ setConfigs (state, configs) { state.configs = configs },
+ setEntries (state, entries) { state.entries = entries },
+ 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: {
+ loadData (context) {
+ axios.get('/api/configurator/configs').then(result => {
+ context.commit('setConfigs', result.data)
+ })
+ axios.get('/api/configurator/entries').then(result => {
+ context.commit('setEntries', result.data)
+ })
+ }
+ }
+}
diff --git a/webapp/src/store/groups.js b/webapp/src/store/groups.js
index 4d206de..39875d1 100644
--- a/webapp/src/store/groups.js
+++ b/webapp/src/store/groups.js
@@ -19,13 +19,12 @@ export default {
setShowAll: (state, { index, value }) => { state.tabChain[index].tabShowAll = value },
deleteFromTabChain: (state, { index, count }) => { state.tabChain.splice(index, count) },
setTab: (state, { index, item }) => {
- console.log(item)
if (state.tabChain.length > index + 1 && (state.tabChain[index].tabType !== item.tabType || state.tabChain[index].id !== item.id)) {
state.tabChain = state.tabChain.slice(0, index + 1)
}
state.tabChain.splice(index, 1, item)
},
- setDialog (state, { show, info, selected }) {
+ setDialog (state, { show, info }) {
if (info !== undefined) state.dialog.info = info
if (show !== undefined) state.dialog.show = show
}