summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorUdo Walter2019-04-14 03:16:47 +0200
committerUdo Walter2019-04-14 03:16:47 +0200
commit175065246ec68835111967c4174de526a6356406 (patch)
tree8f89abc22120ca4fc425f78a40b95a6f6e140563 /webapp
parent[datatable] exportButton -> copyButton (diff)
downloadbas-175065246ec68835111967c4174de526a6356406.tar.gz
bas-175065246ec68835111967c4174de526a6356406.tar.xz
bas-175065246ec68835111967c4174de526a6356406.zip
[ipranges] new webapp module and ip for ipranges
Diffstat (limited to 'webapp')
-rw-r--r--webapp/src/components/DataTable.vue6
-rw-r--r--webapp/src/components/IprangeModule.vue236
-rw-r--r--webapp/src/components/SelectBox.vue6
-rw-r--r--webapp/src/config/dashboard.js2
-rw-r--r--webapp/src/config/i18n.js6
5 files changed, 250 insertions, 6 deletions
diff --git a/webapp/src/components/DataTable.vue b/webapp/src/components/DataTable.vue
index b3da534..36b44f3 100644
--- a/webapp/src/components/DataTable.vue
+++ b/webapp/src/components/DataTable.vue
@@ -176,7 +176,6 @@
v-model="copyDialog"
scrollable
:max-width="300"
- :fullscreen="$vuetify.breakpoint.smAndDown"
lazy
>
<v-card class="non-selectable">
@@ -456,14 +455,15 @@ export default {
},
copyToClipboard () {
let result = ''
- const keys = this.headersWithText.filter(h => h.includeInCopy).map(x => x.key)
+ const keys = this.headersWithText.filter(h => h.includeInCopy).map(x => x.copyKey || x.key)
if (this.copyFormat === 0) {
const topIndex = keys.length - 1
this.filteredRows.forEach(row => {
let rowString = ''
for (let i in keys) {
let value = row.data[keys[i]]
- if (typeof value === 'string' && value.indexOf(',') !== -1) value = '"' + value + '"'
+ if (typeof value === 'string' && (value.indexOf(',') !== -1 || value.indexOf('"') !== -1)) value = JSON.stringify(value)
+ else if (typeof value === 'object') value = JSON.stringify(JSON.stringify(value))
if (value) rowString += value
if (i < topIndex) rowString += ','
}
diff --git a/webapp/src/components/IprangeModule.vue b/webapp/src/components/IprangeModule.vue
new file mode 100644
index 0000000..5c37f60
--- /dev/null
+++ b/webapp/src/components/IprangeModule.vue
@@ -0,0 +1,236 @@
+<i18n>
+{
+ "en": {
+ "ipranges": "IP Ranges",
+ "deleteIpranges": "Delete one iprange | Delete {0} ipranges",
+ "createIprange": "Create IP range",
+ "id": "ID",
+ "startIp": "Start IP",
+ "endIp": "End IP",
+ "group": "Group",
+ "deleteTitle": "Delete this IP range? | Delete these {0} IP ranges?",
+ "createTitle": "Create IP Range",
+ "editTitle": "Edit IP Range",
+ "required": "Required.",
+ "invalidIp": "Invalid IP Address.",
+ "deleteSuccess": "Successfully deleted IP ranges.",
+ "updateSuccess": "Successfully updated IP range.",
+ "createSuccess": "Successfully created IP range."
+ },
+ "de": {
+ "ipranges": "IP Bereiche",
+ "deleteIpranges": "Einen IP Bereich löschen | {0} IP Bereiche löschen",
+ "createIprange": "IP Bereich erstellen",
+ "id": "ID",
+ "startIp": "Start IP",
+ "endIp": "End IP",
+ "group": "Gruppe",
+ "deleteTitle": "Diesen IP Bereich löschen? | Diese {0} IP Bereiche löschen?",
+ "createTitle": "IP Bereich erstellen",
+ "editTitle": "IP Bereich bearbeiten",
+ "required": "Benötigt.",
+ "invalidIp": "Ungültige IP Adresse.",
+ "deleteSuccess": "IP Bereiche erfolgreich gelöscht.",
+ "updateSuccess": "IP Bereich erfolgreich aktualisiert.",
+ "createSuccess": "IP Bereich erfolgreich erstellen."
+ }
+}
+</i18n>
+
+<template>
+ <v-container fill-height>
+ <v-layout>
+ <v-flex xl10 offset-xl1 lg12>
+ <v-card class="tabbar-card">
+ <v-tabs v-model="tabs" grow hide-slider :dark="tabsDark" :color="tabsColor" :slider-color="tabsSliderColor">
+ <v-tab><v-icon class="tabbar-tabicon">settings_ethernet</v-icon>{{ $t('ipranges') }}</v-tab>
+ </v-tabs>
+ </v-card>
+ <v-tabs-items v-model="tabs" style="padding-bottom: 20px">
+ <v-tab-item>
+ <v-subheader>{{ $t('ipranges') }}</v-subheader>
+ <v-card>
+ <data-table v-model="selected" :headers="headers" :items="ipranges" @dblclick="dialog = { show: true, type: 'edit', iprange: { ...$event } }" min-width="840px" copy-button>
+ <template #group="row">
+ <v-btn small flat :to="{ name: 'GroupModule.group', params: { id: row.item.groupId } }" class="ma-0" style="text-transform: none;">
+ {{ row.item.group.name }}
+ <v-icon small class="ml-2">open_in_new</v-icon>
+ </v-btn>
+ </template>
+ <template #actions="row">
+ <div style="text-align: right">
+ <v-btn flat icon @click.stop="dialog = { show: true, type: 'edit', iprange: { ...row.item } }" class="ma-0"><v-icon color="primary">edit</v-icon></v-btn>
+ </div>
+ </template>
+ </data-table>
+ </v-card>
+ <div class="text-xs-right">
+ <v-btn flat color="error" @click="dialog = { show: true, type: 'delete' }" :disabled="selected.length === 0">
+ <v-icon left>delete</v-icon>{{ $tc('deleteIpranges', selected.length, [selected.length]) }}
+ </v-btn>
+ <v-btn flat color="success" @click="dialog = { show: true, type: 'create', iprange: {} }"><v-icon left>create</v-icon>{{ $t('createIprange') }}</v-btn>
+ </div>
+ </v-tab-item>
+ </v-tabs-items>
+ </v-flex>
+ </v-layout>
+
+ <v-dialog
+ v-model="dialog.show"
+ :max-width="dialog.type === 'delete' ? '500px' : '500px'"
+ scrollable
+ :persistent="dialog.type !== 'delete'"
+ :fullscreen="$vuetify.breakpoint.smAndDown"
+ lazy
+ >
+ <v-card>
+ <v-card-title class="elevation-3">
+ <div class="headline">{{ dialogTitle }}</div>
+ </v-card-title>
+ <v-card-text style="height: 100%">
+
+ <div v-if="dialog.type === 'delete'" style="padding: 14px;">
+ <RecycleScroller
+ :items="selected"
+ :item-size="24"
+ page-mode
+ >
+ <div slot-scope="{ item }">[{{ item.id }}] {{ item.startIp }} - {{ item.endIp }}, {{ item.group.name }}</div>
+ </RecycleScroller>
+ </div>
+
+ <v-form v-else ref="form" style="padding: 14px;">
+ <select-box :rules="[rules.group]" prepend-icon="category" :label="$t('group')" v-model="dialog.iprange.selectedGroup" :items="groupList" single-select></select-box>
+ <v-text-field :rules="[rules.ip]" validate-on-blur prepend-icon="vertical_align_top" :label="$t('startIp')" color="primary" v-model="dialog.iprange.startIp"></v-text-field>
+ <v-text-field :rules="[rules.ip]" validate-on-blur prepend-icon="vertical_align_bottom" :label="$t('endIp')" color="primary" v-model="dialog.iprange.endIp"></v-text-field>
+ </v-form>
+
+ </v-card-text>
+
+ <v-divider></v-divider>
+
+ <v-card-actions>
+ <v-spacer></v-spacer>
+ <v-btn flat="flat" @click="dialog.show = false">{{ $t('cancel') }}</v-btn>
+ <v-btn :color="dialogColor" @click="submitAction">{{ dialogButtonText }}</v-btn>
+ </v-card-actions>
+ </v-card>
+
+ </v-dialog>
+ </v-container>
+</template>
+
+<script>
+import SelectBox from '@/components/SelectBox'
+import DataTable from '@/components/DataTable'
+import { mapState, mapGetters } from 'vuex'
+
+export default {
+ name: 'IprangeModule',
+ components: {
+ SelectBox,
+ DataTable
+ },
+ data () {
+ return {
+ tabs: 0,
+ ipranges: [],
+ selected: [],
+ dialog: { show: false, type: null, iprange: {} }
+ }
+ },
+ computed: {
+ ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor']),
+ ...mapState('groups', ['groupList']),
+ headers () {
+ return [
+ { key: 'id', text: this.$t('id'), width: '50px' },
+ { key: 'startIp', text: this.$t('startIp'), width: '120px' },
+ { key: 'endIp', text: this.$t('endIp'), width: '140px' },
+ { key: 'group', text: this.$t('group'), copyKey: 'groupId', sortKey: 'groupName' },
+ { key: 'actions', width: '60px' }
+ ]
+ },
+ rules () {
+ return {
+ group: value => value.length > 0 || this.$t('required'),
+ ip: value => {
+ if (!value) return this.$t('required')
+ value = value.split('.')
+ if (value.length !== 4) return this.$t('invalidIp')
+ for (let i in value) {
+ if (value[i] < 0 || value[i] > 255) return this.$t('invalidIp')
+ }
+ return true
+ }
+ }
+ },
+ dialogColor () {
+ if (this.dialog.type === 'delete') return 'error'
+ if (this.dialog.type === 'create') return 'success'
+ return 'primary'
+ },
+ dialogTitle () {
+ if (this.dialog.type === 'delete') return this.$tc('deleteTitle', this.selected.length, [this.selected.length])
+ if (this.dialog.type === 'create') return this.$t('createTitle')
+ return this.$t('editTitle')
+ },
+ dialogButtonText () {
+ if (this.dialog.type === 'delete') return this.$t('delete')
+ if (this.dialog.type === 'create') return this.$t('create')
+ return this.$t('save')
+ }
+ },
+ watch: {
+ dialog (value) {
+ if (this.$refs.form) this.$refs.form.resetValidation()
+ if (value.type === 'edit') this.$set(this.dialog.iprange, 'selectedGroup', [this.dialog.iprange.group])
+ }
+ },
+ methods: {
+ async submitAction () {
+ let text = ''
+ if (this.dialog.type === 'delete') {
+ await this.$http.post('/api/ipranges/?delete', {
+ ids: this.selected.map(x => x.id)
+ })
+ text = this.$t('deleteSuccess')
+ } else {
+ if (this.$refs.form && !this.$refs.form.validate()) return
+ let url = '/api/ipranges'
+ if (this.dialog.iprange.id) {
+ url += '/' + this.dialog.iprange.id
+ text = this.$t('updateSuccess')
+ } else {
+ text = this.$t('createSuccess')
+ }
+ await this.$http.post(url, {
+ startIp: this.dialog.iprange.startIp,
+ endIp: this.dialog.iprange.endIp,
+ groupId: this.dialog.iprange.selectedGroup[0].id
+ })
+ }
+ this.dialog.show = false
+ this.$snackbar({ text, color: 'success' })
+ this.loadIpranges()
+ },
+ loadIpranges () {
+ this.$http.get('/api/ipranges').then(response => {
+ response.data.forEach(iprange => {
+ iprange.groupName = iprange.group.name
+ })
+ this.ipranges = Object.freeze(response.data)
+ })
+ }
+ },
+ created () {
+ this.$store.dispatch('groups/loadGroupList')
+ this.loadIpranges()
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+
+</style>
diff --git a/webapp/src/components/SelectBox.vue b/webapp/src/components/SelectBox.vue
index 900b604..d01429e 100644
--- a/webapp/src/components/SelectBox.vue
+++ b/webapp/src/components/SelectBox.vue
@@ -12,7 +12,7 @@
</i18n>
<template>
- <v-input class="v-text-field select-input" :class="inputClasses" :hide-details="hideDetails" :prepend-icon="prependIcon">
+ <v-input class="v-text-field select-input" :class="inputClasses" :hide-details="hideDetails" :prepend-icon="prependIcon" :value="value" :rules="rules">
<v-menu v-model="menu" offset-y :close-on-content-click="false" lazy class="select-menu non-selectable" :nudge-bottom="1">
<div slot="activator" class="select-input-content">
<label v-if="label !== undefined && (!singleLine || (singleLine && value.length === 0))"
@@ -107,6 +107,10 @@ export default {
prependIcon: {
type: String
},
+ rules: {
+ type: Array,
+ default: () => []
+ },
singleSelect: {
type: Boolean,
default: false
diff --git a/webapp/src/config/dashboard.js b/webapp/src/config/dashboard.js
index e049dc5..95b59fb 100644
--- a/webapp/src/config/dashboard.js
+++ b/webapp/src/config/dashboard.js
@@ -1,4 +1,5 @@
import GroupModule from '@/components/GroupModule'
+import IprangeModule from '@/components/IprangeModule'
import ConfiguratorModule from '@/components/ConfiguratorModule'
import RegistrationModule from '@/components/RegistrationModule'
import BackendModule from '@/components/BackendModule'
@@ -10,6 +11,7 @@ import EventModule from '@/components/EventModule'
export default [
{ path: 'groups', component: GroupModule, icon: 'category' },
+ { path: 'ipranges', component: IprangeModule, icon: 'settings_ethernet' },
{ path: 'configurator', component: ConfiguratorModule, icon: 'list' },
{ path: 'events', component: EventModule, icon: 'event' },
{ path: 'registration', component: RegistrationModule, icon: 'assignment' },
diff --git a/webapp/src/config/i18n.js b/webapp/src/config/i18n.js
index 6b84418..a92541f 100644
--- a/webapp/src/config/i18n.js
+++ b/webapp/src/config/i18n.js
@@ -31,7 +31,8 @@ export default {
'IpxeBuilderModule': 'iPXE Builder',
'UserModule': 'User Management',
'LogModule': 'System Log',
- 'EventModule': 'Event Manager'
+ 'EventModule': 'Event Manager',
+ 'IprangeModule': 'IP Ranges'
}
},
'de': {
@@ -66,7 +67,8 @@ export default {
'IpxeBuilderModule': 'iPXE Builder',
'UserModule': 'Benutzerverwaltung',
'LogModule': 'System Protokoll',
- 'EventModule': 'Event Verwaltung'
+ 'EventModule': 'Event Verwaltung',
+ 'IprangeModule': 'IP Bereiche'
}
}
}