blob: 6d26a148ea011ad2f9f524b0ed891bbe4b1ced47 [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 Kanso54809672019-07-24 18:40:2822 case Protocol.BackgroundService.ServiceName.PeriodicBackgroundSync:
23 return ls`Periodic Background Sync`;
Rayan Kansoc0bfdd82019-04-24 12:32:2224 default:
25 return '';
26 }
27 }
28
29 /**
Rayan Kanso8fe8ee22019-03-04 14:58:4630 * @param {!Protocol.BackgroundService.ServiceName} serviceName
31 * @param {!Resources.BackgroundServiceModel} model
Rayan Kanso68904202019-02-21 14:16:2532 */
Rayan Kanso8fe8ee22019-03-04 14:58:4633 constructor(serviceName, model) {
Rayan Kanso68904202019-02-21 14:16:2534 super(true);
35 this.registerRequiredCSS('resources/backgroundServiceView.css');
Rayan Kanso6156d222019-04-29 23:40:5536 this.registerRequiredCSS('ui/emptyWidget.css');
Rayan Kanso68904202019-02-21 14:16:2537
Rayan Kanso8fe8ee22019-03-04 14:58:4638 /** @const {!Protocol.BackgroundService.ServiceName} */
Rayan Kanso68904202019-02-21 14:16:2539 this._serviceName = serviceName;
40
Rayan Kanso8fe8ee22019-03-04 14:58:4641 /** @const {!Resources.BackgroundServiceModel} */
42 this._model = model;
43 this._model.addEventListener(
44 Resources.BackgroundServiceModel.Events.RecordingStateChanged, this._onRecordingStateChanged, this);
Rayan Kansof40e3152019-03-11 13:49:4345 this._model.addEventListener(
46 Resources.BackgroundServiceModel.Events.BackgroundServiceEventReceived, this._onEventReceived, this);
Rayan Kanso8fe8ee22019-03-04 14:58:4647 this._model.enable(this._serviceName);
48
Rayan Kansoaca06e72019-03-27 11:57:0649 /** @const {?SDK.ServiceWorkerManager} */
50 this._serviceWorkerManager = this._model.target().model(SDK.ServiceWorkerManager);
51
52 /** @const {?SDK.SecurityOriginManager} */
53 this._securityOriginManager = this._model.target().model(SDK.SecurityOriginManager);
54 this._securityOriginManager.addEventListener(
55 SDK.SecurityOriginManager.Events.MainSecurityOriginChanged, () => this._onOriginChanged());
56
Rayan Kansocc02ea32019-05-02 21:26:5257
58 /** @const {!UI.Action} */
59 this._recordAction = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
Joel Einbinder346980e2019-07-16 15:35:4860 /** @type {?UI.ToolbarButton} */
Rayan Kanso8fe8ee22019-03-04 14:58:4661 this._recordButton = null;
62
Rayan Kansoaca06e72019-03-27 11:57:0663 /** @type {?UI.ToolbarCheckbox} */
64 this._originCheckbox = null;
65
Rayan Kansob852ba82019-04-08 13:48:0766 /** @type {?UI.ToolbarButton} */
67 this._saveButton = null;
68
Rayan Kanso68904202019-02-21 14:16:2569 /** @const {!UI.Toolbar} */
70 this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
71 this._setupToolbar();
Rayan Kanso3252d5e2019-03-27 11:37:2472
Rayan Kansob451b4f2019-04-04 23:12:1173 /**
74 * This will contain the DataGrid for displaying events, and a panel at the bottom for showing
75 * extra metadata related to the selected event.
76 * @const {!UI.SplitWidget}
77 */
78 this._splitWidget = new UI.SplitWidget(/* isVertical= */ false, /* secondIsSidebar= */ true);
79 this._splitWidget.show(this.contentElement);
80
Rayan Kanso3252d5e2019-03-27 11:37:2481 /** @const {!DataGrid.DataGrid} */
82 this._dataGrid = this._createDataGrid();
Rayan Kansob451b4f2019-04-04 23:12:1183
84 /** @const {!UI.VBox} */
85 this._previewPanel = new UI.VBox();
86
Rayan Kansoc0bfdd82019-04-24 12:32:2287 /** @type {?Resources.BackgroundServiceView.EventDataNode} */
88 this._selectedEventNode = null;
89
Rayan Kansob451b4f2019-04-04 23:12:1190 /** @type {?UI.Widget} */
91 this._preview = null;
92
93 this._splitWidget.setMainWidget(this._dataGrid.asWidget());
94 this._splitWidget.setSidebarWidget(this._previewPanel);
95
96 this._showPreview(null);
Rayan Kanso68904202019-02-21 14:16:2597 }
98
99 /**
100 * Creates the toolbar UI element.
101 */
Rayan Kanso8fe8ee22019-03-04 14:58:46102 async _setupToolbar() {
Rayan Kansocc02ea32019-05-02 21:26:52103 this._recordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kanso8fe8ee22019-03-04 14:58:46104 this._toolbar.appendToolbarItem(this._recordButton);
Rayan Kanso68904202019-02-21 14:16:25105
Rayan Kansob852ba82019-04-08 13:48:07106 const clearButton = new UI.ToolbarButton(ls`Clear`, 'largeicon-clear');
Rayan Kansob451b4f2019-04-04 23:12:11107 clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._clearEvents());
Rayan Kanso68904202019-02-21 14:16:25108 this._toolbar.appendToolbarItem(clearButton);
109
110 this._toolbar.appendSeparator();
111
Rayan Kansob852ba82019-04-08 13:48:07112 this._saveButton = new UI.ToolbarButton(ls`Save events`, 'largeicon-download');
113 this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._saveToFile());
114 this._saveButton.setEnabled(false);
115 this._toolbar.appendToolbarItem(this._saveButton);
Rayan Kansoc0bfdd82019-04-24 12:32:22116
117 this._toolbar.appendSeparator();
118
119 this._originCheckbox =
120 new UI.ToolbarCheckbox(ls`Show events from other domains`, undefined, () => this._refreshView());
121 this._toolbar.appendToolbarItem(this._originCheckbox);
Rayan Kanso68904202019-02-21 14:16:25122 }
Rayan Kanso8fe8ee22019-03-04 14:58:46123
124 /**
Rayan Kansob451b4f2019-04-04 23:12:11125 * Displays all available events in the grid.
126 */
127 _refreshView() {
128 this._clearView();
129 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
130 for (const event of events)
131 this._addEvent(event);
132 }
133
134 /**
135 * Clears the grid and panel.
136 */
137 _clearView() {
Rayan Kansoc0bfdd82019-04-24 12:32:22138 this._selectedEventNode = null;
Rayan Kansob451b4f2019-04-04 23:12:11139 this._dataGrid.rootNode().removeChildren();
Rayan Kansob852ba82019-04-08 13:48:07140 this._saveButton.setEnabled(false);
Rayan Kansoc0bfdd82019-04-24 12:32:22141 this._showPreview(null);
Rayan Kansob451b4f2019-04-04 23:12:11142 }
143
144 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46145 * Called when the `Toggle Record` button is clicked.
146 */
147 _toggleRecording() {
148 this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
149 }
150
151 /**
Rayan Kanso3252d5e2019-03-27 11:37:24152 * Called when the `Clear` button is clicked.
153 */
Rayan Kansob451b4f2019-04-04 23:12:11154 _clearEvents() {
Rayan Kanso3252d5e2019-03-27 11:37:24155 this._model.clearEvents(this._serviceName);
156 this._clearView();
157 }
158
159 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46160 * @param {!Common.Event} event
161 */
162 _onRecordingStateChanged(event) {
163 const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
164 if (state.serviceName !== this._serviceName)
165 return;
Rayan Kansoc0bfdd82019-04-24 12:32:22166
167 if (state.isRecording === this._recordButton.toggled())
168 return;
169
Rayan Kanso0b30aba2019-05-17 13:39:59170 this._recordButton.setToggled(state.isRecording);
Rayan Kansoc0bfdd82019-04-24 12:32:22171 this._showPreview(this._selectedEventNode);
Rayan Kanso8fe8ee22019-03-04 14:58:46172 }
Rayan Kansof40e3152019-03-11 13:49:43173
174 /**
175 * @param {!Common.Event} event
176 */
177 _onEventReceived(event) {
178 const serviceEvent = /** @type {!Protocol.BackgroundService.BackgroundServiceEvent} */ (event.data);
Rayan Kanso3252d5e2019-03-27 11:37:24179 if (!this._acceptEvent(serviceEvent))
Rayan Kansof40e3152019-03-11 13:49:43180 return;
Rayan Kanso3252d5e2019-03-27 11:37:24181 this._addEvent(serviceEvent);
182 }
183
Rayan Kansoaca06e72019-03-27 11:57:06184 _onOriginChanged() {
185 // No need to refresh the view if we are already showing all events.
186 if (this._originCheckbox.checked())
187 return;
188 this._refreshView();
189 }
190
Rayan Kanso3252d5e2019-03-27 11:37:24191 /**
192 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
193 */
194 _addEvent(serviceEvent) {
Rayan Kansoaca06e72019-03-27 11:57:06195 const data = this._createEventData(serviceEvent);
196 const dataNode = new Resources.BackgroundServiceView.EventDataNode(data, serviceEvent.eventMetadata);
Rayan Kanso3252d5e2019-03-27 11:37:24197 this._dataGrid.rootNode().appendChild(dataNode);
Rayan Kansob852ba82019-04-08 13:48:07198
Rayan Kansoc0bfdd82019-04-24 12:32:22199 if (this._dataGrid.rootNode().children.length === 1) {
200 this._saveButton.setEnabled(true);
201 this._showPreview(this._selectedEventNode);
202 }
Rayan Kanso3252d5e2019-03-27 11:37:24203 }
204
205 /**
206 * @return {!DataGrid.DataGrid}
207 */
208 _createDataGrid() {
209 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
Rayan Kansob852ba82019-04-08 13:48:07210 {id: 'id', title: ls`#`, weight: 1},
211 {id: 'timestamp', title: ls`Timestamp`, weight: 8},
Rayan Kansoc0bfdd82019-04-24 12:32:22212 {id: 'eventName', title: ls`Event`, weight: 10},
Rayan Kansob852ba82019-04-08 13:48:07213 {id: 'origin', title: ls`Origin`, weight: 10},
Rayan Kansocc02ea32019-05-02 21:26:52214 {id: 'swScope', title: ls`SW Scope`, weight: 2},
Rayan Kansob852ba82019-04-08 13:48:07215 {id: 'instanceId', title: ls`Instance ID`, weight: 10},
Rayan Kanso3252d5e2019-03-27 11:37:24216 ]);
217 const dataGrid = new DataGrid.DataGrid(columns);
218 dataGrid.setStriped(true);
Rayan Kansob451b4f2019-04-04 23:12:11219
220 dataGrid.addEventListener(
221 DataGrid.DataGrid.Events.SelectedNode,
222 event => this._showPreview(/** @type {!Resources.BackgroundServiceView.EventDataNode} */ (event.data)));
223
Rayan Kanso3252d5e2019-03-27 11:37:24224 return dataGrid;
225 }
226
227 /**
Rayan Kansoaca06e72019-03-27 11:57:06228 * Creates the data object to pass to the DataGrid Node.
229 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
230 * @return {!Resources.BackgroundServiceView.EventData}
231 */
232 _createEventData(serviceEvent) {
Rayan Kansocc02ea32019-05-02 21:26:52233 let swScope = '';
Rayan Kansoaca06e72019-03-27 11:57:06234
Rayan Kansocc02ea32019-05-02 21:26:52235 // Try to get the scope of the Service Worker registration to be more user-friendly.
236 const registration = this._serviceWorkerManager.registrations().get(serviceEvent.serviceWorkerRegistrationId);
237 if (registration)
238 swScope = registration.scopeURL.substr(registration.securityOrigin.length);
Rayan Kansoaca06e72019-03-27 11:57:06239
240 return {
Rayan Kansoc79a8bb2019-05-29 21:08:38241 id: this._dataGrid.rootNode().children.length + 1,
Rayan Kansoaca06e72019-03-27 11:57:06242 timestamp: UI.formatTimestamp(serviceEvent.timestamp * 1000, /* full= */ true),
243 origin: serviceEvent.origin,
Rayan Kansocc02ea32019-05-02 21:26:52244 swScope,
Rayan Kansoaca06e72019-03-27 11:57:06245 eventName: serviceEvent.eventName,
246 instanceId: serviceEvent.instanceId,
247 };
248 }
249
250 /**
Rayan Kanso3252d5e2019-03-27 11:37:24251 * Filtration function to know whether event should be shown or not.
252 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} event
253 * @return {boolean}
254 */
255 _acceptEvent(event) {
Rayan Kansoaca06e72019-03-27 11:57:06256 if (event.service !== this._serviceName)
257 return false;
258
259 if (this._originCheckbox.checked())
260 return true;
261
262 // Trim the trailing '/'.
263 const origin = event.origin.substr(0, event.origin.length - 1);
264
265 return this._securityOriginManager.securityOrigins().includes(origin);
Rayan Kanso3252d5e2019-03-27 11:37:24266 }
Rayan Kansob451b4f2019-04-04 23:12:11267
268 /**
269 * @param {?Resources.BackgroundServiceView.EventDataNode} dataNode
270 */
271 _showPreview(dataNode) {
Rayan Kansoc0bfdd82019-04-24 12:32:22272 if (this._selectedEventNode && this._selectedEventNode === dataNode)
273 return;
274
275 this._selectedEventNode = dataNode;
276
Rayan Kansob451b4f2019-04-04 23:12:11277 if (this._preview)
278 this._preview.detach();
279
Rayan Kansoc0bfdd82019-04-24 12:32:22280 if (this._selectedEventNode) {
281 this._preview = this._selectedEventNode.createPreview();
Rayan Kanso4476ca52019-07-22 11:51:24282 this._preview.show(this._previewPanel.contentElement);
283 return;
284 }
285
286 this._preview = new UI.VBox();
287 this._preview.contentElement.classList.add('background-service-preview', 'fill');
288 const centered = this._preview.contentElement.createChild('div');
289
290 if (this._dataGrid.rootNode().children.length) {
Rayan Kansoc0bfdd82019-04-24 12:32:22291 // Inform users that grid entries are clickable.
Rayan Kanso4476ca52019-07-22 11:51:24292 centered.createChild('p').textContent = ls`Select an entry to view metadata`;
Rayan Kansoc0bfdd82019-04-24 12:32:22293 } else if (this._recordButton.toggled()) {
294 // Inform users that we are recording/waiting for events.
Rayan Kanso4476ca52019-07-22 11:51:24295 const featureName = Resources.BackgroundServiceView.getUIString(this._serviceName);
296 centered.createChild('p').textContent = ls`Recording ${featureName} activity...`;
297 centered.createChild('p').textContent =
298 ls`DevTools will record all ${featureName} activity for up to 3 days, even when closed.`;
Rayan Kansoc0bfdd82019-04-24 12:32:22299 } else {
Rayan Kansocc02ea32019-05-02 21:26:52300 const landingRecordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kansoc0bfdd82019-04-24 12:32:22301
Rayan Kanso6156d222019-04-29 23:40:55302 const recordKey = createElementWithClass('b', 'background-service-shortcut');
303 recordKey.textContent =
304 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
305
Rayan Kanso4476ca52019-07-22 11:51:24306 centered.createChild('p').appendChild(UI.formatLocalized(
307 'Click the record button %s or hit %s to start recording.',
308 [UI.createInlineButton(landingRecordButton), recordKey]));
Rayan Kansoc0bfdd82019-04-24 12:32:22309 }
Rayan Kansob451b4f2019-04-04 23:12:11310
311 this._preview.show(this._previewPanel.contentElement);
312 }
Rayan Kansob852ba82019-04-08 13:48:07313
314 /**
315 * Saves all currently displayed events in a file (JSON format).
316 */
317 async _saveToFile() {
318 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
319 const stream = new Bindings.FileOutputStream();
320
321 const accepted = await stream.open(fileName);
322 if (!accepted)
323 return;
324
325 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
326 await stream.write(JSON.stringify(events, undefined, 2));
327 stream.close();
328 }
Rayan Kanso3252d5e2019-03-27 11:37:24329};
330
Rayan Kansoaca06e72019-03-27 11:57:06331/**
332 * @typedef {{
333 * id: number,
334 * timestamp: string,
335 * origin: string,
Rayan Kansocc02ea32019-05-02 21:26:52336 * swScope: string,
Rayan Kansoaca06e72019-03-27 11:57:06337 * eventName: string,
338 * instanceId: string,
339 * }}
340 */
341Resources.BackgroundServiceView.EventData;
342
Rayan Kanso3252d5e2019-03-27 11:37:24343Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
344 /**
Rayan Kansoaca06e72019-03-27 11:57:06345 * @param {!Object<string, string>} data
346 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24347 */
Rayan Kansoaca06e72019-03-27 11:57:06348 constructor(data, eventMetadata) {
349 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24350
351 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoc79a8bb2019-05-29 21:08:38352 this._eventMetadata = eventMetadata.sort((m1, m2) => m1.key.compareTo(m2.key));
Rayan Kanso3252d5e2019-03-27 11:37:24353 }
354
355 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22356 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24357 */
Rayan Kansob451b4f2019-04-04 23:12:11358 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22359 const preview = new UI.VBox();
360 preview.element.classList.add('background-service-metadata');
361
362 for (const entry of this._eventMetadata) {
363 const div = createElementWithClass('div', 'background-service-metadata-entry');
364 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
Rayan Kanso4476ca52019-07-22 11:51:24365 if (entry.value) {
366 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
367 } else {
368 div.createChild('div', 'background-service-metadata-value background-service-empty-value').textContent =
369 ls`empty`;
370 }
Rayan Kansoc0bfdd82019-04-24 12:32:22371 preview.element.appendChild(div);
372 }
373
374 if (!preview.element.children.length) {
375 const div = createElementWithClass('div', 'background-service-metadata-entry');
376 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
377 preview.element.appendChild(div);
378 }
379
380 return preview;
Rayan Kansof40e3152019-03-11 13:49:43381 }
Rayan Kanso68904202019-02-21 14:16:25382};
Rayan Kanso6156d222019-04-29 23:40:55383
384/**
385 * @implements {UI.ActionDelegate}
386 * @unrestricted
387 */
388Resources.BackgroundServiceView.ActionDelegate = class {
389 /**
390 * @override
391 * @param {!UI.Context} context
392 * @param {string} actionId
393 * @return {boolean}
394 */
395 handleAction(context, actionId) {
396 const view = context.flavor(Resources.BackgroundServiceView);
397 switch (actionId) {
398 case 'background-service.toggle-recording':
399 view._toggleRecording();
400 return true;
401 }
402 return false;
403 }
404};