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