305 lines
8.5 KiB
Vue
305 lines
8.5 KiB
Vue
<!-- Copyright (C) 2020 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">
|
||
|
||
<md-card class="changes card">
|
||
<md-content md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||
<h2 class="md-title" style="flex: 1">Log</h2>
|
||
</md-content>
|
||
<div class="filters">
|
||
<md-field>
|
||
<label>Transaction Type</label>
|
||
<md-select v-model="selectedTransactionTypes" multiple>
|
||
<md-option v-for="type in transactionTypes" :value="type">{{ type }}</md-option>
|
||
</md-select>
|
||
</md-field>
|
||
|
||
<div>
|
||
<md-autocomplete v-model="selectedProperty" :md-options="properties">
|
||
<label>Changed property</label>
|
||
</md-autocomplete>
|
||
<!-- TODO(b/159582192): Add way to select value a property has changed to,
|
||
figure out how to handle properties that are objects... -->
|
||
</div>
|
||
|
||
<md-chips v-model="filters" md-placeholder="Add surface id or name...">
|
||
<div class="md-helper-text">Press enter to add</div>
|
||
</md-chips>
|
||
</div>
|
||
|
||
<virtual-list style="height: 360px; overflow-y: auto;"
|
||
:data-key="'timestamp'"
|
||
:data-sources="filteredData"
|
||
:data-component="transactionEntryComponent"
|
||
:extra-props="{'onClick': transactionSelected}"
|
||
ref="loglist"
|
||
/>
|
||
</md-card>
|
||
|
||
<md-card class="changes card">
|
||
<md-content md-tag="md-toolbar" md-elevation="0" class="card-toolbar md-transparent md-dense">
|
||
<h2 class="md-title" style="flex: 1">Changes</h2>
|
||
</md-content>
|
||
<div class="changes-content">
|
||
<tree-view
|
||
:item="selectedTree"
|
||
:collapseChildren="true"
|
||
:useGlobalCollapsedState="true"
|
||
/>
|
||
</div>
|
||
</md-card>
|
||
|
||
</md-card-content>
|
||
</template>
|
||
<script>
|
||
import TreeView from './TreeView.vue';
|
||
import VirtualList from '../libs/virtualList/VirtualList';
|
||
import TransactionEntry from './TransactionEntry.vue';
|
||
|
||
import { transform_json } from './transform.js';
|
||
import { stableIdCompatibilityFixup } from './utils/utils.js'
|
||
|
||
export default {
|
||
name: 'transactionsview',
|
||
props: ['data'],
|
||
data() {
|
||
const transactionTypes = new Set();
|
||
const properties = new Set();
|
||
for (const entry of this.data) {
|
||
if (entry.type == "transaction") {
|
||
for (const transaction of entry.transactions) {
|
||
transactionTypes.add(transaction.type);
|
||
Object.keys(transaction.obj).forEach(item => properties.add(item));
|
||
}
|
||
} else {
|
||
transactionTypes.add(entry.type);
|
||
Object.keys(entry.obj).forEach(item => properties.add(item));
|
||
}
|
||
}
|
||
|
||
return {
|
||
transactionTypes: Array.from(transactionTypes),
|
||
properties: Array.from(properties),
|
||
selectedTransactionTypes: [],
|
||
searchInput: "",
|
||
selectedTree: null,
|
||
filters: [],
|
||
selectedProperty: null,
|
||
transactionEntryComponent: TransactionEntry,
|
||
};
|
||
},
|
||
computed: {
|
||
filteredData() {
|
||
let filteredData = this.data;
|
||
|
||
if (this.selectedTransactionTypes.length > 0) {
|
||
filteredData = filteredData.filter(
|
||
this.filterTransactions(transaction =>
|
||
this.selectedTransactionTypes.includes(transaction.type)));
|
||
}
|
||
|
||
if (this.filters.length > 0) {
|
||
filteredData = filteredData.filter(
|
||
this.filterTransactions(transaction => {
|
||
for (const filter of this.filters) {
|
||
if (isNaN(filter) && transaction.obj?.name?.includes(filter)) {
|
||
// If filter isn't a number then check if the transaction's
|
||
// target surface's name matches the filter — if so keep it.
|
||
return true;
|
||
}
|
||
if (filter == transaction.obj.id) {
|
||
// If filteter is a number then check if the filter matches
|
||
// the transaction's target surface id — if so keep it.
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Exclude transaction if it fails to match filter.
|
||
return false;
|
||
})
|
||
);
|
||
}
|
||
|
||
if (this.selectedProperty) {
|
||
filteredData = filteredData.filter(
|
||
this.filterTransactions(transaction => {
|
||
for (const key in transaction.obj) {
|
||
if (this.isMeaningfulChange(transaction.obj, key)
|
||
&& key === this.selectedProperty) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
})
|
||
);
|
||
}
|
||
|
||
return filteredData;
|
||
},
|
||
|
||
},
|
||
methods: {
|
||
removeNullFields(changeObject) {
|
||
for (const key in changeObject) {
|
||
if (changeObject[key] === null) {
|
||
delete changeObject[key];
|
||
}
|
||
}
|
||
|
||
return changeObject;
|
||
},
|
||
transactionSelected(transaction) {
|
||
let obj = this.removeNullFields(transaction.obj);
|
||
let name = transaction.type;
|
||
|
||
if (transaction.type == "transaction") {
|
||
name = "changes";
|
||
obj = {};
|
||
|
||
const [surfaceChanges, displayChanges] =
|
||
this.aggregateTransactions(transaction.transactions);
|
||
|
||
for (const changeId in surfaceChanges) {
|
||
this.removeNullFields(surfaceChanges[changeId]);
|
||
}
|
||
for (const changeId in displayChanges) {
|
||
this.removeNullFields(displayChanges[changeId]);
|
||
}
|
||
|
||
if (Object.keys(surfaceChanges).length > 0) {
|
||
obj.surfaceChanges = surfaceChanges;
|
||
}
|
||
|
||
if (Object.keys(displayChanges).length > 0) {
|
||
obj.displayChanges = displayChanges;
|
||
}
|
||
}
|
||
|
||
const transactionUnique = transaction.timestamp;
|
||
let tree = transform_json(obj, name, transactionUnique, {
|
||
formatter: () => {}
|
||
});
|
||
|
||
if (tree.name == "changes" && tree.children.length === 1) {
|
||
tree = tree.children[0];
|
||
}
|
||
|
||
this.selectedTree = tree;
|
||
},
|
||
filterTransactions(condition) {
|
||
return (entry) => {
|
||
if (entry.type == "transaction") {
|
||
for (const transaction of entry.transactions) {
|
||
if (condition(transaction)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
} else {
|
||
return condition(entry);
|
||
}
|
||
};
|
||
},
|
||
isMeaningfulChange(object, key) {
|
||
// TODO: Handle cases of non null objects but meaningless change
|
||
return object[key] !== null && object.hasOwnProperty(key)
|
||
},
|
||
mergeChanges(a, b) {
|
||
const res = {};
|
||
|
||
for (const key in a) {
|
||
if (this.isMeaningfulChange(a, key)) {
|
||
res[key] = a[key];
|
||
}
|
||
}
|
||
|
||
for (const key in b) {
|
||
if (this.isMeaningfulChange(b, key)) {
|
||
if (res.hasOwnProperty(key) && key != "id") {
|
||
throw new Error(`Merge failed – key '${key}' already present`);
|
||
}
|
||
res[key] = b[key];
|
||
}
|
||
}
|
||
|
||
return res;
|
||
},
|
||
aggregateTransactions(transactions) {
|
||
const surfaceChanges = {};
|
||
const displayChanges = {};
|
||
|
||
for (const transaction of transactions) {
|
||
const obj = transaction.obj;
|
||
|
||
switch (transaction.type) {
|
||
case "surfaceChange":
|
||
surfaceChanges[obj.id] =
|
||
this.mergeChanges(surfaceChanges[obj.id] ?? {}, obj);
|
||
break;
|
||
|
||
case "displayChange":
|
||
displayChanges[obj.id] =
|
||
this.mergeChanges(displayChanges[obj.id] ?? {}, obj);
|
||
break;
|
||
|
||
default:
|
||
throw new Error(`Unhandled transaction type ${transaction.type}`);
|
||
}
|
||
}
|
||
|
||
return [surfaceChanges, displayChanges];
|
||
},
|
||
},
|
||
components: {
|
||
'virtual-list': VirtualList,
|
||
'tree-view': TreeView,
|
||
}
|
||
}
|
||
|
||
</script>
|
||
<style scoped>
|
||
.container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.transaction-table,
|
||
.changes {
|
||
flex: 1;
|
||
margin: 8px;
|
||
}
|
||
|
||
.scrollBody {
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: scroll;
|
||
}
|
||
|
||
.filters {
|
||
margin-bottom: 15px;
|
||
width: 100%;
|
||
}
|
||
|
||
.changes-content {
|
||
padding: 18px;
|
||
height: 550px;
|
||
overflow: auto;
|
||
}
|
||
|
||
</style>
|