summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/api/ipxe.js63
-rw-r--r--server/ipxe/embedded_bios.ipxe15
-rw-r--r--server/ipxe/embedded_efi.ipxe (renamed from server/ipxe/main.ipxe)5
-rw-r--r--server/lib/shell.js143
-rw-r--r--server/lib/socketio.js1
-rw-r--r--webapp/src/assets/styles.css6
-rw-r--r--webapp/src/components/DashboardPage.vue2
-rw-r--r--webapp/src/components/DataTable.vue556
-rw-r--r--webapp/src/components/DataTableItem.vue88
-rw-r--r--webapp/src/components/GroupModuleGroupList.vue22
-rw-r--r--webapp/src/components/IpxeBuilderModule.vue63
-rw-r--r--webapp/src/components/IpxeBuilderModuleConfig.vue224
-rw-r--r--webapp/src/config/dashboard.js4
-rw-r--r--webapp/src/config/i18n.js6
-rw-r--r--webapp/src/main.js4
15 files changed, 976 insertions, 226 deletions
diff --git a/server/api/ipxe.js b/server/api/ipxe.js
index f50d9d3..549eb6f 100644
--- a/server/api/ipxe.js
+++ b/server/api/ipxe.js
@@ -7,12 +7,71 @@ var router = express.Router()
var noAuthRouter = express.Router()
// GET requests.
+router.get('/build', (req, res) => {
+ shell.buildIpxe(req, res)
+})
+
+router.get('/:version/script', (req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.sendFile(path.join(__appdir, 'ipxe', 'embedded_' + req.params.version + '.ipxe'))
+})
+
+router.get('/:version/certificate', (req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.sendFile(path.join(__appdir, 'bin', 'fullchain.pem'))
+})
+
+router.get('/:version/general', (req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.sendFile(path.join(__appdir, 'ipxe', 'general_' + req.params.version + '.h'))
+})
+
+router.get('/:version/console', (req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ res.sendFile(path.join(__appdir, 'ipxe', 'console_' + req.params.version + '.h'))
+})
+
+router.get('/:version/log', async (req, res) => {
+ res.setHeader('content-type', 'text/plain')
+ const filepath = path.join(__appdir, 'ipxe', 'log_' + req.params.version + '.txt')
+ res.sendFile(filepath, err => {
+ if (err) console.log('Could not send file ' + filepath)
+ })
+})
+
+router.put('/:version/:filename', (req, res) => {
+ var filepath = null
+ // Set the file path
+ if (req.params.filename === 'script') {
+ filepath = path.join(__appdir, 'ipxe', 'embedded_' + req.params.version + '.ipxe')
+ } else if (req.params.filename === 'console' || req.params.filename === 'general') {
+ filepath = path.join(__appdir, 'ipxe', req.params.filename + '_' + req.params.version + '.h')
+ } else if (req.params.filename === 'certificate') {
+ filepath = path.join(__appdir, 'bin', 'fullchain.pem')
+ } else {
+ res.status(500).send({ status: 'UNKNOWN_FILENAME', error: 'The provided filename is unknown' })
+ return
+ }
+
+ // Write File
+ fs.writeFile(filepath, req.body.data, err => {
+ if (err) res.status(500).send({ status: 'WRITE_FILE_ERROR', error: err })
+ else res.status(200).send({ status: 'SUCCESS' })
+ })
+})
/*
* @return: Rebuild the ipxe.
*/
-router.get('/build', (req, res) => {
- shell.buildIpxe(req, res)
+router.get('/:version/build', async (req, res) => {
+ if (req.params.version === 'efi' || req.params.version === 'bios') await shell.buildIpxe(req, res)
+ res.status(200).send({ status: 'success' })
+})
+
+router.get('/:version/clean', (req, res) => {
+ if (req.params.version === 'efi') shell.clean('efi')
+ else if (req.params.version === 'bios') shell.clean('bios')
+ res.send()
})
module.exports.router = router
diff --git a/server/ipxe/embedded_bios.ipxe b/server/ipxe/embedded_bios.ipxe
new file mode 100644
index 0000000..698c683
--- /dev/null
+++ b/server/ipxe/embedded_bios.ipxe
@@ -0,0 +1,15 @@
+#!ipxe
+
+################
+# BIOS Version #
+################
+
+ifopen
+
+# Wallpaper
+set img tftp://10.8.102.124/ipxeWallpaper3_scale.png || shell
+console --picture ${img} --x 800 --y 600 || shell
+
+:loop
+chain https://bas.intra.uni-freiburg.de/api/configloader/${uuid}
+goto loop \ No newline at end of file
diff --git a/server/ipxe/main.ipxe b/server/ipxe/embedded_efi.ipxe
index 3bb8e86..70bfc17 100644
--- a/server/ipxe/main.ipxe
+++ b/server/ipxe/embedded_efi.ipxe
@@ -1,4 +1,9 @@
#!ipxe
+
+################
+# EFI Version #
+################
+
ifopen
# Wallpaper
diff --git a/server/lib/shell.js b/server/lib/shell.js
index 2b1ea0b..080e5d6 100644
--- a/server/lib/shell.js
+++ b/server/lib/shell.js
@@ -1,47 +1,120 @@
+// Those packages needs to be installed to build ipxe:
+// sudo apt install liblzma-dev
+// sudo apt install mkisofs
+
/* global __appdir */
var path = require('path')
var shell = require('shelljs')
var ipxeGIT = 'git://git.ipxe.org/ipxe.git'
+var io = require(path.join(__appdir, 'lib', 'socketio'))
+const fs = require('fs')
module.exports = {
- buildIpxe: function (req, res) {
- if (!shell.which('git')) {
- return res.status(500).send({ status: 'GIT_MISSING', error_message: 'Please install git on the server.' })
- }
+ buildIpxe: async function (req, res) {
+ const ipxeVersion = req.params.version
- var gitclone = 'git clone ' + ipxeGIT
- shell.cd(path.join(__appdir, 'ipxe'))
- shell.exec(gitclone, function (code, stdout, stderr) {
- shell.cd(path.join(__appdir, 'ipxe', 'ipxe', 'src'))
-
- // Remove the general config and paste in the own one
- shell.rm(path.join(__appdir, 'ipxe', 'ipxe', 'src', 'config', 'general.h'))
- // shell.cp(path.join(__appdir, 'ipxe', 'general.h'), path.join(__appdir, 'ipxe', 'ipxe', 'src', 'config'))
- shell.cp(path.join(__appdir, 'ipxe', 'general_efi.h'), path.join(__appdir, 'ipxe', 'ipxe', 'src', 'config', 'general.h'))
- shell.rm(path.join(__appdir, 'ipxe', 'ipxe', 'src', 'config', 'console.h'))
- // shell.cp(path.join(__appdir, 'ipxe', 'console.h'), path.join(__appdir, 'ipxe', 'ipxe', 'src', 'config'))
- shell.cp(path.join(__appdir, 'ipxe', 'console_efi.h'), path.join(__appdir, 'ipxe', 'ipxe', 'src', 'config', 'console.h'))
- // PCBIOS Variant
- // var make = 'make EMBED=' + path.join(__appdir, 'ipxe', 'main.ipxe') + ' TRUST=' + path.join(__appdir, 'bin', 'fullchain.pem')// + ' bin/undionly.kpxe'
-
- // EFI Variant
- var make = 'make bin-x86_64-efi/snponly.efi EMBED=' + path.join(__appdir, 'ipxe', 'main.ipxe') + ' TRUST=' + path.join(__appdir, 'bin', 'fullchain.pem')// + ' bin/undionly.kpxe'
-
- // USB
- // var make = 'make EMBED=' + path.join(__appdir, 'ipxe', 'reboot.ipxe') + ' TRUST=' + path.join(__appdir, 'bin', 'fullchain.pem') + ' bin/ipxe.usb'
- shell.env.DEBUG = ''
- shell.exec(make, function (code, stdout, stderr) {
- // shell.rm(path.join(__appdir, 'tftp', 'ipxe.0'))
- shell.rm(path.join(__appdir, 'ipxe', 'ipxe.0'))
- // shell.cp('bin/undionly.kpxe', path.join(__appdir, 'tftp'))
- shell.cp('bin/undionly.kpxe', path.join(__appdir, 'ipxe'))
- shell.mv(path.join(__appdir, 'ipxe', 'undionly.kpxe'), path.join(__appdir, 'ipxe', 'ipxe.0'))
- // shell.rm('-rf', 'ipxe');
- return res.status(200).send({ status: 'success' })
+ // Cloning git.
+ sendToLog(ipxeVersion, 'Cloning git ...\n', 'primary')
+ await cloneIpxe(ipxeVersion)
+
+ // Copying configs.
+ sendToLog(ipxeVersion, 'Copying configs ...\n', 'primary')
+ copyConfigs(ipxeVersion)
+
+ // Make ipxe.
+ sendToLog(ipxeVersion, 'Make iPXE ...\n', 'primary')
+ shell.cd(path.join(__appdir, 'ipxe', 'ipxe_' + ipxeVersion, 'src'))
+
+ // Different make command for efi / bios are needed.
+ var makeCmd = ''
+ if (ipxeVersion === 'efi') makeCmd = 'make bin-x86_64-efi/snponly.efi EMBED=' + path.join(__appdir, 'ipxe', 'embedded_' + ipxeVersion + '.ipxe') + ' TRUST=' + path.join(__appdir, 'bin', 'fullchain.pem')// + ' bin/undionly.kpxe'
+ else if (ipxeVersion === 'bios') makeCmd = 'make EMBED=' + path.join(__appdir, 'ipxe', 'embedded_' + ipxeVersion + '.ipxe') + ' TRUST=' + path.join(__appdir, 'bin', 'fullchain.pem')// + ' bin/undionly.kpxe'
+ await new Promise((resolve, reject) => {
+ var make = shell.exec(makeCmd, { async: true }, () => {
+ resolve()
+ })
+
+ // Send the output to the frontend log.
+ make.stdout.on('data', data => {
+ sendToLog(ipxeVersion, data, 'normal')
+ })
+ make.stderr.on('data', data => {
+ sendToLog(ipxeVersion, data, 'error')
})
})
+
+ // Copy and rename the ipxe file.
+ shell.cp('bin/undionly.kpxe', path.join(__appdir, 'ipxe'))
+ shell.mv(path.join(__appdir, 'ipxe', 'undionly.kpxe'), path.join(__appdir, 'ipxe', 'ipxe.' + ipxeVersion))
+ },
+
+ clean: async function (ipxeVersion) {
+ shell.cd(path.join(__appdir, 'ipxe'))
+ shell.rm('-rf', 'ipxe_' + ipxeVersion)
+ shell.rm(path.join(__appdir, 'ipxe', 'ipxe.' + ipxeVersion))
+ shell.rm(path.join(__appdir, 'ipxe', 'log_' + ipxeVersion + '.txt'))
+ sendToLog(ipxeVersion, 'iPXE files successfully cleaned', 'success', false)
}
}
-// sudo apt-get install liblzma-dev
-// sudo apt-get install mkisofs
+async function cloneIpxe (ipxeVersion) {
+ // Check if git is installed on the server.
+ if (!shell.which('git')) {
+ return { status: 'GIT_MISSING', error_message: 'Please install git on the server.' }
+ }
+
+ shell.cd(path.join(__appdir, 'ipxe'))
+ return new Promise((resolve, reject) => {
+ var clone = shell.exec('git clone ' + ipxeGIT + ' ipxe_' + ipxeVersion, { async: true }, () => {
+ resolve()
+ })
+ clone.stdout.on('data', data => {
+ sendToLog(ipxeVersion, data, 'normal')
+ })
+ clone.stderr.on('data', data => {
+ sendToLog(ipxeVersion, data, 'error')
+ })
+ })
+}
+
+function copyConfigs (ipxeVersion) {
+ // Remove the default configs and paste in the customized ones.
+ shell.rm(path.join(__appdir, 'ipxe', 'ipxe_' + ipxeVersion, 'src', 'config', 'general.h'))
+ shell.rm(path.join(__appdir, 'ipxe', 'ipxe_' + ipxeVersion, 'src', 'config', 'console.h'))
+ shell.cp(path.join(__appdir, 'ipxe', 'general_' + ipxeVersion + '.h'), path.join(__appdir, 'ipxe', 'ipxe_' + ipxeVersion, 'src', 'config', 'general.h'))
+ shell.cp(path.join(__appdir, 'ipxe', 'console_' + ipxeVersion + '.h'), path.join(__appdir, 'ipxe', 'ipxe_' + ipxeVersion, 'src', 'config', 'console.h'))
+}
+
+function sendToLog (ipxeVersion, msg, status, log = true) {
+ var date = new Date()
+ var dateString = '[' + date.getFullYear() + '-'
+
+ var month = parseInt(date.getMonth()) + 1
+ if (month < 10) dateString += '0' + month + '-'
+ else dateString += month + '-'
+
+ if (parseInt(date.getDate()) < 10) dateString += '0' + date.getDate() + ' '
+ else dateString += date.getDate() + ' '
+
+ if (parseInt(date.getHours()) < 10) dateString += '0' + date.getHours() + ':'
+ else dateString += date.getHours() + ':'
+
+ if (parseInt(date.getMinutes()) < 10) dateString += '0' + date.getMinutes() + ':'
+ else dateString += date.getMinutes() + ':'
+
+ if (parseInt(date.getSeconds()) < 10) dateString += '0' + date.getSeconds()
+ else dateString += date.getSeconds()
+ dateString += ']'
+
+ var logEntry = { date: dateString, status: status, msg: msg }
+
+ var socket = ipxeVersion + ' log'
+ io.in('broadcast ipxeBuild').emit(socket, logEntry)
+ if (log) writeLog(ipxeVersion, logEntry)
+}
+
+function writeLog (ipxeVersion, logEntry) {
+ fs.writeFile(path.join(__appdir, 'ipxe', 'log_' + ipxeVersion + '.txt'), logEntry.date + '\t' + logEntry.status + '\t' + logEntry.msg, { flag: 'a+' }, (err) => {
+ if (err) throw err
+ })
+}
diff --git a/server/lib/socketio.js b/server/lib/socketio.js
index 07c2f22..5cbedb7 100644
--- a/server/lib/socketio.js
+++ b/server/lib/socketio.js
@@ -27,6 +27,7 @@ io.on('connection', socket => {
// Join broadcast rooms (TODO: check if user has permission to join those rooms)
socket.join('broadcast newClient')
+ socket.join('broadcast ipxeBuild')
// Notification broadcasts
socket.on('notifications clearAll', () => socket.to(userId).emit('notifications clearAll'))
diff --git a/webapp/src/assets/styles.css b/webapp/src/assets/styles.css
index 0df7f2a..c540d57 100644
--- a/webapp/src/assets/styles.css
+++ b/webapp/src/assets/styles.css
@@ -23,6 +23,10 @@ html {
background-color: #666666;
}
+::-webkit-scrollbar-corner {
+ background: rgba(0,0,0,0);
+}
+
::-webkit-scrollbar-button {
display: none;
}
@@ -45,4 +49,4 @@ html {
.CodeMirror {
min-height: 200px;
height: auto;
-} \ No newline at end of file
+}
diff --git a/webapp/src/components/DashboardPage.vue b/webapp/src/components/DashboardPage.vue
index f408ebb..9696466 100644
--- a/webapp/src/components/DashboardPage.vue
+++ b/webapp/src/components/DashboardPage.vue
@@ -59,7 +59,7 @@
</v-touch>
</v-navigation-drawer>
- <v-toolbar dark app :clipped-left="settings.clipped" class="non-selectable">
+ <v-toolbar dark app :clipped-left="settings.clipped" class="non-selectable" style="z-index: 10;">
<v-toolbar-side-icon class="drawer-icon" @click.stop="toggleDrawer"></v-toolbar-side-icon>
<div class="logo"><img class="non-draggable hidden-md-and-down" src="@/assets/logo.svg" /></div>
<v-spacer></v-spacer>
diff --git a/webapp/src/components/DataTable.vue b/webapp/src/components/DataTable.vue
index 721f137..0443918 100644
--- a/webapp/src/components/DataTable.vue
+++ b/webapp/src/components/DataTable.vue
@@ -3,77 +3,170 @@
"en": {
"search": "Search",
"all": "All",
- "pageText": "{0}-{1} of {2}",
- "pageTextZero": "0 of 0",
- "rowsPerPageText": "Rows per page:"
+ "entries": "Entries",
+ "noResult": "No entries.",
+ "height": "Height",
+ "regex": "Regular Expressions",
+ "caseSensitive": "Case Sensitive",
+ "selected": "selected"
},
"de": {
"search": "Suche",
"all": "Alle",
- "pageText": "{0}-{1} von {2}",
- "pageTextZero": "0 von 0",
- "rowsPerPageText": "Reihen pro page:"
+ "entries": "Einträge",
+ "noResult": "Keine Einträge.",
+ "height": "Höhe",
+ "regex": "Regulärer Ausdruck",
+ "caseSensitive": "Groß-/Kleinschreibung beachten",
+ "selected": "ausgewählt"
}
}
</i18n>
<template>
<div>
- <v-layout wrap align-center class="actions-container">
- <v-flex md3 sm5 xs12 order-md1 order-sm1 order-xs1 class="text-xs-left">
- <v-text-field
- class="search-field"
- :placeholder="$t('search')"
- v-model="search"
- hide-details
- prepend-inner-icon="search"
- ></v-text-field>
+ <v-layout wrap>
+ <v-flex xs12 sm6 class="d-flex align-center search-wrapper">
+ <div class="search-field-wrapper">
+ <div v-for="(s, index) in search" :key="index">
+ <v-text-field
+ color="primary"
+ solo flat
+ style="padding: 0: margin: 0;"
+ class="search-field"
+ :placeholder="$t('search')"
+ :value="s.text.raw"
+ @input="prerocessSearch(s, $event)"
+ hide-details
+ prepend-inner-icon="search"
+ clearable
+ ></v-text-field>
+ <v-select
+ solo flat
+ class="column-select"
+ v-model="s.key"
+ :items="[ { text: $t('all'), key: null }, ...searchableHeaders ]"
+ item-text="text"
+ item-value="key"
+ color="primary"
+ hide-details
+ :menu-props="{
+ offsetY: true,
+ left: true
+ }"
+ ></v-select>
+ <v-btn v-if="index === 0" icon @click="newSearchField"><v-icon>add</v-icon></v-btn>
+ <v-btn v-else icon @click="search.splice(search.indexOf(s), 1)"><v-icon>remove</v-icon></v-btn>
+ </div>
+ </div>
+ <v-divider class="hidden-sm-and-up"></v-divider>
+ <v-divider vertical class="hidden-xs-only"></v-divider>
</v-flex>
- <v-flex md4 sm12 xs12 offset-md1 offset-sm0 offset-xs0 order-md2 order-sm3 order-xs3
- class="text-md-center text-xs-right caption font-weight-thin">
- {{ $t('rowsPerPageText') }}
- <v-select
- v-model="rowCount"
- :items="[ 5, 10, 20, 50, 100, { text: $t('all'), value: '-1' }]"
- color="primary"
- hide-details
- :menu-props="{
- offsetY: ''
- }"
- ></v-select>
+ <v-flex style="padding: 6px 16px" class="d-flex align-start non-selectable" xs12 sm6>
+ <div class="d-flex align-center">
+ <div>
+ <v-tooltip top open-delay="800">
+ <v-btn icon class="toggle-button" @click="regex = !regex" slot="activator">
+ <span :class="regex ? 'primary--text' : ''">.*?</span>
+ </v-btn>
+ <span>{{ $t('regex') }}</span>
+ </v-tooltip>
+ <v-tooltip top open-delay="800">
+ <v-btn icon class="toggle-button" @click="caseSensitive = !caseSensitive" slot="activator">
+ <span :class="caseSensitive ? 'primary--text' : ''">Aa</span>
+ </v-btn>
+ <span>{{ $t('caseSensitive') }}</span>
+ </v-tooltip>
+ </div>
+ <div class="text-xs-center muted-text-color">
+ {{ filterdRows.length + ' ' + $t('entries') }}
+ <span
+ @click="onlyShowSelected = !onlyShowSelected"
+ :class="{ 'show-only-selected': onlyShowSelected }"
+ :style="{ cursor: 'pointer' }"
+ >
+ {{ '(' + selected.length + ' ' + $t('selected') + ')' }}
+ </span>
+ </div>
+ <div class="text-xs-right muted-text-color">
+ {{ $t('height') }}:
+ </div>
+ <v-select
+ solo flat
+ class="rowcount-select"
+ v-model="rowCount"
+ :items="[ 5, 10, 20, 50, 100, { text: $t('all'), value: '-1' }]"
+ color="primary"
+ hide-details
+ :menu-props="{
+ offsetY: true,
+ left: true,
+ contentClass: 'data-table-rowcount-select-content'
+ }"
+ ></v-select>
+ </div>
</v-flex>
</v-layout>
+
<v-divider></v-divider>
- <RecycleScroller
- class="scroller"
- :style="scrollerStyle"
- :items="filterdRows"
- :item-height="48"
- :page-mode="rowCount <= 0"
- >
- <div slot-scope="{ item }" class="table-row"
- :style="item.selected && { backgroundColor: $vuetify.theme.primary + '11' }"
- @click="selectItem(item)"
- >
- <div>
- <v-icon style="cursor: pointer">{{ item.selected ? 'check_box' : 'check_box_outline_blank' }}</v-icon>
- </div>
- <div v-for="header in headers" :key="header.key" :style="{ width: header.width || headerWidthPercent }">
- {{ item.data[header.key] }}
+ <RecycleScroller
+ class="scroller non-selectable"
+ :style="scrollerStyle"
+ :items="filterdRows"
+ :item-height="48"
+ :page-mode="rowCount <= 0"
+ @click.native.capture.passive="setShiftState"
+ >
+ <template slot="before-container">
+ <div class="table-head-wrapper non-selectable" :style="{ 'min-width': minWidth }">
+ <div class="table-head">
+ <div class="non-selectable header-cell">
+ <v-icon @click="toggleSelectAll">
+ {{ selectState === 0 ? 'check_box_outline_blank' : selectState === 1 ? 'indeterminate_check_box' : 'check_box' }}
+ </v-icon>
+ </div>
+ <div v-for="header in headers" :key="header.key" class="header-cell"
+ @click="toggleHeaderSort(header)"
+ :style="{ width: header.width, cursor: header.text !== undefined ? 'pointer' : '' }"
+ :class="{
+ 'header-sorted': headerSortState[header.key] !== undefined,
+ 'header-sorted-desc': headerSortState[header.key] === 'desc',
+ 'auto-width': header.width === undefined
+ }"
+ >
+ {{ header.text }}
+ <v-icon v-if="header.text !== undefined" class="header-sort-icon" style="font-size: 16px; opacity: 0;">
+ arrow_upward
+ </v-icon>
+ </div>
</div>
+ <v-progress-linear v-if="loading" :indeterminate="true" :height="2" style="margin: 0"></v-progress-linear>
+ <div v-else class="header-separator"></div>
</div>
- </RecycleScroller>
-
+ </template>
+ <div slot-scope="{ item, index }" class="table-row"
+ :style="item.selected && { backgroundColor: $vuetify.theme.primary + '11' }"
+ @click="selectItem(item, index)"
+ @dblclick="$emit('dblclick', item.data)"
+ >
+ <div class="non-selectable">
+ <v-icon style="cursor: pointer" :color="item.selected ? 'primary' : ''">{{ item.selected ? 'check_box' : 'check_box_outline_blank' }}</v-icon>
+ </div>
+ <div v-for="header in headers" :key="header.key" :style="{ width: header.width }" :class="{ 'auto-width': header.width === undefined }">
+ <span style="user-select: text" v-if="$scopedSlots[header.key] === undefined" @dblclick.stop>{{ item.data[header.key] }}</span>
+ <slot v-else :name="header.key" :item="item.data" />
+ </div>
+ </div>
+ <div v-if="filterdRows.length === 0" slot="after-container" class="no-result">{{ $t('noResult') }}</div>
+ </RecycleScroller>
</div>
</template>
<script>
-import DataTableItem from '@/components/DataTableItem'
export default {
name: 'DataTable',
- components: { DataTableItem },
props: {
headers: {
type: Array
@@ -82,64 +175,210 @@ export default {
type: Array,
required: true
},
+ loading: {
+ type: Boolean,
+ default: false
+ },
value: {
type: Array,
default: () => []
},
minWidth: {
- type: Number,
- default: 600
+ type: String,
+ default: '600px'
}
},
data () {
return {
- search: '',
- lastSelectId: null,
- shiftKey: false,
- rowCount: 10
+ selected: [],
+ search: [{ text: { raw: '', upper: '', regex: new RegExp(), regexCI: new RegExp() }, key: null }],
+ rowCount: 10,
+ selectState: 0,
+ headerSortState: {},
+ regex: false,
+ caseSensitive: false,
+ onlyShowSelected: false,
+ lastSelectIndex: null,
+ shiftKey: false
}
},
computed: {
+ searchableHeaders () {
+ return this.headers.filter(h => h.text !== undefined)
+ },
scrollerStyle () {
- const style = { '--min-table-width': this.minWidth + 'px' }
- if (this.rowCount > 0) style.height = Math.min(this.rowCount, this.filterdRows.length) * 48 + 'px'
+ const style = { '--min-table-width': this.minWidth }
+ if (this.rowCount > 0) style.height = (Math.max(Math.min(this.rowCount, this.filterdRows.length), 1) * 48 + 58) + 'px'
return style
},
- headerWidthPercent () { return 100/this.headers.length + '%' },
dataKeys () { return this.headers.map(x => x.key) },
rows () {
return this.items.map(item => ({ data: item, selected: false, id: item.id }))
},
+ sortedRows () {
+ const rows = this.rows.slice(0)
+ for (let key in this.headerSortState) {
+ const direction = this.headerSortState[key]
+ if (direction === 'asc') rows.sort((a, b) => String(a.data[key]).localeCompare(String(b.data[key])))
+ if (direction === 'desc') rows.sort((b, a) => String(a.data[key]).localeCompare(String(b.data[key])))
+ }
+ return rows
+ },
+ filterFunction () {
+ if (this.regex && this.caseSensitive) return (s, str) => s.text.regex.test(str)
+ else if (this.regex && !this.caseSensitive) return (s, str) => s.text.regexCI.test(str)
+ else if (!this.regex && this.caseSensitive) return (s, str) => str.indexOf(s.text.raw) !== -1
+ else if (!this.regex && !this.caseSensitive) return (s, str) => str.toUpperCase().indexOf(s.text.upper) !== -1
+ },
filterdRows () {
- const search = String(this.search).toLowerCase().trim()
- return search === '' ? this.rows : this.rows.filter(row => {
- return Object.keys(row.data).some(key => {
- return this.dataKeys.includes(key) && String(row.data[key]).toLowerCase().includes(search)
- })
- })
+ if (this.search.every(s => s.text.raw === '') && !this.onlyShowSelected) return this.sortedRows
+ const onlySelected = this.onlyShowSelected
+ const search = this.search
+ const dataKeys = this.dataKeys
+ const test = this.filterFunction
+ return this.sortedRows.filter(row => (!onlySelected || (onlySelected && row.selected)) && search.every(s => {
+ if (s.key === null) {
+ return dataKeys.some(key => {
+ return test(s, String(row.data[key]))
+ })
+ } else {
+ return test(s, String(row.data[s.key]))
+ }
+ }))
}
},
watch: {
- value (v) {
-
- },
items () {
+ this.selectState = 0
this.$emit('input', [])
+ },
+ value: {
+ immediate: true,
+ handler (newValue) {
+ if (this.selected !== newValue) {
+ const tmp = {}
+ newValue.forEach(x => { tmp[x.id] = true })
+ this.selected = []
+ this.rows.forEach(row => {
+ if (tmp[row.data.id] === true) {
+ row.selected = true
+ this.selected.push(row.data)
+ }
+ })
+ this.calcSelectState()
+ this.$emit('input', this.selected)
+ }
+ }
+ },
+ filterdRows () {
+ this.lastSelectIndex = null
+ this.calcSelectState()
}
},
methods: {
- setShiftState (event) {
- this.shiftKey = event.shiftKey
+ selectItem (row, index) {
+ // Select or deselect this row
+ const selected = row.selected = !row.selected
+ if (selected) {
+ this.selected.push(row.data)
+ // Shift click: Select all rows between this row and the last selected row
+ if (this.shiftKey && this.lastSelectIndex !== null) this.selectRows(index, this.lastSelectIndex)
+ } else {
+ this.selected.splice(this.selected.indexOf(row.data), 1)
+ // Shift click: Deselect all rows between this row and the last selected row
+ if (this.shiftKey && this.lastSelectIndex !== null) this.deselectRows(index, this.lastSelectIndex)
+ }
+ // Update variables and emit the new value
+ this.lastSelectIndex = index
+ this.calcSelectState()
+ this.$emit('input', this.selected)
},
- selectItem (item) {
- item.selected = !item.selected
- var tmp = this.value
- if (item.selected) {
- tmp.push(item.data)
+ toggleSelectAll () {
+ if (this.filterdRows.length === 0) return
+ if (this.selectState <= 1) {
+ this.selectRows(0, this.filterdRows.length - 1)
} else {
- tmp.splice(tmp.indexOf(item.data), 1)
+ this.deselectRows(0, this.filterdRows.length - 1)
+ }
+ // Update variables and emit the new value
+ this.calcSelectState()
+ this.$emit('input', this.selected)
+ },
+ selectRows (start, end) {
+ var rows
+ if (this.onlyShowSelected) rows = this.filterdRows.slice(0)
+ else rows = this.filterdRows
+ for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
+ let row = rows[i]
+ if (row.selected !== true) {
+ row.selected = true
+ this.selected.push(row.data)
+ }
+ }
+ },
+ deselectRows (start, end) {
+ var rows
+ if (this.onlyShowSelected) rows = this.filterdRows.slice(0)
+ else rows = this.filterdRows
+ const deselected = {}
+ for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
+ let row = rows[i]
+ if (row.selected === true) {
+ row.selected = false
+ deselected[row.data.id] = true
+ }
+ }
+ var newSelected = []
+ this.selected.forEach(item => {
+ if (deselected[item.id] !== true) newSelected.push(item)
+ })
+ this.selected = newSelected
+ },
+ calcSelectState () {
+ const displayedRows = this.filterdRows
+ var seenTrue = false
+ var seenFalse = false
+ for (let i = 0; i < displayedRows.length; i++) {
+ if (displayedRows[i].selected) seenTrue = true
+ else seenFalse = true
+ if (seenTrue && seenFalse) break
}
- this.$emit('input', tmp)
+ this.selectState = seenTrue && seenFalse ? 1 : seenTrue && !seenFalse ? 2 : 0
+ },
+ prerocessSearch (s, raw) {
+ if (typeof raw !== 'string') raw = ''
+ const tmp = { raw, upper: raw.toUpperCase() }
+ try {
+ tmp.regex = new RegExp(raw)
+ tmp.regexCI = new RegExp(raw, 'i')
+ } catch (e) {
+ tmp.regex = { test: () => false }
+ tmp.regexCI = { test: () => false }
+ }
+ s.text = tmp
+ },
+ newSearchField () {
+ this.search.push({ text: {
+ raw: '',
+ upper: '',
+ regex: new RegExp(),
+ regexCI: new RegExp()
+ },
+ key: null })
+ },
+ toggleHeaderSort (header) {
+ if (header.text === undefined) return
+ const state = this.headerSortState[header.key]
+ const newSortState = {}
+ if (state === undefined || state === 'desc') {
+ newSortState[header.key] = 'asc'
+ } else if (state === 'asc') {
+ newSortState[header.key] = 'desc'
+ }
+ this.headerSortState = newSortState
+ },
+ setShiftState (event) {
+ this.shiftKey = event.shiftKey
}
}
}
@@ -148,31 +387,182 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
-.scroller-wrapper {
- overflow-x: auto;
+.muted-text-color {
+ font-size: 12px;
+}
+
+.theme--dark .muted-text-color {
+ color: rgba(255,255,255,0.7);
+}
+
+.theme--light .muted-text-color {
+ color: rgba(0,0,0,0.54);
+ font-weight: 500;
+}
+
+.theme--dark .show-only-selected {
+ color: rgb(255,255,255);
+}
+
+.theme--light .show-only-selected {
+ color: rgb(0,0,0,0.87);
+ font-weight: 600;
+}
+
+.toggle-button {
+ margin: 0;
+ padding: 0;
+ min-width: 36px;
+ text-transform: none;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+@media (max-width: 599px) {
+ .search-wrapper {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+
+.search-field-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.search-field-wrapper > div {
+ display: flex;
+ align-items: center;
+}
+
+.search-field {
+ margin: 0;
+ padding: 0;
+}
+
+.column-select {
+ flex: 0 1 0
+}
+
+.rowcount-select {
+ padding: 0;
+ display: inline-block;
+ margin: 0 0 0 10px;
+ max-width: 80px;
+}
+
+.rowcount-select >>> .v-input__control {
+ min-height: auto;
+}
+
+.no-result {
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.scroller {
overflow-x: auto;
+ font-size: 13px;
}
.scroller >>> .vue-recycle-scroller__item-wrapper {
min-width: var(--min-table-width);
}
+.table-head-wrapper {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.table-head {
+ height: 56px;
+ display: flex;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.theme--dark .table-head {
+ color: rgba(255,255,255,.7);
+}
+
+.theme--light .table-head {
+ color: rgba(0,0,0,.54);
+}
+
+.table-head > .header-cell {
+ display: flex;
+ align-items: center;
+ transition: color .3s cubic-bezier(.25,.8,.5,1);
+}
+
+.table-head > .header-cell.header-sorted.header-sorted-desc > .header-sort-icon {
+ transform: rotate(180deg);
+}
+
+.table-head > .header-cell:hover > .header-sort-icon {
+ opacity: 0.6 !important;
+}
+
+.table-head > .header-cell.header-sorted > .header-sort-icon {
+ opacity: 1 !important;
+}
+
+.theme--dark .table-head > .header-cell:hover, .theme--dark .table-head > .header-cell.header-sorted {
+ color: rgb(255,255,255);
+}
+
+.theme--light .table-head > .header-cell:hover, .theme--light .table-head > .header-cell.header-sorted {
+ color: rgba(0,0,0,.87);
+}
+
+.header-separator {
+ height: 2px;
+}
+
+.theme--dark .header-separator {
+ background-color: rgba(255,255,255,.5);
+}
+
+.theme--light .header-separator {
+ background-color: rgba(0,0,0,.5);
+}
+
+.theme--dark .table-head-wrapper {
+ background-color: #424242;
+}
+
+.theme--light .table-head-wrapper {
+ background-color: #fff;
+}
+
+.table-row .table-actions {
+ padding: 0;
+ text-align: right;
+}
+
.table-row {
height: 48px;
display: flex;
align-items: center;
+ transition: background .3s cubic-bezier(.25,.8,.5,1);
+ will-change: background;
}
-.table-row > div {
- padding: 0 24px;
+.table-row > div, .table-head > div {
+ margin: 0 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+.auto-width {
+ flex-grow: 1;
+ flex-basis: 0;
+}
+
.theme--dark .table-row {
border-bottom: 1px solid hsla(0,0%,100%,.12);
}
@@ -181,7 +571,6 @@ export default {
border-bottom: 1px solid rgba(0,0,0,.12);
}
-
.theme--dark .scroller >>> .hover > .table-row {
background-color: #616161;
}
@@ -192,3 +581,12 @@ export default {
</style>
+<style>
+.data-table-rowcount-select-content .v-list__tile {
+ height: 36px;
+}
+
+.data-table-rowcount-select-content .v-list__tile__title {
+ text-align: right;
+}
+</style>
diff --git a/webapp/src/components/DataTableItem.vue b/webapp/src/components/DataTableItem.vue
deleted file mode 100644
index 5210efa..0000000
--- a/webapp/src/components/DataTableItem.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<i18n>
-{
- "en": {
- },
- "de": {
- }
-}
-</i18n>
-
-<template>
- <tr
- :style="row.selected && { backgroundColor: $vuetify.theme.primary + '11' }"
- @click="selectRow(row)"
- @dblclick="$emit('dbclick', row.item)"
- >
- <td class="narrow-td">
- <v-icon style="cursor: pointer">{{ row.selected ? 'check_box' : 'check_box_outline_blank' }}</v-icon>
- </td>
- <td v-for="header in headers" :key="header.key">
- {{ row.item[header.key] }}
- </td>
- </tr>
-</template>
-
-<script>
-
-export default {
- name: 'DataTableItem',
- props: {
- row: {
- type: Object,
- required: true
- },
- headers: {
- type: Array,
- required: true
- }
- },
- data () {
- return {
- }
- },
- methods: {
- selectRow (row) {
- row.selected = !row.selected
- this.$emit('select', row)
- }
- }
-}
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-.narrow-td {
- width: 10px;
-}
-
-tr {
- height: 48px;
- box-sizing: border-box;
-}
-
-td {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.theme--dark td {
- border-bottom: 1px solid hsla(0,0%,100%,.12);
-}
-
-.theme--light td {
- border-bottom: 1px solid rgba(0,0,0,.12);
-}
-
-.theme--dark tr:hover {
- background: #616161;
-}
-
-.theme--light tr:hover {
- background: #eee;
-}
-
-td {
- padding: 0 24px;
-}
-</style>
diff --git a/webapp/src/components/GroupModuleGroupList.vue b/webapp/src/components/GroupModuleGroupList.vue
index 88add11..1c981fc 100644
--- a/webapp/src/components/GroupModuleGroupList.vue
+++ b/webapp/src/components/GroupModuleGroupList.vue
@@ -24,7 +24,11 @@
<template>
<div>
<v-card>
- <data-table v-model="selected" :headers="headers" :items="groups" :loading="loading" />
+ <data-table v-model="selected" :headers="headers" :items="groups" :loading="loading" @dblclick="loadGroup($event)">
+ <div slot="actions" slot-scope="row" style="text-align: right">
+ <v-btn icon @click="loadGroup(row.item)"><v-icon>keyboard_arrow_right</v-icon></v-btn>
+ </div>
+ </data-table>
</v-card>
<div v-if="tabIndex === 0" class="text-xs-right">
<v-btn flat color="error" @click="deleteSelected" :disabled="selected.length === 0">
@@ -59,20 +63,16 @@ export default {
computed: {
headers () {
return [
- { text: this.$t('id'), key: 'id', width: '100px' },
- { text: this.$t('name'), key: 'name' },
- { text: this.$t('description'), key: 'description' }
+ { key: 'id', text: this.$t('id'), width: '60px' },
+ { key: 'name', text: this.$t('name'), width: '180px' },
+ { key: 'description', text: this.$t('description') },
+ { key: 'actions', width: '48px' }
]
},
loading () {
return this.$store.state.groups.tabChain[this.tabIndex].loading
}
},
- watch: {
- groups () {
- this.selected = []
- }
- },
methods: {
...mapMutations('groups', ['setActiveTab', 'setTab', 'setDialog']),
loadGroup (item) {
@@ -97,10 +97,6 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
-.narrow-td {
- width: 10px;
-}
-
.next-arrow {
margin-left: 20px;
margin-right: -10px;
diff --git a/webapp/src/components/IpxeBuilderModule.vue b/webapp/src/components/IpxeBuilderModule.vue
new file mode 100644
index 0000000..9cb9bea
--- /dev/null
+++ b/webapp/src/components/IpxeBuilderModule.vue
@@ -0,0 +1,63 @@
+<i18n>
+{
+ "en": {
+ "efi": "EFI",
+ "bios": "BIOS"
+ },
+ "de": {
+ "efi": "EFI",
+ "bios": "BIOS"
+ }
+}
+</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('efi') }}</v-tab>
+ <v-tab>{{ $t('bios') }}</v-tab>
+ </v-tabs>
+ </v-card>
+
+ <v-tabs-items v-model="tabs" style="padding-bottom: 20px">
+ <v-tab-item>
+ <ipxe-builder-module-config :ipxeVersion="'efi'"></ipxe-builder-module-config>
+ </v-tab-item>
+ <v-tab-item>
+ <ipxe-builder-module-config :ipxeVersion="'bios'"></ipxe-builder-module-config>
+ </v-tab-item>
+
+ </v-tabs-items>
+ </v-flex>
+ </v-layout>
+ </v-container>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import IpxeBuilderModuleConfig from '@/components/IpxeBuilderModuleConfig'
+
+export default {
+ name: 'IpxeBuilder',
+ components: {
+ IpxeBuilderModuleConfig
+ },
+ data () {
+ return {
+ tabs: 0
+ }
+ },
+ computed: {
+ ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor'])
+ },
+ watch: {
+ },
+ methods: {
+ },
+ created () {
+ }
+}
+</script>
diff --git a/webapp/src/components/IpxeBuilderModuleConfig.vue b/webapp/src/components/IpxeBuilderModuleConfig.vue
new file mode 100644
index 0000000..e4f619a
--- /dev/null
+++ b/webapp/src/components/IpxeBuilderModuleConfig.vue
@@ -0,0 +1,224 @@
+<i18n>
+{
+ "en": {
+ "efi": "EFI",
+ "bios": "BIOS",
+ "general": "general.h",
+ "console": "console.h",
+ "script": "Embedded script (EMBED=)",
+ "trust": "Embedded certificate (TRUST=)",
+ "ipxe": "iPXE",
+ "buildIpxe": "Build iPXE",
+ "scriptSaved": "Embedded script saved successfully",
+ "certificateSaved": "Certificate saved successfully",
+ "generalSaved": "general.h saved successfully",
+ "consoleSaved": "console.h saved successfully",
+ "buildingIpxe": "Building iPXE ...",
+ "cleanIpxe": "Clean iPXE",
+ "cleaningIpxe": "Cleaning iPXE ..."
+ },
+ "de": {
+ "efi": "EFI",
+ "bios": "BIOS",
+ "general": "general.h",
+ "console": "console.h",
+ "script": "Eingebettetes Skript (EMBED=)",
+ "trust": "Eingebettetes Zertifikat (TRUST=)",
+ "ipxe": "iPXE",
+ "buildIpxe": "iPXE bauen",
+ "scriptSaved": "Eingebettetes Skript wurde erfolgreich gespeichert",
+ "certificateSaved": "Zertifikat wurde erfolgreich gespeichert",
+ "generalSaved": "general.h wurde erfolgreich gespeichert",
+ "consoleSaved": "console.h wurde erfolgreich gespeichert",
+ "buildingIpxe": "iPXE wird gebaut ...",
+ "cleanIpxe": "iPXE aufräumen",
+ "cleaningIpxe": "iPXE wird aufgeräumt .."
+ }
+}
+</i18n>
+
+<template>
+ <div>
+
+ <v-subheader>{{ $t('ipxe') }}</v-subheader>
+ <v-card style="padding-left: 24px;padding-right: 24px; padding-top: 12px; padding-bottom: 12px; height: 50vh; overflow: auto;" ref="log">
+ <template v-for="(entry, index) in log">
+ <pre :class="entry.status + '--text'" style="margin-bottom: 0px" :key="index"><span>{{ entry.date }} {{ entry.msg }}</span></pre>
+ </template>
+ </v-card>
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="cleanIpxe"><v-icon left>delete</v-icon>{{ $t('cleanIpxe') }}</v-btn>
+ <v-btn flat color="primary" @click="buildIpxe"><v-icon left>gavel</v-icon>{{ $t('buildIpxe') }}</v-btn>
+ </div>
+
+ <v-subheader></v-subheader>
+ <v-expansion-panel v-model="scriptExpanded">
+ <v-expansion-panel-content>
+ <div slot="header">{{ $t('script') }}</div>
+ <v-card>
+ <codemirror class="script-editor" ref="script" v-model="script"></codemirror>
+ </v-card>
+ <!--<v-divider></v-divider>-->
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="undo('script')"><v-icon left>undo</v-icon>{{ $t('undo') }}</v-btn>
+ <v-btn flat color="success" @click="redo('script')"><v-icon left>redo</v-icon>{{ $t('redo') }}</v-btn>
+ <v-btn color="primary" @click="save('script')"><v-icon left>save</v-icon>{{ $t('save') }}</v-btn>
+ </div>
+ </v-expansion-panel-content>
+ </v-expansion-panel>
+
+ <v-subheader></v-subheader>
+ <v-expansion-panel v-model="certificateExpanded">
+ <v-expansion-panel-content>
+ <div slot="header">{{ $t('trust') }}</div>
+ <v-card>
+ <codemirror class="script-editor" ref="certificate" v-model="certificate"></codemirror>
+ </v-card>
+ <!--<v-divider></v-divider>-->
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="undo('certificate')"><v-icon left>undo</v-icon>{{ $t('undo') }}</v-btn>
+ <v-btn flat color="success" @click="redo('certificate')"><v-icon left>redo</v-icon>{{ $t('redo') }}</v-btn>
+ <v-btn color="primary" @click="save('certificate')"><v-icon left>save</v-icon>{{ $t('save') }}</v-btn>
+ </div>
+ </v-expansion-panel-content>
+ </v-expansion-panel>
+
+ <v-subheader></v-subheader>
+ <v-expansion-panel v-model="generalExpanded">
+ <v-expansion-panel-content>
+ <div slot="header">{{ $t('general') }}</div>
+ <v-card>
+ <codemirror class="script-editor" ref="general" v-model="general"></codemirror>
+ </v-card>
+ <!--<v-divider></v-divider>-->
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="undo('general')"><v-icon left>undo</v-icon>{{ $t('undo') }}</v-btn>
+ <v-btn flat color="success" @click="redo('general')"><v-icon left>redo</v-icon>{{ $t('redo') }}</v-btn>
+ <v-btn color="primary" @click="save('general')"><v-icon left>save</v-icon>{{ $t('save') }}</v-btn>
+ </div>
+ </v-expansion-panel-content>
+ </v-expansion-panel>
+
+ <v-subheader></v-subheader>
+ <v-expansion-panel v-model="consoleExpanded">
+ <v-expansion-panel-content>
+ <div slot="header">{{ $t('console') }}</div>
+ <v-card>
+ <codemirror class="script-editor" ref="console" v-model="console"></codemirror>
+ </v-card>
+ <!--<v-divider></v-divider>-->
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="undo('console')"><v-icon left>undo</v-icon>{{ $t('undo') }}</v-btn>
+ <v-btn flat color="success" @click="redo('console')"><v-icon left>redo</v-icon>{{ $t('redo') }}</v-btn>
+ <v-btn color="primary" @click="save('console')"><v-icon left>save</v-icon>{{ $t('save') }}</v-btn>
+ </div>
+ </v-expansion-panel-content>
+ </v-expansion-panel>
+
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import axios from 'axios'
+
+export default {
+ name: 'IpxeBuilder',
+ props: ['ipxeVersion'],
+ components: {
+ },
+ data () {
+ return {
+ tabs: 0,
+ script: '',
+ certificate: '',
+ general: '',
+ console: '',
+ scriptExpanded: null,
+ certificateExpanded: null,
+ generalExpanded: null,
+ consoleExpanded: null,
+ log: []
+ }
+ },
+ computed: {
+ ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor'])
+ },
+ watch: {
+ scriptExpanded: function () {
+ if (this.scriptExpanded === 0) this.$refs.script.refresh()
+ },
+ certificateExpanded: function () {
+ if (this.certificateExpanded === 0) this.$refs.certificate.refresh()
+ },
+ generalExpanded: function () {
+ if (this.generalExpanded === 0) this.$refs.general.refresh()
+ },
+ consoleExpanded: function () {
+ if (this.consoleExpanded === 0) this.$refs.console.refresh()
+ }
+ },
+ methods: {
+ save (apiPath) {
+ axios.put('/api/ipxe/' + this.ipxeVersion + '/' + apiPath, { data: this[apiPath] }).then(result => {
+ const saveMsg = apiPath + 'Saved'
+ if (result.data.status === 'SUCCESS') this.$snackbar({ color: 'success', text: this.$tc(saveMsg) })
+ else this.$snackbar({ color: 'error', text: result.data.error })
+ })
+ },
+ undo (element) {
+ this.$refs[element].codemirror.undo()
+ },
+ redo (element) {
+ this.$refs[element].codemirror.redo()
+ },
+ buildIpxe () {
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/build').then(result => {
+ this.$snackbar({ color: 'primary', text: this.$tc('buildingIpxe') })
+ })
+ },
+ cleanIpxe () {
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/clean').then(result => {
+ this.$snackbar({ color: 'primary', text: this.$tc('cleaningIpxe') })
+ })
+ }
+ },
+ created () {
+ // Load the data.
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/script').then(result => {
+ this.script = result.data
+ })
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/certificate').then(result => {
+ this.certificate = result.data
+ })
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/general').then(result => {
+ this.general = result.data
+ })
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/console').then(result => {
+ this.console = result.data
+ })
+
+ axios.get('/api/ipxe/' + this.ipxeVersion + '/log').then(result => {
+ var lines = result.data.split('\n')
+ for (var line in lines) {
+ if (lines[line] === '') continue
+ var attr = lines[line].split('\t')
+ this.log.push({ date: attr[0], status: attr[1], msg: attr[2] })
+ }
+ })
+
+ // Socket io event listeners
+ this.$socket.on(this.ipxeVersion + ' log', entry => {
+ this.log.push({ msg: entry.msg, status: entry.status, date: entry.date })
+ })
+ },
+ updated () {
+ this.$refs.log.$el.scrollTop = this.$refs.log.$el.scrollHeight
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/webapp/src/config/dashboard.js b/webapp/src/config/dashboard.js
index a092cdc..ac8a7b1 100644
--- a/webapp/src/config/dashboard.js
+++ b/webapp/src/config/dashboard.js
@@ -3,11 +3,13 @@ import ConfiguratorModule from '@/components/ConfiguratorModule'
import RegistrationModule from '@/components/RegistrationModule'
import BackendModule from '@/components/BackendModule'
import PermissionModule from '@/components/PermissionModule'
+import IpxeBuilder from '@/components/IpxeBuilderModule'
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' }
+ { path: 'permissions', component: PermissionModule, icon: 'lock_open' },
+ { path: 'ipxe', component: IpxeBuilder, icon: 'merge_type' }
]
diff --git a/webapp/src/config/i18n.js b/webapp/src/config/i18n.js
index 818f57c..3f71729 100644
--- a/webapp/src/config/i18n.js
+++ b/webapp/src/config/i18n.js
@@ -27,7 +27,8 @@ export default {
'ConfiguratorModule': 'iPXE Configurator',
'RegistrationModule': 'Client Registration',
'BackendModule': 'External Backends',
- 'PermissionModule': 'Permission Manager'
+ 'PermissionModule': 'Permission Manager',
+ 'IpxeBuilder': 'iPXE Builder'
}
},
'de': {
@@ -58,7 +59,8 @@ export default {
'ConfiguratorModule': 'iPXE Konfigurator',
'RegistrationModule': 'Client Registrierung',
'BackendModule': 'Externe Backends',
- 'PermissionModule': 'Rechteverwaltung'
+ 'PermissionModule': 'Rechteverwaltung',
+ 'IpxeBuilder': 'iPXE Builder'
}
}
}
diff --git a/webapp/src/main.js b/webapp/src/main.js
index bba65c5..69af37c 100644
--- a/webapp/src/main.js
+++ b/webapp/src/main.js
@@ -78,10 +78,6 @@ socket.on('error', err => {
}
})
-socket.on('hello', data => {
- console.log(data)
-})
-
socket.open()
Vue.prototype.$http = axios