blob: 6e8fd06356fe103d11378130c6fc132feb05ef4f [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright (c) 2015 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/**
5 * @implements {SDK.SDKModelObserver<!SDK.ServiceWorkerManager>}
6 */
7Resources.ServiceWorkersView = class extends UI.VBox {
8 constructor() {
9 super(true);
10 this.registerRequiredCSS('resources/serviceWorkersView.css');
11
12 this._currentWorkersView = new UI.ReportView(Common.UIString('Service Workers'));
13 this._currentWorkersView.setBodyScrollable(false);
14 this.contentElement.classList.add('service-worker-list');
15 this._currentWorkersView.show(this.contentElement);
16 this._currentWorkersView.element.classList.add('service-workers-this-origin');
17
18 this._toolbar = this._currentWorkersView.createToolbar();
Erik Luo5e5a8362018-05-31 23:43:2219 this._toolbar.makeWrappable(true /* growVertically */);
Blink Reformat4c46d092018-04-07 15:32:3720
21 /** @type {!Map<!SDK.ServiceWorkerRegistration, !Resources.ServiceWorkersView.Section>} */
22 this._sections = new Map();
23
24 /** @type {?SDK.ServiceWorkerManager} */
25 this._manager = null;
26 /** @type {?SDK.SecurityOriginManager} */
27 this._securityOriginManager = null;
28
29 this._filterThrottler = new Common.Throttler(300);
30
31 this._otherWorkers = this.contentElement.createChild('div', 'service-workers-other-origin');
32 this._otherSWFilter = this._otherWorkers.createChild('div', 'service-worker-filter');
33 this._otherSWFilter.setAttribute('tabindex', 0);
34 this._otherSWFilter.setAttribute('role', 'switch');
35 this._otherSWFilter.setAttribute('aria-checked', false);
36 this._otherSWFilter.addEventListener('keydown', event => {
Harley Lif8b7b4b2018-11-02 22:51:2537 if (event.target !== this._otherSWFilter)
38 return;
Blink Reformat4c46d092018-04-07 15:32:3739 if (isEnterKey(event) || event.keyCode === UI.KeyboardShortcut.Keys.Space.code)
40 this._toggleFilter();
41 });
42 const filterLabel = this._otherSWFilter.createChild('label', 'service-worker-filter-label');
43 filterLabel.textContent = Common.UIString('Service workers from other domains');
44 filterLabel.setAttribute('for', 'expand-all');
45 filterLabel.addEventListener('click', () => this._toggleFilter());
46
47 const toolbar = new UI.Toolbar('service-worker-filter-toolbar', this._otherSWFilter);
48 this._filter = new UI.ToolbarInput('Filter', 1);
49 this._filter.addEventListener(UI.ToolbarInput.Event.TextChanged, () => this._filterChanged());
50 toolbar.appendToolbarItem(this._filter);
51
52 this._otherWorkersView = new UI.ReportView();
53 this._otherWorkersView.setBodyScrollable(false);
54 this._otherWorkersView.show(this._otherWorkers);
55 this._otherWorkersView.element.classList.add('service-workers-for-other-origins');
56
57 this._updateCollapsedStyle();
58
59 this._toolbar.appendToolbarItem(MobileThrottling.throttlingManager().createOfflineToolbarCheckbox());
60 const updateOnReloadSetting = Common.settings.createSetting('serviceWorkerUpdateOnReload', false);
61 updateOnReloadSetting.setTitle(Common.UIString('Update on reload'));
62 const forceUpdate = new UI.ToolbarSettingCheckbox(
Harley Li38795b32019-01-10 22:32:2663 updateOnReloadSetting, ls`On page reload, force the service worker to update, and activate it`);
Blink Reformat4c46d092018-04-07 15:32:3764 this._toolbar.appendToolbarItem(forceUpdate);
65 const bypassServiceWorkerSetting = Common.settings.createSetting('bypassServiceWorker', false);
66 bypassServiceWorkerSetting.setTitle(Common.UIString('Bypass for network'));
67 const fallbackToNetwork = new UI.ToolbarSettingCheckbox(
Harley Li38795b32019-01-10 22:32:2668 bypassServiceWorkerSetting, ls`Bypass service worker and load resources from the network`);
Blink Reformat4c46d092018-04-07 15:32:3769 this._toolbar.appendToolbarItem(fallbackToNetwork);
70
71 /** @type {!Map<!SDK.ServiceWorkerManager, !Array<!Common.EventTarget.EventDescriptor>>}*/
72 this._eventListeners = new Map();
73 SDK.targetManager.observeModels(SDK.ServiceWorkerManager, this);
74 this._updateListVisibility();
75 }
76
77 /**
78 * @override
79 * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
80 */
81 modelAdded(serviceWorkerManager) {
82 if (this._manager)
83 return;
84 this._manager = serviceWorkerManager;
85 this._securityOriginManager = serviceWorkerManager.target().model(SDK.SecurityOriginManager);
86
87 for (const registration of this._manager.registrations().values())
88 this._updateRegistration(registration);
89
90 this._eventListeners.set(serviceWorkerManager, [
91 this._manager.addEventListener(
92 SDK.ServiceWorkerManager.Events.RegistrationUpdated, this._registrationUpdated, this),
93 this._manager.addEventListener(
94 SDK.ServiceWorkerManager.Events.RegistrationDeleted, this._registrationDeleted, this),
95 this._securityOriginManager.addEventListener(
96 SDK.SecurityOriginManager.Events.SecurityOriginAdded, this._updateSectionVisibility, this),
97 this._securityOriginManager.addEventListener(
98 SDK.SecurityOriginManager.Events.SecurityOriginRemoved, this._updateSectionVisibility, this),
99 ]);
100 }
101
102 /**
103 * @override
104 * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
105 */
106 modelRemoved(serviceWorkerManager) {
107 if (!this._manager || this._manager !== serviceWorkerManager)
108 return;
109
110 Common.EventTarget.removeEventListeners(this._eventListeners.get(serviceWorkerManager));
111 this._eventListeners.delete(serviceWorkerManager);
112 this._manager = null;
113 this._securityOriginManager = null;
114 }
115
116 _updateSectionVisibility() {
117 let hasOthers = false;
118 let hasThis = false;
119 const movedSections = [];
120 for (const section of this._sections.values()) {
121 const expectedView = this._getReportViewForOrigin(section._registration.securityOrigin);
122 hasOthers |= expectedView === this._otherWorkersView;
123 hasThis |= expectedView === this._currentWorkersView;
124 if (section._section.parentWidget() !== expectedView)
125 movedSections.push(section);
126 }
127
128 for (const section of movedSections) {
129 const registration = section._registration;
130 this._removeRegistrationFromList(registration);
131 this._updateRegistration(registration, true);
132 }
133
134 const scorer = new Sources.FilePathScoreFunction(this._filter.value());
135 this._otherWorkersView.sortSections((a, b) => {
136 const cmp = scorer.score(b.title(), null) - scorer.score(a.title(), null);
137 return cmp === 0 ? a.title().localeCompare(b.title()) : cmp;
138 });
139 for (const section of this._sections.values()) {
140 if (section._section.parentWidget() === this._currentWorkersView ||
141 this._isRegistrationVisible(section._registration))
142 section._section.showWidget();
143 else
144 section._section.hideWidget();
145 }
146 this.contentElement.classList.toggle('service-worker-has-current', hasThis);
147 this._otherWorkers.classList.toggle('hidden', !hasOthers);
148 this._updateListVisibility();
149 }
150
151 /**
152 * @param {!Common.Event} event
153 */
154 _registrationUpdated(event) {
155 const registration = /** @type {!SDK.ServiceWorkerRegistration} */ (event.data);
156 this._updateRegistration(registration);
157 this._gcRegistrations();
158 }
159
160 _gcRegistrations() {
161 let hasNonDeletedRegistrations = false;
162 const securityOrigins = new Set(this._securityOriginManager.securityOrigins());
163 for (const registration of this._manager.registrations().values()) {
164 if (!securityOrigins.has(registration.securityOrigin) && !this._isRegistrationVisible(registration))
165 continue;
166 if (!registration.canBeRemoved()) {
167 hasNonDeletedRegistrations = true;
168 break;
169 }
170 }
171
172 if (!hasNonDeletedRegistrations)
173 return;
174
175 for (const registration of this._manager.registrations().values()) {
176 const visible = securityOrigins.has(registration.securityOrigin) || this._isRegistrationVisible(registration);
177 if (!visible && registration.canBeRemoved())
178 this._removeRegistrationFromList(registration);
179 }
180 }
181
182 /**
183 * @param {string} origin
184 * @return {!UI.ReportView}
185 */
186 _getReportViewForOrigin(origin) {
187 if (this._securityOriginManager.securityOrigins().includes(origin))
188 return this._currentWorkersView;
189 else
190 return this._otherWorkersView;
191 }
192
193 /**
194 * @param {!SDK.ServiceWorkerRegistration} registration
195 * @param {boolean=} skipUpdate
196 */
197 _updateRegistration(registration, skipUpdate) {
198 let section = this._sections.get(registration);
199 if (!section) {
200 const title = Resources.ServiceWorkersView._displayScopeURL(registration.scopeURL);
201 section = new Resources.ServiceWorkersView.Section(
202 /** @type {!SDK.ServiceWorkerManager} */ (this._manager),
203 this._getReportViewForOrigin(registration.securityOrigin).appendSection(title), registration);
204 this._sections.set(registration, section);
205 }
206 if (skipUpdate)
207 return;
208 this._updateSectionVisibility();
209 section._scheduleUpdate();
210 }
211
212 /**
213 * @param {!Common.Event} event
214 */
215 _registrationDeleted(event) {
216 const registration = /** @type {!SDK.ServiceWorkerRegistration} */ (event.data);
217 this._removeRegistrationFromList(registration);
218 }
219
220 /**
221 * @param {!SDK.ServiceWorkerRegistration} registration
222 */
223 _removeRegistrationFromList(registration) {
224 const section = this._sections.get(registration);
225 if (section)
226 section._section.detach();
227 this._sections.delete(registration);
228 this._updateSectionVisibility();
229 }
230
231 /**
232 * @param {!SDK.ServiceWorkerRegistration} registration
233 * @return {boolean}
234 */
235 _isRegistrationVisible(registration) {
236 const filterString = this._filter.value();
237 if (!filterString || !registration.scopeURL)
238 return true;
239
240 const regex = String.filterRegex(filterString);
241 return registration.scopeURL.match(regex);
242 }
243
244 _filterChanged() {
245 this._updateCollapsedStyle();
246 this._filterThrottler.schedule(() => Promise.resolve(this._updateSectionVisibility()));
247 }
248
249 _updateCollapsedStyle() {
250 const expanded = this._otherSWFilter.getAttribute('aria-checked') === 'true';
251 this._otherWorkers.classList.toggle('service-worker-filter-collapsed', !expanded);
252 if (expanded)
253 this._otherWorkersView.showWidget();
254 else
255 this._otherWorkersView.hideWidget();
256 this._otherWorkersView.setHeaderVisible(false);
257 }
258
259 /**
260 * @param {string} scopeURL
261 * @return {string}
262 */
263 static _displayScopeURL(scopeURL) {
264 const parsedURL = scopeURL.asParsedURL();
265 let path = parsedURL.path;
266 if (path.endsWith('/'))
267 path = path.substring(0, path.length - 1);
268 return parsedURL.host + path;
269 }
270
271 _updateListVisibility() {
272 this.contentElement.classList.toggle('service-worker-list-empty', this._sections.size === 0);
273 }
274
275 _toggleFilter() {
276 const expanded = this._otherSWFilter.getAttribute('aria-checked') === 'true';
277 this._otherSWFilter.setAttribute('aria-checked', !expanded);
278 this._filterChanged();
279 }
280};
281
282Resources.ServiceWorkersView.Section = class {
283 /**
284 * @param {!SDK.ServiceWorkerManager} manager
285 * @param {!UI.ReportView.Section} section
286 * @param {!SDK.ServiceWorkerRegistration} registration
287 */
288 constructor(manager, section, registration) {
289 this._manager = manager;
290 this._section = section;
291 this._registration = registration;
292 /** @type {?symbol} */
293 this._fingerprint = null;
294 this._pushNotificationDataSetting =
295 Common.settings.createLocalSetting('pushData', Common.UIString('Test push message from DevTools.'));
296 this._syncTagNameSetting = Common.settings.createLocalSetting('syncTagName', 'test-tag-from-devtools');
297
298 this._toolbar = section.createToolbar();
299 this._toolbar.renderAsLinks();
300 this._updateButton = new UI.ToolbarButton(Common.UIString('Update'), undefined, Common.UIString('Update'));
301 this._updateButton.addEventListener(UI.ToolbarButton.Events.Click, this._updateButtonClicked, this);
302 this._toolbar.appendToolbarItem(this._updateButton);
303 this._deleteButton =
304 new UI.ToolbarButton(Common.UIString('Unregister service worker'), undefined, Common.UIString('Unregister'));
305 this._deleteButton.addEventListener(UI.ToolbarButton.Events.Click, this._unregisterButtonClicked, this);
306 this._toolbar.appendToolbarItem(this._deleteButton);
307
308 // Preserve the order.
309 this._sourceField = this._wrapWidget(this._section.appendField(Common.UIString('Source')));
310 this._statusField = this._wrapWidget(this._section.appendField(Common.UIString('Status')));
311 this._clientsField = this._wrapWidget(this._section.appendField(Common.UIString('Clients')));
312 this._createSyncNotificationField(
313 Common.UIString('Push'), this._pushNotificationDataSetting.get(), Common.UIString('Push data'),
314 this._push.bind(this));
315 this._createSyncNotificationField(
316 Common.UIString('Sync'), this._syncTagNameSetting.get(), Common.UIString('Sync tag'), this._sync.bind(this));
317
318 this._linkifier = new Components.Linkifier();
319 /** @type {!Map<string, !Protocol.Target.TargetInfo>} */
320 this._clientInfoCache = new Map();
321 this._throttler = new Common.Throttler(500);
322 }
323
324 /**
325 * @param {string} label
326 * @param {string} initialValue
327 * @param {string} placeholder
328 * @param {function(string)} callback
329 */
330 _createSyncNotificationField(label, initialValue, placeholder, callback) {
331 const form =
332 this._wrapWidget(this._section.appendField(label)).createChild('form', 'service-worker-editor-with-button');
333 const editor = form.createChild('input', 'source-code service-worker-notification-editor');
334 const button = UI.createTextButton(label);
335 button.type = 'submit';
336 form.appendChild(button);
337
338 editor.value = initialValue;
339 editor.placeholder = placeholder;
340
341 form.addEventListener('submit', e => {
342 callback(editor.value || '');
343 e.consume(true);
344 });
345 }
346
347 _scheduleUpdate() {
348 if (Resources.ServiceWorkersView._noThrottle) {
349 this._update();
350 return;
351 }
352 this._throttler.schedule(this._update.bind(this));
353 }
354
355 /**
356 * @param {string} versionId
357 * @return {?SDK.Target}
358 */
359 _targetForVersionId(versionId) {
360 const version = this._manager.findVersion(versionId);
361 if (!version || !version.targetId)
362 return null;
363 return SDK.targetManager.targetById(version.targetId);
364 }
365
366 /**
367 * @param {!Element} versionsStack
368 * @param {string} icon
369 * @param {string} label
370 * @return {!Element}
371 */
372 _addVersion(versionsStack, icon, label) {
373 const installingEntry = versionsStack.createChild('div', 'service-worker-version');
374 installingEntry.createChild('div', icon);
375 installingEntry.createChild('span').textContent = label;
376 return installingEntry;
377 }
378
379 /**
380 * @param {!SDK.ServiceWorkerVersion} version
381 */
382 _updateClientsField(version) {
383 this._clientsField.removeChildren();
384 this._section.setFieldVisible(Common.UIString('Clients'), version.controlledClients.length);
385 for (const client of version.controlledClients) {
386 const clientLabelText = this._clientsField.createChild('div', 'service-worker-client');
387 if (this._clientInfoCache.has(client)) {
388 this._updateClientInfo(
389 clientLabelText, /** @type {!Protocol.Target.TargetInfo} */ (this._clientInfoCache.get(client)));
390 }
391 this._manager.target().targetAgent().getTargetInfo(client).then(this._onClientInfo.bind(this, clientLabelText));
392 }
393 }
394
395 /**
396 * @param {!SDK.ServiceWorkerVersion} version
397 */
398 _updateSourceField(version) {
399 this._sourceField.removeChildren();
400 const fileName = Common.ParsedURL.extractName(version.scriptURL);
401 const name = this._sourceField.createChild('div', 'report-field-value-filename');
402 name.appendChild(Components.Linkifier.linkifyURL(version.scriptURL, {text: fileName}));
403 if (this._registration.errors.length) {
404 const errorsLabel = UI.createLabel(String(this._registration.errors.length), 'smallicon-error');
405 errorsLabel.classList.add('link');
406 errorsLabel.addEventListener('click', () => Common.console.show());
407 name.appendChild(errorsLabel);
408 }
John Abd-El-Malek7fa90c82018-08-27 18:39:55409 this._sourceField.createChild('div', 'report-field-value-subtitle').textContent =
410 Common.UIString('Received %s', new Date(version.scriptResponseTime * 1000).toLocaleString());
Blink Reformat4c46d092018-04-07 15:32:37411 }
412
413 /**
414 * @return {!Promise}
415 */
416 _update() {
417 const fingerprint = this._registration.fingerprint();
418 if (fingerprint === this._fingerprint)
419 return Promise.resolve();
420 this._fingerprint = fingerprint;
421
422 this._toolbar.setEnabled(!this._registration.isDeleted);
423
424 const versions = this._registration.versionsByMode();
425 const scopeURL = Resources.ServiceWorkersView._displayScopeURL(this._registration.scopeURL);
426 const title = this._registration.isDeleted ? Common.UIString('%s - deleted', scopeURL) : scopeURL;
427 this._section.setTitle(title);
428
429 const active = versions.get(SDK.ServiceWorkerVersion.Modes.Active);
430 const waiting = versions.get(SDK.ServiceWorkerVersion.Modes.Waiting);
431 const installing = versions.get(SDK.ServiceWorkerVersion.Modes.Installing);
432 const redundant = versions.get(SDK.ServiceWorkerVersion.Modes.Redundant);
433
434 this._statusField.removeChildren();
435 const versionsStack = this._statusField.createChild('div', 'service-worker-version-stack');
436 versionsStack.createChild('div', 'service-worker-version-stack-bar');
437
438 if (active) {
439 this._updateSourceField(active);
440 const activeEntry = this._addVersion(
441 versionsStack, 'service-worker-active-circle',
442 Common.UIString('#%s activated and is %s', active.id, active.runningStatus));
443
444 if (active.isRunning() || active.isStarting()) {
445 createLink(activeEntry, Common.UIString('stop'), this._stopButtonClicked.bind(this, active.id));
446 if (!this._targetForVersionId(active.id))
447 createLink(activeEntry, Common.UIString('inspect'), this._inspectButtonClicked.bind(this, active.id));
448 } else if (active.isStartable()) {
449 createLink(activeEntry, Common.UIString('start'), this._startButtonClicked.bind(this));
450 }
451 this._updateClientsField(active);
452 } else if (redundant) {
453 this._updateSourceField(redundant);
454 this._addVersion(
455 versionsStack, 'service-worker-redundant-circle', Common.UIString('#%s is redundant', redundant.id));
456 this._updateClientsField(redundant);
457 }
458
459 if (waiting) {
460 const waitingEntry = this._addVersion(
461 versionsStack, 'service-worker-waiting-circle', Common.UIString('#%s waiting to activate', waiting.id));
462 createLink(waitingEntry, Common.UIString('skipWaiting'), this._skipButtonClicked.bind(this));
John Abd-El-Malek7fa90c82018-08-27 18:39:55463 waitingEntry.createChild('div', 'service-worker-subtitle').textContent =
464 Common.UIString('Received %s', new Date(waiting.scriptResponseTime * 1000).toLocaleString());
Blink Reformat4c46d092018-04-07 15:32:37465 if (!this._targetForVersionId(waiting.id) && (waiting.isRunning() || waiting.isStarting()))
466 createLink(waitingEntry, Common.UIString('inspect'), this._inspectButtonClicked.bind(this, waiting.id));
467 }
468 if (installing) {
469 const installingEntry = this._addVersion(
470 versionsStack, 'service-worker-installing-circle', Common.UIString('#%s installing', installing.id));
John Abd-El-Malek7fa90c82018-08-27 18:39:55471 installingEntry.createChild('div', 'service-worker-subtitle').textContent =
472 Common.UIString('Received %s', new Date(installing.scriptResponseTime * 1000).toLocaleString());
Blink Reformat4c46d092018-04-07 15:32:37473 if (!this._targetForVersionId(installing.id) && (installing.isRunning() || installing.isStarting()))
474 createLink(installingEntry, Common.UIString('inspect'), this._inspectButtonClicked.bind(this, installing.id));
475 }
476
477 /**
478 * @param {!Element} parent
479 * @param {string} title
480 * @param {function()} listener
481 * @return {!Element}
482 */
483 function createLink(parent, title, listener) {
484 const span = parent.createChild('span', 'link');
485 span.textContent = title;
486 span.addEventListener('click', listener, false);
487 return span;
488 }
489 return Promise.resolve();
490 }
491
492 /**
493 * @param {!Common.Event} event
494 */
495 _unregisterButtonClicked(event) {
496 this._manager.deleteRegistration(this._registration.id);
497 }
498
499 /**
500 * @param {!Common.Event} event
501 */
502 _updateButtonClicked(event) {
503 this._manager.updateRegistration(this._registration.id);
504 }
505
506 /**
507 * @param {string} data
508 */
509 _push(data) {
510 this._pushNotificationDataSetting.set(data);
511 this._manager.deliverPushMessage(this._registration.id, data);
512 }
513
514 /**
515 * @param {string} tag
516 */
517 _sync(tag) {
518 this._syncTagNameSetting.set(tag);
519 this._manager.dispatchSyncEvent(this._registration.id, tag, true);
520 }
521
522 /**
523 * @param {!Element} element
524 * @param {?Protocol.Target.TargetInfo} targetInfo
525 */
526 _onClientInfo(element, targetInfo) {
527 if (!targetInfo)
528 return;
529 this._clientInfoCache.set(targetInfo.targetId, targetInfo);
530 this._updateClientInfo(element, targetInfo);
531 }
532
533 /**
534 * @param {!Element} element
535 * @param {!Protocol.Target.TargetInfo} targetInfo
536 */
537 _updateClientInfo(element, targetInfo) {
538 if (targetInfo.type !== 'page' && targetInfo.type === 'iframe') {
Harley Lia8a622f2018-11-01 23:58:09539 const clientString = element.createChild('span', 'service-worker-client-string');
540 clientString.createTextChild(ls`Worker: ` + targetInfo.url);
Blink Reformat4c46d092018-04-07 15:32:37541 return;
542 }
543 element.removeChildren();
Harley Lia8a622f2018-11-01 23:58:09544 const clientString = element.createChild('span', 'service-worker-client-string');
545 clientString.createTextChild(targetInfo.url);
546 const focusLabel = element.createChild('label', 'link service-worker-client-focus-link');
Blink Reformat4c46d092018-04-07 15:32:37547 focusLabel.createTextChild('focus');
548 focusLabel.addEventListener('click', this._activateTarget.bind(this, targetInfo.targetId), true);
549 }
550
551 /**
552 * @param {string} targetId
553 */
554 _activateTarget(targetId) {
555 this._manager.target().targetAgent().activateTarget(targetId);
556 }
557
558 _startButtonClicked() {
559 this._manager.startWorker(this._registration.scopeURL);
560 }
561
562 _skipButtonClicked() {
563 this._manager.skipWaiting(this._registration.scopeURL);
564 }
565
566 /**
567 * @param {string} versionId
568 */
569 _stopButtonClicked(versionId) {
570 this._manager.stopWorker(versionId);
571 }
572
573 /**
574 * @param {string} versionId
575 */
576 _inspectButtonClicked(versionId) {
577 this._manager.inspectWorker(versionId);
578 }
579
580 /**
581 * @param {!Element} container
582 * @return {!Element}
583 */
584 _wrapWidget(container) {
585 const shadowRoot = UI.createShadowRootWithCoreStyles(container);
586 UI.appendStyle(shadowRoot, 'resources/serviceWorkersView.css');
587 const contentElement = createElement('div');
588 shadowRoot.appendChild(contentElement);
589 return contentElement;
590 }
591};