summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorUdo Walter2019-04-13 14:56:11 +0200
committerUdo Walter2019-04-13 14:56:11 +0200
commitc3f65d27c8fe7021ae807dec5cb82ae191697b69 (patch)
treee617d44ca132317703845cc701db0943dedb7c41 /webapp
parent[server/grouputil] fix bug: not checking for duplicate children (diff)
downloadbas-c3f65d27c8fe7021ae807dec5cb82ae191697b69.tar.gz
bas-c3f65d27c8fe7021ae807dec5cb82ae191697b69.tar.xz
bas-c3f65d27c8fe7021ae807dec5cb82ae191697b69.zip
[webapp/groups] add copy to clipboard button
Diffstat (limited to 'webapp')
-rw-r--r--webapp/src/components/DataTable.vue123
-rw-r--r--webapp/src/components/GroupModuleClientList.vue2
-rw-r--r--webapp/src/components/GroupModuleGroupList.vue2
-rw-r--r--webapp/src/components/GroupModuleGroupView.vue218
4 files changed, 231 insertions, 114 deletions
diff --git a/webapp/src/components/DataTable.vue b/webapp/src/components/DataTable.vue
index a537af3..69656ed 100644
--- a/webapp/src/components/DataTable.vue
+++ b/webapp/src/components/DataTable.vue
@@ -8,7 +8,12 @@
"regex": "Regex",
"caseSensitive": "Case sensitive",
"onlyShowSelected": "Selected entries only",
- "selected": "selected"
+ "selected": "selected",
+ "copyDone": "Copied to Clipboard",
+ "nothingToCopy": "Nothing to Copy",
+ "copyHeading": "Copy to Clipboard",
+ "copy": "Copy",
+ "close": "Close"
},
"de": {
"all": "Alle",
@@ -18,7 +23,12 @@
"regex": "Regex",
"caseSensitive": "Groß-/Kleinschreibung beachten",
"onlyShowSelected": "Nur ausgewählte Einträg",
- "selected": "ausgewählt"
+ "selected": "ausgewählt",
+ "copyDone": "In die Zwischenablage kopiert",
+ "nothingToCopy": "Nichts zu kopieren",
+ "copyHeading": "In die Zwischenablage kopieren",
+ "copy": "Kopieren",
+ "close": "Schließen"
}
}
</i18n>
@@ -32,7 +42,7 @@
@filter="filteredRows = $event"
:selected="value"
:items="sortedRows"
- :data-keys="headers.filter(h => h.text !== undefined)"
+ :data-keys="headersWithText"
nested-data
:regex="regex"
:case-sensitive="caseSensitive"
@@ -133,6 +143,9 @@
arrow_upward
</v-icon>
</div>
+ <div v-if="exportButton" class="copy-button">
+ <v-btn icon @click="copyDialog = !copyDialog"><v-icon style="opacity: 0.3;">file_copy</v-icon></v-btn>
+ </div>
</div>
<div class="header-separator" style="height: 2px">
<loading-bar :loading="loading" :transparent="false"></loading-bar>
@@ -158,6 +171,42 @@
<div v-if="filteredRows.length === 0" class="no-result">{{ $t('noResult') }}</div>
</template>
</RecycleScroller>
+ <v-dialog
+ v-model="copyDialog"
+ scrollable
+ :max-width="300"
+ :fullscreen="$vuetify.breakpoint.smAndDown"
+ lazy
+ >
+ <v-card class="non-selectable">
+ <v-card-title class="elevation-3">
+ <div class="subheading">{{ $t('copyHeading') }}</div>
+ <v-spacer></v-spacer>
+ <v-btn-toggle v-model="copyFormat" mandatory class="copy-format-toggle">
+ <v-btn small :color="copyFormat === 0 ? 'primary' : ''">CSV</v-btn>
+ <v-btn small :color="copyFormat === 1 ? 'primary' : ''">JSON</v-btn>
+ </v-btn-toggle>
+ </v-card-title>
+ <v-card-text style="height: 100%">
+ <textarea ref="copyHelper" style="display: none"></textarea>
+ <v-checkbox
+ v-for="header in headersWithText"
+ v-model="header.includeInCopy"
+ :key="header.key"
+ color="primary"
+ :label="header.text"
+ hide-details
+ class="copy-header-checkbox"
+ ></v-checkbox>
+ </v-card-text>
+ <v-divider></v-divider>
+ <v-card-actions>
+ <v-spacer></v-spacer>
+ <v-btn small flat @click="copyDialog = false">{{ $t('close') }}</v-btn>
+ <v-btn small color="primary" @click="copyToClipboard">{{ $t('copy') }}</v-btn>
+ </v-card-actions>
+ </v-card>
+ </v-dialog>
</div>
</template>
@@ -208,6 +257,10 @@ export default {
noSort: {
type: Boolean,
default: false
+ },
+ exportButton: {
+ type: Boolean,
+ default: false
}
},
data () {
@@ -221,7 +274,9 @@ export default {
onlyShowSelected: false,
lastSelectIndex: null,
shiftKey: false,
- filteredRows: []
+ filteredRows: [],
+ copyDialog: false,
+ copyFormat: 0
}
},
computed: {
@@ -254,6 +309,9 @@ export default {
selectedIconMap () {
if (this.singleSelect) return { 'true': 'radio_button_checked', 'false': 'radio_button_unchecked' }
return { 'true': 'check_box', 'false': 'check_box_outline_blank' }
+ },
+ headersWithText () {
+ return this.headers.filter(h => h.text !== undefined)
}
},
watch: {
@@ -394,6 +452,43 @@ export default {
})
if (this.onlyShowSelected && this.$refs.search) this.$refs.search.filterRows()
if (this.filteredRows) this.calcSelectState()
+ },
+ copyToClipboard () {
+ let result = ''
+ const keys = this.headersWithText.filter(h => h.includeInCopy).map(x => 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 (value) rowString += value
+ if (i < topIndex) rowString += ','
+ }
+ if (rowString) result += rowString + '\n'
+ })
+ } else if (this.copyFormat === 1) {
+ const rows = this.filteredRows.map(row => {
+ let filterdRow = {}
+ for (let i in keys) {
+ let k = keys[i]
+ filterdRow[k] = row.data[k]
+ }
+ return filterdRow
+ })
+ result = JSON.stringify(rows, null, 4)
+ }
+ if (!result) {
+ this.$snackbar({ text: this.$t('nothingToCopy'), color: 'error', timeout: 1200 })
+ return
+ }
+ const copyHelper = this.$refs.copyHelper
+ copyHelper.style.display = 'block'
+ copyHelper.value = result
+ copyHelper.select()
+ document.execCommand('copy')
+ copyHelper.style.display = 'none'
+ this.$snackbar({ text: this.$t('copyDone'), color: 'primary', timeout: 1200 })
}
}
}
@@ -405,6 +500,25 @@ export default {
white-space: nowrap;
}
+.copy-button {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 8px;
+ margin: 0 !important;
+ display: flex;
+ align-items: center;
+}
+
+.copy-header-checkbox {
+ margin: 0;
+ padding: 0;
+}
+
+.copy-format-toggle .v-btn--active {
+ color: white;
+}
+
.toggle-button {
margin: 0;
padding: 0;
@@ -466,6 +580,7 @@ export default {
}
.table-head {
+ position: relative;
height: 56px;
display: flex;
font-size: 12px;
diff --git a/webapp/src/components/GroupModuleClientList.vue b/webapp/src/components/GroupModuleClientList.vue
index e3ab36a..9630e55 100644
--- a/webapp/src/components/GroupModuleClientList.vue
+++ b/webapp/src/components/GroupModuleClientList.vue
@@ -30,7 +30,7 @@
<template>
<div>
<v-divider></v-divider>
- <data-table v-model="selected" :headers="headers" :items="clients" :loading="loading" @dblclick="loadClient($event)" min-width="1300px">
+ <data-table v-model="selected" :headers="headers" :items="clients" :loading="loading" @dblclick="loadClient($event)" min-width="1300px" export-button>
<div slot="actions" slot-scope="row" style="text-align: right">
<v-btn icon @click.stop @mousedown="loadClient(row.item)" style="margin: 0"><v-icon>keyboard_arrow_right</v-icon></v-btn>
</div>
diff --git a/webapp/src/components/GroupModuleGroupList.vue b/webapp/src/components/GroupModuleGroupList.vue
index 7fbc070..2c4ac3f 100644
--- a/webapp/src/components/GroupModuleGroupList.vue
+++ b/webapp/src/components/GroupModuleGroupList.vue
@@ -24,7 +24,7 @@
<template>
<div>
<v-divider></v-divider>
- <data-table v-model="selected" :headers="headers" :items="groups" :loading="loading" @dblclick="loadGroup($event)">
+ <data-table v-model="selected" :headers="headers" :items="groups" :loading="loading" @dblclick="loadGroup($event)" export-button>
<div slot="actions" slot-scope="row" style="text-align: right">
<v-btn icon @click.stop @mousedown="loadGroup(row.item)" style="margin: 0"><v-icon>keyboard_arrow_right</v-icon></v-btn>
</div>
diff --git a/webapp/src/components/GroupModuleGroupView.vue b/webapp/src/components/GroupModuleGroupView.vue
index 6706e0a..ebbcfbc 100644
--- a/webapp/src/components/GroupModuleGroupView.vue
+++ b/webapp/src/components/GroupModuleGroupView.vue
@@ -63,128 +63,130 @@
color="primary"
></v-switch>
- <v-tab-item v-if="group.id !== 0" :transition="false" :reverse-transition="false">
- <v-divider></v-divider>
- <v-card-text>
- <v-layout wrap>
- <v-flex lg4 sm6 xs12 order-lg1 order-xs2>
- <v-layout column>
- <v-flex>
- <div class="info-box">
- <div class="body-2 info-heading"><v-icon>label</v-icon><span>{{ $t('name') }}</span></div>
- <div class="info-text">
- <v-text-field v-if="editMode" class="info-input" color="primary" v-model="info.name" hide-details tabindex="1"></v-text-field>
- <div v-else>{{ group.name || '-' }}</div>
+ <v-tabs-items touchless>
+ <v-tab-item v-if="group.id !== 0" :transition="false" :reverse-transition="false">
+ <v-divider></v-divider>
+ <v-card-text>
+ <v-layout wrap>
+ <v-flex lg4 sm6 xs12 order-lg1 order-xs2>
+ <v-layout column>
+ <v-flex>
+ <div class="info-box">
+ <div class="body-2 info-heading"><v-icon>label</v-icon><span>{{ $t('name') }}</span></div>
+ <div class="info-text">
+ <v-text-field v-if="editMode" class="info-input" color="primary" v-model="info.name" hide-details tabindex="1"></v-text-field>
+ <div v-else>{{ group.name || '-' }}</div>
+ </div>
</div>
- </div>
- </v-flex>
- <v-flex>
- <div class="info-box">
- <div class="body-2 info-heading"><v-icon>device_hub</v-icon><span>{{ $t('parents') }}</span></div>
- <div class="info-text">
- <select-box v-if="editMode" v-model="parents" :items="groupList" class="info-input" hide-details></select-box>
- <div v-else class="chip-container non-selectable">
- <v-tooltip v-for="parent in firstParents" :key="parent.id" top open-delay="800">
- <template #activator="{ on }">
- <v-chip v-on="on" small label style="width: calc(50% - 8px)" @click="openParent(parent)">
- <span class="chip-text">{{ parent.name || parent.id }}</span>
- </v-chip>
- </template>
- <span>{{ parent.name || parent.id }}</span>
- </v-tooltip>
- <span v-if="group.parents && group.parents.length > 5" class="and-more">+ {{ group.parents.length - 5 }} {{ $t('more') }}</span>
- <span v-else-if="group.parents === undefined || group.parents.length === 0">-</span>
+ </v-flex>
+ <v-flex>
+ <div class="info-box">
+ <div class="body-2 info-heading"><v-icon>device_hub</v-icon><span>{{ $t('parents') }}</span></div>
+ <div class="info-text">
+ <select-box v-if="editMode" v-model="parents" :items="groupList" class="info-input" hide-details></select-box>
+ <div v-else class="chip-container non-selectable">
+ <v-tooltip v-for="parent in firstParents" :key="parent.id" top open-delay="800">
+ <template #activator="{ on }">
+ <v-chip v-on="on" small label style="width: calc(50% - 8px)" @click="openParent(parent)">
+ <span class="chip-text">{{ parent.name || parent.id }}</span>
+ </v-chip>
+ </template>
+ <span>{{ parent.name || parent.id }}</span>
+ </v-tooltip>
+ <span v-if="group.parents && group.parents.length > 5" class="and-more">+ {{ group.parents.length - 5 }} {{ $t('more') }}</span>
+ <span v-else-if="group.parents === undefined || group.parents.length === 0">-</span>
+ </div>
</div>
</div>
- </div>
- </v-flex>
- <v-flex>
- <div class="info-box">
- <div class="body-2 info-heading"><v-icon>list</v-icon><span>{{ $t('config') }}</span></div>
- <div class="info-text">
- <v-select v-if="editMode"
- class="info-input"
- clearable
- item-text="name"
- item-value="id"
- :menu-props="{ offsetY: '' }"
- hide-details
- color="primary"
- v-model="info.configId"
- :items="configList"
- ></v-select>
- <div v-else>{{ configName || '-' }}</div>
+ </v-flex>
+ <v-flex>
+ <div class="info-box">
+ <div class="body-2 info-heading"><v-icon>list</v-icon><span>{{ $t('config') }}</span></div>
+ <div class="info-text">
+ <v-select v-if="editMode"
+ class="info-input"
+ clearable
+ item-text="name"
+ item-value="id"
+ :menu-props="{ offsetY: '' }"
+ hide-details
+ color="primary"
+ v-model="info.configId"
+ :items="configList"
+ ></v-select>
+ <div v-else>{{ configName || '-' }}</div>
+ </div>
</div>
+ </v-flex>
+ </v-layout>
+ </v-flex>
+ <v-flex lg4 sm6 xs12 order-lg2 order-xs3>
+ <div class="info-box">
+ <div class="body-2 info-heading"><v-icon>description</v-icon><span>{{ $t('description') }}</span></div>
+ <div class="info-text">
+ <v-textarea v-if="editMode" class="info-input" rows="1" auto-grow hide-details color="primary" v-model="info.description" tabindex="2"></v-textarea>
+ <div v-else style="white-space: pre-wrap;">{{ group.description || '-' }}</div>
</div>
- </v-flex>
- </v-layout>
- </v-flex>
- <v-flex lg4 sm6 xs12 order-lg2 order-xs3>
- <div class="info-box">
- <div class="body-2 info-heading"><v-icon>description</v-icon><span>{{ $t('description') }}</span></div>
- <div class="info-text">
- <v-textarea v-if="editMode" class="info-input" rows="1" auto-grow hide-details color="primary" v-model="info.description" tabindex="2"></v-textarea>
- <div v-else style="white-space: pre-wrap;">{{ group.description || '-' }}</div>
</div>
- </div>
- <div class="info-box">
- <div class="body-2 info-heading"><v-icon>settings_ethernet</v-icon><span>{{ $t('ipranges') }}</span></div>
- <div class="info-text">
- <div v-if="editMode">
- <div v-for="(iprange, index) in ipranges" :key="index">
- <div class="iprange">
- <v-btn class="iprange-button" small icon @click="removeIprange(index)"><v-icon>remove</v-icon></v-btn>
- <v-text-field solo flat hide-details :label="$t('startIp')" color="primary" v-model="iprange.startIp" single-line></v-text-field>
- <span class="ip-seperator">-</span>
- <v-text-field solo flat hide-details :label="$t('endIp')" color="primary" v-model="iprange.endIp" single-line></v-text-field>
+ <div class="info-box">
+ <div class="body-2 info-heading"><v-icon>settings_ethernet</v-icon><span>{{ $t('ipranges') }}</span></div>
+ <div class="info-text">
+ <div v-if="editMode">
+ <div v-for="(iprange, index) in ipranges" :key="index">
+ <div class="iprange">
+ <v-btn class="iprange-button" small icon @click="removeIprange(index)"><v-icon>remove</v-icon></v-btn>
+ <v-text-field solo flat hide-details :label="$t('startIp')" color="primary" v-model="iprange.startIp" single-line></v-text-field>
+ <span class="ip-seperator">-</span>
+ <v-text-field solo flat hide-details :label="$t('endIp')" color="primary" v-model="iprange.endIp" single-line></v-text-field>
+ </div>
+ <v-divider></v-divider>
+ </div>
+ <div class="iprange-add-wrapper">
+ <v-btn class="iprange-button" small icon @click="addIprange"><v-icon>add</v-icon></v-btn>
</div>
- <v-divider></v-divider>
</div>
- <div class="iprange-add-wrapper">
- <v-btn class="iprange-button" small icon @click="addIprange"><v-icon>add</v-icon></v-btn>
+ <div v-else>
+ <table>
+ <tr v-for="(iprange, index) in group.ipranges" :key="index">
+ <td class="text-xs-right">{{ iprange.startIp }}</td>
+ <td class="ip-seperator">-</td>
+ <td>{{ iprange.endIp }}</td>
+ </tr>
+ </table>
+ <div v-if="group.ipranges && group.ipranges.length === 0">-</div>
</div>
</div>
+ </div>
+ </v-flex>
+ <v-flex lg4 xs12 order-lg3 order-xs1 class="text-xs-right">
+ <div class="info-box">
+ <div v-if="!editMode">
+ <v-btn color="error" flat @click="deleteGroup" class="info-buttons">
+ <v-icon left>delete</v-icon>{{ $t('delete') }}
+ </v-btn>
+ <v-btn color="primary" flat @click="editInfo" class="info-buttons">
+ <v-icon left>create</v-icon>{{ $t('edit') }}
+ </v-btn>
+ </div>
<div v-else>
- <table>
- <tr v-for="(iprange, index) in group.ipranges" :key="index">
- <td class="text-xs-right">{{ iprange.startIp }}</td>
- <td class="ip-seperator">-</td>
- <td>{{ iprange.endIp }}</td>
- </tr>
- </table>
- <div v-if="group.ipranges && group.ipranges.length === 0">-</div>
+ <v-btn color="primary" flat @click="cancelEdit" class="info-buttons">{{ $t('cancel') }}</v-btn>
+ <v-btn color="primary" @click="saveData" class="info-buttons" tabindex="3">
+ <v-icon left>save</v-icon>{{ $t('save') }}
+ </v-btn>
</div>
</div>
- </div>
- </v-flex>
- <v-flex lg4 xs12 order-lg3 order-xs1 class="text-xs-right">
- <div class="info-box">
- <div v-if="!editMode">
- <v-btn color="error" flat @click="deleteGroup" class="info-buttons">
- <v-icon left>delete</v-icon>{{ $t('delete') }}
- </v-btn>
- <v-btn color="primary" flat @click="editInfo" class="info-buttons">
- <v-icon left>create</v-icon>{{ $t('edit') }}
- </v-btn>
- </div>
- <div v-else>
- <v-btn color="primary" flat @click="cancelEdit" class="info-buttons">{{ $t('cancel') }}</v-btn>
- <v-btn color="primary" @click="saveData" class="info-buttons" tabindex="3">
- <v-icon left>save</v-icon>{{ $t('save') }}
- </v-btn>
- </div>
- </div>
- </v-flex>
- </v-layout>
- </v-card-text>
- </v-tab-item>
- <v-tab-item v-if="group.id !== 'create'" lazy :transition="false" :reverse-transition="false">
- <group-module-group-list :tabIndex="tabIndex" :groupId="group.id" :groups="group.subgroups || []" />
- </v-tab-item>
- <v-tab-item v-if="group.id !== 'create'" lazy :transition="false" :reverse-transition="false">
- <group-module-client-list :tabIndex="tabIndex" :groupId="group.id" :clients="group.clients || []" />
- </v-tab-item>
+ </v-flex>
+ </v-layout>
+ </v-card-text>
+ </v-tab-item>
+ <v-tab-item v-if="group.id !== 'create'" lazy :transition="false" :reverse-transition="false">
+ <group-module-group-list :tabIndex="tabIndex" :groupId="group.id" :groups="group.subgroups || []" />
+ </v-tab-item>
+ <v-tab-item v-if="group.id !== 'create'" lazy :transition="false" :reverse-transition="false">
+ <group-module-client-list :tabIndex="tabIndex" :groupId="group.id" :clients="group.clients || []" />
+ </v-tab-item>
+ </v-tabs-items>
</v-tabs>
</v-card>
</template>