blob: 7a65f4b6d9b32c75fb5ecb9e72e5e72cef9e5a44 [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 Kansoc0bfdd82019-04-24 12:32:2220 default:
21 return '';
22 }
23 }
24
25 /**
Rayan Kanso8fe8ee22019-03-04 14:58:4626 * @param {!Protocol.BackgroundService.ServiceName} serviceName
27 * @param {!Resources.BackgroundServiceModel} model
Rayan Kanso68904202019-02-21 14:16:2528 */
Rayan Kanso8fe8ee22019-03-04 14:58:4629 constructor(serviceName, model) {
Rayan Kanso68904202019-02-21 14:16:2530 super(true);
31 this.registerRequiredCSS('resources/backgroundServiceView.css');
Rayan Kanso6156d222019-04-29 23:40:5532 this.registerRequiredCSS('ui/emptyWidget.css');
Rayan Kanso68904202019-02-21 14:16:2533
Rayan Kanso8fe8ee22019-03-04 14:58:4634 /** @const {!Protocol.BackgroundService.ServiceName} */
Rayan Kanso68904202019-02-21 14:16:2535 this._serviceName = serviceName;
36
Rayan Kanso8fe8ee22019-03-04 14:58:4637 /** @const {!Resources.BackgroundServiceModel} */
38 this._model = model;
39 this._model.addEventListener(
40 Resources.BackgroundServiceModel.Events.RecordingStateChanged, this._onRecordingStateChanged, this);
Rayan Kansof40e3152019-03-11 13:49:4341 this._model.addEventListener(
42 Resources.BackgroundServiceModel.Events.BackgroundServiceEventReceived, this._onEventReceived, this);
Rayan Kanso8fe8ee22019-03-04 14:58:4643 this._model.enable(this._serviceName);
44
Rayan Kansoaca06e72019-03-27 11:57:0645 /** @const {?SDK.ServiceWorkerManager} */
46 this._serviceWorkerManager = this._model.target().model(SDK.ServiceWorkerManager);
47
48 /** @const {?SDK.SecurityOriginManager} */
49 this._securityOriginManager = this._model.target().model(SDK.SecurityOriginManager);
50 this._securityOriginManager.addEventListener(
51 SDK.SecurityOriginManager.Events.MainSecurityOriginChanged, () => this._onOriginChanged());
52
Rayan Kansocc02ea32019-05-02 21:26:5253
54 /** @const {!UI.Action} */
55 this._recordAction = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
Rayan Kanso8fe8ee22019-03-04 14:58:4656 /** @type {?UI.ToolbarToggle} */
57 this._recordButton = null;
58
Rayan Kansoaca06e72019-03-27 11:57:0659 /** @type {?UI.ToolbarCheckbox} */
60 this._originCheckbox = null;
61
Rayan Kansob852ba82019-04-08 13:48:0762 /** @type {?UI.ToolbarButton} */
63 this._saveButton = null;
64
Rayan Kanso68904202019-02-21 14:16:2565 /** @const {!UI.Toolbar} */
66 this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
67 this._setupToolbar();
Rayan Kanso3252d5e2019-03-27 11:37:2468
Rayan Kansob451b4f2019-04-04 23:12:1169 /**
70 * This will contain the DataGrid for displaying events, and a panel at the bottom for showing
71 * extra metadata related to the selected event.
72 * @const {!UI.SplitWidget}
73 */
74 this._splitWidget = new UI.SplitWidget(/* isVertical= */ false, /* secondIsSidebar= */ true);
75 this._splitWidget.show(this.contentElement);
76
Rayan Kanso3252d5e2019-03-27 11:37:2477 /** @const {!DataGrid.DataGrid} */
78 this._dataGrid = this._createDataGrid();
Rayan Kansob451b4f2019-04-04 23:12:1179
80 /** @const {!UI.VBox} */
81 this._previewPanel = new UI.VBox();
82
Rayan Kansoc0bfdd82019-04-24 12:32:2283 /** @type {?Resources.BackgroundServiceView.EventDataNode} */
84 this._selectedEventNode = null;
85
Rayan Kansob451b4f2019-04-04 23:12:1186 /** @type {?UI.Widget} */
87 this._preview = null;
88
89 this._splitWidget.setMainWidget(this._dataGrid.asWidget());
90 this._splitWidget.setSidebarWidget(this._previewPanel);
91
92 this._showPreview(null);
Rayan Kanso68904202019-02-21 14:16:2593 }
94
95 /**
96 * Creates the toolbar UI element.
97 */
Rayan Kanso8fe8ee22019-03-04 14:58:4698 async _setupToolbar() {
Rayan Kansocc02ea32019-05-02 21:26:5299 this._recordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kanso8fe8ee22019-03-04 14:58:46100 this._toolbar.appendToolbarItem(this._recordButton);
Rayan Kanso68904202019-02-21 14:16:25101
Rayan Kansob852ba82019-04-08 13:48:07102 const clearButton = new UI.ToolbarButton(ls`Clear`, 'largeicon-clear');
Rayan Kansob451b4f2019-04-04 23:12:11103 clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._clearEvents());
Rayan Kanso68904202019-02-21 14:16:25104 this._toolbar.appendToolbarItem(clearButton);
105
106 this._toolbar.appendSeparator();
107
Rayan Kansob852ba82019-04-08 13:48:07108 this._saveButton = new UI.ToolbarButton(ls`Save events`, 'largeicon-download');
109 this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._saveToFile());
110 this._saveButton.setEnabled(false);
111 this._toolbar.appendToolbarItem(this._saveButton);
Rayan Kansoc0bfdd82019-04-24 12:32:22112
113 this._toolbar.appendSeparator();
114
115 this._originCheckbox =
116 new UI.ToolbarCheckbox(ls`Show events from other domains`, undefined, () => this._refreshView());
117 this._toolbar.appendToolbarItem(this._originCheckbox);
Rayan Kanso68904202019-02-21 14:16:25118 }
Rayan Kanso8fe8ee22019-03-04 14:58:46119
120 /**
Rayan Kansob451b4f2019-04-04 23:12:11121 * Displays all available events in the grid.
122 */
123 _refreshView() {
124 this._clearView();
125 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
126 for (const event of events)
127 this._addEvent(event);
128 }
129
130 /**
131 * Clears the grid and panel.
132 */
133 _clearView() {
Rayan Kansoc0bfdd82019-04-24 12:32:22134 this._selectedEventNode = null;
Rayan Kansob451b4f2019-04-04 23:12:11135 this._dataGrid.rootNode().removeChildren();
Rayan Kansob852ba82019-04-08 13:48:07136 this._saveButton.setEnabled(false);
Rayan Kansoc0bfdd82019-04-24 12:32:22137 this._showPreview(null);
Rayan Kansob451b4f2019-04-04 23:12:11138 }
139
140 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46141 * Called when the `Toggle Record` button is clicked.
142 */
143 _toggleRecording() {
144 this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
145 }
146
147 /**
Rayan Kanso3252d5e2019-03-27 11:37:24148 * Called when the `Clear` button is clicked.
149 */
Rayan Kansob451b4f2019-04-04 23:12:11150 _clearEvents() {
Rayan Kanso3252d5e2019-03-27 11:37:24151 this._model.clearEvents(this._serviceName);
152 this._clearView();
153 }
154
155 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46156 * @param {!Common.Event} event
157 */
158 _onRecordingStateChanged(event) {
159 const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
160 if (state.serviceName !== this._serviceName)
161 return;
Rayan Kansoc0bfdd82019-04-24 12:32:22162
163 if (state.isRecording === this._recordButton.toggled())
164 return;
165
Rayan Kanso0b30aba2019-05-17 13:39:59166 this._recordButton.setToggled(state.isRecording);
Rayan Kansoc0bfdd82019-04-24 12:32:22167 this._showPreview(this._selectedEventNode);
Rayan Kanso8fe8ee22019-03-04 14:58:46168 }
Rayan Kansof40e3152019-03-11 13:49:43169
170 /**
171 * @param {!Common.Event} event
172 */
173 _onEventReceived(event) {
174 const serviceEvent = /** @type {!Protocol.BackgroundService.BackgroundServiceEvent} */ (event.data);
Rayan Kanso3252d5e2019-03-27 11:37:24175 if (!this._acceptEvent(serviceEvent))
Rayan Kansof40e3152019-03-11 13:49:43176 return;
Rayan Kanso3252d5e2019-03-27 11:37:24177 this._addEvent(serviceEvent);
178 }
179
Rayan Kansoaca06e72019-03-27 11:57:06180 _onOriginChanged() {
181 // No need to refresh the view if we are already showing all events.
182 if (this._originCheckbox.checked())
183 return;
184 this._refreshView();
185 }
186
Rayan Kanso3252d5e2019-03-27 11:37:24187 /**
188 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
189 */
190 _addEvent(serviceEvent) {
Rayan Kansoaca06e72019-03-27 11:57:06191 const data = this._createEventData(serviceEvent);
192 const dataNode = new Resources.BackgroundServiceView.EventDataNode(data, serviceEvent.eventMetadata);
Rayan Kanso3252d5e2019-03-27 11:37:24193 this._dataGrid.rootNode().appendChild(dataNode);
Rayan Kansob852ba82019-04-08 13:48:07194
Rayan Kansoc0bfdd82019-04-24 12:32:22195 if (this._dataGrid.rootNode().children.length === 1) {
196 this._saveButton.setEnabled(true);
197 this._showPreview(this._selectedEventNode);
198 }
Rayan Kanso3252d5e2019-03-27 11:37:24199 }
200
201 /**
202 * @return {!DataGrid.DataGrid}
203 */
204 _createDataGrid() {
205 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
Rayan Kansob852ba82019-04-08 13:48:07206 {id: 'id', title: ls`#`, weight: 1},
207 {id: 'timestamp', title: ls`Timestamp`, weight: 8},
Rayan Kansoc0bfdd82019-04-24 12:32:22208 {id: 'eventName', title: ls`Event`, weight: 10},
Rayan Kansob852ba82019-04-08 13:48:07209 {id: 'origin', title: ls`Origin`, weight: 10},
Rayan Kansocc02ea32019-05-02 21:26:52210 {id: 'swScope', title: ls`SW Scope`, weight: 2},
Rayan Kansob852ba82019-04-08 13:48:07211 {id: 'instanceId', title: ls`Instance ID`, weight: 10},
Rayan Kanso3252d5e2019-03-27 11:37:24212 ]);
213 const dataGrid = new DataGrid.DataGrid(columns);
214 dataGrid.setStriped(true);
Rayan Kansob451b4f2019-04-04 23:12:11215
216 dataGrid.addEventListener(
217 DataGrid.DataGrid.Events.SelectedNode,
218 event => this._showPreview(/** @type {!Resources.BackgroundServiceView.EventDataNode} */ (event.data)));
219
Rayan Kanso3252d5e2019-03-27 11:37:24220 return dataGrid;
221 }
222
223 /**
Rayan Kansoaca06e72019-03-27 11:57:06224 * Creates the data object to pass to the DataGrid Node.
225 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
226 * @return {!Resources.BackgroundServiceView.EventData}
227 */
228 _createEventData(serviceEvent) {
Rayan Kansocc02ea32019-05-02 21:26:52229 let swScope = '';
Rayan Kansoaca06e72019-03-27 11:57:06230
Rayan Kansocc02ea32019-05-02 21:26:52231 // Try to get the scope of the Service Worker registration to be more user-friendly.
232 const registration = this._serviceWorkerManager.registrations().get(serviceEvent.serviceWorkerRegistrationId);
233 if (registration)
234 swScope = registration.scopeURL.substr(registration.securityOrigin.length);
Rayan Kansoaca06e72019-03-27 11:57:06235
236 return {
237 id: this._dataGrid.rootNode().children.length,
238 timestamp: UI.formatTimestamp(serviceEvent.timestamp * 1000, /* full= */ true),
239 origin: serviceEvent.origin,
Rayan Kansocc02ea32019-05-02 21:26:52240 swScope,
Rayan Kansoaca06e72019-03-27 11:57:06241 eventName: serviceEvent.eventName,
242 instanceId: serviceEvent.instanceId,
243 };
244 }
245
246 /**
Rayan Kanso3252d5e2019-03-27 11:37:24247 * Filtration function to know whether event should be shown or not.
248 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} event
249 * @return {boolean}
250 */
251 _acceptEvent(event) {
Rayan Kansoaca06e72019-03-27 11:57:06252 if (event.service !== this._serviceName)
253 return false;
254
255 if (this._originCheckbox.checked())
256 return true;
257
258 // Trim the trailing '/'.
259 const origin = event.origin.substr(0, event.origin.length - 1);
260
261 return this._securityOriginManager.securityOrigins().includes(origin);
Rayan Kanso3252d5e2019-03-27 11:37:24262 }
Rayan Kansob451b4f2019-04-04 23:12:11263
264 /**
265 * @param {?Resources.BackgroundServiceView.EventDataNode} dataNode
266 */
267 _showPreview(dataNode) {
Rayan Kansoc0bfdd82019-04-24 12:32:22268 if (this._selectedEventNode && this._selectedEventNode === dataNode)
269 return;
270
271 this._selectedEventNode = dataNode;
272
Rayan Kansob451b4f2019-04-04 23:12:11273 if (this._preview)
274 this._preview.detach();
275
Rayan Kansoc0bfdd82019-04-24 12:32:22276 if (this._selectedEventNode) {
277 this._preview = this._selectedEventNode.createPreview();
278 } else if (this._dataGrid.rootNode().children.length) {
279 // Inform users that grid entries are clickable.
280 this._preview = new UI.EmptyWidget(ls`Select an entry to view metadata`);
281 } else if (this._recordButton.toggled()) {
282 // Inform users that we are recording/waiting for events.
283 this._preview = new UI.EmptyWidget(
284 ls`Recording ${Resources.BackgroundServiceView.getUIString(this._serviceName)} activity...`);
285 } else {
286 this._preview = new UI.VBox();
Rayan Kanso6156d222019-04-29 23:40:55287 this._preview.contentElement.classList.add('empty-view-scroller');
288 const centered = this._preview.contentElement.createChild('div', 'empty-view');
Rayan Kansoc0bfdd82019-04-24 12:32:22289
Rayan Kansocc02ea32019-05-02 21:26:52290 const landingRecordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kansoc0bfdd82019-04-24 12:32:22291
Rayan Kanso6156d222019-04-29 23:40:55292 const recordKey = createElementWithClass('b', 'background-service-shortcut');
293 recordKey.textContent =
294 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
295
296 centered.createChild('h2').appendChild(UI.formatLocalized(
297 'Click the record button %s or hit %s to start recording.',
298 [UI.createInlineButton(landingRecordButton), recordKey]));
Rayan Kansoc0bfdd82019-04-24 12:32:22299 }
Rayan Kansob451b4f2019-04-04 23:12:11300
301 this._preview.show(this._previewPanel.contentElement);
302 }
Rayan Kansob852ba82019-04-08 13:48:07303
304 /**
305 * Saves all currently displayed events in a file (JSON format).
306 */
307 async _saveToFile() {
308 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
309 const stream = new Bindings.FileOutputStream();
310
311 const accepted = await stream.open(fileName);
312 if (!accepted)
313 return;
314
315 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
316 await stream.write(JSON.stringify(events, undefined, 2));
317 stream.close();
318 }
Rayan Kanso3252d5e2019-03-27 11:37:24319};
320
Rayan Kansoaca06e72019-03-27 11:57:06321/**
322 * @typedef {{
323 * id: number,
324 * timestamp: string,
325 * origin: string,
Rayan Kansocc02ea32019-05-02 21:26:52326 * swScope: string,
Rayan Kansoaca06e72019-03-27 11:57:06327 * eventName: string,
328 * instanceId: string,
329 * }}
330 */
331Resources.BackgroundServiceView.EventData;
332
Rayan Kanso3252d5e2019-03-27 11:37:24333Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
334 /**
Rayan Kansoaca06e72019-03-27 11:57:06335 * @param {!Object<string, string>} data
336 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24337 */
Rayan Kansoaca06e72019-03-27 11:57:06338 constructor(data, eventMetadata) {
339 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24340
341 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoaca06e72019-03-27 11:57:06342 this._eventMetadata = eventMetadata;
Rayan Kanso3252d5e2019-03-27 11:37:24343 }
344
345 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22346 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24347 */
Rayan Kansob451b4f2019-04-04 23:12:11348 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22349 const preview = new UI.VBox();
350 preview.element.classList.add('background-service-metadata');
351
352 for (const entry of this._eventMetadata) {
353 const div = createElementWithClass('div', 'background-service-metadata-entry');
354 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
355 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
356 preview.element.appendChild(div);
357 }
358
359 if (!preview.element.children.length) {
360 const div = createElementWithClass('div', 'background-service-metadata-entry');
361 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
362 preview.element.appendChild(div);
363 }
364
365 return preview;
Rayan Kansof40e3152019-03-11 13:49:43366 }
Rayan Kanso68904202019-02-21 14:16:25367};
Rayan Kanso6156d222019-04-29 23:40:55368
369/**
370 * @implements {UI.ActionDelegate}
371 * @unrestricted
372 */
373Resources.BackgroundServiceView.ActionDelegate = class {
374 /**
375 * @override
376 * @param {!UI.Context} context
377 * @param {string} actionId
378 * @return {boolean}
379 */
380 handleAction(context, actionId) {
381 const view = context.flavor(Resources.BackgroundServiceView);
382 switch (actionId) {
383 case 'background-service.toggle-recording':
384 view._toggleRecording();
385 return true;
386 }
387 return false;
388 }
389};