summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorUdo Walter2019-02-28 20:26:08 +0100
committerUdo Walter2019-02-28 20:26:08 +0100
commit4dd3c027dc088f74c3bd6f3148b6d94eca8719d3 (patch)
treea4ecc11dcc56afa5a3e3658e12b05649506953bf /webapp
parent[webapp/groups] performance improvements (diff)
downloadbas-4dd3c027dc088f74c3bd6f3148b6d94eca8719d3.tar.gz
bas-4dd3c027dc088f74c3bd6f3148b6d94eca8719d3.tar.xz
bas-4dd3c027dc088f74c3bd6f3148b6d94eca8719d3.zip
[webapp/datatable] improve search performance
Diffstat (limited to 'webapp')
-rw-r--r--webapp/src/components/DataTable.vue172
-rw-r--r--webapp/src/components/DataTableSearch.vue203
-rw-r--r--webapp/src/main.js2
3 files changed, 230 insertions, 147 deletions
diff --git a/webapp/src/components/DataTable.vue b/webapp/src/components/DataTable.vue
index bd97394..ab530fc 100644
--- a/webapp/src/components/DataTable.vue
+++ b/webapp/src/components/DataTable.vue
@@ -1,7 +1,6 @@
<i18n>
{
"en": {
- "search": "Search",
"all": "All",
"entries": "Entries",
"noResult": "No entries.",
@@ -12,7 +11,6 @@
"selected": "selected"
},
"de": {
- "search": "Suche",
"all": "Alle",
"entries": "Einträge",
"noResult": "Keine Einträge.",
@@ -29,41 +27,16 @@
<div>
<v-layout wrap :class="{ 'd-block': slim }">
<v-flex xs12 :md6="!slim" class="d-flex align-center search-wrapper" :class="{ 'slim-search-wrapper': slim }">
- <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
- v-if="!slim"
- 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>
- <template v-if="!slim">
- <v-btn v-if="index === 0" icon @click="newSearchField"><v-icon>add</v-icon></v-btn>
- <v-btn v-else icon @click="removeSearchField(s)"><v-icon>remove</v-icon></v-btn>
- </template>
- </div>
- </div>
+ <data-table-search
+ ref="search"
+ @filter="filteredRows = $event"
+ :rows="sortedRows"
+ :headers="headers"
+ :regex="regex"
+ :case-sensitive="caseSensitive"
+ :only-show-selected="onlyShowSelected"
+ :slim="slim"
+ ></data-table-search>
<v-divider :class="{ 'hidden-md-and-up': !slim }"></v-divider>
<v-divider v-if="!slim" vertical class="hidden-sm-and-down"></v-divider>
</v-flex>
@@ -173,9 +146,13 @@
</template>
<script>
+import DataTableSearch from '@/components/DataTableSearch'
export default {
name: 'DataTable',
+ components: {
+ DataTableSearch
+ },
props: {
headers: {
type: Array
@@ -206,7 +183,6 @@ export default {
data () {
return {
selected: [],
- search: [{ text: { raw: '', upper: '', regex: new RegExp(), regexCI: new RegExp() }, key: null }],
internalRowCount: 10,
selectState: 0,
headerSortState: {},
@@ -215,14 +191,10 @@ export default {
onlyShowSelected: false,
lastSelectIndex: null,
shiftKey: false,
- filteredRows: [],
- filteringLoop: { running: false, cancel: () => true }
+ filteredRows: []
}
},
computed: {
- searchableHeaders () {
- return this.headers.filter(h => h.text !== undefined)
- },
computedMinWidth () {
if (this.minWidth) return this.minWidth
if (this.slim) return '200px'
@@ -236,24 +208,19 @@ export default {
if (this.computedRowCount > 0) style.height = (Math.max(Math.min(this.computedRowCount, this.filteredRows.length), 1) * 48 + 58) + 'px'
return style
},
- dataKeys () { return this.headers.map(x => x.key) },
rows () {
return Object.freeze(this.items.map(item => ({ data: item, selected: false, id: item.id })))
},
sortedRows () {
const rows = this.rows.slice(0)
for (let key in this.headerSortState) {
+ if (!this.headers.some(header => header.key === key)) continue
+ console.log('asdf')
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 Object.freeze(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
+ return Object.freeze(rows)
}
},
watch: {
@@ -267,24 +234,6 @@ export default {
this.processSelected(newValue)
}
},
- sortedRows () {
- this.filterRows()
- },
- regex () {
- this.filterRows()
- },
- caseSensitive () {
- this.filterRows()
- },
- onlyShowSelected () {
- this.filterRows()
- },
- search: {
- deep: true,
- handler () {
- this.filterRows()
- }
- },
filteredRows () {
this.lastSelectIndex = null
this.calcSelectState()
@@ -292,11 +241,18 @@ export default {
},
methods: {
resetData () {
+ if (this.$refs.search) this.$refs.search.resetData()
Object.assign(this.$data, this.$options.data.call(this))
this.processSelected(this.value)
this.$emit('input', this.selected)
if (this.$refs.scroller) this.$refs.scroller.$el.scrollTop = 0
},
+ resetSearch () {
+ if (this.$refs.search) this.$refs.search.resetData()
+ this.processSelected(this.value)
+ this.$emit('input', this.selected)
+ if (this.$refs.scroller) this.$refs.scroller.$el.scrollTop = 0
+ },
selectItem (row, index) {
// Select or deselect this row
const selected = row.selected = !row.selected
@@ -367,31 +323,6 @@ export default {
}
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 })
- },
- removeSearchField (s) {
- this.search.splice(this.search.indexOf(s), 1)
- window.dispatchEvent(new Event('scroll'))
- },
toggleHeaderSort (header) {
if (header.text === undefined) return
const state = this.headerSortState[header.key]
@@ -418,40 +349,9 @@ export default {
row.selected = false
}
})
- if (this.onlyShowSelected) this.filterRows()
+ if (this.onlyShowSelected && this.$refs.search) this.$refs.search.filterRows()
if (this.filteredRows) this.calcSelectState()
- },
- filterRows () {
- // Cancel the last filtering loop
- this.filteringLoop.cancel()
-
- // Skip filtering if all search strings are empty
- if (this.search.every(s => s.text.raw === '') && !this.onlyShowSelected) {
- this.filteredRows = Object.freeze(this.sortedRows)
- return
- }
-
- // Filter rows using a time slicing loop
- const result = []
- this.filteringLoop = this.$loop(this.sortedRows.length, 1000,
- i => {
- const row = this.sortedRows[i]
- if ((!this.onlyShowSelected || (this.onlyShowSelected && row.selected)) && this.search.every(s => {
- if (s.key === null) {
- return this.dataKeys.some(key => {
- return this.filterFunction(s, String(row.data[key]))
- })
- } else {
- return this.filterFunction(s, String(row.data[s.key]))
- }
- })) result.push(row)
- },
- () => { this.filteredRows = result }
- )
}
- },
- created () {
- this.filterRows()
}
}
</script>
@@ -483,26 +383,6 @@ export default {
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;
- font-family: 'Roboto Mono';
-}
-
-.column-select {
- flex: 0 1 0
-}
-
.rowcount-select {
padding: 0;
display: inline-block;
diff --git a/webapp/src/components/DataTableSearch.vue b/webapp/src/components/DataTableSearch.vue
new file mode 100644
index 0000000..aaa2cc3
--- /dev/null
+++ b/webapp/src/components/DataTableSearch.vue
@@ -0,0 +1,203 @@
+<i18n>
+{
+ "en": {
+ "search": "Search",
+ "all": "All"
+ },
+ "de": {
+ "search": "Suche",
+ "all": "Alle"
+ }
+}
+</i18n>
+
+<template>
+ <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
+ v-if="!slim"
+ 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>
+ <template v-if="!slim">
+ <v-btn v-if="index === 0" icon @click="newSearchField"><v-icon>add</v-icon></v-btn>
+ <v-btn v-else icon @click="removeSearchField(s)"><v-icon>remove</v-icon></v-btn>
+ </template>
+ </div>
+ </div>
+</template>
+
+<script>
+
+export default {
+ name: 'DataTableSearch',
+ props: {
+ rows: {
+ type: Array
+ },
+ headers: {
+ type: Array
+ },
+ regex: {
+ type: Boolean,
+ default: false
+ },
+ caseSensitive: {
+ type: Boolean,
+ default: false
+ },
+ onlyShowSelected: {
+ type: Boolean,
+ default: false
+ },
+ slim: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ search: [{ text: { raw: '', upper: '', regex: new RegExp(), regexCI: new RegExp() }, key: null }],
+ filteringLoop: { running: false, cancel: () => true }
+ }
+ },
+ computed: {
+ searchableHeaders () {
+ return this.headers.filter(h => h.text !== undefined)
+ },
+ dataKeys () { return this.headers.map(x => x.key) },
+ 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
+ }
+ },
+ watch: {
+ rows () {
+ this.filterRows()
+ },
+ regex () {
+ this.filterRows()
+ },
+ caseSensitive () {
+ this.filterRows()
+ },
+ onlyShowSelected () {
+ this.filterRows()
+ },
+ search: {
+ deep: true,
+ handler () {
+ this.filterRows()
+ }
+ }
+ },
+ methods: {
+ resetData () {
+ Object.assign(this.$data, this.$options.data.call(this))
+ },
+ 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 })
+ },
+ removeSearchField (s) {
+ this.search.splice(this.search.indexOf(s), 1)
+ window.dispatchEvent(new Event('scroll'))
+ },
+ filterRows () {
+ // Cancel the last filtering loop
+ this.filteringLoop.cancel()
+
+ // Skip filtering if all search strings are empty
+ if (this.search.every(s => s.text.raw === '') && !this.onlyShowSelected) {
+ this.$emit('filter', Object.freeze(this.rows))
+ return
+ }
+
+ // Filter rows using a time slicing loop
+ const result = []
+ this.filteringLoop = this.$loop(this.rows.length, 1000,
+ i => {
+ const row = this.rows[i]
+ if ((!this.onlyShowSelected || (this.onlyShowSelected && row.selected)) && this.search.every(s => {
+ if (s.key === null) {
+ return this.dataKeys.some(key => {
+ return this.filterFunction(s, String(row.data[key]))
+ })
+ } else {
+ return this.filterFunction(s, String(row.data[s.key]))
+ }
+ })) result.push(row)
+ },
+ () => { this.$emit('filter', Object.freeze(result)) }
+ )
+ }
+ },
+ created () {
+ this.filterRows()
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.search-field-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.search-field-wrapper > div {
+ display: flex;
+ align-items: center;
+}
+
+.search-field {
+ margin: 0;
+ padding: 0;
+ font-family: 'Roboto Mono';
+}
+
+.column-select {
+ flex: 0 1 0
+}
+</style>
diff --git a/webapp/src/main.js b/webapp/src/main.js
index 20dde73..70afb40 100644
--- a/webapp/src/main.js
+++ b/webapp/src/main.js
@@ -39,7 +39,7 @@ const i18n = new VueI18n({
Vue.use(Vuetify, {
theme: {
- primary: '#0195ff'
+ primary: '#0095ff'
},
lang: {
t: (key, ...params) => i18n.t(key, params)