blob: a92d8b1dde7f19a67d640de1c5fd86bf93853f12 [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 Kansocc02ea32019-05-02 21:26:5249
50 /** @const {!UI.Action} */
51 this._recordAction = /** @type {!UI.Action} */ (UI.actionRegistry.action('background-service.toggle-recording'));
Rayan Kanso8fe8ee22019-03-04 14:58:4652 /** @type {?UI.ToolbarToggle} */
53 this._recordButton = null;
54
Rayan Kansoaca06e72019-03-27 11:57:0655 /** @type {?UI.ToolbarCheckbox} */
56 this._originCheckbox = null;
57
Rayan Kansob852ba82019-04-08 13:48:0758 /** @type {?UI.ToolbarButton} */
59 this._saveButton = null;
60
Rayan Kanso68904202019-02-21 14:16:2561 /** @const {!UI.Toolbar} */
62 this._toolbar = new UI.Toolbar('background-service-toolbar', this.contentElement);
63 this._setupToolbar();
Rayan Kanso3252d5e2019-03-27 11:37:2464
Rayan Kansob451b4f2019-04-04 23:12:1165 /**
66 * This will contain the DataGrid for displaying events, and a panel at the bottom for showing
67 * extra metadata related to the selected event.
68 * @const {!UI.SplitWidget}
69 */
70 this._splitWidget = new UI.SplitWidget(/* isVertical= */ false, /* secondIsSidebar= */ true);
71 this._splitWidget.show(this.contentElement);
72
Rayan Kanso3252d5e2019-03-27 11:37:2473 /** @const {!DataGrid.DataGrid} */
74 this._dataGrid = this._createDataGrid();
Rayan Kansob451b4f2019-04-04 23:12:1175
76 /** @const {!UI.VBox} */
77 this._previewPanel = new UI.VBox();
78
Rayan Kansoc0bfdd82019-04-24 12:32:2279 /** @type {?Resources.BackgroundServiceView.EventDataNode} */
80 this._selectedEventNode = null;
81
Rayan Kansob451b4f2019-04-04 23:12:1182 /** @type {?UI.Widget} */
83 this._preview = null;
84
85 this._splitWidget.setMainWidget(this._dataGrid.asWidget());
86 this._splitWidget.setSidebarWidget(this._previewPanel);
87
88 this._showPreview(null);
Rayan Kanso68904202019-02-21 14:16:2589 }
90
91 /**
92 * Creates the toolbar UI element.
93 */
Rayan Kanso8fe8ee22019-03-04 14:58:4694 async _setupToolbar() {
Rayan Kansocc02ea32019-05-02 21:26:5295 this._recordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kanso8fe8ee22019-03-04 14:58:4696 this._toolbar.appendToolbarItem(this._recordButton);
Rayan Kanso68904202019-02-21 14:16:2597
Rayan Kansob852ba82019-04-08 13:48:0798 const clearButton = new UI.ToolbarButton(ls`Clear`, 'largeicon-clear');
Rayan Kansob451b4f2019-04-04 23:12:1199 clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._clearEvents());
Rayan Kanso68904202019-02-21 14:16:25100 this._toolbar.appendToolbarItem(clearButton);
101
102 this._toolbar.appendSeparator();
103
Rayan Kansob852ba82019-04-08 13:48:07104 this._saveButton = new UI.ToolbarButton(ls`Save events`, 'largeicon-download');
105 this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._saveToFile());
106 this._saveButton.setEnabled(false);
107 this._toolbar.appendToolbarItem(this._saveButton);
Rayan Kansoc0bfdd82019-04-24 12:32:22108
109 this._toolbar.appendSeparator();
110
111 this._originCheckbox =
112 new UI.ToolbarCheckbox(ls`Show events from other domains`, undefined, () => this._refreshView());
113 this._toolbar.appendToolbarItem(this._originCheckbox);
Rayan Kanso68904202019-02-21 14:16:25114 }
Rayan Kanso8fe8ee22019-03-04 14:58:46115
116 /**
Rayan Kansob451b4f2019-04-04 23:12:11117 * Displays all available events in the grid.
118 */
119 _refreshView() {
120 this._clearView();
121 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
122 for (const event of events)
123 this._addEvent(event);
124 }
125
126 /**
127 * Clears the grid and panel.
128 */
129 _clearView() {
Rayan Kansoc0bfdd82019-04-24 12:32:22130 this._selectedEventNode = null;
Rayan Kansob451b4f2019-04-04 23:12:11131 this._dataGrid.rootNode().removeChildren();
Rayan Kansob852ba82019-04-08 13:48:07132 this._saveButton.setEnabled(false);
Rayan Kansoc0bfdd82019-04-24 12:32:22133 this._showPreview(null);
Rayan Kansob451b4f2019-04-04 23:12:11134 }
135
136 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46137 * Called when the `Toggle Record` button is clicked.
138 */
139 _toggleRecording() {
140 this._model.setRecording(!this._recordButton.toggled(), this._serviceName);
141 }
142
143 /**
Rayan Kanso3252d5e2019-03-27 11:37:24144 * Called when the `Clear` button is clicked.
145 */
Rayan Kansob451b4f2019-04-04 23:12:11146 _clearEvents() {
Rayan Kanso3252d5e2019-03-27 11:37:24147 this._model.clearEvents(this._serviceName);
148 this._clearView();
149 }
150
151 /**
Rayan Kanso8fe8ee22019-03-04 14:58:46152 * @param {!Common.Event} event
153 */
154 _onRecordingStateChanged(event) {
155 const state = /** @type {!Resources.BackgroundServiceModel.RecordingState} */ (event.data);
156 if (state.serviceName !== this._serviceName)
157 return;
Rayan Kansoc0bfdd82019-04-24 12:32:22158
159 if (state.isRecording === this._recordButton.toggled())
160 return;
161
Rayan Kanso0b30aba2019-05-17 13:39:59162 this._recordButton.setToggled(state.isRecording);
Rayan Kansoc0bfdd82019-04-24 12:32:22163 this._showPreview(this._selectedEventNode);
Rayan Kanso8fe8ee22019-03-04 14:58:46164 }
Rayan Kansof40e3152019-03-11 13:49:43165
166 /**
167 * @param {!Common.Event} event
168 */
169 _onEventReceived(event) {
170 const serviceEvent = /** @type {!Protocol.BackgroundService.BackgroundServiceEvent} */ (event.data);
Rayan Kanso3252d5e2019-03-27 11:37:24171 if (!this._acceptEvent(serviceEvent))
Rayan Kansof40e3152019-03-11 13:49:43172 return;
Rayan Kanso3252d5e2019-03-27 11:37:24173 this._addEvent(serviceEvent);
174 }
175
Rayan Kansoaca06e72019-03-27 11:57:06176 _onOriginChanged() {
177 // No need to refresh the view if we are already showing all events.
178 if (this._originCheckbox.checked())
179 return;
180 this._refreshView();
181 }
182
Rayan Kanso3252d5e2019-03-27 11:37:24183 /**
184 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
185 */
186 _addEvent(serviceEvent) {
Rayan Kansoaca06e72019-03-27 11:57:06187 const data = this._createEventData(serviceEvent);
188 const dataNode = new Resources.BackgroundServiceView.EventDataNode(data, serviceEvent.eventMetadata);
Rayan Kanso3252d5e2019-03-27 11:37:24189 this._dataGrid.rootNode().appendChild(dataNode);
Rayan Kansob852ba82019-04-08 13:48:07190
Rayan Kansoc0bfdd82019-04-24 12:32:22191 if (this._dataGrid.rootNode().children.length === 1) {
192 this._saveButton.setEnabled(true);
193 this._showPreview(this._selectedEventNode);
194 }
Rayan Kanso3252d5e2019-03-27 11:37:24195 }
196
197 /**
198 * @return {!DataGrid.DataGrid}
199 */
200 _createDataGrid() {
201 const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
Rayan Kansob852ba82019-04-08 13:48:07202 {id: 'id', title: ls`#`, weight: 1},
203 {id: 'timestamp', title: ls`Timestamp`, weight: 8},
Rayan Kansoc0bfdd82019-04-24 12:32:22204 {id: 'eventName', title: ls`Event`, weight: 10},
Rayan Kansob852ba82019-04-08 13:48:07205 {id: 'origin', title: ls`Origin`, weight: 10},
Rayan Kansocc02ea32019-05-02 21:26:52206 {id: 'swScope', title: ls`SW Scope`, weight: 2},
Rayan Kansob852ba82019-04-08 13:48:07207 {id: 'instanceId', title: ls`Instance ID`, weight: 10},
Rayan Kanso3252d5e2019-03-27 11:37:24208 ]);
209 const dataGrid = new DataGrid.DataGrid(columns);
210 dataGrid.setStriped(true);
Rayan Kansob451b4f2019-04-04 23:12:11211
212 dataGrid.addEventListener(
213 DataGrid.DataGrid.Events.SelectedNode,
214 event => this._showPreview(/** @type {!Resources.BackgroundServiceView.EventDataNode} */ (event.data)));
215
Rayan Kanso3252d5e2019-03-27 11:37:24216 return dataGrid;
217 }
218
219 /**
Rayan Kansoaca06e72019-03-27 11:57:06220 * Creates the data object to pass to the DataGrid Node.
221 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} serviceEvent
222 * @return {!Resources.BackgroundServiceView.EventData}
223 */
224 _createEventData(serviceEvent) {
Rayan Kansocc02ea32019-05-02 21:26:52225 let swScope = '';
Rayan Kansoaca06e72019-03-27 11:57:06226
Rayan Kansocc02ea32019-05-02 21:26:52227 // Try to get the scope of the Service Worker registration to be more user-friendly.
228 const registration = this._serviceWorkerManager.registrations().get(serviceEvent.serviceWorkerRegistrationId);
229 if (registration)
230 swScope = registration.scopeURL.substr(registration.securityOrigin.length);
Rayan Kansoaca06e72019-03-27 11:57:06231
232 return {
233 id: this._dataGrid.rootNode().children.length,
234 timestamp: UI.formatTimestamp(serviceEvent.timestamp * 1000, /* full= */ true),
235 origin: serviceEvent.origin,
Rayan Kansocc02ea32019-05-02 21:26:52236 swScope,
Rayan Kansoaca06e72019-03-27 11:57:06237 eventName: serviceEvent.eventName,
238 instanceId: serviceEvent.instanceId,
239 };
240 }
241
242 /**
Rayan Kanso3252d5e2019-03-27 11:37:24243 * Filtration function to know whether event should be shown or not.
244 * @param {!Protocol.BackgroundService.BackgroundServiceEvent} event
245 * @return {boolean}
246 */
247 _acceptEvent(event) {
Rayan Kansoaca06e72019-03-27 11:57:06248 if (event.service !== this._serviceName)
249 return false;
250
251 if (this._originCheckbox.checked())
252 return true;
253
254 // Trim the trailing '/'.
255 const origin = event.origin.substr(0, event.origin.length - 1);
256
257 return this._securityOriginManager.securityOrigins().includes(origin);
Rayan Kanso3252d5e2019-03-27 11:37:24258 }
Rayan Kansob451b4f2019-04-04 23:12:11259
260 /**
261 * @param {?Resources.BackgroundServiceView.EventDataNode} dataNode
262 */
263 _showPreview(dataNode) {
Rayan Kansoc0bfdd82019-04-24 12:32:22264 if (this._selectedEventNode && this._selectedEventNode === dataNode)
265 return;
266
267 this._selectedEventNode = dataNode;
268
Rayan Kansob451b4f2019-04-04 23:12:11269 if (this._preview)
270 this._preview.detach();
271
Rayan Kansoc0bfdd82019-04-24 12:32:22272 if (this._selectedEventNode) {
273 this._preview = this._selectedEventNode.createPreview();
274 } else if (this._dataGrid.rootNode().children.length) {
275 // Inform users that grid entries are clickable.
276 this._preview = new UI.EmptyWidget(ls`Select an entry to view metadata`);
277 } else if (this._recordButton.toggled()) {
278 // Inform users that we are recording/waiting for events.
279 this._preview = new UI.EmptyWidget(
280 ls`Recording ${Resources.BackgroundServiceView.getUIString(this._serviceName)} activity...`);
281 } else {
282 this._preview = new UI.VBox();
Rayan Kanso6156d222019-04-29 23:40:55283 this._preview.contentElement.classList.add('empty-view-scroller');
284 const centered = this._preview.contentElement.createChild('div', 'empty-view');
Rayan Kansoc0bfdd82019-04-24 12:32:22285
Rayan Kansocc02ea32019-05-02 21:26:52286 const landingRecordButton = UI.Toolbar.createActionButton(this._recordAction);
Rayan Kansoc0bfdd82019-04-24 12:32:22287
Rayan Kanso6156d222019-04-29 23:40:55288 const recordKey = createElementWithClass('b', 'background-service-shortcut');
289 recordKey.textContent =
290 UI.shortcutRegistry.shortcutDescriptorsForAction('background-service.toggle-recording')[0].name;
291
292 centered.createChild('h2').appendChild(UI.formatLocalized(
293 'Click the record button %s or hit %s to start recording.',
294 [UI.createInlineButton(landingRecordButton), recordKey]));
Rayan Kansoc0bfdd82019-04-24 12:32:22295 }
Rayan Kansob451b4f2019-04-04 23:12:11296
297 this._preview.show(this._previewPanel.contentElement);
298 }
Rayan Kansob852ba82019-04-08 13:48:07299
300 /**
301 * Saves all currently displayed events in a file (JSON format).
302 */
303 async _saveToFile() {
304 const fileName = `${this._serviceName}-${new Date().toISO8601Compact()}.json`;
305 const stream = new Bindings.FileOutputStream();
306
307 const accepted = await stream.open(fileName);
308 if (!accepted)
309 return;
310
311 const events = this._model.getEvents(this._serviceName).filter(event => this._acceptEvent(event));
312 await stream.write(JSON.stringify(events, undefined, 2));
313 stream.close();
314 }
Rayan Kanso3252d5e2019-03-27 11:37:24315};
316
Rayan Kansoaca06e72019-03-27 11:57:06317/**
318 * @typedef {{
319 * id: number,
320 * timestamp: string,
321 * origin: string,
Rayan Kansocc02ea32019-05-02 21:26:52322 * swScope: string,
Rayan Kansoaca06e72019-03-27 11:57:06323 * eventName: string,
324 * instanceId: string,
325 * }}
326 */
327Resources.BackgroundServiceView.EventData;
328
Rayan Kanso3252d5e2019-03-27 11:37:24329Resources.BackgroundServiceView.EventDataNode = class extends DataGrid.DataGridNode {
330 /**
Rayan Kansoaca06e72019-03-27 11:57:06331 * @param {!Object<string, string>} data
332 * @param {!Array<!Protocol.BackgroundService.EventMetadata>} eventMetadata
Rayan Kanso3252d5e2019-03-27 11:37:24333 */
Rayan Kansoaca06e72019-03-27 11:57:06334 constructor(data, eventMetadata) {
335 super(data);
Rayan Kanso3252d5e2019-03-27 11:37:24336
337 /** @const {!Array<!Protocol.BackgroundService.EventMetadata>} */
Rayan Kansoaca06e72019-03-27 11:57:06338 this._eventMetadata = eventMetadata;
Rayan Kanso3252d5e2019-03-27 11:37:24339 }
340
341 /**
Rayan Kansoc0bfdd82019-04-24 12:32:22342 * @return {!UI.VBox}
Rayan Kanso3252d5e2019-03-27 11:37:24343 */
Rayan Kansob451b4f2019-04-04 23:12:11344 createPreview() {
Rayan Kansoc0bfdd82019-04-24 12:32:22345 const preview = new UI.VBox();
346 preview.element.classList.add('background-service-metadata');
347
348 for (const entry of this._eventMetadata) {
349 const div = createElementWithClass('div', 'background-service-metadata-entry');
350 div.createChild('div', 'background-service-metadata-name').textContent = entry.key + ': ';
351 div.createChild('div', 'background-service-metadata-value source-code').textContent = entry.value;
352 preview.element.appendChild(div);
353 }
354
355 if (!preview.element.children.length) {
356 const div = createElementWithClass('div', 'background-service-metadata-entry');
357 div.createChild('div', 'background-service-metadata-name').textContent = ls`No metadata for this event`;
358 preview.element.appendChild(div);
359 }
360
361 return preview;
Rayan Kansof40e3152019-03-11 13:49:43362 }
Rayan Kanso68904202019-02-21 14:16:25363};
Rayan Kanso6156d222019-04-29 23:40:55364
365/**
366 * @implements {UI.ActionDelegate}
367 * @unrestricted
368 */
369Resources.BackgroundServiceView.ActionDelegate = class {
370 /**
371 * @override
372 * @param {!UI.Context} context
373 * @param {string} actionId
374 * @return {boolean}
375 */
376 handleAction(context, actionId) {
377 const view = context.flavor(Resources.BackgroundServiceView);
378 switch (actionId) {
379 case 'background-service.toggle-recording':
380 view._toggleRecording();
381 return true;
382 }
383 return false;
384 }
385};