Commit 3747f392 authored by James Long's avatar James Long Committed by Commit Bot

[Lorenz] Modify filter list to use checkboxes

As part of UX discussions, the filter list is now a list of nodes that
can be used in the filter. Whether a node is actually in the filter or
not is toggled by its checkbox. "Select all" and "Deselect all" buttons
have been added to make checkbox manipulation easier.

A node enters the filter list through the filter input or the node
details panel. It always enters in the checked state. A node exits the
filter list through the user manually clicking the x beside it. Once in
the filter list, the node's filter visibility can be toggled by a
checkbox beside it or through the node details panel.

Checkbox information is also encoded in the URL.

Bug: 1111836
Change-Id: If92d64a9bdcfc93d6b86d1a335b795a5ab628c01
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2333066
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: default avatarMohamed Heikal <mheikal@chromium.org>
Reviewed-by: default avatarSamuel Huang <huangs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#795184}
parent e861f454
......@@ -16,48 +16,88 @@ const GraphEdgeColor = {
};
/**
* A container representing the visualization's node filter. Nodes included in
* the filter are allowed to be displayed on the graph.
* Underlying data for node filtering. The UI shows a "filter list" that
* displays nodes of interest, and each can be toggled on/off using a checkbox.
* Each node is classified as:
* 1. Ignored: Not in filter list; hidden in visualizer.
* 2. Unchecked: In filter list; hidden in visualizer.
* 3. Checked (Selected): In filter list; shown in visualizer.
*/
class NodeFilterData {
/**
* Vue does not currently support reactivity on ES6 Sets. (Planned addition
* for 3.0 https://github.com/vuejs/vue/issues/2410#issuecomment-434990853).
* For performance, we maintain a Set for lookups when filtering nodes/edges
* and expose an Array to the UI for reactivity. We sync the data in these
* two structures manually.
*/
constructor() {
/** @public {!Set<string>} */
this.nodeSet = new Set();
/** @public {!Array<string>} */
this.nodeList = [];
/**
* @typedef {Object} NodeFilterEntry An entry in the filter list.
* @property {string} name The name of the node to be filtered.
* @property {boolean} checked Whether the node is checked (selected). If
* true, then the node is shown in the visualizer.
*/
/**
* List of filter list entries, i.e., nodes in unchecked or checked state.
* @public {!Array<!NodeFilterEntry>)
*/
this.filterList = [];
}
/**
* Adds a node to the node set + array.
* @param {string} nodeName The name of the node to add.
* Finds a node in the filter list, creating and adding one if necessary.
* @param {string} nodeName The name of the node to find.
* @return {!NodeFilterEntry} The node's entry in the filter list.
*/
addNode(nodeName) {
if (!this.nodeSet.has(nodeName)) {
this.nodeSet.add(nodeName);
this.nodeList.push(nodeName);
addOrFindNode(nodeName) {
const foundIndex = this.filterList.findIndex(
filterEntry => filterEntry.name === nodeName);
if (foundIndex >= 0) {
return this.filterList[foundIndex];
}
const entryToAdd = {
name: nodeName,
checked: true,
};
this.filterList.push(entryToAdd);
return entryToAdd;
}
/**
* Removes a node from the node set + array.
* Removes a node from the filter list (i.e., set state to ignored) if it
* exists.
* @param {string} nodeName The name of the node to remove.
*/
removeNode(nodeName) {
const deleted = this.nodeSet.delete(nodeName);
if (deleted) {
const deleteIndex = this.nodeList.indexOf(nodeName);
const deleteIndex = this.filterList.findIndex(
filterEntry => filterEntry.name === nodeName);
if (deleteIndex >= 0) {
// TODO(yjlong): If order turns out to be unimportant, just swap the
// last element and the deleted element, then pop.
this.nodeList.splice(deleteIndex, 1);
this.filterList.splice(deleteIndex, 1);
}
}
/**
* Sets all nodes in the filter list to checked.
*/
checkAll() {
for (const filterEntry of this.filterList) {
filterEntry.checked = true;
}
}
/**
* Sets all nodes in the filter list to unchecked.
*/
uncheckAll() {
for (const filterEntry of this.filterList) {
filterEntry.checked = false;
}
}
/**
* @return {!Set<string>} A set of nodes that are checked in the filter.
*/
getSelectedNodeSet() {
return new Set(this.filterList.filter(filterEntry => filterEntry.checked)
.map(filterEntry => filterEntry.name));
}
}
/** Data store containing graph display-related settings. */
......@@ -89,9 +129,12 @@ class DisplaySettingsData {
urlProcessor.append(
URL_PARAM_KEYS.COLOR_ONLY_ON_HOVER, this.colorOnlyOnHover);
urlProcessor.append(URL_PARAM_KEYS.EDGE_COLOR, this.graphEdgeColor);
if (this.nodeFilterData.nodeList.length > 0) {
urlProcessor.appendArray(
URL_PARAM_KEYS.FILTER, this.nodeFilterData.nodeList);
if (this.nodeFilterData.filterList.length > 0) {
urlProcessor.appendArray(URL_PARAM_KEYS.FILTER_NAMES,
this.nodeFilterData.filterList.map(filterEntry => filterEntry.name));
urlProcessor.appendArray(URL_PARAM_KEYS.FILTER_CHECKED,
this.nodeFilterData.filterList.map(
filterEntry => filterEntry.checked));
}
}
......@@ -110,8 +153,20 @@ class DisplaySettingsData {
URL_PARAM_KEYS.COLOR_ONLY_ON_HOVER, this.colorOnlyOnHover);
this.graphEdgeColor = urlProcessor.getString(
URL_PARAM_KEYS.EDGE_COLOR, this.graphEdgeColor);
for (const filterItem of urlProcessor.getArray(URL_PARAM_KEYS.FILTER, [])) {
this.nodeFilterData.addNode(filterItem);
const filterNames = urlProcessor.getArray(URL_PARAM_KEYS.FILTER_NAMES, []);
const filterChecked = urlProcessor.getArray(
URL_PARAM_KEYS.FILTER_CHECKED, []);
for (const [filterIdx, filterName] of filterNames.entries()) {
const filterEntry = this.nodeFilterData.addOrFindNode(filterName);
// If there is no corresponding entry in `filterChecked` (e.g., if the
// checked param is empty), use true as a default value.
if (filterIdx < filterChecked.length) {
const filterElemChecked = (filterChecked[filterIdx] === 'true');
filterEntry.checked = filterElemChecked;
} else {
filterEntry.checked = true;
}
}
}
}
......
......@@ -14,7 +14,8 @@ const PagePathName = {
// Keys for identifying URL params.
const URL_PARAM_KEYS = {
// Common keys:
FILTER: 'f',
FILTER_NAMES: 'fn',
FILTER_CHECKED: 'fc',
INBOUND_DEPTH: 'ibd',
OUTBOUND_DEPTH: 'obd',
CURVE_EDGES: 'ce',
......
......@@ -7,10 +7,12 @@
<div id="page-controls">
<GraphFilterInput
:node-ids="pageModel.getNodeIds()"
@[CUSTOM_EVENTS.FILTER_SUBMITTED]="addNodeToFilter"/>
@[CUSTOM_EVENTS.FILTER_SUBMITTED]="filterAddOrCheckNode"/>
<GraphFilterItems
:node-filter-data="displaySettingsData.nodeFilterData"
@[CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED]="removeNodeFromFilter"/>
@[CUSTOM_EVENTS.FILTER_REMOVE]="filterRemoveNode"
@[CUSTOM_EVENTS.FILTER_CHECK_ALL]="filterCheckAll"
@[CUSTOM_EVENTS.FILTER_UNCHECK_ALL]="filterUncheckAll"/>
<NumericInput
description="Change inbound (blue) depth:"
input-id="inbound-input"
......@@ -37,8 +39,8 @@
:selected-hull-display.sync="displaySettingsData.hullDisplay"/>
<GraphSelectedNodeDetails
:selected-node-details-data="pageModel.selectedNodeDetailsData"
@[CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED]="addNodeToFilter"
@[CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED]="removeNodeFromFilter"/>
@[CUSTOM_EVENTS.DETAILS_CHECK_NODE]="filterAddOrCheckNode"
@[CUSTOM_EVENTS.DETAILS_UNCHECK_NODE]="filterUncheckNode"/>
<ClassDetailsPanel
:selected-class="pageModel.selectedNodeDetailsData.selectedNode"/>
</div>
......@@ -143,13 +145,13 @@ const ClassGraphPage = {
const pageUrlProcessor = new UrlProcessor(pageUrl.searchParams);
this.displaySettingsData.readUrlProcessor(pageUrlProcessor);
if (this.displaySettingsData.nodeFilterData.nodeList.length === 0) {
if (this.displaySettingsData.nodeFilterData.filterList.length === 0) {
// TODO(yjlong): This is test data. Remove this when no longer needed.
this.addNodesToFilter([
[
'org.chromium.chrome.browser.tabmodel.AsyncTabParams',
'org.chromium.chrome.browser.ActivityTabProvider',
'org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver',
]);
].forEach(nodeName => this.filterAddOrCheckNode(nodeName));
}
},
methods: {
......@@ -160,25 +162,22 @@ const ClassGraphPage = {
const pageUrl = urlProcessor.getUrl(document.URL, PagePathName.CLASS);
history.replaceState(null, '', pageUrl);
},
/**
* @param {string} nodeName The node to add.
*/
addNodeToFilter: function(nodeName) {
this.displaySettingsData.nodeFilterData.addNode(nodeName);
filterRemoveNode: function(nodeName) {
this.displaySettingsData.nodeFilterData.removeNode(nodeName);
},
/**
* @param {!Array<string>} nodeNames The nodes to add.
*/
addNodesToFilter: function(nodeNames) {
for (const nodeName of nodeNames) {
this.displaySettingsData.nodeFilterData.addNode(nodeName);
}
filterAddOrCheckNode: function(nodeName) {
this.displaySettingsData.nodeFilterData.addOrFindNode(
nodeName).checked = true;
},
/**
* @param {string} nodeName The node to remove.
*/
removeNodeFromFilter: function(nodeName) {
this.displaySettingsData.nodeFilterData.removeNode(nodeName);
filterUncheckNode: function(nodeName) {
this.displaySettingsData.nodeFilterData.addOrFindNode(
nodeName).checked = false;
},
filterCheckAll: function() {
this.displaySettingsData.nodeFilterData.checkAll();
},
filterUncheckAll: function() {
this.displaySettingsData.nodeFilterData.uncheckAll();
},
/**
* @param {number} depth The new inbound depth.
......
......@@ -3,14 +3,31 @@
found in the LICENSE file. -->
<template>
<ul class="filter-items">
<li
v-for="node in nodeList"
:key="node.id"
@click="removeFilter">
{{ node }}
</li>
</ul>
<div id="filter-items-container">
<div id="controls">
<button @click="checkAll">
Check All
</button>
<button @click="uncheckAll">
Uncheck All
</button>
</div>
<ul id="filter-list">
<li
v-for="node in filterList"
:key="node.name">
<div class="filter-list-item">
<div @click="removeFromFilter(node.name)">
x
</div>
<input
v-model="node.checked"
type="checkbox">
<div>{{ node.name }}</div>
</div>
</li>
</ul>
</div>
</template>
<script>
......@@ -25,9 +42,14 @@ const GraphFilterItems = {
return this.nodeFilterData;
},
methods: {
removeFilter: function(e) {
const filterText = e.target.textContent;
this.$emit(CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED, filterText.trim());
removeFromFilter: function(nodeName) {
this.$emit(CUSTOM_EVENTS.FILTER_REMOVE, nodeName);
},
checkAll: function() {
this.$emit(CUSTOM_EVENTS.FILTER_CHECK_ALL);
},
uncheckAll: function() {
this.$emit(CUSTOM_EVENTS.FILTER_UNCHECK_ALL);
},
},
};
......@@ -36,10 +58,31 @@ export default GraphFilterItems;
</script>
<style scoped>
.filter-items {
overflow: hidden;
overflow-y: scroll;
min-width: 100px;
ul {
list-style-type: none;
}
#filter-items-container {
display: flex;
flex-direction: column;
margin-right: 20px;
min-width: 100px;
}
#filter-list {
margin: 0;
overflow-x: hidden;
overflow-y: scroll;
padding: 0;
}
#controls {
display: flex;
flex-direction: row;
}
.filter-list-item {
display: flex;
flex-direction: row;
}
</style>
......@@ -16,13 +16,13 @@
</ul>
<button
v-if="selectedNode.visualizationState.selectedByFilter"
@click="removeSelectedFromFilter">
Remove from filter
@click="uncheckNodeInFilter">
Uncheck in filter
</button>
<button
v-else
@click="addSelectedToFilter">
Add to filter
@click="checkNodeInFilter">
Add/check in filter
</button>
</template>
<div v-else>
......@@ -43,12 +43,11 @@ const GraphSelectedNodeDetails = {
return this.selectedNodeDetailsData;
},
methods: {
addSelectedToFilter: function() {
this.$emit(CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED, this.selectedNode.id);
checkNodeInFilter: function(check) {
this.$emit(CUSTOM_EVENTS.DETAILS_CHECK_NODE, this.selectedNode.id);
},
removeSelectedFromFilter: function() {
this.$emit(
CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED, this.selectedNode.id);
uncheckNodeInFilter: function(check) {
this.$emit(CUSTOM_EVENTS.DETAILS_UNCHECK_NODE, this.selectedNode.id);
},
},
};
......
......@@ -42,7 +42,7 @@ const GraphVisualization = {
graphUpdateTriggers: {
handler: function() {
const d3Data = this.pageModel.graphModel.getDataForD3(
this.displaySettingsData.nodeFilterData.nodeSet,
this.displaySettingsData.nodeFilterData.getSelectedNodeSet(),
this.displaySettingsData.inboundDepth,
this.displaySettingsData.outboundDepth,
);
......
......@@ -19,7 +19,7 @@ const LinkToGraph = {
computed: {
url: function() {
const urlProcessor = UrlProcessor.createForOutput();
urlProcessor.appendArray(URL_PARAM_KEYS.FILTER, this.filter);
urlProcessor.appendArray(URL_PARAM_KEYS.FILTER_NAMES, this.filter);
return urlProcessor.getUrl(document.URL, this.graphType);
},
},
......
......@@ -7,10 +7,12 @@
<div id="page-controls">
<GraphFilterInput
:node-ids="pageModel.getNodeIds()"
@[CUSTOM_EVENTS.FILTER_SUBMITTED]="addNodeToFilter"/>
@[CUSTOM_EVENTS.FILTER_SUBMITTED]="filterAddOrCheckNode"/>
<GraphFilterItems
:node-filter-data="displaySettingsData.nodeFilterData"
@[CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED]="removeNodeFromFilter"/>
@[CUSTOM_EVENTS.FILTER_REMOVE]="filterRemoveNode"
@[CUSTOM_EVENTS.FILTER_CHECK_ALL]="filterCheckAll"
@[CUSTOM_EVENTS.FILTER_UNCHECK_ALL]="filterUncheckAll"/>
<NumericInput
description="Change inbound (blue) depth:"
input-id="inbound-input"
......@@ -24,7 +26,7 @@
<GraphVisualization
:graph-update-triggers="[
displaySettingsData,
displaySettingsData.nodeFilterData.nodeList,
displaySettingsData.nodeFilterData.filterList,
]"
:page-model="pageModel"
:display-settings-data="displaySettingsData"
......@@ -33,13 +35,11 @@
<GraphDisplaySettings
:display-settings-data="displaySettingsData"/>
<GraphSelectedNodeDetails
:selected-node-details-data="
pageModel.selectedNodeDetailsData"
@[CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED]="addNodeToFilter"
@[CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED]="removeNodeFromFilter"/>
:selected-node-details-data="pageModel.selectedNodeDetailsData"
@[CUSTOM_EVENTS.DETAILS_CHECK_NODE]="filterAddOrCheckNode"
@[CUSTOM_EVENTS.DETAILS_UNCHECK_NODE]="filterUncheckNode"/>
<PackageDetailsPanel
:selected-package="
pageModel.selectedNodeDetailsData.selectedNode"/>
:selected-package="pageModel.selectedNodeDetailsData.selectedNode"/>
</div>
</div>
</div>
......@@ -119,15 +119,15 @@ const PackageGraphPage = {
const pageUrlProcessor = new UrlProcessor(pageUrl.searchParams);
this.displaySettingsData.readUrlProcessor(pageUrlProcessor);
if (this.displaySettingsData.nodeFilterData.nodeList.length === 0) {
if (this.displaySettingsData.nodeFilterData.filterList.length === 0) {
// TODO(yjlong): This is test data. Remove this when no longer needed.
this.addNodesToFilter([
[
'org.chromium.base',
'org.chromium.chrome.browser.gsa',
'org.chromium.chrome.browser.omaha',
'org.chromium.chrome.browser.media',
'org.chromium.ui.base',
]);
].forEach(nodeName => this.filterAddOrCheckNode(nodeName));
}
},
methods: {
......@@ -138,25 +138,22 @@ const PackageGraphPage = {
const pageUrl = urlProcessor.getUrl(document.URL, PagePathName.PACKAGE);
history.replaceState(null, '', pageUrl);
},
/**
* @param {string} nodeName The node to add.
*/
addNodeToFilter: function(nodeName) {
this.displaySettingsData.nodeFilterData.addNode(nodeName);
filterRemoveNode: function(nodeName) {
this.displaySettingsData.nodeFilterData.removeNode(nodeName);
},
/**
* @param {!Array<string>} nodeNames The nodes to add.
*/
addNodesToFilter: function(nodeNames) {
for (const nodeName of nodeNames) {
this.displaySettingsData.nodeFilterData.addNode(nodeName);
}
filterAddOrCheckNode: function(nodeName) {
this.displaySettingsData.nodeFilterData.addOrFindNode(
nodeName).checked = true;
},
/**
* @param {string} nodeName The node to remove.
*/
removeNodeFromFilter: function(nodeName) {
this.displaySettingsData.nodeFilterData.removeNode(nodeName);
filterUncheckNode: function(nodeName) {
this.displaySettingsData.nodeFilterData.addOrFindNode(
nodeName).checked = false;
},
filterCheckAll: function() {
this.displaySettingsData.nodeFilterData.checkAll();
},
filterUncheckAll: function() {
this.displaySettingsData.nodeFilterData.uncheckAll();
},
/**
* @param {number} depth The new inbound depth.
......
......@@ -5,11 +5,13 @@
// A collection of names for the custom events to be emitted by the page's
// components (in vue_components/).
const CUSTOM_EVENTS = {
ADD_TO_FILTER_CLICKED: 'add-to-filter-clicked',
FILTER_ELEMENT_CLICKED: 'filter-element-clicked',
DETAILS_CHECK_NODE: 'details-check-node',
DETAILS_UNCHECK_NODE: 'details-uncheck-node',
FILTER_REMOVE: 'filter-remove',
FILTER_CHECK_ALL: 'filter-check-all',
FILTER_UNCHECK_ALL: 'filter-uncheck-all',
FILTER_SUBMITTED: 'filter-submitted',
NODE_CLICKED: 'node-clicked',
REMOVE_FROM_FILTER_CLICKED: 'remove-from-filter-clicked',
};
export {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment