Add functionality to search + navigate tags/errors.
Add a searchbar to bottom of Winscope that can be used to navigate through all the tags/transitions/errors present in a trace (integrating existing timestamp search). Choose the desired tab to set the type of search. Typing input into the relevant search will filter the elements. Click on the start or end timestamp in the scrollable search table to navigate to that timestamp in the timeline. Click on a transition in the search table to navigate to the start timestamp for that transition. Click on a transition in the timeline to toggle between start and end timestamps for that transition. Bug: b/196197875 Test: upload the traces on the bug and test the above implementations on the x20. Change-Id: Ib3a0fff770cc65a38e50a8d99f130752b2f98eeb
This commit is contained in:
@@ -57,8 +57,12 @@
|
||||
</div>
|
||||
|
||||
<overlay
|
||||
:presentTags="Object.freeze(presentTags)"
|
||||
:presentErrors="Object.freeze(presentErrors)"
|
||||
:tagAndErrorTraces="tagAndErrorTraces"
|
||||
:store="store"
|
||||
:ref="overlayRef"
|
||||
:searchTypes="searchTypes"
|
||||
v-if="dataLoaded"
|
||||
v-on:bottom-nav-height-change="handleBottomNavHeightChange"
|
||||
/>
|
||||
@@ -77,7 +81,8 @@ import FileType from './mixins/FileType.js';
|
||||
import SaveAsZip from './mixins/SaveAsZip';
|
||||
import FocusedDataViewFinder from './mixins/FocusedDataViewFinder';
|
||||
import {DIRECTION} from './utils/utils';
|
||||
import {NAVIGATION_STYLE} from './utils/consts';
|
||||
import Searchbar from './Searchbar.vue';
|
||||
import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts';
|
||||
|
||||
const APP_NAME = 'Winscope';
|
||||
|
||||
@@ -102,6 +107,10 @@ export default {
|
||||
mainContentStyle: {
|
||||
'padding-bottom': `${CONTENT_BOTTOM_PADDING}px`,
|
||||
},
|
||||
presentTags: [],
|
||||
presentErrors: [],
|
||||
searchTypes: [SEARCH_TYPE.TIMESTAMP],
|
||||
tagAndErrorTraces: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -113,7 +122,50 @@ export default {
|
||||
window.removeEventListener('keydown', this.onKeyDown);
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** get states from either tag files or error files */
|
||||
getUpdatedStates(files) {
|
||||
var states = [];
|
||||
for (const file of files) {
|
||||
states.push(...file.data);
|
||||
}
|
||||
return states;
|
||||
},
|
||||
/** get tags from all uploaded tag files*/
|
||||
getUpdatedTags() {
|
||||
var tagStates = this.getUpdatedStates(this.tagFiles);
|
||||
var tags = [];
|
||||
tagStates.forEach(tagState => {
|
||||
tagState.tags.forEach(tag => {
|
||||
tag.timestamp = tagState.timestamp;
|
||||
tags.push(tag);
|
||||
});
|
||||
});
|
||||
return tags;
|
||||
},
|
||||
/** get tags from all uploaded error files*/
|
||||
getUpdatedErrors() {
|
||||
var errorStates = this.getUpdatedStates(this.errorFiles);
|
||||
var errors = [];
|
||||
//TODO (b/196201487) add check if errors empty
|
||||
errorStates.forEach(errorState => {
|
||||
errorState.errors.forEach(error => {
|
||||
error.timestamp = errorState.timestamp;
|
||||
errors.push(error);
|
||||
});
|
||||
});
|
||||
return errors;
|
||||
},
|
||||
/** set flicker mode check for if there are tag/error traces uploaded*/
|
||||
shouldUpdateTagAndErrorTraces() {
|
||||
return this.tagFiles.length > 0 || this.errorFiles.length > 0;
|
||||
},
|
||||
/** activate flicker search tab if tags/errors uploaded*/
|
||||
updateSearchTypes() {
|
||||
this.searchTypes = [SEARCH_TYPE.TIMESTAMP];
|
||||
if (this.tagAndErrorTraces) this.searchTypes.push(SEARCH_TYPE.TAG);
|
||||
},
|
||||
clear() {
|
||||
this.$store.commit('clearFiles');
|
||||
},
|
||||
@@ -139,6 +191,10 @@ export default {
|
||||
},
|
||||
onDataReady(files) {
|
||||
this.$store.dispatch('setFiles', files);
|
||||
this.tagAndErrorTraces = this.shouldUpdateTagAndErrorTraces();
|
||||
this.presentTags = this.getUpdatedTags();
|
||||
this.presentErrors = this.getUpdatedErrors();
|
||||
this.updateSearchTypes();
|
||||
this.updateFocusedView();
|
||||
},
|
||||
setStatus(status) {
|
||||
@@ -176,6 +232,15 @@ export default {
|
||||
dataViewFiles() {
|
||||
return this.files.filter((f) => this.hasDataView(f));
|
||||
},
|
||||
tagFiles() {
|
||||
return this.$store.getters.tagFiles;
|
||||
},
|
||||
errorFiles() {
|
||||
return this.$store.getters.errorFiles;
|
||||
},
|
||||
timelineFiles() {
|
||||
return this.$store.getters.timelineFiles;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
title() {
|
||||
@@ -187,6 +252,7 @@ export default {
|
||||
dataview: DataView,
|
||||
datainput: DataInput,
|
||||
dataadb: DataAdb,
|
||||
searchbar: Searchbar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -194,7 +260,7 @@ export default {
|
||||
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@600&display=swap');
|
||||
|
||||
#app .md-app-container {
|
||||
/* Get rid of tranforms which prevent fixed position from being used */
|
||||
/* Get rid of transforms which prevent fixed position from being used */
|
||||
transform: none!important;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -245,15 +311,6 @@ h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
|
||||
.data-inputs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -293,4 +350,4 @@ a {
|
||||
hyphens: auto;
|
||||
padding: 10px 10px 10px 10px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -52,6 +52,15 @@
|
||||
>
|
||||
<div class="nav-content">
|
||||
<div class="">
|
||||
<searchbar
|
||||
class="search-bar"
|
||||
v-if="search"
|
||||
:searchTypes="searchTypes"
|
||||
:store="store"
|
||||
:presentTags="Object.freeze(presentTags)"
|
||||
:presentErrors="Object.freeze(presentErrors)"
|
||||
:timeline="mergedTimeline.timeline"
|
||||
/>
|
||||
<md-toolbar
|
||||
md-elevation="0"
|
||||
class="md-transparent">
|
||||
@@ -66,16 +75,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<md-button
|
||||
@click="toggleSearch()"
|
||||
class="drop-search"
|
||||
>Show/hide search bar</md-button>
|
||||
|
||||
<div class="active-timeline" v-show="minimized">
|
||||
<md-field class="seek-timestamp-field">
|
||||
<label>Search for timestamp</label>
|
||||
<md-input v-model="searchTimestamp"></md-input>
|
||||
</md-field>
|
||||
|
||||
<md-button
|
||||
@click="updateSearchForTimestamp"
|
||||
>Search</md-button>
|
||||
|
||||
<div
|
||||
class="active-timeline-icon"
|
||||
@click="$refs.navigationTypeSelection.$el
|
||||
@@ -154,9 +159,10 @@
|
||||
{{ seekTime }}
|
||||
</label>
|
||||
<timeline
|
||||
:store="store"
|
||||
:flickerMode="flickerMode"
|
||||
:tags="Object.freeze(tags)"
|
||||
:errorTimestamps="Object.freeze(errorTimestamps)"
|
||||
:tags="Object.freeze(presentTags)"
|
||||
:errors="Object.freeze(presentErrors)"
|
||||
:timeline="Object.freeze(minimizedTimeline.timeline)"
|
||||
:selected-index="minimizedTimeline.selectedIndex"
|
||||
:scale="scale"
|
||||
@@ -280,16 +286,17 @@ import TimelineSelection from './TimelineSelection.vue';
|
||||
import DraggableDiv from './DraggableDiv.vue';
|
||||
import VideoView from './VideoView.vue';
|
||||
import MdIconOption from './components/IconSelection/IconSelectOption.vue';
|
||||
import Searchbar from './Searchbar.vue';
|
||||
import FileType from './mixins/FileType.js';
|
||||
import {NAVIGATION_STYLE} from './utils/consts';
|
||||
import {TRACE_ICONS} from '@/decode.js';
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import {nanos_to_string, string_to_nanos} from './transform.js';
|
||||
import {nanos_to_string} from './transform.js';
|
||||
|
||||
export default {
|
||||
name: 'overlay',
|
||||
props: ['store'],
|
||||
props: ['store', 'presentTags', 'presentErrors', 'tagAndErrorTraces', 'searchTypes'],
|
||||
mixins: [FileType],
|
||||
data() {
|
||||
return {
|
||||
@@ -310,15 +317,12 @@ export default {
|
||||
crop: null,
|
||||
cropIntent: null,
|
||||
TRACE_ICONS,
|
||||
searchTimestamp: '',
|
||||
tags: [],
|
||||
errorTimestamps: [],
|
||||
search: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.mergedTimeline = this.computeMergedTimeline();
|
||||
this.$store.commit('setMergedTimeline', this.mergedTimeline);
|
||||
this.updateTagsAndErrors();
|
||||
this.updateNavigationFileFilter();
|
||||
},
|
||||
mounted() {
|
||||
@@ -357,12 +361,6 @@ export default {
|
||||
timelineFiles() {
|
||||
return this.$store.getters.timelineFiles;
|
||||
},
|
||||
tagFiles() {
|
||||
return this.$store.getters.tagFiles;
|
||||
},
|
||||
errorFiles() {
|
||||
return this.$store.getters.errorFiles;
|
||||
},
|
||||
focusedFile() {
|
||||
return this.$store.state.focusedFile;
|
||||
},
|
||||
@@ -485,9 +483,6 @@ export default {
|
||||
flickerMode() {
|
||||
return this.navigationStyle === NAVIGATION_STYLE.FLICKER;
|
||||
},
|
||||
tagAndErrorTraces() {
|
||||
return this.tagFiles.length > 0 || this.errorFiles.length >0;
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
@@ -499,34 +494,8 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getStates(files) {
|
||||
var states = [];
|
||||
for (const file of files) {
|
||||
states.push(...file.data);
|
||||
}
|
||||
return states;
|
||||
},
|
||||
updateTags() {
|
||||
var tagStates = this.getStates(this.tagFiles);
|
||||
var tags = [];
|
||||
tagStates.forEach(tagState => {
|
||||
const time = this.findClosestTimestamp(tagState.timestamp);
|
||||
tagState.tags.forEach(tag => {
|
||||
tag.timestamp = time;
|
||||
tags.push(tag);
|
||||
});
|
||||
});;
|
||||
return tags;
|
||||
},
|
||||
updateErrorTimestamps() {
|
||||
var errorStates = this.getStates(this.errorFiles);
|
||||
var errorTimestamps = [];
|
||||
//TODO (b/196201487): update more than one error for each state, add check if errors empty
|
||||
errorStates.forEach(errorState => {
|
||||
errorTimestamps.push(errorState.timestamp);
|
||||
}
|
||||
);
|
||||
return errorTimestamps;
|
||||
toggleSearch() {
|
||||
this.search = !(this.search);
|
||||
},
|
||||
emitBottomHeightUpdate() {
|
||||
if (this.$refs.bottomNav) {
|
||||
@@ -547,9 +516,10 @@ export default {
|
||||
timelines.push(file.timeline);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
var timelineToAdvance = 0;
|
||||
while (timelineToAdvance !== undefined) {
|
||||
timelineToAdvance = undefined;
|
||||
let minTime = Infinity;
|
||||
let timelineToAdvance;
|
||||
|
||||
for (let i = 0; i < timelines.length; i++) {
|
||||
const timeline = timelines[i];
|
||||
@@ -715,28 +685,6 @@ export default {
|
||||
clearSelection() {
|
||||
this.crop = null;
|
||||
},
|
||||
updateSearchForTimestamp() {
|
||||
if (/^\d+$/.test(this.searchTimestamp)) {
|
||||
var roundedTimestamp = parseInt(this.searchTimestamp);
|
||||
} else {
|
||||
var roundedTimestamp = string_to_nanos(this.searchTimestamp);
|
||||
}
|
||||
var closestTimestamp = this.findClosestTimestamp(roundedTimestamp);
|
||||
this.$store.dispatch('updateTimelineTime', parseInt(closestTimestamp));
|
||||
},
|
||||
findClosestTimestamp(roundedTimestamp) {
|
||||
return this.mergedTimeline.timeline.reduce(function(prev, curr) {
|
||||
return (Math.abs(curr-roundedTimestamp) < Math.abs(prev-roundedTimestamp) ? curr : prev);
|
||||
});
|
||||
},
|
||||
updateTagsAndErrors() {
|
||||
if (this.tagFiles) {
|
||||
this.tags = this.updateTags();
|
||||
}
|
||||
if (this.errorFiles) {
|
||||
this.errorTimestamps = this.updateErrorTimestamps();
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
'timeline': Timeline,
|
||||
@@ -745,6 +693,7 @@ export default {
|
||||
'videoview': VideoView,
|
||||
'draggable-div': DraggableDiv,
|
||||
'md-icon-option': MdIconOption,
|
||||
'searchbar': Searchbar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -948,4 +897,8 @@ export default {
|
||||
margin-bottom: 15px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.drop-search:hover {
|
||||
background-color: #9af39f;
|
||||
}
|
||||
</style>
|
||||
|
||||
308
tools/winscope/src/Searchbar.vue
Normal file
308
tools/winscope/src/Searchbar.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<!-- Copyright (C) 2017 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-content class="searchbar">
|
||||
<div class="search-timestamp" v-if="isTimestampSearch()">
|
||||
<md-button
|
||||
class="search-timestamp-button"
|
||||
@click="updateSearchForTimestamp"
|
||||
>
|
||||
Navigate to timestamp
|
||||
</md-button>
|
||||
<md-field class="search-input">
|
||||
<label>Enter timestamp</label>
|
||||
<md-input v-model="searchInput"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-content" v-if="isTagSearch()">
|
||||
<table>
|
||||
<tr class="header">
|
||||
<th style="width: 10%">Start</th>
|
||||
<th style="width: 10%">End</th>
|
||||
<th style="width: 80%">Description</th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="item in filteredTransitionsAndErrors" :key="item">
|
||||
<td
|
||||
v-if="isTransition(item)"
|
||||
class="inline-time"
|
||||
@click="
|
||||
setCurrentTimestamp(transitionStart(transitionTags(item.id)))
|
||||
"
|
||||
>
|
||||
<span>{{ transitionTags(item.id)[0].desc }}</span>
|
||||
</td>
|
||||
<td
|
||||
v-if="isTransition(item)"
|
||||
class="inline-time"
|
||||
@click="setCurrentTimestamp(transitionEnd(transitionTags(item.id)))"
|
||||
>
|
||||
<span>{{ transitionTags(item.id)[1].desc }}</span>
|
||||
</td>
|
||||
<td
|
||||
v-if="isTransition(item)"
|
||||
class="inline-transition"
|
||||
:style="{color: transitionTextColor(item.transition)}"
|
||||
@click="
|
||||
setCurrentTimestamp(transitionStart(transitionTags(item.id)))
|
||||
"
|
||||
>
|
||||
{{ transitionDesc(item.transition) }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="!isTransition(item)"
|
||||
class="inline-time"
|
||||
@click="setCurrentTimestamp(item.timestamp)"
|
||||
>
|
||||
{{ errorDesc(item.timestamp) }}
|
||||
</td>
|
||||
<td v-if="!isTransition(item)">-</td>
|
||||
<td
|
||||
v-if="!isTransition(item)"
|
||||
class="inline-error"
|
||||
@click="setCurrentTimestamp(item.timestamp)"
|
||||
>
|
||||
Error
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<md-field class="search-input">
|
||||
<label
|
||||
>Filter by transition or error message. Click to navigate to closest
|
||||
timestamp in active timeline.</label
|
||||
>
|
||||
<md-input v-model="searchInput"></md-input>
|
||||
</md-field>
|
||||
</div>
|
||||
|
||||
<div class="tab-container">
|
||||
<md-button
|
||||
v-for="searchType in searchTypes"
|
||||
:key="searchType"
|
||||
@click="setSearchType(searchType)"
|
||||
:class="tabClass(searchType)"
|
||||
>
|
||||
{{ searchType }}
|
||||
</md-button>
|
||||
</div>
|
||||
</md-content>
|
||||
</template>
|
||||
<script>
|
||||
import { transitionMap, SEARCH_TYPE } from "./utils/consts";
|
||||
import { nanos_to_string, string_to_nanos } from "./transform";
|
||||
|
||||
const regExpTimestampSearch = new RegExp(/^\d+$/);
|
||||
|
||||
export default {
|
||||
name: "searchbar",
|
||||
props: ["store", "presentTags", "timeline", "presentErrors", "searchTypes"],
|
||||
data() {
|
||||
return {
|
||||
searchType: SEARCH_TYPE.TIMESTAMP,
|
||||
searchInput: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/** set search type depending on tab selected */
|
||||
setSearchType(searchType) {
|
||||
this.searchType = searchType;
|
||||
},
|
||||
/** set tab class to determine color highlight for active tab */
|
||||
tabClass(searchType) {
|
||||
var isActive = (this.searchType === searchType) ? 'active' : 'inactive';
|
||||
return ['tab', isActive];
|
||||
},
|
||||
|
||||
/** filter all the tags present in the trace by the searchbar input */
|
||||
filteredTags() {
|
||||
var tags = [];
|
||||
var filter = this.searchInput.toUpperCase();
|
||||
this.presentTags.forEach((tag) => {
|
||||
if (tag.transition.includes(filter)) tags.push(tag);
|
||||
});
|
||||
return tags;
|
||||
},
|
||||
/** add filtered errors to filtered tags to integrate both into table*/
|
||||
filteredTagsAndErrors() {
|
||||
var tagsAndErrors = [...this.filteredTags()];
|
||||
var filter = this.searchInput.toUpperCase();
|
||||
this.presentErrors.forEach((error) => {
|
||||
if (error.message.includes(filter)) tagsAndErrors.push(error);
|
||||
});
|
||||
// sort into chronological order
|
||||
tagsAndErrors.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1));
|
||||
|
||||
return tagsAndErrors;
|
||||
},
|
||||
/** each transition has two tags present
|
||||
* isolate the tags for the desire transition
|
||||
* add a desc to display the timestamps as strings
|
||||
*/
|
||||
transitionTags(id) {
|
||||
var tags = this.filteredTags().filter((tag) => tag.id === id);
|
||||
tags.forEach((tag) => {
|
||||
tag.desc = nanos_to_string(tag.timestamp);
|
||||
});
|
||||
return tags;
|
||||
},
|
||||
|
||||
/** find the start as minimum timestamp in transition tags */
|
||||
transitionStart(tags) {
|
||||
var times = tags.map((tag) => tag.timestamp);
|
||||
return times[0];
|
||||
},
|
||||
/** find the end as maximum timestamp in transition tags */
|
||||
transitionEnd(tags) {
|
||||
var times = tags.map((tag) => tag.timestamp);
|
||||
return times[times.length - 1];
|
||||
},
|
||||
/** upon selecting a start/end tag in the dropdown;
|
||||
* navigates to that timestamp in the timeline */
|
||||
setCurrentTimestamp(timestamp) {
|
||||
this.$store.dispatch("updateTimelineTime", timestamp);
|
||||
},
|
||||
|
||||
/** colour codes text of transition in dropdown */
|
||||
transitionTextColor(transition) {
|
||||
return transitionMap.get(transition).color;
|
||||
},
|
||||
/** displays transition description rather than variable name */
|
||||
transitionDesc(transition) {
|
||||
return transitionMap.get(transition).desc;
|
||||
},
|
||||
/** add a desc to display the error timestamps as strings */
|
||||
errorDesc(timestamp) {
|
||||
return nanos_to_string(timestamp);
|
||||
},
|
||||
|
||||
/** navigates to closest timestamp in timeline to search input*/
|
||||
updateSearchForTimestamp() {
|
||||
if (regExpTimestampSearch.test(this.searchInput)) {
|
||||
var roundedTimestamp = parseInt(this.searchInput);
|
||||
} else {
|
||||
var roundedTimestamp = string_to_nanos(this.searchInput);
|
||||
}
|
||||
var closestTimestamp = this.timeline.reduce(function (prev, curr) {
|
||||
return Math.abs(curr - roundedTimestamp) <
|
||||
Math.abs(prev - roundedTimestamp)
|
||||
? curr
|
||||
: prev;
|
||||
});
|
||||
this.setCurrentTimestamp(closestTimestamp);
|
||||
},
|
||||
|
||||
isTagSearch() {
|
||||
return this.searchType === SEARCH_TYPE.TAG;
|
||||
},
|
||||
isTimestampSearch() {
|
||||
return this.searchType === SEARCH_TYPE.TIMESTAMP;
|
||||
},
|
||||
isTransition(item) {
|
||||
return item.stacktrace === undefined;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredTransitionsAndErrors() {
|
||||
var ids = [];
|
||||
return this.filteredTagsAndErrors().filter((item) => {
|
||||
if (this.isTransition(item) && !ids.includes(item.id)) {
|
||||
item.transitionStart = true;
|
||||
ids.push(item.id);
|
||||
}
|
||||
return !this.isTransition(item) || this.isTransition(item) && item.transitionStart;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.searchbar {
|
||||
background-color: rgb(250, 243, 233) !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
padding: 0px 20px 0px 20px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background-color: rgb(236, 222, 202);
|
||||
}
|
||||
|
||||
.tab.inactive {
|
||||
background-color: rgb(250, 243, 233);
|
||||
}
|
||||
|
||||
.search-timestamp {
|
||||
padding: 5px 20px 0px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-timestamp-button {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
padding: 5px 20px 0px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content table {
|
||||
overflow-y: scroll;
|
||||
height: 150px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content table td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown-content table th {
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.inline-time:hover {
|
||||
background: #9af39f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inline-transition {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.inline-transition:hover {
|
||||
background: #9af39f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inline-error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.inline-error:hover {
|
||||
background: #9af39f;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -20,10 +20,13 @@
|
||||
v-for="transition in timelineTransitions"
|
||||
:key="transition.type"
|
||||
:startPos="transition.startPos"
|
||||
:startTime="transition.startTime"
|
||||
:endTime="transition.endTime"
|
||||
:width="transition.width"
|
||||
:color="transition.color"
|
||||
:overlap="transition.overlap"
|
||||
:tooltip="transition.tooltip"
|
||||
:store="store"
|
||||
/>
|
||||
</div>
|
||||
<svg
|
||||
@@ -74,7 +77,7 @@ export default {
|
||||
components: {
|
||||
'transition-container': TransitionContainer,
|
||||
},
|
||||
props: ["selectedIndex", "crop", "disabled"],
|
||||
props: ["selectedIndex", "crop", "disabled", "store"],
|
||||
data() {
|
||||
return {
|
||||
pointHeight: 15,
|
||||
@@ -82,7 +85,6 @@ export default {
|
||||
};
|
||||
},
|
||||
mixins: [TimelineMixin],
|
||||
methods: {},
|
||||
computed: {
|
||||
timestamps() {
|
||||
if (this.timeline.length == 1) {
|
||||
@@ -114,6 +116,9 @@ export default {
|
||||
.timeline-container {
|
||||
width: 100%;
|
||||
}
|
||||
.container:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag-timeline {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@@ -136,4 +141,4 @@ export default {
|
||||
stroke: rgb(255, 0, 0);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<template>
|
||||
<div class="transition-container" :style="transitionStyle">
|
||||
<div class="transition-container" :style="transitionStyle" @click="handleTransitionClick()">
|
||||
<md-tooltip md-direction="left"> {{tooltip}} </md-tooltip>
|
||||
<arrow class="arrow-start" :style="transitionComponentColor"/>
|
||||
<div class="connector" :style="transitionComponentColor"/>
|
||||
@@ -22,6 +22,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import Arrow from './Arrow.vue';
|
||||
import {LocalStore} from '../../localstore.js';
|
||||
|
||||
var transitionCount = false;
|
||||
|
||||
export default {
|
||||
name: 'transition-container',
|
||||
components: {
|
||||
@@ -34,6 +38,12 @@ export default {
|
||||
'startPos': {
|
||||
type: Number,
|
||||
},
|
||||
'startTime': {
|
||||
type: Number,
|
||||
},
|
||||
'endTime': {
|
||||
type: Number,
|
||||
},
|
||||
'color': {
|
||||
type: String,
|
||||
},
|
||||
@@ -43,6 +53,20 @@ export default {
|
||||
'tooltip': {
|
||||
type: String,
|
||||
},
|
||||
'store': {
|
||||
type: LocalStore,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleTransitionClick() {
|
||||
if (transitionCount) {
|
||||
this.$store.dispatch('updateTimelineTime', this.startTime);
|
||||
transitionCount = false;
|
||||
} else {
|
||||
this.$store.dispatch('updateTimelineTime', this.endTime);
|
||||
transitionCount = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
transitionStyle() {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import _ from "lodash";
|
||||
import { nanos_to_string } from "../transform";
|
||||
import TransitionType from "../flickerlib/tags/TransitionType";
|
||||
import { transitionMap } from "../utils/consts";
|
||||
|
||||
/**
|
||||
* Represents a continuous section of the timeline that is rendered into the
|
||||
@@ -42,14 +42,18 @@ class Transition {
|
||||
* Create a transition.
|
||||
* @param {number} startPos - The position of the start tag as a percentage
|
||||
* of the timeline width.
|
||||
* @param {number} startTime - The start timestamp in ms of the transition.
|
||||
* @param {number} endTime - The end timestamp in ms of the transition.
|
||||
* @param {number} width - The width of the transition as a percentage of the
|
||||
* timeline width.
|
||||
* @param {string} color - the color of transition depending on type.
|
||||
* @param {number} overlap - number of transitions with which this transition overlaps.
|
||||
* @param {string} tooltip - The tooltip of the transition, minus the type of transition.
|
||||
*/
|
||||
constructor(startPos, width, color, overlap, tooltip) {
|
||||
constructor(startPos, startTime, endTime, width, color, overlap, tooltip) {
|
||||
this.startPos = startPos;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.width = width;
|
||||
this.color = color;
|
||||
this.overlap = overlap;
|
||||
@@ -61,16 +65,6 @@ class Transition {
|
||||
* This Mixin should only be injected into components which have the following:
|
||||
* - An element in the template referenced as 'timeline' (this.$refs.timeline).
|
||||
*/
|
||||
const transitionMap = new Map([
|
||||
[TransitionType.ROTATION, {desc: 'Rotation', color: '#9900ffff'}],
|
||||
[TransitionType.PIP_ENTER, {desc: 'Entering PIP mode', color: '#4a86e8ff'}],
|
||||
[TransitionType.PIP_RESIZE, {desc: 'Resizing PIP mode', color: '#2b9e94ff'}],
|
||||
[TransitionType.PIP_EXIT, {desc: 'Exiting PIP mode', color: 'darkblue'}],
|
||||
[TransitionType.APP_LAUNCH, {desc: 'Launching app', color: '#ef6befff'}],
|
||||
[TransitionType.APP_CLOSE, {desc: 'Closing app', color: '#d10ddfff'}],
|
||||
[TransitionType.IME_APPEAR, {desc: 'IME appearing', color: '#ff9900ff'}],
|
||||
[TransitionType.IME_DISAPPEAR, {desc: 'IME disappearing', color: '#ad6800ff'}],
|
||||
]);
|
||||
|
||||
export default {
|
||||
name: 'timeline',
|
||||
@@ -91,7 +85,7 @@ export default {
|
||||
'tags': {
|
||||
type: Array,
|
||||
},
|
||||
'errorTimestamps': {
|
||||
'errors': {
|
||||
type: Array,
|
||||
},
|
||||
'flickerMode': {
|
||||
@@ -195,7 +189,7 @@ export default {
|
||||
},
|
||||
errorPositions() {
|
||||
if (!this.flickerMode) return [];
|
||||
const errorPositions = this.errorTimestamps.map(timestamp => this.position(timestamp));
|
||||
const errorPositions = this.errors.map(error => this.position(error.timestamp));
|
||||
return Object.freeze(errorPositions);
|
||||
},
|
||||
},
|
||||
@@ -358,7 +352,7 @@ export default {
|
||||
/**
|
||||
* Generate a transition object that can be used by the tag-timeline to render
|
||||
* a transformed transition that starts at `startTs` and ends at `endTs`.
|
||||
* @param {number} startPos - The timestamp at which the transition starts.
|
||||
* @param {number} startTs - The timestamp at which the transition starts.
|
||||
* @param {number} endTs - The timestamp at which the transition ends.
|
||||
* @param {string} transitionType - The type of transition.
|
||||
* @param {number} overlap - The degree to which the transition overlaps with others.
|
||||
@@ -370,7 +364,7 @@ export default {
|
||||
const transitionDesc = transitionMap.get(transitionType).desc;
|
||||
const transitionColor = transitionMap.get(transitionType).color;
|
||||
const tooltip = `${transitionDesc}. Start: ${nanos_to_string(startTs)}. End: ${nanos_to_string(endTs)}.`;
|
||||
return new Transition(this.position(startTs), transitionWidth, transitionColor, overlap, tooltip);
|
||||
return new Transition(this.position(startTs), startTs, endTs, transitionWidth, transitionColor, overlap, tooltip);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import TransitionType from "../flickerlib/tags/TransitionType";
|
||||
|
||||
/**
|
||||
* Should be kept in sync with ENUM is in Google3 under:
|
||||
* google3/wireless/android/tools/android_bug_tool/extension/common/actions
|
||||
@@ -32,6 +34,11 @@ const NAVIGATION_STYLE = {
|
||||
FLICKER: 'Flicker',
|
||||
};
|
||||
|
||||
const SEARCH_TYPE = {
|
||||
TAG: 'Transitions and Errors',
|
||||
TIMESTAMP: 'Timestamp',
|
||||
};
|
||||
|
||||
const logLevel = {
|
||||
INFO: 'info',
|
||||
DEBUG: 'debug',
|
||||
@@ -41,4 +48,15 @@ const logLevel = {
|
||||
WTF: 'wtf',
|
||||
};
|
||||
|
||||
export { WebContentScriptMessageType, NAVIGATION_STYLE, logLevel };
|
||||
const transitionMap = new Map([
|
||||
[TransitionType.ROTATION, {desc: 'Rotation', color: '#9900ffff'}],
|
||||
[TransitionType.PIP_ENTER, {desc: 'Entering PIP mode', color: '#4a86e8ff'}],
|
||||
[TransitionType.PIP_RESIZE, {desc: 'Resizing PIP mode', color: '#2b9e94ff'}],
|
||||
[TransitionType.PIP_EXIT, {desc: 'Exiting PIP mode', color: 'darkblue'}],
|
||||
[TransitionType.APP_LAUNCH, {desc: 'Launching app', color: '#ef6befff'}],
|
||||
[TransitionType.APP_CLOSE, {desc: 'Closing app', color: '#d10ddfff'}],
|
||||
[TransitionType.IME_APPEAR, {desc: 'IME appearing', color: '#ff9900ff'}],
|
||||
[TransitionType.IME_DISAPPEAR, {desc: 'IME disappearing', color: '#ad6800ff'}],
|
||||
])
|
||||
|
||||
export { WebContentScriptMessageType, NAVIGATION_STYLE, SEARCH_TYPE, logLevel, transitionMap };
|
||||
|
||||
Reference in New Issue
Block a user