blob: ce9ff10d430c618376ea55d1758da14a5d456fda [file] [log] [blame]
<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<md-card-content class="container">
<div class="navigation">
<md-button
class="md-dense md-primary"
@click.native="scrollToRow(lastOccuredVisibleIndex)"
>
Jump to latest entry
</md-button>
<md-button
class="md-icon-button" :class="{'md-primary': pinnedToLatest}"
@click.native="togglePin"
>
<md-icon>push_pin</md-icon>
<md-tooltip md-direction="top" v-if="pinnedToLatest">
Unpin to latest message
</md-tooltip>
<md-tooltip md-direction="top" v-else>
Pin to latest message
</md-tooltip>
</md-button>
</div>
<div class="filters">
<md-field>
<label>Log Levels</label>
<md-select v-model="selectedLogLevels" multiple>
<md-option v-for="level in logLevels" :value="level">{{ level }}</md-option>
</md-select>
</md-field>
<md-field>
<label>Tags</label>
<md-select v-model="selectedTags" multiple>
<md-option v-for="tag in tags" :value="tag">{{ tag }}</md-option>
</md-select>
</md-field>
<md-autocomplete v-model="selectedSourceFile" :md-options="sourceFiles">
<label>Source file</label>
<template slot="md-autocomplete-item" slot-scope="{ item, term }">
<md-highlight-text :md-term="term">{{ item }}</md-highlight-text>
</template>
<template slot="md-autocomplete-empty" slot-scope="{ term }">
No source file matching "{{ term }}" was found.
</template>
</md-autocomplete>
<md-field class="search-message-field" md-clearable>
<md-input placeholder="Search messages..." v-model="searchInput"></md-input>
</md-field>
</div>
<div v-if="processedData.length > 0" style="overflow-y: auto;">
<virtual-list style="height: 600px; overflow-y: auto;"
:data-key="'uid'"
:data-sources="processedData"
:data-component="logEntryComponent"
ref="loglist"
/>
</div>
<div class="no-logs-message" v-else>
<md-icon>error_outline</md-icon>
<span class="message">No logs founds...</span>
</div>
</md-card-content>
</template>
<script>
import { findLastMatchingSorted } from './utils/utils.js';
import { logLevel } from './utils/consts';
import LogEntryComponent from './LogEntry.vue';
import VirtualList from '../libs/virtualList/VirtualList';
export default {
name: 'logview',
data() {
const data = this.file.data;
const tags = new Set();
const sourceFiles = new Set();
for (const line of data) {
tags.add(line.tag);
sourceFiles.add(line.at);
}
data.forEach((entry, index) => entry.index = index);
const logLevels = Object.values(logLevel);
return {
data,
isSelected: false,
prevLastOccuredIndex: -1,
lastOccuredIndex: 0,
selectedTags: [],
selectedSourceFile: null,
searchInput: null,
sourceFiles: Object.freeze(Array.from(sourceFiles)),
tags: Object.freeze(Array.from(tags)),
pinnedToLatest: true,
logEntryComponent: LogEntryComponent,
logLevels,
selectedLogLevels: [],
}
},
methods: {
arrowUp() {
this.isSelected = !this.isSelected;
return !this.isSelected;
},
arrowDown() {
this.isSelected = !this.isSelected;
return !this.isSelected;
},
getRowEl(idx) {
return this.$refs.tableBody.querySelectorAll('tr')[idx];
},
togglePin() {
this.pinnedToLatest = !this.pinnedToLatest;
},
scrollToRow(index) {
if (!this.$refs.loglist) {
return;
}
const itemOffset = this.$refs.loglist.virtual.getOffset(index);
const itemSize = 35;
const loglistSize = this.$refs.loglist.getClientSize();
this.$refs.loglist.scrollToOffset(itemOffset - loglistSize + itemSize);
},
getLastOccuredIndex(data, timestamp) {
if (this.data.length === 0) {
return 0;
}
return findLastMatchingSorted(data,
(array, idx) => array[idx].timestamp <= timestamp);
},
},
watch: {
pinnedToLatest(isPinned) {
if (isPinned) {
this.scrollToRow(this.lastOccuredVisibleIndex);
}
},
currentTimestamp: {
immediate: true,
handler(newTimestamp) {
this.prevLastOccuredIndex = this.lastOccuredIndex;
this.lastOccuredIndex = this.getLastOccuredIndex(this.data, newTimestamp);
if (this.pinnedToLatest) {
this.scrollToRow(this.lastOccuredVisibleIndex);
}
},
}
},
props: ['file'],
computed: {
lastOccuredVisibleIndex() {
return this.getLastOccuredIndex(this.processedData, this.currentTimestamp);
},
currentTimestamp() {
return this.$store.state.currentTimestamp;
},
processedData() {
const filteredData = this.data.filter(line => {
if (this.selectedLogLevels.length > 0 &&
!this.selectedLogLevels.includes(line.level.toLowerCase())) {
return false;
}
if (this.sourceFiles.includes(this.selectedSourceFile)) {
// Only filter once source file is fully inputed
if (line.at != this.selectedSourceFile) {
return false;
}
}
if (this.selectedTags.length > 0 && !this.selectedTags.includes(line.tag)) {
return false;
}
if (this.searchInput && !line.text.includes(this.searchInput)) {
return false;
}
return true;
});
for (const entry of filteredData) {
entry.new = this.prevLastOccuredIndex < entry.index &&
entry.index <= this.lastOccuredIndex;
entry.occured = entry.index <= this.lastOccuredIndex;
entry.justInactivated = this.lastOccuredIndex < entry.index &&
entry.index <= this.prevLastOccuredIndex;
// Force refresh if any of these changes
entry.uid = `${entry.index}${entry.new ? '-new' : ''}${entry.index}${entry.justInactivated ? '-just-inactivated' : ''}${entry.occured ? '-occured' : ''}`
}
return filteredData;
}
},
components: {
'virtual-list': VirtualList,
'logentry': LogEntryComponent,
}
}
</script>
<style>
.container {
display: flex;
flex-wrap: wrap;
}
.filters, .navigation {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
}
.navigation {
justify-content: flex-end;
}
.navigation > button {
margin: 0;
}
.filters > div {
margin: 10px;
}
.log-header {
display: inline-flex;
color: var(--md-theme-default-text-accent-on-background, rgba(0,0,0,0.54));
font-weight: bold;
}
.log-header > div {
padding: 6px 10px;
border-bottom: 1px solid #f1f1f1;
}
.log-header .time-column {
width: 13em;
}
.log-header .tag-column {
width: 10em;
}
.log-header .at-column {
width: 30em;
}
.column-title {
font-size: 12px;
}
.no-logs-message {
margin: 15px;
display: flex;
align-content: center;
align-items: center;
}
.no-logs-message .message {
margin-left: 10px;
font-size: 15px;
}
</style>