1<!-- Copyright (C) 2019 The Android Open Source Project
2
3     Licensed under the Apache License, Version 2.0 (the "License");
4     you may not use this file except in compliance with the License.
5     You may obtain a copy of the License at
6
7          http://www.apache.org/licenses/LICENSE-2.0
8
9     Unless required by applicable law or agreed to in writing, software
10     distributed under the License is distributed on an "AS IS" BASIS,
11     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12     See the License for the specific language governing permissions and
13     limitations under the License.
14-->
15<template>
16  <md-card-content class="container">
17    <div class="navigation">
18      <md-content
19        md-tag="md-toolbar"
20        md-elevation="0"
21        class="card-toolbar md-transparent md-dense"
22      >
23        <h2 class="md-title" style="flex: 1">Log View</h2>
24        <md-button
25          class="md-dense md-primary"
26          @click.native="scrollToRow(lastOccuredVisibleIndex)"
27        >
28          Jump to latest entry
29        </md-button>
30        <md-button
31          class="md-icon-button" :class="{'md-primary': pinnedToLatest}"
32          @click.native="togglePin"
33        >
34          <md-icon>push_pin</md-icon>
35          <md-tooltip md-direction="top" v-if="pinnedToLatest">
36            Unpin to latest message
37          </md-tooltip>
38          <md-tooltip md-direction="top" v-else>
39            Pin to latest message
40          </md-tooltip>
41        </md-button>
42      </md-content>
43    </div>
44
45    <div class="filters">
46      <md-field>
47        <label>Log Levels</label>
48        <md-select v-model="selectedLogLevels" multiple>
49          <md-option v-for="level in logLevels" :value="level">{{ level }}</md-option>
50        </md-select>
51      </md-field>
52
53      <md-field>
54        <label>Tags</label>
55        <md-select v-model="selectedTags" multiple>
56          <md-option v-for="tag in tags" :value="tag">{{ tag }}</md-option>
57        </md-select>
58      </md-field>
59
60      <md-autocomplete v-model="selectedSourceFile" :md-options="sourceFiles">
61        <label>Source file</label>
62
63        <template slot="md-autocomplete-item" slot-scope="{ item, term }">
64          <md-highlight-text :md-term="term">{{ item }}</md-highlight-text>
65        </template>
66
67        <template slot="md-autocomplete-empty" slot-scope="{ term }">
68          No source file matching "{{ term }}" was found.
69        </template>
70      </md-autocomplete>
71
72      <md-field class="search-message-field" md-clearable>
73        <md-input placeholder="Search messages..." v-model="searchInput"></md-input>
74      </md-field>
75    </div>
76
77    <div v-if="processedData.length > 0" style="overflow-y: auto;">
78      <virtual-list style="height: 600px; overflow-y: auto;"
79        :data-key="'uid'"
80        :data-sources="processedData"
81        :data-component="logEntryComponent"
82        ref="loglist"
83      />
84    </div>
85    <div class="no-logs-message" v-else>
86      <md-icon>error_outline</md-icon>
87      <span class="message">No logs founds...</span>
88    </div>
89  </md-card-content>
90</template>
91<script>
92import { findLastMatchingSorted } from './utils/utils.js';
93import { logLevel } from './utils/consts';
94import LogEntryComponent from './LogEntry.vue';
95import VirtualList from '../libs/virtualList/VirtualList';
96
97export default {
98  name: 'logview',
99  data() {
100    const data = this.file.data;
101
102    const tags = new Set();
103    const sourceFiles = new Set();
104    for (const line of data) {
105      tags.add(line.tag);
106      sourceFiles.add(line.at);
107    }
108
109    data.forEach((entry, index) => entry.index = index);
110
111    const logLevels = Object.values(logLevel);
112
113    return {
114      data,
115      isSelected: false,
116      prevLastOccuredIndex: -1,
117      lastOccuredIndex: 0,
118      selectedTags: [],
119      selectedSourceFile: null,
120      searchInput: null,
121      sourceFiles: Object.freeze(Array.from(sourceFiles)),
122      tags: Object.freeze(Array.from(tags)),
123      pinnedToLatest: true,
124      logEntryComponent: LogEntryComponent,
125      logLevels,
126      selectedLogLevels: [],
127    }
128  },
129  methods: {
130    arrowUp() {
131      this.isSelected = !this.isSelected;
132      return !this.isSelected;
133    },
134    arrowDown() {
135      this.isSelected = !this.isSelected;
136      return !this.isSelected;
137    },
138    getRowEl(idx) {
139      return this.$refs.tableBody.querySelectorAll('tr')[idx];
140    },
141    togglePin() {
142      this.pinnedToLatest = !this.pinnedToLatest;
143    },
144    scrollToRow(index) {
145      if (!this.$refs.loglist) {
146        return;
147      }
148
149      const itemOffset = this.$refs.loglist.virtual.getOffset(index);
150      const itemSize = 35;
151      const loglistSize = this.$refs.loglist.getClientSize();
152
153      this.$refs.loglist.scrollToOffset(itemOffset - loglistSize + itemSize);
154    },
155    getLastOccuredIndex(data, timestamp) {
156      if (this.data.length === 0) {
157          return 0;
158      }
159      return findLastMatchingSorted(data,
160        (array, idx) => array[idx].timestamp <= timestamp);
161    },
162  },
163  watch: {
164    pinnedToLatest(isPinned) {
165      if (isPinned) {
166        this.scrollToRow(this.lastOccuredVisibleIndex);
167      }
168    },
169    currentTimestamp: {
170      immediate: true,
171      handler(newTimestamp) {
172        this.prevLastOccuredIndex = this.lastOccuredIndex;
173        this.lastOccuredIndex = this.getLastOccuredIndex(this.data, newTimestamp);
174
175        if (this.pinnedToLatest) {
176          this.scrollToRow(this.lastOccuredVisibleIndex);
177        }
178      },
179    }
180  },
181  props: ['file'],
182  computed: {
183    lastOccuredVisibleIndex() {
184      return this.getLastOccuredIndex(this.processedData, this.currentTimestamp);
185    },
186    currentTimestamp() {
187      return this.$store.state.currentTimestamp;
188    },
189    processedData() {
190      const filteredData = this.data.filter(line => {
191        if (this.selectedLogLevels.length > 0 &&
192            !this.selectedLogLevels.includes(line.level.toLowerCase())) {
193          return false;
194        }
195
196        if (this.sourceFiles.includes(this.selectedSourceFile)) {
197          // Only filter once source file is fully inputed
198          if (line.at != this.selectedSourceFile) {
199            return false;
200          }
201        }
202
203        if (this.selectedTags.length > 0 && !this.selectedTags.includes(line.tag)) {
204          return false;
205        }
206
207        if (this.searchInput && !line.text.includes(this.searchInput)) {
208          return false;
209        }
210
211        return true;
212      });
213
214      for (const entry of filteredData) {
215        entry.new = this.prevLastOccuredIndex < entry.index &&
216          entry.index <= this.lastOccuredIndex;
217        entry.occured = entry.index <= this.lastOccuredIndex;
218        entry.justInactivated = this.lastOccuredIndex < entry.index &&
219          entry.index <= this.prevLastOccuredIndex;
220
221        // Force refresh if any of these changes
222        entry.uid = `${entry.index}${entry.new ? '-new' : ''}${entry.index}${entry.justInactivated ? '-just-inactivated' : ''}${entry.occured ? '-occured' : ''}`
223      }
224
225      return filteredData;
226    }
227  },
228  components: {
229    'virtual-list': VirtualList,
230    'logentry': LogEntryComponent,
231  }
232}
233
234</script>
235<style>
236.container {
237  display: flex;
238  flex-wrap: wrap;
239}
240
241.filters, .navigation {
242  width: 100%;
243  display: flex;
244  flex-direction: row;
245  align-items: center;
246}
247
248.navigation {
249  justify-content: flex-end;
250}
251
252.navigation > button {
253  margin: 0;
254}
255
256.filters > div {
257  margin: 10px;
258}
259
260.log-header {
261  display: inline-flex;
262  color: var(--md-theme-default-text-accent-on-background, rgba(0,0,0,0.54));
263  font-weight: bold;
264}
265
266.log-header > div {
267  padding: 6px 10px;
268  border-bottom: 1px solid #f1f1f1;
269}
270
271.log-header .time-column {
272  width: 13em;
273}
274
275.log-header .tag-column {
276  width: 10em;
277}
278
279.log-header .at-column {
280  width: 30em;
281}
282
283.column-title {
284  font-size: 12px;
285}
286
287.no-logs-message {
288  margin: 15px;
289  display: flex;
290  align-content: center;
291  align-items: center;
292}
293
294.no-logs-message .message {
295  margin-left: 10px;
296  font-size: 15px;
297}
298</style>
299