Commit 8e4dfac1 authored by James Long's avatar James Long Committed by Commit Bot

Migrate Android dependency visualization to Vue components

Vue instances initialized in `index.js` have been moved to a component
folder in support of the upcoming changes which will involve having
multiple endpoints (for class and package graphs). The move to a
components-based structure will hopefully reduce duplicate code between
the two very similar (for now) pages.

Bug: 1093962
Change-Id: I22ae0e5b5604386e341db9293a39aa5121474a9d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2281169
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: default avatarSamuel Huang <huangs@chromium.org>
Reviewed-by: default avatarHenrique Nakashima <hnakashima@chromium.org>
Cr-Commit-Position: refs/heads/master@{#785972}
parent c3cb5809
...@@ -21,7 +21,10 @@ class NodeVisualizationState { ...@@ -21,7 +21,10 @@ class NodeVisualizationState {
} }
} }
/** A node in a directed graph. */ /** A node in a directed graph.
*
* TODO(yjlong): Maybe rename to GraphNode to avoid confusion with window.Node?
*/
class Node { class Node {
/** /**
* @param {string} id The unique ID for the node. * @param {string} id The unique ID for the node.
......
...@@ -111,7 +111,7 @@ class GraphView { ...@@ -111,7 +111,7 @@ class GraphView {
/** @private @type {?OnNodeClickedCallback} */ /** @private @type {?OnNodeClickedCallback} */
this.onNodeClicked_ = null; this.onNodeClicked_ = null;
const svg = d3.select('svg'); const svg = d3.select('#graph-svg');
const graphGroup = svg.append('g'); // Contains entire graph (for zoom/pan). const graphGroup = svg.append('g'); // Contains entire graph (for zoom/pan).
const svgDefs = svg.append('defs'); const svgDefs = svg.append('defs');
......
...@@ -18,13 +18,13 @@ ...@@ -18,13 +18,13 @@
flex-direction: row; flex-direction: row;
} }
#selected-node-details { .selected-node-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 400px; min-width: 400px;
} }
#url-generator { .url-generator {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
flex-direction: column; flex-direction: column;
} }
#filter-items { .filter-items {
overflow: hidden; overflow: hidden;
overflow-y: scroll; overflow-y: scroll;
min-width: 100px; min-width: 100px;
......
...@@ -6,51 +6,7 @@ ...@@ -6,51 +6,7 @@
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head> </head>
<body> <body>
<div id="page-container"> <div id="package-graph-page"></div>
<div id="page-controls">
<div id="filter-input-group" class="user-input-group">
<label for="filter-input">Add node to filter (exact name):</label>
<input v-model="filterInputText" type="text" id="filter-input">
<button @click="submitFilter" type="button">Add</button>
</div>
<ul id="filter-items">
<li @click="removeFilter" v-for="node in nodeList">
{{node}}
</li>
</ul>
<div id="filter-inbound-group" class="user-input-group">
<label for="filter-inbound">Change inbound (blue) depth:</label>
<input v-model.number="inboundDepth" type="number" id="filter-inbound">
<button @click="submitInbound" type="button">Update Inbound</button>
</div>
<div id="filter-outbound-group" class="user-input-group">
<label for="filter-outbound">Change outbound (yellow) depth:</label>
<input v-model.number="outboundDepth" type="number" id="filter-outbound">
<button @click="submitOutbound" type="button">Update Outbound</button>
</div>
</div>
<div id="graph-and-node-details-container">
<div id="graph-container">
<svg id="graph-svg" width="960" height="600"></svg>
</div>
<div id="selected-node-details">
<template v-if="selectedNode !== null">
<ul>
<li>Name: {{selectedNode.id}}</li>
<li>Display Name: {{selectedNode.displayName}}</li>
<li v-for="(value, key) in selectedNode.visualizationState">{{key}}: {{value}}</li>
</ul>
<button @click="removeSelectedFromFilter" v-if="selectedNode.visualizationState.selectedByFilter">Remove from filter</button>
<button @click="addSelectedToFilter" v-else>Add to filter</button>
</template>
<div v-else>Click a node for more details.</div>
</div>
</div>
<div id="url-generator">
<button @click="generateUrl">Generate Current URL</button>
<input type="text" readonly ref="input">
</div>
</div>
<script type="module" src="./index.js"></script> <script type="module" src="./index.js"></script>
</body> </body>
</html> </html>
......
...@@ -2,149 +2,22 @@ ...@@ -2,149 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {parseGraphModelFromJson} from './process_graph_json.js';
import {PageModel} from './page_model.js'; import {PackageGraphPage} from './vue_components/package_graph_page.js';
import {PageController} from './page_controller.js';
import {GraphView} from './graph_view.js';
// For ease of development, we currently serve all our JSON and other assets // For ease of development, we currently serve all our JSON and other assets
// through a simple Python server at localhost:8888. This should be changed // through a simple Python server at localhost:8888. This should be changed
// as we find other ways to serve the assets (user upload or hosted externally). // as we find other ways to serve the assets (user upload or hosted externally).
const LOCALHOST = 'http://localhost:8888'; const LOCALHOST = 'http://localhost:8888';
// Keys for identifying URL params.
const URL_PARAM_KEYS = {
FILTER: 'filter',
};
/**
* Converts a URL to the node filter contained within its querystring.
* @param {string} url The URL to convert.
* @return {!Array<string>} The array of node names in the URL's filter, or an
* empty array if there was no filter in the URL.
*/
function generateFilterFromUrl(url) {
const pageUrl = new URL(url);
const filterNodes = pageUrl.searchParams.get(URL_PARAM_KEYS.FILTER);
if (filterNodes !== null) {
return filterNodes.split(',');
}
return [];
}
/**
* Converts a node filter into a URL containing the filter information. The
* filter information will be stored in the querystring of the supplied URL.
* @param {!Array<string>} filter The node name filter to store in the URL.
* @param {string} currentUrl The URL of the current page.
* @return {string} The new URL containing the filter information.
*/
function generateUrlFromFilter(filter, currentUrl) {
const filterNameString = filter.join(',');
const pageUrl = new URL(currentUrl);
const searchParams = new URLSearchParams();
if (filter.length > 0) {
searchParams.append(URL_PARAM_KEYS.FILTER, filterNameString);
}
return `${pageUrl.origin}${pageUrl.pathname}?${searchParams.toString()}`;
}
// TODO(yjlong): Currently we take JSON served by a Python server running on // TODO(yjlong): Currently we take JSON served by a Python server running on
// the side. Replace this with a user upload or pull from some other source. // the side. Replace this with a user upload or pull from some other source.
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
d3.json(`${LOCALHOST}/json_graph.txt`).then(data => { d3.json(`${LOCALHOST}/json_graph.txt`).then(data => {
const graphModel = parseGraphModelFromJson(data.package_graph); new PackageGraphPage({
const pageModel = new PageModel(graphModel); el: '#package-graph-page',
const graphView = new GraphView(); propsData: {
const pageController = new PageController(pageModel, graphView); graphJson: data.package_graph,
const includedNodesInUrl = generateFilterFromUrl(document.URL);
pageController.setOutboundDepth(1);
if (includedNodesInUrl.length !== 0) {
pageController.addIncludedNodes(includedNodesInUrl);
} else {
// TODO(yjlong): This is test data. Remove this when no longer needed.
pageController.addIncludedNodes([
'org.chromium.base',
'org.chromium.chrome.browser.gsa',
'org.chromium.chrome.browser.omaha',
'org.chromium.chrome.browser.media',
'org.chromium.ui.base',
]);
}
new Vue({
el: '#selected-node-details',
data: pageModel.selectedNodeDetailsData,
methods: {
addSelectedToFilter: function() {
pageController.addIncludedNodes([this.selectedNode.id]);
},
removeSelectedFromFilter: function() {
pageController.removeIncludedNode(this.selectedNode.id);
},
},
});
new Vue({
el: '#filter-input-group',
data: {
filterInputText: '',
},
methods: {
submitFilter: function() {
pageController.addIncludedNodes([this.filterInputText]);
},
},
});
new Vue({
el: '#filter-items',
data: pageModel.nodeFilterData,
methods: {
removeFilter: function(e) {
const filterText = e.target.textContent;
pageController.removeIncludedNode(filterText.trim());
},
},
});
new Vue({
el: '#filter-inbound-group',
data: pageModel.inboundDepthData,
methods: {
submitInbound: function() {
pageController.setInboundDepth(this.inboundDepth);
},
},
});
new Vue({
el: '#filter-outbound-group',
data: pageModel.outboundDepthData,
methods: {
submitOutbound: function() {
pageController.setOutboundDepth(this.outboundDepth);
},
},
});
new Vue({
el: '#url-generator',
data: pageModel.nodeFilterData,
methods: {
/**
* Generates an URL for the current page containing the filter in its
* querystring, then copies the URL to the input elem and highlights it.
*/
generateUrl: function() {
const pageUrl = generateUrlFromFilter(this.nodeList, document.URL);
this.$refs.input.value = pageUrl;
this.$refs.input.select();
},
}, },
}); });
}); });
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {PageModel} from './page_model.js';
import {GraphView} from './graph_view.js';
import {Node} from './graph_model.js';
/** Controller class for page-wide events and communication. */
class PageController {
/**
* @param {!PageModel} pageModel The model holding all page-related state.
* @param {!GraphView} graphView The view which renders the visualization.
*/
constructor(pageModel, graphView) {
/** @private {!PageModel} */
this.pageModel_ = pageModel;
/** @private {!GraphView} */
this.graphView_ = graphView;
this.graphView_.registerOnNodeClicked(node => {
this.pageModel_.selectedNodeDetailsData.selectedNode = node;
});
}
/**
* Adds all supplied nodes to the node filter, then calls `updateGraphData`
* once at the very end, even if `nodeNames` is empty.
* @param {!Array<string>} nodeNames The nodes to add.
*/
addIncludedNodes(nodeNames) {
for (const nodeName of nodeNames) {
this.pageModel_.nodeFilterData.addNode(nodeName);
}
this.graphView_.updateGraphData(this.pageModel_.getDataForD3());
}
/**
* @param {string} nodeName The node to remove.
*/
removeIncludedNode(nodeName) {
this.pageModel_.nodeFilterData.removeNode(nodeName);
this.graphView_.updateGraphData(this.pageModel_.getDataForD3());
}
/**
* @param {number} depth The new inbound depth.
*/
setInboundDepth(depth) {
this.pageModel_.inboundDepthData.inboundDepth = depth;
this.graphView_.updateGraphData(this.pageModel_.getDataForD3());
}
/**
* @param {number} depth The new outbound depth.
*/
setOutboundDepth(depth) {
this.pageModel_.outboundDepthData.outboundDepth = depth;
this.graphView_.updateGraphData(this.pageModel_.getDataForD3());
}
}
export {
PageController,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Keys for identifying URL params.
const URL_PARAM_KEYS = {
FILTER: 'filter',
};
/**
* Converts a URL to the node filter contained within its querystring.
* @param {string} url The URL to convert.
* @return {!Array<string>} The array of node names in the URL's filter, or an
* empty array if there was no filter in the URL.
*/
function generateFilterFromUrl(url) {
const pageUrl = new URL(url);
const filterNodes = pageUrl.searchParams.get(URL_PARAM_KEYS.FILTER);
return (filterNodes === null) ? [] : filterNodes.split(',');
}
/**
* Converts a node filter into a URL containing the filter information. The
* filter information will be stored in the querystring of the supplied URL.
* @param {!Array<string>} filter The node name filter to store in the URL.
* @param {string} currentUrl The URL of the current page.
* @return {string} The new URL containing the filter information.
*/
function generateUrlFromFilter(filter, currentUrl) {
const pageUrl = new URL(currentUrl);
const searchParams = new URLSearchParams();
if (filter.length > 0) {
searchParams.append(URL_PARAM_KEYS.FILTER, filter.join(','));
}
return `${pageUrl.origin}${pageUrl.pathname}?${searchParams.toString()}`;
}
export {
generateFilterFromUrl,
generateUrlFromFilter,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
const GraphFilterInput = {
data: function() {
return {
filterInputText: '',
};
},
methods: {
submitFilter() {
this.$emit(CUSTOM_EVENTS.FILTER_SUBMITTED, this.filterInputText);
},
},
template: `
<div class="user-input-group">
<label for="filter-input">Add node to filter (exact name):</label>
<input v-model="filterInputText" type="text" id="filter-input">
<button @click="submitFilter" type="button">Add</button>
</div>`,
};
export {
GraphFilterInput,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
const GraphFilterItems = {
props: ['nodeFilterData'],
data: function() {
return this.nodeFilterData;
},
methods: {
removeFilter: function(e) {
const filterText = e.target.textContent;
this.$emit(CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED, filterText.trim());
},
},
template: `
<ul class="filter-items">
<li @click="removeFilter" v-for="node in nodeList">
{{node}}
</li>
</ul>`,
};
export {
GraphFilterItems,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
const GraphInboundInput = {
props: ['inboundDepthData'],
data: function() {
return this.inboundDepthData;
},
methods: {
submitInbound: function() {
this.$emit(CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED, this.inboundDepth);
},
},
template: `
<div class="user-input-group">
<label for="filter-inbound">Change inbound (blue) depth:</label>
<input v-model.number="inboundDepth" type="number" id="filter-inbound">
<button @click="submitInbound" type="button">Update Inbound</button>
</div>`,
};
export {
GraphInboundInput,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
const GraphOutboundInput = {
props: ['outboundDepthData'],
data: function() {
return this.outboundDepthData;
},
methods: {
submitOutbound: function() {
this.$emit(CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED, this.outboundDepth);
},
},
template: `
<div class="user-input-group">
<label for="filter-outbound">Change outbound (yellow) depth:</label>
<input v-model.number="outboundDepth" type="number" id="filter-outbound">
<button @click="submitOutbound" type="button">Update Outbound</button>
</div>`,
};
export {
GraphOutboundInput,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
const GraphSelectedNodeDetails = {
props: ['selectedNodeDetailsData'],
data: function() {
return this.selectedNodeDetailsData;
},
methods: {
addSelectedToFilter: function() {
this.$emit(CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED, this.selectedNode.id);
},
removeSelectedFromFilter: function() {
this.$emit(
CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED, this.selectedNode.id);
},
},
template: `
<div class="selected-node-details">
<template v-if="selectedNode !== null">
<ul>
<li>Name: {{selectedNode.id}}</li>
<li>Display Name: {{selectedNode.displayName}}</li>
<li v-for="(value, key) in selectedNode.visualizationState">
{{key}}: {{value}}
</li>
</ul>
<button
v-if="selectedNode.visualizationState.selectedByFilter"
@click="removeSelectedFromFilter">
Remove from filter
</button>
<button v-else @click="addSelectedToFilter">Add to filter</button>
</template>
<div v-else>Click a node for more details.</div>
</div>`,
};
export {
GraphSelectedNodeDetails,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
import {GraphView} from '../graph_view.js';
const GraphVisualization = {
props: ['graphDataUpdateTicker', 'pageModel'],
/**
* Initializes the `GraphView` backing this visualization component. It's
* important we initialize on `mounted`, since GraphView's constructor binds
* to a DOM element which must exist at the time of construction.
*/
mounted: function() {
this.graphView = new GraphView();
this.graphView.registerOnNodeClicked(
node => this.$emit(CUSTOM_EVENTS.NODE_CLICKED, node));
},
watch: {
/**
* Watching a "ticker" variable is used for now since we don't always want
* `graphView` to be reactive with respect to `pageModel` (eg. if the user
* is typing but has not submitted yet). This ticker hence becomes the only
* way to force the visualization to update its underlying data.
*/
graphDataUpdateTicker: function() {
this.graphView.updateGraphData(this.pageModel.getDataForD3());
},
},
template: `
<div id="graph-container">
<svg id="graph-svg" width="960" height="600"></svg>
</div>`,
};
export {
GraphVisualization,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CUSTOM_EVENTS} from '../vue_custom_events.js';
import {GraphFilterInput} from './graph_filter_input.js';
import {GraphFilterItems} from './graph_filter_items.js';
import {GraphInboundInput} from './graph_inbound_input.js';
import {GraphOutboundInput} from './graph_outbound_input.js';
import {GraphVisualization} from './graph_visualization.js';
import {GraphSelectedNodeDetails} from './graph_selected_node_details.js';
import {PageUrlGenerator} from './page_url_generator.js';
import {parseGraphModelFromJson} from '../process_graph_json.js';
import {generateFilterFromUrl} from '../url_processor.js';
import {PageModel} from '../page_model.js';
import {Node} from '../graph_model.js';
const PackageGraphPage = Vue.component('package-graph-page', {
components: {
'graph-filter-input': GraphFilterInput,
'graph-filter-items': GraphFilterItems,
'graph-inbound-input': GraphInboundInput,
'graph-outbound-input': GraphOutboundInput,
'graph-visualization': GraphVisualization,
'graph-selected-node-details': GraphSelectedNodeDetails,
'page-url-generator': PageUrlGenerator,
},
props: ['graphJson'],
/**
* Various references to objects used across the entire page.
* @typedef {Object} PageData
* @property {PageModel} pageModel The data store for the page.
* @property {number} graphDataUpdateTicker Incremented every time we want to
* trigger a visualization update. See graph_visualization.js for further
* explanation on this variable.
*/
/**
* @return {PageData} The objects used throughout the page.
*/
data: function() {
const graphModel = parseGraphModelFromJson(this.graphJson);
const pageModel = new PageModel(graphModel);
return {
pageModel,
graphDataUpdateTicker: 0,
};
},
/**
* Parses out data from the current URL to initialize the visualization with.
*/
mounted: function() {
const includedNodesInUrl = generateFilterFromUrl(document.URL);
if (includedNodesInUrl.length !== 0) {
this.addNodesToFilter(includedNodesInUrl);
} else {
// 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',
]);
}
this.setOutboundDepth(1);
this.graphDataUpdateTicker++;
},
methods: {
/**
* @param {string} nodeName The node to add.
*/
addNodeToFilter: function(nodeName) {
this.pageModel.nodeFilterData.addNode(nodeName);
this.graphDataUpdateTicker++;
},
/**
* Adds all supplied nodes to the node filter, then increments
* `graphDataUpdateTicker` once at the end, even if `nodeNames` is empty.
* @param {!Array<string>} nodeNames The nodes to add.
*/
addNodesToFilter: function(nodeNames) {
for (const nodeName of nodeNames) {
this.pageModel.nodeFilterData.addNode(nodeName);
}
this.graphDataUpdateTicker++;
},
/**
* @param {string} nodeName The node to remove.
*/
removeNodeFromFilter: function(nodeName) {
this.pageModel.nodeFilterData.removeNode(nodeName);
this.graphDataUpdateTicker++;
},
/**
* @param {number} depth The new inbound depth.
*/
setInboundDepth: function(depth) {
this.pageModel.inboundDepthData.inboundDepth = depth;
this.graphDataUpdateTicker++;
},
/**
* @param {number} depth The new outbound depth.
*/
setOutboundDepth: function(depth) {
this.pageModel.outboundDepthData.outboundDepth = depth;
this.graphDataUpdateTicker++;
},
/**
* @param {?Node} node The selected node. May be `null`, which will reset
* the selection to the state with no node.
*/
graphNodeClicked: function(node) {
this.pageModel.selectedNodeDetailsData.selectedNode = node;
},
},
template: `
<div id="page-container">
<div id="page-controls">
<graph-filter-input
@${CUSTOM_EVENTS.FILTER_SUBMITTED}="this.addNodeToFilter"
></graph-filter-input>
<graph-filter-items
:node-filter-data="this.pageModel.nodeFilterData"
@${CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED}="this.removeNodeFromFilter"
></graph-filter-items>
<graph-inbound-input
:inbound-depth-data="this.pageModel.inboundDepthData"
@${CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED}="this.setInboundDepth"
></graph-inbound-input>
<graph-outbound-input
:outbound-depth-data="this.pageModel.outboundDepthData"
@${CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED}="this.setOutboundDepth"
></graph-outbound-input>
</div>
<div id="graph-and-node-details-container">
<graph-visualization
:graph-data-update-ticker="this.graphDataUpdateTicker"
:page-model="this.pageModel"
@${CUSTOM_EVENTS.NODE_CLICKED}="graphNodeClicked"
></graph-visualization>
<graph-selected-node-details
:selected-node-details-data="this.pageModel.selectedNodeDetailsData"
@${CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED}="addNodeToFilter"
@${CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED}="removeNodeFromFilter"
></graph-selected-node-details>
</div>
<page-url-generator
:node-filter-data="this.pageModel.nodeFilterData"
></page-url-generator>
</div>`,
});
export {
PackageGraphPage,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {generateUrlFromFilter} from '../url_processor.js';
const PageUrlGenerator = {
props: ['nodeFilterData'],
data: function() {
return this.nodeFilterData;
},
methods: {
/**
* Generates an URL for the current page containing the filter in its
* querystring, then copies the URL to the input elem and highlights it.
*/
generateUrl: function() {
const pageUrl = generateUrlFromFilter(this.nodeList, document.URL);
this.$refs.input.value = pageUrl;
this.$refs.input.select();
},
},
template: `
<div class="url-generator">
<button @click="generateUrl">Generate Current URL</button>
<input type="text" readonly ref="input">
</div>`,
};
export {
PageUrlGenerator,
};
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// 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',
FILTER_SUBMITTED: 'filter-submitted',
INBOUND_DEPTH_UPDATED: 'inbound-depth-updated',
NODE_CLICKED: 'node-clicked',
OUTBOUND_DEPTH_UPDATED: 'outbound-depth-updated',
REMOVE_FROM_FILTER_CLICKED: 'remove-from-filter-clicked',
};
export {
CUSTOM_EVENTS,
};
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