blob: fca3e672e2bef7565550efafd488aba7ff5d7d11 [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`;
Fergus Dall0be4f4c2019-05-21 00:01:2916 case Protocol.BackgroundService.ServiceName.PushMessaging:
17 return ls`Push Messaging`;
18 case Protocol.BackgroundService.ServiceName.Notifications:
19 return ls`Notifications`;
Rayan Kanso8d7918a2019-07-03 21:03:3820 case Protocol.BackgroundService.ServiceName.PaymentHandler:
21 return ls`Payment Handler`;
Rayan Kansoc0bfdd82019-04-24 12:32:2222 default:
23 return '';
24 }
25 }
26
27 /**
Rayan Kanso8fe8ee22019-03-04 14:58:4628 * @param {!Protocol.BackgroundService.ServiceName} serviceName
29 * @param {!Resources.BackgroundServiceModel} model
Rayan Kanso68904202019-02-21 14:16:2530 */
Rayan Kanso8fe8ee22019-03-04 14:58:4631 constructor(serviceName, model) {
Rayan Kanso68904202019-02-21 14:16:2532 super(true);
33 this.registerRequiredCSS('resources/backgroundServiceView.css');
Rayan Kanso6156d222019-04-29 23:40:5534 this.registerRequiredCSS('ui/emptyWidget.css');
Rayan Kanso68904202019-02-21 14:16:2535
Rayan Kanso8fe8ee22019-03-04 14:58:4636 /** @const {!Protocol.BackgroundService.ServiceName} */
Rayan Kanso68904202019-02-21 14:16:2537 this._serviceName = serviceName;
38
Rayan Kanso8fe8ee22019-03-04 14:58:4639 /** @const {!Resources.BackgroundServiceModel} */
40 this._model = model;
41 this._model.addEventListener(
42 Resources.BackgroundServiceModel.Events.RecordingStateChanged, this._onRecordingStateChanged, this);
Rayan Kansof40e3152019-03-11 13:49:4343 this._model.addEventListener(
44 Resources.BackgroundServiceModel.Events.BackgroundServiceEventReceived, this._onEventReceived, this);
Rayan Kanso8fe8ee22019-03-04 14:58:4645 this._model.enable(this._serviceName);
46
Rayan Kansoaca06e72019-03-27 11:57:0647 /** @const {?SDK.ServiceWorkerManager} */
48 this._serviceWorkerManager = this._model.target().model(SDK.ServiceWorkerManager);
49
50 /** @const {?SDK.SecurityOriginManager} */
51 this._securityOriginManager = this._model.target().model(SDK.SecurityOriginManager);
52 this._securityOriginManager.addEventListener(
53 SDK.SecurityOriginManager.Events.MainSecurityOriginChanged, () => this._onOriginChanged());
54
Rayan Kansocc02ea32019-05-02 21:26:5255
56 /** @const {!UI.Action} */
57 this._recordAction = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
Joel Einbinder346980e2019-07-16 15:35:4858 /** @type {?UI.ToolbarButton} */
Rayan Kanso8fe8ee22019-03-04 14:58:4659 this._recordButton = null;
60
Rayan Kansoaca06e72019-03-27 11:57:0661 /** @type {?UI.ToolbarCheckbox} */
62 this._originCheckbox = null;
63
Rayan Kansob852ba82019-04-08 13:48:0764 /** @type {?UI.ToolbarButton} */
65 this._saveButton = null;
66
Rayan Kanso68904202019-02-21 14:16:2567 /** @const {!UI.Toolbar} */
68 this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
69 this._setupToolbar();
Rayan Kanso3252d5e2019-03-27 11:37:2470
Rayan Kansob451b4f2019-04-04 23:12:1171 /**
72 * This will contain the DataGrid for displaying events, and a panel at the bottom for showing
73 * extra metadata related to the selected event.
74 * @const {!UI.SplitWidget}
75 */
76 this._splitWidget = new UI.SplitWidget(/* isVertical= */ false, /* secondIsSidebar= */ true);
77 this._splitWidget.show(this.contentElement);
78
Rayan Kanso3252d5e2019-03-27 11:37:2479 /** @const {!DataGrid.DataGrid} */
80 this._dataGrid = this._createDataGrid();
Rayan Kansob451b4f2019-04-04 23:12:1181
82 /** @const {!UI.VBox} */
83 this._previewPanel = new UI.VBox();
84
Rayan Kansoc0bfdd82019-04-24 12:32:2285 /** @type {?Resources.BackgroundServiceView.EventDataNode} */
86 this._selectedEventNode = null;
87
Rayan Kansob451b4f2019-04-04 23:12:1188 /** @type {?UI.Widget} */
89 this._preview = null;
90
91 this._splitWidget.setMainWidget(this._dataGrid.asWidget());
92 this._splitWidget.setSidebarWidget(this._previewPanel);
93
94 this._showPreview(null);
Rayan Kanso68904202019-02-21 14:16:2595 }
96
97 /**
98 * Creates the toolbar UI element.
99 */
Rayan Kanso8fe8ee22019-03-04 14:58:46100 async _setupToolbar() {
Rayan Kansocc02ea32019-05-02 21:26:52101 this._recordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kanso8fe8ee22019-03-04 14:58:46102 this._toolbar.appendToolbarItem(this._recordButton);
Rayan Kanso68904202019-02-21 14:16:25103
Rayan Kansob852ba82019-04-08 13:48:07104 const clearButton = new UI.ToolbarButton(ls`Clear`, 'largeicon-clear');
Rayan Kansob451b4f2019-04-04 23:12:11105 clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._clearEvents());
Rayan Kanso68904202019-02-21 14:16:25106 this._toolbar.appendToolbarItem(clearButton);
107
108 this._toolbar.appendSeparator();
109
Rayan Kansob852ba82019-04-08 13:48:07110 this._saveButton = new UI.ToolbarButton(ls`Save events`, 'largeicon-download');
111 this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._saveToFile());
112 this._saveButton.setEnabled(false);
113 this._toolbar.appendToolbarItem(this._saveButton);
Rayan Kansoc0bfdd82019-04-24 12:32:22114
115 this._toolbar.appendSeparator();
116
117 this._originCheckbox =
118 new UI.ToolbarCheckbox(ls`Show events from other domains`, undefined, () => this._refreshView());
119 this._toolbar.appendToolbarItem(this._originCheckbox);
Rayan Kanso68904202019-02-21 14:16:25120 }
Rayan Kanso8fe8ee22019-03-04 14:58:46121
122 /**
Rayan Kansob451b4f2019-04-04 23:12:11123 * Displays all available events in the grid.
124 */
125 _refreshView() {
126 this._clearView();
127 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
128 for (const event of events)
129 this._addEvent(event);
130 }
131
132 /**
133 * Clears the grid and panel.
134 */
135 _clearView() {
Rayan Kansoc0bfdd82019-04-24 12:32:22136 this._selectedEventNode = null;
Rayan Kansob451b4f2019-04-04 23:12:11137 this._dataGrid.rootNode().removeChildren();
Rayan Kansob852ba82019-04-08 13:48:07138 this._saveButton.setEnabled(false);
Rayan Kansoc0bfdd82019-04-24 12:32:22139 this._showPreview(null);
Rayan Kansob451b4f2019-04-04 23:12:11140 }
141
142 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46143 * Called when the `Toggle Record` button is clicked.
144 */
145 _toggleRecording() {
146 this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
147 }
148
149 /**
Rayan Kanso3252d5e2019-03-27 11:37:24150 * Called when the `Clear` button is clicked.
151 */
Rayan Kansob451b4f2019-04-04 23:12:11152 _clearEvents() {
Rayan Kanso3252d5e2019-03-27 11:37:24153 this._model.clearEvents(this._serviceName);
154 this._clearView();
155 }
156
157 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46158 * @param {!Common.Event} event
159 */
160 _onRecordingStateChanged(event) {
161 const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
162 if (state.serviceName !== this._serviceName)
163 return;
Rayan Kansoc0bfdd82019-04-24 12:32:22164
165 if (state.isRecording === this._recordButton.toggled())
166 return;
167
Rayan Kanso0b30aba2019-05-17 13:39:59168 this._recordButton.setToggled(state.isRecording);
Rayan Kansoc0bfdd82019-04-24 12:32:22169 this._showPreview(this._selectedEventNode);
Rayan Kanso8fe8ee22019-03-04 14:58:46170 }
Rayan Kansof40e3152019-03-11 13:49:43171
172 /**
173 * @param {!Common.Event} event
174 */
175 _onEventReceived(event) {
176 const serviceEvent = /** @type {!Protocol.BackgroundService.BackgroundServiceEvent} */ (event.data);
Rayan Kanso3252d5e2019-03-27 11:37:24177 if (!this._acceptEvent(serviceEvent))
Rayan Kansof40e3152019-03-11 13:49:43178 return;
Rayan Kanso3252d5e2019-03-27 11:37:24179 this._addEvent(serviceEvent);
180 }
181
Rayan Kansoaca06e72019-03-27 11:57:06182 _onOriginChanged() {
183 // No need to refresh the view if we are already showing all events.
184 if (this._originCheckbox.checked())
185 return;
186 this._refreshView();
187 }
188
Rayan Kanso3252d5e2019-03-27 11:37:24189 /**
190 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
191 */
192 _addEvent(serviceEvent) {
Rayan Kansoaca06e72019-03-27 11:57:06193 const data = this._createEventData(serviceEvent);
194 const dataNode = new Resources.BackgroundServiceView.EventDataNode(data, serviceEvent.eventMetadata);
Rayan Kanso3252d5e2019-03-27 11:37:24195 this._dataGrid.rootNode().appendChild(dataNode);
Rayan Kansob852ba82019-04-08 13:48:07196
Rayan Kansoc0bfdd82019-04-24 12:32:22197 if (this._dataGrid.rootNode().children.length === 1) {
198 this._saveButton.setEnabled(true);
199 this._showPreview(this._selectedEventNode);
200 }
Rayan Kanso3252d5e2019-03-27 11:37:24201 }
202
203 /**
204 * @return {!DataGrid.DataGrid}
205 */
206 _createDataGrid() {
207 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
Rayan Kansob852ba82019-04-08 13:48:07208 {id: 'id', title: ls`#`, weight: 1},
209 {id: 'timestamp', title: ls`Timestamp`, weight: 8},
Rayan Kansoc0bfdd82019-04-24 12:32:22210 {id: 'eventName', title: ls`Event`, weight: 10},
Rayan Kansob852ba82019-04-08 13:48:07211 {id: 'origin', title: ls`Origin`, weight: 10},
Rayan Kansocc02ea32019-05-02 21:26:52212 {id: 'swScope', title: ls`SW Scope`, weight: 2},
Rayan Kansob852ba82019-04-08 13:48:07213 {id: 'instanceId', title: ls`Instance ID`, weight: 10},
Rayan Kanso3252d5e2019-03-27 11:37:24214 ]);
215 const dataGrid = new DataGrid.DataGrid(columns);
216 dataGrid.setStriped(true);
Rayan Kansob451b4f2019-04-04 23:12:11217
218 dataGrid.addEventListener(
219 DataGrid.DataGrid.Events.SelectedNode,
220 event => this._showPreview(/** @type {!Resources.BackgroundServiceView.EventDataNode} */ (event.data)));
221
Rayan Kanso3252d5e2019-03-27 11:37:24222 return dataGrid;
223 }
224
225 /**
Rayan Kansoaca06e72019-03-27 11:57:06226 * Creates the data object to pass to the DataGrid Node.
227 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
228 * @return {!Resources.BackgroundServiceView.EventData}
229 */
230 _createEventData(serviceEvent) {
Rayan Kansocc02ea32019-05-02 21:26:52231 let swScope = '';
Rayan Kansoaca06e72019-03-27 11:57:06232
Rayan Kansocc02ea32019-05-02 21:26:52233 // Try to get the scope of the Service Worker registration to be more user-friendly.
234 const registration = this._serviceWorkerManager.registrations().get(serviceEvent.serviceWorkerRegistrationId);
235 if (registration)
236 swScope = registration.scopeURL.substr(registration.securityOrigin.length);
Rayan Kansoaca06e72019-03-27 11:57:06237
238 return {
Rayan Kansoc79a8bb2019-05-29 21:08:38239 id: this._dataGrid.rootNode().children.length + 1,
Rayan Kansoaca06e72019-03-27 11:57:06240 timestamp: UI.formatTimestamp(serviceEvent.timestamp * 1000, /* full= */ true),
241 origin: serviceEvent.origin,
Rayan Kansocc02ea32019-05-02 21:26:52242 swScope,
Rayan Kansoaca06e72019-03-27 11:57:06243 eventName: serviceEvent.eventName,
244 instanceId: serviceEvent.instanceId,
245 };
246 }
247
248 /**
Rayan Kanso3252d5e2019-03-27 11:37:24249 * Filtration function to know whether event should be shown or not.
250 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} event
251 * @return {boolean}
252 */
253 _acceptEvent(event) {
Rayan Kansoaca06e72019-03-27 11:57:06254 if (event.service !== this._serviceName)
255 return false;
256
257 if (this._originCheckbox.checked())
258 return true;
259
260 // Trim the trailing '/'.
261 const origin = event.origin.substr(0, event.origin.length - 1);
262
263 return this._securityOriginManager.securityOrigins().includes(origin);
Rayan Kanso3252d5e2019-03-27 11:37:24264 }
Rayan Kansob451b4f2019-04-04 23:12:11265
266 /**
267 * @param {?Resources.BackgroundServiceView.EventDataNode} dataNode
268 */
269 _showPreview(dataNode) {
Rayan Kansoc0bfdd82019-04-24 12:32:22270 if (this._selectedEventNode && this._selectedEventNode === dataNode)
271 return;
272
273 this._selectedEventNode = dataNode;
274
Rayan Kansob451b4f2019-04-04 23:12:11275 if (this._preview)
276 this._preview.detach();
277
Rayan Kansoc0bfdd82019-04-24 12:32:22278 if (this._selectedEventNode) {
279 this._preview = this._selectedEventNode.createPreview();
Rayan Kanso4476ca52019-07-22 11:51:24280 this._preview.show(this._previewPanel.contentElement);
281 return;
282 }
283
284 this._preview = new UI.VBox();
285 this._preview.contentElement.classList.add('background-service-preview', 'fill');
286 const centered = this._preview.contentElement.createChild('div');
287
288 if (this._dataGrid.rootNode().children.length) {
Rayan Kansoc0bfdd82019-04-24 12:32:22289 // Inform users that grid entries are clickable.
Rayan Kanso4476ca52019-07-22 11:51:24290 centered.createChild('p').textContent = ls`Select an entry to view metadata`;
Rayan Kansoc0bfdd82019-04-24 12:32:22291 } else if (this._recordButton.toggled()) {
292 // Inform users that we are recording/waiting for events.
Rayan Kanso4476ca52019-07-22 11:51:24293 const featureName = Resources.BackgroundServiceView.getUIString(this._serviceName);
294 centered.createChild('p').textContent = ls`Recording ${featureName} activity...`;
295 centered.createChild('p').textContent =
296 ls`DevTools will record all ${featureName} activity for up to 3 days, even when closed.`;
Rayan Kansoc0bfdd82019-04-24 12:32:22297 } else {
Rayan Kansocc02ea32019-05-02 21:26:52298 const landingRecordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kansoc0bfdd82019-04-24 12:32:22299
Rayan Kanso6156d222019-04-29 23:40:55300 const recordKey = createElementWithClass('b', 'background-service-shortcut');
301 recordKey.textContent =
302 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
303
Rayan Kanso4476ca52019-07-22 11:51:24304 centered.createChild('p').appendChild(UI.formatLocalized(
305 'Click the record button %s or hit %s to start recording.',
306 [UI.createInlineButton(landingRecordButton), recordKey]));
Rayan Kansoc0bfdd82019-04-24 12:32:22307 }
Rayan Kansob451b4f2019-04-04 23:12:11308
309 this._preview.show(this._previewPanel.contentElement);
310 }
Rayan Kansob852ba82019-04-08 13:48:07311
312 /**
313 * Saves all currently displayed events in a file (JSON format).
314 */
315 async _saveToFile() {
316 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
317 const stream = new Bindings.FileOutputStream();
318
319 const accepted = await stream.open(fileName);
320 if (!accepted)
321 return;
322
323 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
324 await stream.write(JSON.stringify(events, undefined, 2));
325 stream.close();
326 }
Rayan Kanso3252d5e2019-03-27 11:37:24327};
328
Rayan Kansoaca06e72019-03-27 11:57:06329/**
330 * @typedef {{
331 * id: number,
332 * timestamp: string,
333 * origin: string,
Rayan Kansocc02ea32019-05-02 21:26:52334 * swScope: string,
Rayan Kansoaca06e72019-03-27 11:57:06335 * eventName: string,
336 * instanceId: string,
337 * }}
338 */
339Resources.BackgroundServiceView.EventData;
340
Rayan Kanso3252d5e2019-03-27 11:37:24341Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
342 /**
Rayan Kansoaca06e72019-03-27 11:57:06343 * @param {!Object<string, string>} data
344 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24345 */
Rayan Kansoaca06e72019-03-27 11:57:06346 constructor(data, eventMetadata) {
347 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24348
349 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoc79a8bb2019-05-29 21:08:38350 this._eventMetadata = eventMetadata.sort((m1, m2) => m1.key.compareTo(m2.key));
Rayan Kanso3252d5e2019-03-27 11:37:24351 }
352
353 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22354 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24355 */
Rayan Kansob451b4f2019-04-04 23:12:11356 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22357 const preview = new UI.VBox();
358 preview.element.classList.add('background-service-metadata');
359
360 for (const entry of this._eventMetadata) {
361 const div = createElementWithClass('div', 'background-service-metadata-entry');
362 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
Rayan Kanso4476ca52019-07-22 11:51:24363 if (entry.value) {
364 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
365 } else {
366 div.createChild('div', 'background-service-metadata-value background-service-empty-value').textContent =
367 ls`empty`;
368 }
Rayan Kansoc0bfdd82019-04-24 12:32:22369 preview.element.appendChild(div);
370 }
371
372 if (!preview.element.children.length) {
373 const div = createElementWithClass('div', 'background-service-metadata-entry');
374 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
375 preview.element.appendChild(div);
376 }
377
378 return preview;
Rayan Kansof40e3152019-03-11 13:49:43379 }
Rayan Kanso68904202019-02-21 14:16:25380};
Rayan Kanso6156d222019-04-29 23:40:55381
382/**
383 * @implements {UI.ActionDelegate}
384 * @unrestricted
385 */
386Resources.BackgroundServiceView.ActionDelegate = class {
387 /**
388 * @override
389 * @param {!UI.Context} context
390 * @param {string} actionId
391 * @return {boolean}
392 */
393 handleAction(context, actionId) {
394 const view = context.flavor(Resources.BackgroundServiceView);
395 switch (actionId) {
396 case 'background-service.toggle-recording':
397 view._toggleRecording();
398 return true;
399 }
400 return false;
401 }
402};