blob: d827e1fe023e2053865465190c157212395e276a [file] [log] [blame]
Rayan Kanso68904202019-02-21 14:16:251// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5Resources.BackgroundServiceView = class extends UI.VBox {
6 /**
Rayan Kansoc0bfdd82019-04-24 12:32:227 * @param {string} serviceName The name of the background service.
8 * @return {string} The UI String to display.
9 */
10 static getUIString(serviceName) {
11 switch (serviceName) {
12 case Protocol.BackgroundService.ServiceName.BackgroundFetch:
13 return ls`Background Fetch`;
14 case Protocol.BackgroundService.ServiceName.BackgroundSync:
15 return ls`Background Sync`;
16 default:
17 return '';
18 }
19 }
20
21 /**
Rayan Kanso8fe8ee22019-03-04 14:58:4622 * @param {!Protocol.BackgroundService.ServiceName} serviceName
23 * @param {!Resources.BackgroundServiceModel} model
Rayan Kanso68904202019-02-21 14:16:2524 */
Rayan Kanso8fe8ee22019-03-04 14:58:4625 constructor(serviceName, model) {
Rayan Kanso68904202019-02-21 14:16:2526 super(true);
27 this.registerRequiredCSS('resources/backgroundServiceView.css');
Rayan Kanso6156d222019-04-29 23:40:5528 this.registerRequiredCSS('ui/emptyWidget.css');
Rayan Kanso68904202019-02-21 14:16:2529
Rayan Kanso8fe8ee22019-03-04 14:58:4630 /** @const {!Protocol.BackgroundService.ServiceName} */
Rayan Kanso68904202019-02-21 14:16:2531 this._serviceName = serviceName;
32
Rayan Kanso8fe8ee22019-03-04 14:58:4633 /** @const {!Resources.BackgroundServiceModel} */
34 this._model = model;
35 this._model.addEventListener(
36 Resources.BackgroundServiceModel.Events.RecordingStateChanged, this._onRecordingStateChanged, this);
Rayan Kansof40e3152019-03-11 13:49:4337 this._model.addEventListener(
38 Resources.BackgroundServiceModel.Events.BackgroundServiceEventReceived, this._onEventReceived, this);
Rayan Kanso8fe8ee22019-03-04 14:58:4639 this._model.enable(this._serviceName);
40
Rayan Kansoaca06e72019-03-27 11:57:0641 /** @const {?SDK.ServiceWorkerManager} */
42 this._serviceWorkerManager = this._model.target().model(SDK.ServiceWorkerManager);
43
44 /** @const {?SDK.SecurityOriginManager} */
45 this._securityOriginManager = this._model.target().model(SDK.SecurityOriginManager);
46 this._securityOriginManager.addEventListener(
47 SDK.SecurityOriginManager.Events.MainSecurityOriginChanged, () => this._onOriginChanged());
48
Rayan Kanso8fe8ee22019-03-04 14:58:4649 /** @type {?UI.ToolbarToggle} */
50 this._recordButton = null;
51
Rayan Kansoaca06e72019-03-27 11:57:0652 /** @type {?UI.ToolbarCheckbox} */
53 this._originCheckbox = null;
54
Rayan Kansob852ba82019-04-08 13:48:0755 /** @type {?UI.ToolbarButton} */
56 this._saveButton = null;
57
Rayan Kanso68904202019-02-21 14:16:2558 /** @const {!UI.Toolbar} */
59 this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
60 this._setupToolbar();
Rayan Kanso3252d5e2019-03-27 11:37:2461
Rayan Kansob451b4f2019-04-04 23:12:1162 /**
63 * This will contain the DataGrid for displaying events, and a panel at the bottom for showing
64 * extra metadata related to the selected event.
65 * @const {!UI.SplitWidget}
66 */
67 this._splitWidget = new UI.SplitWidget(/* isVertical= */ false, /* secondIsSidebar= */ true);
68 this._splitWidget.show(this.contentElement);
69
Rayan Kanso3252d5e2019-03-27 11:37:2470 /** @const {!DataGrid.DataGrid} */
71 this._dataGrid = this._createDataGrid();
Rayan Kansob451b4f2019-04-04 23:12:1172
73 /** @const {!UI.VBox} */
74 this._previewPanel = new UI.VBox();
75
Rayan Kansoc0bfdd82019-04-24 12:32:2276 /** @type {?Resources.BackgroundServiceView.EventDataNode} */
77 this._selectedEventNode = null;
78
Rayan Kansob451b4f2019-04-04 23:12:1179 /** @type {?UI.Widget} */
80 this._preview = null;
81
82 this._splitWidget.setMainWidget(this._dataGrid.asWidget());
83 this._splitWidget.setSidebarWidget(this._previewPanel);
84
85 this._showPreview(null);
Rayan Kanso68904202019-02-21 14:16:2586 }
87
88 /**
89 * Creates the toolbar UI element.
90 */
Rayan Kanso8fe8ee22019-03-04 14:58:4691 async _setupToolbar() {
Rayan Kanso6156d222019-04-29 23:40:5592 const action = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
93 this._recordButton = UI.Toolbar.createActionButton(action);
Rayan Kanso8fe8ee22019-03-04 14:58:4694 this._toolbar.appendToolbarItem(this._recordButton);
Rayan Kanso68904202019-02-21 14:16:2595
Rayan Kansob852ba82019-04-08 13:48:0796 const clearButton = new UI.ToolbarButton(ls`Clear`, 'largeicon-clear');
Rayan Kansob451b4f2019-04-04 23:12:1197 clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._clearEvents());
Rayan Kanso68904202019-02-21 14:16:2598 this._toolbar.appendToolbarItem(clearButton);
99
100 this._toolbar.appendSeparator();
101
Rayan Kansob852ba82019-04-08 13:48:07102 this._saveButton = new UI.ToolbarButton(ls`Save events`, 'largeicon-download');
103 this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._saveToFile());
104 this._saveButton.setEnabled(false);
105 this._toolbar.appendToolbarItem(this._saveButton);
Rayan Kansoc0bfdd82019-04-24 12:32:22106
107 this._toolbar.appendSeparator();
108
109 this._originCheckbox =
110 new UI.ToolbarCheckbox(ls`Show events from other domains`, undefined, () => this._refreshView());
111 this._toolbar.appendToolbarItem(this._originCheckbox);
Rayan Kanso68904202019-02-21 14:16:25112 }
Rayan Kanso8fe8ee22019-03-04 14:58:46113
114 /**
Rayan Kansob451b4f2019-04-04 23:12:11115 * Displays all available events in the grid.
116 */
117 _refreshView() {
118 this._clearView();
119 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
120 for (const event of events)
121 this._addEvent(event);
122 }
123
124 /**
125 * Clears the grid and panel.
126 */
127 _clearView() {
Rayan Kansoc0bfdd82019-04-24 12:32:22128 this._selectedEventNode = null;
Rayan Kansob451b4f2019-04-04 23:12:11129 this._dataGrid.rootNode().removeChildren();
Rayan Kansob852ba82019-04-08 13:48:07130 this._saveButton.setEnabled(false);
Rayan Kansoc0bfdd82019-04-24 12:32:22131 this._showPreview(null);
Rayan Kansob451b4f2019-04-04 23:12:11132 }
133
134 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46135 * Called when the `Toggle Record` button is clicked.
136 */
137 _toggleRecording() {
138 this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
139 }
140
141 /**
Rayan Kanso3252d5e2019-03-27 11:37:24142 * Called when the `Clear` button is clicked.
143 */
Rayan Kansob451b4f2019-04-04 23:12:11144 _clearEvents() {
Rayan Kanso3252d5e2019-03-27 11:37:24145 this._model.clearEvents(this._serviceName);
146 this._clearView();
147 }
148
149 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46150 * @param {!Common.Event} event
151 */
152 _onRecordingStateChanged(event) {
153 const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
154 if (state.serviceName !== this._serviceName)
155 return;
Rayan Kansoc0bfdd82019-04-24 12:32:22156
157 if (state.isRecording === this._recordButton.toggled())
158 return;
159
Rayan Kanso8fe8ee22019-03-04 14:58:46160 this._recordButton.setToggled(state.isRecording);
Rayan Kansoc0bfdd82019-04-24 12:32:22161 this._showPreview(this._selectedEventNode);
Rayan Kanso8fe8ee22019-03-04 14:58:46162 }
Rayan Kansof40e3152019-03-11 13:49:43163
164 /**
165 * @param {!Common.Event} event
166 */
167 _onEventReceived(event) {
168 const serviceEvent = /** @type {!Protocol.BackgroundService.BackgroundServiceEvent} */ (event.data);
Rayan Kanso3252d5e2019-03-27 11:37:24169 if (!this._acceptEvent(serviceEvent))
Rayan Kansof40e3152019-03-11 13:49:43170 return;
Rayan Kanso3252d5e2019-03-27 11:37:24171 this._addEvent(serviceEvent);
172 }
173
Rayan Kansoaca06e72019-03-27 11:57:06174 _onOriginChanged() {
175 // No need to refresh the view if we are already showing all events.
176 if (this._originCheckbox.checked())
177 return;
178 this._refreshView();
179 }
180
Rayan Kanso3252d5e2019-03-27 11:37:24181 /**
182 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
183 */
184 _addEvent(serviceEvent) {
Rayan Kansoaca06e72019-03-27 11:57:06185 const data = this._createEventData(serviceEvent);
186 const dataNode = new Resources.BackgroundServiceView.EventDataNode(data, serviceEvent.eventMetadata);
Rayan Kanso3252d5e2019-03-27 11:37:24187 this._dataGrid.rootNode().appendChild(dataNode);
Rayan Kansob852ba82019-04-08 13:48:07188
Rayan Kansoc0bfdd82019-04-24 12:32:22189 if (this._dataGrid.rootNode().children.length === 1) {
190 this._saveButton.setEnabled(true);
191 this._showPreview(this._selectedEventNode);
192 }
Rayan Kanso3252d5e2019-03-27 11:37:24193 }
194
195 /**
196 * @return {!DataGrid.DataGrid}
197 */
198 _createDataGrid() {
199 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
Rayan Kansob852ba82019-04-08 13:48:07200 {id: 'id', title: ls`#`, weight: 1},
201 {id: 'timestamp', title: ls`Timestamp`, weight: 8},
Rayan Kansoc0bfdd82019-04-24 12:32:22202 {id: 'eventName', title: ls`Event`, weight: 10},
Rayan Kansob852ba82019-04-08 13:48:07203 {id: 'origin', title: ls`Origin`, weight: 10},
204 {id: 'swSource', title: ls`SW Source`, weight: 4},
Rayan Kansob852ba82019-04-08 13:48:07205 {id: 'instanceId', title: ls`Instance ID`, weight: 10},
Rayan Kanso3252d5e2019-03-27 11:37:24206 ]);
207 const dataGrid = new DataGrid.DataGrid(columns);
208 dataGrid.setStriped(true);
Rayan Kansob451b4f2019-04-04 23:12:11209
210 dataGrid.addEventListener(
211 DataGrid.DataGrid.Events.SelectedNode,
212 event => this._showPreview(/** @type {!Resources.BackgroundServiceView.EventDataNode} */ (event.data)));
213
Rayan Kanso3252d5e2019-03-27 11:37:24214 return dataGrid;
215 }
216
217 /**
Rayan Kansoaca06e72019-03-27 11:57:06218 * Creates the data object to pass to the DataGrid Node.
219 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
220 * @return {!Resources.BackgroundServiceView.EventData}
221 */
222 _createEventData(serviceEvent) {
223 let swSource = '';
224
225 // Try to get the script name of the Service Worker registration to be more user-friendly.
226 const registrations = this._serviceWorkerManager.registrations().get(serviceEvent.serviceWorkerRegistrationId);
227 if (registrations && registrations.versions.size) {
228 // Any version will do since we care about the script URL.
229 const version = registrations.versions.values().next().value;
230 // Get the relative path.
231 swSource = version.scriptURL.substr(version.securityOrigin.length);
232 }
233
234 return {
235 id: this._dataGrid.rootNode().children.length,
236 timestamp: UI.formatTimestamp(serviceEvent.timestamp * 1000, /* full= */ true),
237 origin: serviceEvent.origin,
238 swSource,
239 eventName: serviceEvent.eventName,
240 instanceId: serviceEvent.instanceId,
241 };
242 }
243
244 /**
Rayan Kanso3252d5e2019-03-27 11:37:24245 * Filtration function to know whether event should be shown or not.
246 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} event
247 * @return {boolean}
248 */
249 _acceptEvent(event) {
Rayan Kansoaca06e72019-03-27 11:57:06250 if (event.service !== this._serviceName)
251 return false;
252
253 if (this._originCheckbox.checked())
254 return true;
255
256 // Trim the trailing '/'.
257 const origin = event.origin.substr(0, event.origin.length - 1);
258
259 return this._securityOriginManager.securityOrigins().includes(origin);
Rayan Kanso3252d5e2019-03-27 11:37:24260 }
Rayan Kansob451b4f2019-04-04 23:12:11261
262 /**
263 * @param {?Resources.BackgroundServiceView.EventDataNode} dataNode
264 */
265 _showPreview(dataNode) {
Rayan Kansoc0bfdd82019-04-24 12:32:22266 if (this._selectedEventNode && this._selectedEventNode === dataNode)
267 return;
268
269 this._selectedEventNode = dataNode;
270
Rayan Kansob451b4f2019-04-04 23:12:11271 if (this._preview)
272 this._preview.detach();
273
Rayan Kansoc0bfdd82019-04-24 12:32:22274 if (this._selectedEventNode) {
275 this._preview = this._selectedEventNode.createPreview();
276 } else if (this._dataGrid.rootNode().children.length) {
277 // Inform users that grid entries are clickable.
278 this._preview = new UI.EmptyWidget(ls`Select an entry to view metadata`);
279 } else if (this._recordButton.toggled()) {
280 // Inform users that we are recording/waiting for events.
281 this._preview = new UI.EmptyWidget(
282 ls`Recording ${Resources.BackgroundServiceView.getUIString(this._serviceName)} activity...`);
283 } else {
284 this._preview = new UI.VBox();
Rayan Kanso6156d222019-04-29 23:40:55285 this._preview.contentElement.classList.add('empty-view-scroller');
286 const centered = this._preview.contentElement.createChild('div', 'empty-view');
Rayan Kansoc0bfdd82019-04-24 12:32:22287
Rayan Kanso6156d222019-04-29 23:40:55288 const action = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
289 const landingRecordButton = UI.Toolbar.createActionButton(action);
Rayan Kansoc0bfdd82019-04-24 12:32:22290
Rayan Kanso6156d222019-04-29 23:40:55291 const recordKey = createElementWithClass('b', 'background-service-shortcut');
292 recordKey.textContent =
293 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
294
295 centered.createChild('h2').appendChild(UI.formatLocalized(
296 'Click the record button %s or hit %s to start recording.',
297 [UI.createInlineButton(landingRecordButton), recordKey]));
Rayan Kansoc0bfdd82019-04-24 12:32:22298 }
Rayan Kansob451b4f2019-04-04 23:12:11299
300 this._preview.show(this._previewPanel.contentElement);
301 }
Rayan Kansob852ba82019-04-08 13:48:07302
303 /**
304 * Saves all currently displayed events in a file (JSON format).
305 */
306 async _saveToFile() {
307 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
308 const stream = new Bindings.FileOutputStream();
309
310 const accepted = await stream.open(fileName);
311 if (!accepted)
312 return;
313
314 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
315 await stream.write(JSON.stringify(events, undefined, 2));
316 stream.close();
317 }
Rayan Kanso3252d5e2019-03-27 11:37:24318};
319
Rayan Kansoaca06e72019-03-27 11:57:06320/**
321 * @typedef {{
322 * id: number,
323 * timestamp: string,
324 * origin: string,
325 * swSource: string,
326 * eventName: string,
327 * instanceId: string,
328 * }}
329 */
330Resources.BackgroundServiceView.EventData;
331
Rayan Kanso3252d5e2019-03-27 11:37:24332Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
333 /**
Rayan Kansoaca06e72019-03-27 11:57:06334 * @param {!Object<string, string>} data
335 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24336 */
Rayan Kansoaca06e72019-03-27 11:57:06337 constructor(data, eventMetadata) {
338 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24339
340 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoaca06e72019-03-27 11:57:06341 this._eventMetadata = eventMetadata;
Rayan Kanso3252d5e2019-03-27 11:37:24342 }
343
344 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22345 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24346 */
Rayan Kansob451b4f2019-04-04 23:12:11347 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22348 const preview = new UI.VBox();
349 preview.element.classList.add('background-service-metadata');
350
351 for (const entry of this._eventMetadata) {
352 const div = createElementWithClass('div', 'background-service-metadata-entry');
353 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
354 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
355 preview.element.appendChild(div);
356 }
357
358 if (!preview.element.children.length) {
359 const div = createElementWithClass('div', 'background-service-metadata-entry');
360 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
361 preview.element.appendChild(div);
362 }
363
364 return preview;
Rayan Kansof40e3152019-03-11 13:49:43365 }
Rayan Kanso68904202019-02-21 14:16:25366};
Rayan Kanso6156d222019-04-29 23:40:55367
368/**
369 * @implements {UI.ActionDelegate}
370 * @unrestricted
371 */
372Resources.BackgroundServiceView.ActionDelegate = class {
373 /**
374 * @override
375 * @param {!UI.Context} context
376 * @param {string} actionId
377 * @return {boolean}
378 */
379 handleAction(context, actionId) {
380 const view = context.flavor(Resources.BackgroundServiceView);
381 switch (actionId) {
382 case 'background-service.toggle-recording':
383 view._toggleRecording();
384 return true;
385 }
386 return false;
387 }
388};