summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorUdo Walter2019-03-04 06:17:17 +0100
committerUdo Walter2019-03-04 06:17:17 +0100
commiteff95f0a279aabe5b3ae0d9ee20cccd46da6554b (patch)
tree251f2af21b346bb4822c7390b0ead3cc07cc5c63 /webapp
parentForgot to remove the console log. -.- (diff)
downloadbas-eff95f0a279aabe5b3ae0d9ee20cccd46da6554b.tar.gz
bas-eff95f0a279aabe5b3ae0d9ee20cccd46da6554b.tar.xz
bas-eff95f0a279aabe5b3ae0d9ee20cccd46da6554b.zip
[webapp/log] first system log implementation (wip, frontend only atm)
Diffstat (limited to 'webapp')
-rw-r--r--webapp/src/components/DataTable.vue112
-rw-r--r--webapp/src/components/DataTableSearch.vue66
-rw-r--r--webapp/src/components/LogModule.vue328
-rw-r--r--webapp/src/components/SelectBox.vue7
-rw-r--r--webapp/src/components/SettingsModule.vue2
-rw-r--r--webapp/src/config/dashboard.js4
-rw-r--r--webapp/src/config/i18n.js6
7 files changed, 448 insertions, 77 deletions
diff --git a/webapp/src/components/DataTable.vue b/webapp/src/components/DataTable.vue
index 03684d1..c55ed96 100644
--- a/webapp/src/components/DataTable.vue
+++ b/webapp/src/components/DataTable.vue
@@ -30,8 +30,9 @@
<data-table-search
ref="search"
@filter="filteredRows = $event"
- :rows="sortedRows"
- :headers="headers"
+ :items="sortedRows"
+ :data-keys="headers.filter(h => h.text !== undefined)"
+ nested-data
:regex="regex"
:case-sensitive="caseSensitive"
:only-show-selected="onlyShowSelected"
@@ -45,28 +46,34 @@
<div class="d-flex align-center" style="font-size: 12px">
<div class="nowrap">
<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>
+ <template #activator="{ on }">
+ <v-btn v-on="on" icon class="toggle-button" @click="regex = !regex">
+ <span :class="regex ? 'primary--text' : ''">.*?</span>
+ </v-btn>
+ </template>
<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>
+ <template #activator="{ on }">
+ <v-btn v-on="on" icon class="toggle-button" @click="caseSensitive = !caseSensitive">
+ <span :class="caseSensitive ? 'primary--text' : ''">Aa</span>
+ </v-btn>
+ </template>
<span>{{ $t('caseSensitive') }}</span>
</v-tooltip>
- <v-tooltip top open-delay="800">
- <v-btn icon class="toggle-button" @click="onlyShowSelected = !onlyShowSelected" slot="activator">
- <v-icon small :class="onlyShowSelected ? 'primary--text' : ''">{{ singleSelect ? 'radio_button_checked' : 'check_box' }}</v-icon>
- </v-btn>
+ <v-tooltip v-if="!noSelect" top open-delay="800">
+ <template #activator="{ on }">
+ <v-btn v-on="on" icon class="toggle-button" @click="onlyShowSelected = !onlyShowSelected">
+ <v-icon small :class="onlyShowSelected ? 'primary--text' : ''">{{ singleSelect ? 'radio_button_checked' : 'check_box' }}</v-icon>
+ </v-btn>
+ </template>
<span>{{ $t('onlyShowSelected') }}</span>
</v-tooltip>
</div>
<div :class="{ 'text-xs-center': rowCount === undefined, 'text-xs-right': rowCount !== undefined }">
<span class="nowrap">{{ filteredRows.length + ' ' + $t('entries') }}</span>
- <span class="nowrap">{{ '(' + selected.length + ' ' + $t('selected') + ')' }}</span>
+ <span v-if="!noSelect" class="nowrap">{{ '(' + selected.length + ' ' + $t('selected') + ')' }}</span>
</div>
<div v-if="rowCount === undefined" class="text-xs-right hidden-xs-only">
@@ -101,20 +108,21 @@
:page-mode="computedRowCount <= 0"
@click.native.capture.passive="setShiftState"
>
- <template slot="before">
+ <template #before>
<div class="table-head-wrapper" :style="{ 'min-width': computedMinWidth }">
<div class="table-head">
- <div class="header-cell" style="width: 24px">
+ <div v-if="!noSelect" class="header-cell" style="width: 24px">
<v-icon v-if="!singleSelect" @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' : '' }"
+ :style="{ width: header.width }"
:class="{
- 'header-sorted': headerSortState[header.key] !== undefined,
- 'header-sorted-desc': headerSortState[header.key] === 'desc',
+ 'sortable-header': !noSort && header.text !== undefined,
+ 'header-sorted': headerSortState[header.sortKey || header.key] !== undefined,
+ 'header-sorted-desc': headerSortState[header.sortKey || header.key] === 'desc',
'auto-width': header.width === undefined
}"
>
@@ -129,20 +137,24 @@
</div>
</div>
</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' : ''">{{ selectedIconMap[item.selected] }}</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" />
+ <template #default="{ item, index }">
+ <div class="table-row"
+ :style="item.selected && { backgroundColor: $vuetify.theme.primary + '11' }"
+ @click="selectItem(item, index)"
+ @dblclick="$emit('dblclick', item.data)"
+ >
+ <div v-if="!noSelect" class="non-selectable">
+ <v-icon style="cursor: pointer" :color="item.selected ? 'primary' : ''">{{ selectedIconMap[item.selected] }}</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>
- <div v-if="filteredRows.length === 0" slot="after" class="no-result">{{ $t('noResult') }}</div>
+ </template>
+ <template #after>
+ <div v-if="filteredRows.length === 0" class="no-result">{{ $t('noResult') }}</div>
+ </template>
</RecycleScroller>
</div>
</template>
@@ -186,6 +198,14 @@ export default {
singleSelect: {
type: Boolean,
default: false
+ },
+ noSelect: {
+ type: Boolean,
+ default: false
+ },
+ noSort: {
+ type: Boolean,
+ default: false
}
},
data () {
@@ -222,7 +242,7 @@ export default {
sortedRows () {
const rows = this.rows.slice(0)
for (let key in this.headerSortState) {
- if (!this.headers.some(header => header.key === key)) continue
+ if (!this.headers.some(header => header.sortKey || header.key === key)) continue
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])))
@@ -236,17 +256,19 @@ export default {
},
watch: {
rows () {
+ if (this.noSelect) return
this.processSelected(this.value)
this.$emit('input', this.selected)
},
value: {
immediate: true,
handler (newValue) {
+ if (this.noSelect) return
this.processSelected(newValue)
}
},
filteredRows () {
- if (this.singleSelect) return
+ if (this.noSelect || this.singleSelect) return
this.lastSelectIndex = null
this.calcSelectState()
}
@@ -266,6 +288,7 @@ export default {
if (this.$refs.scroller) this.$refs.scroller.$el.scrollTop = 0
},
selectItem (row, index) {
+ if (this.noSelect) return
if (this.singleSelect) {
this.$emit('input', [row.data])
return
@@ -341,13 +364,14 @@ export default {
this.selectState = seenTrue && seenFalse ? 1 : seenTrue && !seenFalse ? 2 : 0
},
toggleHeaderSort (header) {
- if (header.text === undefined) return
- const state = this.headerSortState[header.key]
+ if (header.text === undefined || this.noSort) return
+ const key = header.sortKey || header.key
+ const state = this.headerSortState[key]
const newSortState = {}
if (state === undefined || state === 'desc') {
- newSortState[header.key] = 'asc'
+ newSortState[key] = 'asc'
} else if (state === 'asc') {
- newSortState[header.key] = 'desc'
+ newSortState[key] = 'desc'
}
this.headerSortState = newSortState
},
@@ -454,23 +478,27 @@ export default {
transition: color .3s cubic-bezier(.25,.8,.5,1);
}
-.table-head > .header-cell.header-sorted.header-sorted-desc > .header-sort-icon {
+.table-head > .sortable-header {
+ cursor: pointer;
+}
+
+.table-head > .sortable-header.header-sorted.header-sorted-desc > .header-sort-icon {
transform: rotate(180deg);
}
-.table-head > .header-cell:hover > .header-sort-icon {
+.table-head > .sortable-header:hover > .header-sort-icon {
opacity: 0.6 !important;
}
-.table-head > .header-cell.header-sorted > .header-sort-icon {
+.table-head > .sortable-header.header-sorted > .header-sort-icon {
opacity: 1 !important;
}
-.theme--dark .table-head > .header-cell:hover, .theme--dark .table-head > .header-cell.header-sorted {
+.theme--dark .table-head > .sortable-header:hover, .theme--dark .table-head > .sortable-header.header-sorted {
color: rgb(255,255,255);
}
-.theme--light .table-head > .header-cell:hover, .theme--light .table-head > .header-cell.header-sorted {
+.theme--light .table-head > .sortable-header:hover, .theme--light .table-head > .sortable-header.header-sorted {
color: rgba(0,0,0,.87);
}
diff --git a/webapp/src/components/DataTableSearch.vue b/webapp/src/components/DataTableSearch.vue
index 4e75d7d..c971835 100644
--- a/webapp/src/components/DataTableSearch.vue
+++ b/webapp/src/components/DataTableSearch.vue
@@ -31,7 +31,7 @@
solo flat
class="column-select"
v-model="s.key"
- :items="[ { text: $t('all'), key: null }, ...searchableHeaders ]"
+ :items="[ { text: $t('all'), key: null }, ...dataKeys ]"
item-text="text"
item-value="key"
color="primary"
@@ -55,12 +55,17 @@ import ScrollParent from 'scrollparent'
export default {
name: 'DataTableSearch',
props: {
- rows: {
- type: Array
+ items: {
+ type: Array,
+ required: true
},
- headers: {
+ dataKeys: {
type: Array,
- default: () => []
+ required: true
+ },
+ nestedData: {
+ type: Boolean,
+ default: false
},
regex: {
type: Boolean,
@@ -90,34 +95,37 @@ export default {
}
},
computed: {
- searchableHeaders () {
- return this.headers.filter(h => h.text !== undefined)
+ dataSearchKeys () {
+ return this.dataKeys.map(x => x.searchKey || x.key || x)
},
- 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
+ var str
+ if (this.nestedData) str = (item, key) => String(item.data[key])
+ else str = (item, key) => String(item[key])
+
+ if (this.regex && this.caseSensitive) return (s, item, key) => s.text.regex.test(str(item, key))
+ else if (this.regex && !this.caseSensitive) return (s, item, key) => s.text.regexCI.test(str(item, key))
+ else if (!this.regex && this.caseSensitive) return (s, item, key) => str(item, key).indexOf(s.text.raw) !== -1
+ else if (!this.regex && !this.caseSensitive) return (s, item, key) => str(item, key).toUpperCase().indexOf(s.text.upper) !== -1
}
},
watch: {
- rows () {
- this.filterRows()
+ items () {
+ this.filterItems()
},
regex () {
- this.filterRows()
+ this.filterItems()
},
caseSensitive () {
- this.filterRows()
+ this.filterItems()
},
onlyShowSelected () {
- this.filterRows()
+ this.filterItems()
},
search: {
deep: true,
handler () {
- this.filterRows()
+ this.filterItems()
}
}
},
@@ -150,37 +158,37 @@ export default {
this.search.splice(this.search.indexOf(s), 1)
if (this.dispatchScroll) ScrollParent(this.$el).dispatchEvent(new Event('scroll'))
},
- filterRows () {
+ filterItems () {
// 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))
+ this.$emit('filter', Object.freeze(this.items))
return
}
- // Filter rows using a time slicing loop
+ // Filter items using a time slicing loop
const result = []
- this.filteringLoop = this.$loop(this.rows.length, 1000,
+ this.filteringLoop = this.$loop(this.items.length, 1000,
i => {
- const row = this.rows[i]
- if ((!this.onlyShowSelected || (this.onlyShowSelected && row.selected)) && this.search.every(s => {
+ const item = this.items[i]
+ if ((!this.onlyShowSelected || (this.onlyShowSelected && item.selected)) && this.search.every(s => {
if (s.key === null) {
- return this.dataKeys.some(key => {
- return this.filterFunction(s, String(row.data[key]))
+ return this.dataSearchKeys.some(key => {
+ return this.filterFunction(s, item, key)
})
} else {
- return this.filterFunction(s, String(row.data[s.key]))
+ return this.filterFunction(s, item, s.key)
}
- })) result.push(row)
+ })) result.push(item)
},
() => { this.$emit('filter', Object.freeze(result)) }
)
}
},
created () {
- this.filterRows()
+ this.filterItems()
}
}
</script>
diff --git a/webapp/src/components/LogModule.vue b/webapp/src/components/LogModule.vue
new file mode 100644
index 0000000..69b1664
--- /dev/null
+++ b/webapp/src/components/LogModule.vue
@@ -0,0 +1,328 @@
+<i18n>
+{
+ "en": {
+ "systemLog": "System Log",
+ "from": "From",
+ "to": "To",
+ "today": "Today",
+ "now": "Now",
+ "log": "Log",
+ "filter": "Filter",
+ "timestamp": "Timestamp",
+ "category": "Category",
+ "categories": "Categories",
+ "description": "Description",
+ "group": "Group",
+ "groups": "Groups",
+ "client": "Client",
+ "clients": "Clients",
+ "includeSubgroups": "Include Subgroups"
+ },
+ "de": {
+ "systemLog": "System Protokoll",
+ "from": "Von",
+ "to": "Bis",
+ "today": "Heute",
+ "now": "Jetzt",
+ "log": "Protokoll",
+ "filter": "Filter",
+ "timestamp": "Zeitstempel",
+ "category": "Kategorie",
+ "categories": "Kategorien",
+ "description": "Beschreibung",
+ "group": "Gruppe",
+ "groups": "Gruppen",
+ "client": "Client",
+ "clients": "Clients",
+ "includeSubgroups": "Inklusive Untergruppen"
+ }
+}
+</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">error_outline</v-icon>{{ $t('systemLog') }}</v-tab>
+ </v-tabs>
+ </v-card>
+ <v-tabs-items v-model="tabs" style="padding-bottom: 20px">
+ <v-tab-item>
+ <v-subheader>{{ $t('filter') }}</v-subheader>
+ <v-card>
+ <v-card-text>
+ <v-layout wrap>
+ <v-flex xs12 md5 order-xs2 order-md1>
+ <select-box
+ class="select-box"
+ v-model="categoryFilter"
+ :items="categories"
+ :max-columns="selectBoxColumnCount"
+ prepend-icon="all_inbox"
+ :label="$t('categories')"
+ hide-details
+ ></select-box>
+ </v-flex>
+ <v-flex xs12 md7 order-xs1 order-md2 class="text-md-right">
+ <div style="display: inline-block; white-space: nowrap">
+ <span class="text-xs-right picker-label">{{ $t('from') }}</span>
+
+ <v-menu left offset-y :close-on-content-click="false" style="display: inline-block">
+ <template #activator="{ on }">
+ <v-btn v-on="on" small class="date-picker-button"><v-icon small class="mr-1">today</v-icon>{{ fromDate }}</v-btn>
+ </template>
+ <v-date-picker
+ v-model="fromDate"
+ color="primary"
+ :locale="locale"
+ :width="pickerWidth"
+ ></v-date-picker>
+ </v-menu>
+
+ <v-menu left offset-y :close-on-content-click="false" style="display: inline-block">
+ <template #activator="{ on }">
+ <v-btn v-on="on" small class="time-picker-button"><v-icon small class="mr-1">schedule</v-icon>{{ fromTime }}</v-btn>
+ </template>
+ <v-time-picker
+ v-model="fromTime"
+ color="primary"
+ :locale="locale"
+ format="24hr"
+ :width="pickerWidth"
+ ></v-time-picker>
+ </v-menu>
+
+ </div>
+ <div style="display: inline-block; white-space: nowrap">
+ <span class="text-xs-right picker-label">{{ $t('to') }}</span>
+
+ <v-menu v-model="toDateMenu" left offset-y :close-on-content-click="false" style="display: inline-block">
+ <template #activator="{ on }">
+ <v-btn v-on="on" small class="date-picker-button">
+ <v-icon small class="mr-1">today</v-icon>
+ {{ toDate ? toDate : $t('today') }}
+ </v-btn>
+ </template>
+ <template #default>
+ <div style="position: relative">
+ <v-btn @click="toNow" dark color="primary" small style="position: absolute; z-index: 1; top: 0; right: 0; min-width: fit-content">{{ $t('now') }}</v-btn>
+ <v-date-picker
+ v-model="toDate"
+ color="primary"
+ :locale="locale"
+ :width="pickerWidth"
+ ></v-date-picker>
+ </div>
+ </template>
+ </v-menu>
+ <v-menu v-model="toTimeMenu" left offset-y :close-on-content-click="false" style="display: inline-block">
+ <template #activator="{ on }">
+ <v-btn v-on="on" small class="time-picker-button">
+ <v-icon small class="mr-1">schedule</v-icon>
+ {{ toTime ? toTime : $t('now') }}
+ </v-btn>
+ </template>
+ <template #default>
+ <div style="position: relative">
+ <v-btn @click="toNow" dark color="primary" small style="position: absolute; z-index: 1; top: 0; left: 0; min-width: fit-content">{{ $t('now') }}</v-btn>
+ <v-time-picker
+ v-model="toTime"
+ color="primary"
+ :locale="locale"
+ format="24hr"
+ :width="pickerWidth"
+ ></v-time-picker>
+ </div>
+ </template>
+ </v-menu>
+
+ </div>
+ </v-flex>
+
+ <v-flex xs12 md5 order-xs3>
+ <select-box
+ class="select-box"
+ v-model="groupFilter"
+ :items="groupList"
+ :max-columns="selectBoxColumnCount"
+ prepend-icon="device_hub"
+ :label="$t('groups')"
+ hide-details
+ ></select-box>
+ </v-flex>
+ <v-flex xs12 md5 order-xs4>
+ <select-box
+ class="select-box"
+ v-model="clientFilter"
+ :items="clientList"
+ :max-columns="selectBoxColumnCount"
+ prepend-icon="computer"
+ :label="$t('clients')"
+ hide-details
+ ></select-box>
+ </v-flex>
+ <v-flex xs12 md2 order-xs5 style="display: flex; align-items: flex-end; justify-content: flex-end">
+ <v-btn color="primary" :loading="loading" @click="loadLog"><v-icon left>filter_list</v-icon>{{ $t('filter') }}</v-btn>
+ </v-flex>
+ </v-layout>
+ </v-card-text>
+ </v-card>
+ <v-subheader>{{ $t('log') }}</v-subheader>
+ <v-card>
+ <data-table :headers="headers" :items="log" min-width="1100px" no-select no-sort></data-table>
+ </v-card>
+ </v-tab-item>
+ </v-tabs-items>
+ </v-flex>
+ </v-layout>
+ </v-container>
+</template>
+
+<script>
+import SelectBox from '@/components/SelectBox'
+import DataTable from '@/components/DataTable'
+import { mapState, mapGetters } from 'vuex'
+
+export default {
+ name: 'LogModule',
+ components: {
+ SelectBox,
+ DataTable
+ },
+ data () {
+ return {
+ tabs: 0,
+ categories: [],
+ log: [],
+ headers: [
+ { key: 'timestamp', text: this.$t('timestamp'), width: '160px' },
+ { key: 'category', text: this.$t('category'), width: '160px' },
+ { key: 'description', text: this.$t('description') },
+ { key: 'group', text: this.$t('group'), width: '180px' },
+ { key: 'client', text: this.$t('client'), width: '180px' }
+ ],
+ loading: false,
+ fromDate: null,
+ fromTime: null,
+ toDate: null,
+ toTime: null,
+ toDateMenu: false,
+ toTimeMenu: false,
+ categoryFilter: [],
+ groupFilter: [],
+ groupRecursive: false,
+ clientFilter: []
+ }
+ },
+ computed: {
+ ...mapState('groups', ['groupList', 'clientList']),
+ ...mapGetters(['tabsDark', 'tabsColor', 'tabsSliderColor']),
+ locale () { return this.$store.state.settings.locale },
+ selectBoxColumnCount () {
+ if (this.$vuetify.breakpoint.mdOnly || this.$vuetify.breakpoint.xsOnly) return 1
+ return 2
+ },
+ pickerWidth () {
+ return this.$vuetify.breakpoint.xsOnly ? 250 : undefined
+ },
+ defaultStartDate () {
+ const date = new Date()
+ date.setDate(date.getDate() - 7)
+ return date
+ }
+ },
+ watch: {
+ toDate (value) {
+ if (value) this.toTime = this.formatDate(new Date(), { date: false, seconds: false })
+ },
+ toTime (value) {
+ if (value) this.toDate = this.formatDate(new Date(), { time: false })
+ }
+ },
+ methods: {
+ formatDate(date, options = {}) {
+ var result = ''
+ const pad = x => x < 10 ? '0' + x : x
+ if (options.date !== false) {
+ result += date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate())
+ }
+ if (options.time !== false) {
+ if (result !== '') result += ' '
+ result += pad(date.getHours()) + ':' + pad(date.getMinutes() + 1)
+ if (options.seconds !== false) result += ':' + pad(date.getSeconds())
+ }
+ return result
+ },
+ toNow () {
+ this.toDate = null
+ this.toTime = null
+ this.toDateMenu = false
+ this.toTimeMenu = false
+ },
+ loadLog () {
+ this.loading = true
+ this.$http.get('/api/log').then(response => {
+ response.data.forEach(item => {
+ item.timestamp = new Date(item.timestamp * 1000).toISOString().split('.')[0].replace('T', ' ')
+ if (item.group) item.group = item.group.name
+ if (item.client) item.client = item.client.name
+ })
+ this.log = response.data
+ this.loading = false
+ })
+ }
+ },
+ created () {
+ this.$store.dispatch('groups/loadLists')
+ this.$http.get('/api/log/categories').then(response => {
+ this.categories = response.data.map((category, index) => ({ id: index, name: category }))
+ })
+ const date = new Date()
+ date.setDate(date.getDate() - 7)
+ this.fromDate = this.formatDate(this.defaultStartDate, { time: false }),
+ this.fromTime = this.formatDate(this.defaultStartDate, { date: false, seconds: false })
+ this.loadLog()
+ }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+.select-box {
+ margin: 6px !important;
+}
+
+.picker-label {
+ display: inline-block;
+ width: 40px;
+}
+
+.date-picker-button {
+ min-width: 110px;
+ width: 110px;
+ margin-right: 0;
+}
+
+.time-picker-button {
+ min-width: 70px;
+ width: 70px;
+}
+
+.search-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.search-box {
+ flex: 1;
+}
+
+.toggle-button {
+ min-width: 36px;
+ text-transform: none;
+ font-size: 14px;
+ font-weight: 600;
+}
+</style>
diff --git a/webapp/src/components/SelectBox.vue b/webapp/src/components/SelectBox.vue
index 4fe4593..59797af 100644
--- a/webapp/src/components/SelectBox.vue
+++ b/webapp/src/components/SelectBox.vue
@@ -33,7 +33,9 @@
>
<div class="select-input-chip-text">{{ item.name || item.id }}</div>
</v-chip>
- <span v-if="value.length > maxShow" class="text--secondary and-more">+ {{ value.length - maxShow }} {{ $t('more') }}</span>
+ <span v-if="value.length > maxShow" class="text--secondary and-more" :style="{ width: 'calc(' + (100 / maxColumns) + '% - 8px)' }">
+ + {{ value.length - maxShow }} {{ $t('more') }}
+ </span>
</div>
<v-icon class="select-input-arrow" :class="{ 'expand-arrow-flipped': menu }" :color="menu ? 'primary' : ''">arrow_drop_down</v-icon>
</div>
@@ -205,10 +207,11 @@ export default {
}
.and-more {
+ white-space: nowrap;
font-size: 13px;
display: flex;
align-items: center;
- margin: 4px 17px;
+ padding: 4px 17px;
}
.select-label {
diff --git a/webapp/src/components/SettingsModule.vue b/webapp/src/components/SettingsModule.vue
index aa64ec6..dc9731b 100644
--- a/webapp/src/components/SettingsModule.vue
+++ b/webapp/src/components/SettingsModule.vue
@@ -49,7 +49,7 @@
<v-subheader>{{ $t('generalSettings') }}</v-subheader>
<v-card>
<div class="element-container">
- <v-select :value="store.locale" @input="save('locale', $event)" :items="langChoices" :label="$t('language')" prepend-icon="language"></v-select>
+ <v-select :value="store.locale" @input="save('locale', $event)" :items="langChoices" :label="$t('language')" prepend-icon="language" :menu-props="{ offsetY: '' }"></v-select>
</div>
</v-card>
</v-tab-item>
diff --git a/webapp/src/config/dashboard.js b/webapp/src/config/dashboard.js
index a5f6b4e..26de0fc 100644
--- a/webapp/src/config/dashboard.js
+++ b/webapp/src/config/dashboard.js
@@ -5,6 +5,7 @@ import BackendModule from '@/components/BackendModule'
import PermissionModule from '@/components/PermissionModule'
import IpxeBuilderModule from '@/components/IpxeBuilderModule'
import UserModule from '@/components/UserModule'
+import LogModule from '@/components/LogModule'
export default [
{ path: 'groups', component: GroupModule, icon: 'category' },
@@ -13,5 +14,6 @@ export default [
{ path: 'backends', component: BackendModule, icon: 'cloud' },
{ path: 'permissions', component: PermissionModule, icon: 'lock_open' },
{ path: 'ipxe', component: IpxeBuilderModule, icon: 'merge_type' },
- { path: 'users', component: UserModule, icon: 'contacts' }
+ { path: 'users', component: UserModule, icon: 'contacts' },
+ { path: 'log', component: LogModule, icon: 'error_outline' }
]
diff --git a/webapp/src/config/i18n.js b/webapp/src/config/i18n.js
index 8e0b48c..c9cf137 100644
--- a/webapp/src/config/i18n.js
+++ b/webapp/src/config/i18n.js
@@ -29,7 +29,8 @@ export default {
'BackendModule': 'External Backends',
'PermissionModule': 'Permission Manager',
'IpxeBuilderModule': 'iPXE Builder',
- 'UserModule': 'User Management'
+ 'UserModule': 'User Management',
+ 'LogModule': 'System Log'
}
},
'de': {
@@ -62,7 +63,8 @@ export default {
'BackendModule': 'Externe Backends',
'PermissionModule': 'Rechteverwaltung',
'IpxeBuilderModule': 'iPXE Builder',
- 'UserModule': 'Benutzerverwaltung'
+ 'UserModule': 'Benutzerverwaltung',
+ 'LogModule': 'System Protokoll'
}
}
}