blob: 05a63172aad3f63c976715d22585d37684c0cee9 [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'));
Rayan Kanso8fe8ee22019-03-04 14:58:4658 /** @type {?UI.ToolbarToggle} */
59 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();
280 } else if (this._dataGrid.rootNode().children.length) {
281 // Inform users that grid entries are clickable.
282 this._preview = new UI.EmptyWidget(ls`Select an entry to view metadata`);
283 } else if (this._recordButton.toggled()) {
284 // Inform users that we are recording/waiting for events.
285 this._preview = new UI.EmptyWidget(
286 ls`Recording ${Resources.BackgroundServiceView.getUIString(this._serviceName)} activity...`);
287 } else {
288 this._preview = new UI.VBox();
Rayan Kanso6156d222019-04-29 23:40:55289 this._preview.contentElement.classList.add('empty-view-scroller');
290 const centered = this._preview.contentElement.createChild('div', 'empty-view');
Rayan Kansoc0bfdd82019-04-24 12:32:22291
Rayan Kansocc02ea32019-05-02 21:26:52292 const landingRecordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kansoc0bfdd82019-04-24 12:32:22293
Rayan Kanso6156d222019-04-29 23:40:55294 const recordKey = createElementWithClass('b', 'background-service-shortcut');
295 recordKey.textContent =
296 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
297
298 centered.createChild('h2').appendChild(UI.formatLocalized(
299 'Click the record button %s or hit %s to start recording.',
300 [UI.createInlineButton(landingRecordButton), recordKey]));
Rayan Kansoc0bfdd82019-04-24 12:32:22301 }
Rayan Kansob451b4f2019-04-04 23:12:11302
303 this._preview.show(this._previewPanel.contentElement);
304 }
Rayan Kansob852ba82019-04-08 13:48:07305
306 /**
307 * Saves all currently displayed events in a file (JSON format).
308 */
309 async _saveToFile() {
310 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
311 const stream = new Bindings.FileOutputStream();
312
313 const accepted = await stream.open(fileName);
314 if (!accepted)
315 return;
316
317 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
318 await stream.write(JSON.stringify(events, undefined, 2));
319 stream.close();
320 }
Rayan Kanso3252d5e2019-03-27 11:37:24321};
322
Rayan Kansoaca06e72019-03-27 11:57:06323/**
324 * @typedef {{
325 * id: number,
326 * timestamp: string,
327 * origin: string,
Rayan Kansocc02ea32019-05-02 21:26:52328 * swScope: string,
Rayan Kansoaca06e72019-03-27 11:57:06329 * eventName: string,
330 * instanceId: string,
331 * }}
332 */
333Resources.BackgroundServiceView.EventData;
334
Rayan Kanso3252d5e2019-03-27 11:37:24335Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
336 /**
Rayan Kansoaca06e72019-03-27 11:57:06337 * @param {!Object<string, string>} data
338 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24339 */
Rayan Kansoaca06e72019-03-27 11:57:06340 constructor(data, eventMetadata) {
341 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24342
343 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoc79a8bb2019-05-29 21:08:38344 this._eventMetadata = eventMetadata.sort((m1, m2) => m1.key.compareTo(m2.key));
Rayan Kanso3252d5e2019-03-27 11:37:24345 }
346
347 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22348 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24349 */
Rayan Kansob451b4f2019-04-04 23:12:11350 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22351 const preview = new UI.VBox();
352 preview.element.classList.add('background-service-metadata');
353
354 for (const entry of this._eventMetadata) {
355 const div = createElementWithClass('div', 'background-service-metadata-entry');
356 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
357 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
358 preview.element.appendChild(div);
359 }
360
361 if (!preview.element.children.length) {
362 const div = createElementWithClass('div', 'background-service-metadata-entry');
363 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
364 preview.element.appendChild(div);
365 }
366
367 return preview;
Rayan Kansof40e3152019-03-11 13:49:43368 }
Rayan Kanso68904202019-02-21 14:16:25369};
Rayan Kanso6156d222019-04-29 23:40:55370
371/**
372 * @implements {UI.ActionDelegate}
373 * @unrestricted
374 */
375Resources.BackgroundServiceView.ActionDelegate = class {
376 /**
377 * @override
378 * @param {!UI.Context} context
379 * @param {string} actionId
380 * @return {boolean}
381 */
382 handleAction(context, actionId) {
383 const view = context.flavor(Resources.BackgroundServiceView);
384 switch (actionId) {
385 case 'background-service.toggle-recording':
386 view._toggleRecording();
387 return true;
388 }
389 return false;
390 }
391};