chrome-devtools-frontend 1.0.1593518 → 1.0.1595090
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/contributing/settings-experiments-features.md +93 -98
- package/front_end/core/common/README.md +126 -0
- package/front_end/core/host/UserMetrics.ts +1 -2
- package/front_end/core/protocol_client/InspectorBackend.ts +4 -0
- package/front_end/core/root/ExperimentNames.ts +0 -1
- package/front_end/core/sdk/CSSModel.ts +22 -0
- package/front_end/core/sdk/CSSNavigation.ts +33 -0
- package/front_end/core/sdk/CSSRule.ts +12 -2
- package/front_end/core/sdk/EmulationModel.ts +41 -1
- package/front_end/core/sdk/sdk-meta.ts +22 -18
- package/front_end/core/sdk/sdk.ts +2 -0
- package/front_end/design_system_tokens.css +538 -259
- package/front_end/entrypoints/main/MainImpl.ts +0 -6
- package/front_end/entrypoints/main/main-meta.ts +2 -2
- package/front_end/generated/InspectorBackendCommands.ts +6 -4
- package/front_end/generated/SupportedCSSProperties.js +12 -0
- package/front_end/generated/protocol-mapping.d.ts +7 -0
- package/front_end/generated/protocol-proxy-api.d.ts +5 -0
- package/front_end/generated/protocol.ts +47 -0
- package/front_end/models/ai_code_completion/AiCodeCompletion.ts +1 -0
- package/front_end/models/emulation/DeviceModeModel.ts +47 -0
- package/front_end/models/issues_manager/Issue.ts +1 -0
- package/front_end/models/issues_manager/IssueAggregator.ts +9 -9
- package/front_end/models/issues_manager/IssuesManager.ts +5 -5
- package/front_end/models/issues_manager/SelectivePermissionsInterventionIssue.ts +65 -0
- package/front_end/models/issues_manager/descriptions/selectivePermissionsIntervention.md +7 -0
- package/front_end/models/issues_manager/issues_manager.ts +2 -4
- package/front_end/models/javascript_metadata/NativeFunctions.js +48 -9
- package/front_end/models/lighthouse/lighthouse.ts +9 -0
- package/front_end/models/persistence/persistence-meta.ts +4 -4
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +12 -1
- package/front_end/panels/ai_assistance/components/ChatInput.ts +37 -30
- package/front_end/panels/ai_assistance/components/ChatView.ts +4 -2
- package/front_end/panels/ai_assistance/components/chatInput.css +26 -1
- package/front_end/panels/application/StorageView.ts +8 -2
- package/front_end/panels/application/preloading/PreloadingView.ts +105 -7
- package/front_end/panels/application/preloading/preloadingView.css +4 -0
- package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +3 -0
- package/front_end/panels/common/AiCodeGenerationTeaser.ts +3 -0
- package/front_end/panels/console/ConsoleView.ts +2 -2
- package/front_end/panels/console/console-meta.ts +18 -14
- package/front_end/panels/elements/ComputedStyleWidget.ts +12 -7
- package/front_end/panels/elements/ElementsPanel.ts +1 -1
- package/front_end/panels/elements/ElementsTreeOutline.ts +4 -6
- package/front_end/panels/elements/ImagePreviewPopover.ts +6 -9
- package/front_end/panels/elements/StylePropertiesSection.ts +30 -0
- package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +1 -6
- package/front_end/panels/elements/StylesSidebarPane.ts +92 -7
- package/front_end/panels/elements/elements-meta.ts +12 -8
- package/front_end/panels/emulation/DeviceModeToolbar.ts +206 -66
- package/front_end/panels/issues/AffectedSelectivePermissionsInterventionView.ts +120 -0
- package/front_end/panels/issues/HiddenIssuesRow.ts +1 -1
- package/front_end/panels/issues/IssueKindView.ts +2 -1
- package/front_end/panels/issues/IssueView.ts +4 -4
- package/front_end/panels/issues/IssuesPane.ts +8 -2
- package/front_end/panels/issues/issues.ts +2 -0
- package/front_end/panels/lighthouse/LighthouseController.ts +3 -3
- package/front_end/panels/lighthouse/LighthousePanel.ts +9 -5
- package/front_end/panels/lighthouse/LighthouseProtocolService.ts +5 -5
- package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +6 -7
- package/front_end/panels/lighthouse/LighthouseReportSelector.ts +4 -3
- package/front_end/panels/lighthouse/LighthouseStartView.ts +99 -38
- package/front_end/panels/lighthouse/LighthouseTimespanView.ts +53 -14
- package/front_end/panels/lighthouse/RadioSetting.ts +33 -19
- package/front_end/panels/lighthouse/lighthouse.ts +2 -2
- package/front_end/panels/media/PlayerMessagesView.ts +62 -49
- package/front_end/panels/media/media.ts +2 -0
- package/front_end/panels/media/playerMessagesView.css +1 -1
- package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +49 -24
- package/front_end/panels/settings/FrameworkIgnoreListSettingsTab.ts +2 -2
- package/front_end/panels/sources/sources-meta.ts +8 -4
- package/front_end/panels/utils/utils.ts +11 -5
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/third_party/lit/lib/async-directive.d.ts +465 -0
- package/front_end/third_party/lit/lib/async-directive.js +23 -0
- package/front_end/third_party/lit/lib/async-directive.js.map +1 -0
- package/front_end/third_party/lit/lib/decorators.d.ts +7 -1
- package/front_end/third_party/lit/lib/decorators.js +2 -2
- package/front_end/third_party/lit/lib/decorators.js.map +1 -1
- package/front_end/third_party/lit/lib/directive.js.map +1 -1
- package/front_end/third_party/lit/lib/directives.d.ts +27 -8
- package/front_end/third_party/lit/lib/directives.js +8 -8
- package/front_end/third_party/lit/lib/directives.js.map +1 -1
- package/front_end/third_party/lit/lib/lit.d.ts +15 -5
- package/front_end/third_party/lit/lib/lit.js +4 -4
- package/front_end/third_party/lit/lib/lit.js.map +1 -1
- package/front_end/third_party/lit/lib/static-html.js +2 -2
- package/front_end/third_party/lit/lib/static-html.js.map +1 -1
- package/front_end/third_party/lit/lit.ts +2 -1
- package/front_end/third_party/lit/rollup.config.mjs +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/deno.json +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/package.json +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js.map +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.ts +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js.map +1 -1
- package/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.ts +2 -2
- package/front_end/ui/components/markdown_view/MarkdownLinksMap.ts +5 -0
- package/front_end/ui/legacy/SuggestBox.ts +4 -0
- package/front_end/ui/legacy/TextPrompt.ts +1 -1
- package/front_end/ui/legacy/components/utils/ImagePreview.ts +27 -23
- package/front_end/ui/lit/lit.ts +1 -0
- package/front_end/ui/visual_logging/Debugging.ts +1 -1
- package/front_end/ui/visual_logging/KnownContextValues.ts +19 -0
- package/inspector_overlay/highlight_grid_common.ts +11 -8
- package/package.json +1 -1
- package/front_end/models/issues_manager/ContrastCheckTrigger.ts +0 -78
- package/front_end/models/issues_manager/LowTextContrastIssue.ts +0 -63
- package/front_end/panels/issues/AffectedElementsWithLowContrastView.ts +0 -91
- /package/front_end/{panels → models}/lighthouse/LighthouseReporterTypes.ts +0 -0
|
@@ -10,6 +10,7 @@ import * as Host from '../../core/host/host.js';
|
|
|
10
10
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
11
11
|
import * as Platform from '../../core/platform/platform.js';
|
|
12
12
|
import * as EmulationModel from '../../models/emulation/emulation.js';
|
|
13
|
+
import * as uiI18n from '../../ui/i18n/i18n.js';
|
|
13
14
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
14
15
|
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
|
|
15
16
|
import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';
|
|
@@ -19,8 +20,14 @@ const UIStrings = {
|
|
|
19
20
|
/**
|
|
20
21
|
* @description Title of the device dimensions selection item in the Device Mode Toolbar.
|
|
21
22
|
* webpage in pixels.
|
|
23
|
+
* @example {Responsive} PH1
|
|
22
24
|
*/
|
|
23
|
-
dimensions: 'Dimensions',
|
|
25
|
+
dimensions: 'Dimensions: {PH1}',
|
|
26
|
+
/**
|
|
27
|
+
* @description Title of the device pixel ratio selection item in the Device Mode Toolbar.
|
|
28
|
+
* @example {2.0} PH1
|
|
29
|
+
*/
|
|
30
|
+
dpr: 'DPR: {PH1}',
|
|
24
31
|
/**
|
|
25
32
|
* @description Title of the width input textbox in the Device Mode Toolbar, for the width of the
|
|
26
33
|
* webpage in pixels.
|
|
@@ -53,12 +60,12 @@ const UIStrings = {
|
|
|
53
60
|
*/
|
|
54
61
|
moreOptions: 'More options',
|
|
55
62
|
/**
|
|
56
|
-
* @description A
|
|
57
|
-
*
|
|
58
|
-
*
|
|
63
|
+
* @description A menu item in the drop-down box that allows the user to select the zoom level.
|
|
64
|
+
* Labels the value which corresponds to the 'fit to window' zoom level, represented by the
|
|
65
|
+
* placeholder, which is a number. In the Device Mode Toolbar.
|
|
59
66
|
* @example {30.0} PH1
|
|
60
67
|
*/
|
|
61
|
-
|
|
68
|
+
fitToWindowPercentage: '{PH1}% (fit to window)',
|
|
62
69
|
/**
|
|
63
70
|
* @description A checkbox setting that appears in the context menu for the zoom level, in the
|
|
64
71
|
* Device Mode Toolbar.
|
|
@@ -70,7 +77,7 @@ const UIStrings = {
|
|
|
70
77
|
* placeholder, which is a number. In the Device Mode Toolbar.
|
|
71
78
|
* @example {4.3} PH1
|
|
72
79
|
*/
|
|
73
|
-
defaultF: '
|
|
80
|
+
defaultF: '{PH1} (default)',
|
|
74
81
|
/**
|
|
75
82
|
* @description Command to hide the frame (like a picture frame) around the mobile device screen.
|
|
76
83
|
*/
|
|
@@ -163,6 +170,11 @@ const UIStrings = {
|
|
|
163
170
|
* @description Tooltip of the rotate/screen orientation button.
|
|
164
171
|
*/
|
|
165
172
|
screenOrientationOptions: 'Screen orientation options',
|
|
173
|
+
/**
|
|
174
|
+
* @description Tooltip shown on the rotate button when screen orientation is locked by the page
|
|
175
|
+
* via screen.orientation.lock().
|
|
176
|
+
*/
|
|
177
|
+
screenOrientationLocked: 'Screen orientation is locked by the page',
|
|
166
178
|
/**
|
|
167
179
|
* @description Tooltip for a button which turns on/off dual-screen mode, which emulates devices
|
|
168
180
|
* like tablets which have two screens.
|
|
@@ -284,6 +296,9 @@ export class DeviceModeToolbar {
|
|
|
284
296
|
new UI.Toolbar.ToolbarMenuButton(this.appendDeviceMenuItems.bind(this), undefined, undefined, 'device');
|
|
285
297
|
this.deviceSelectItem.turnShrinkable();
|
|
286
298
|
this.deviceSelectItem.setDarkText();
|
|
299
|
+
const dimensionsSpan =
|
|
300
|
+
uiI18n.getFormatLocalizedString(str_, UIStrings.dimensions, {PH1: this.deviceSelectItem.element});
|
|
301
|
+
mainToolbar.append(...dimensionsSpan.childNodes);
|
|
287
302
|
mainToolbar.appendToolbarItem(this.deviceSelectItem);
|
|
288
303
|
|
|
289
304
|
this.widthInput.addEventListener('sizechanged', ({size: width}) => {
|
|
@@ -315,6 +330,11 @@ export class DeviceModeToolbar {
|
|
|
315
330
|
this.scaleItem.turnShrinkable();
|
|
316
331
|
this.scaleItem.setDarkText();
|
|
317
332
|
mainToolbar.appendToolbarItem(this.scaleItem);
|
|
333
|
+
|
|
334
|
+
const autoAdjustScaleButton = new UI.Toolbar.ToolbarSettingToggle(
|
|
335
|
+
this.autoAdjustScaleSetting, 'center-focus-weak', i18nString(UIStrings.autoadjustZoom));
|
|
336
|
+
mainToolbar.appendToolbarItem(autoAdjustScaleButton);
|
|
337
|
+
|
|
318
338
|
mainToolbar.appendToolbarItem(new UI.Toolbar.ToolbarItem(this.createEmptyToolbarElement()));
|
|
319
339
|
|
|
320
340
|
this.deviceScaleItem = new UI.Toolbar.ToolbarMenuButton(
|
|
@@ -323,6 +343,8 @@ export class DeviceModeToolbar {
|
|
|
323
343
|
this.deviceScaleItem.setVisible(this.showDeviceScaleFactorSetting.get());
|
|
324
344
|
setTitleForButton(this.deviceScaleItem, i18nString(UIStrings.devicePixelRatio));
|
|
325
345
|
this.deviceScaleItem.setDarkText();
|
|
346
|
+
const deviceScaleSpan = uiI18n.getFormatLocalizedString(str_, UIStrings.dpr, {PH1: this.deviceScaleItem.element});
|
|
347
|
+
mainToolbar.append(...deviceScaleSpan.childNodes);
|
|
326
348
|
mainToolbar.appendToolbarItem(this.deviceScaleItem);
|
|
327
349
|
mainToolbar.appendToolbarItem(new UI.Toolbar.ToolbarItem(this.createEmptyToolbarElement()));
|
|
328
350
|
this.uaItem =
|
|
@@ -374,11 +396,16 @@ export class DeviceModeToolbar {
|
|
|
374
396
|
return optionsToolbar;
|
|
375
397
|
}
|
|
376
398
|
|
|
399
|
+
private getDevicePostureOptions(): Array<{title: string, value: string, selected: boolean}> {
|
|
400
|
+
const currentPosture = this.currentDevicePosture();
|
|
401
|
+
return ['Continuous', 'Folded'].map(title => ({title, value: title, selected: currentPosture === title}));
|
|
402
|
+
}
|
|
403
|
+
|
|
377
404
|
private appendDevicePostureItems(contextMenu: UI.ContextMenu.ContextMenu): void {
|
|
378
|
-
for (const
|
|
405
|
+
for (const option of this.getDevicePostureOptions()) {
|
|
379
406
|
contextMenu.defaultSection().appendCheckboxItem(
|
|
380
|
-
title, this.spanClicked.bind(this),
|
|
381
|
-
{checked:
|
|
407
|
+
option.title, this.spanClicked.bind(this),
|
|
408
|
+
{checked: option.selected, jslogContext: option.title.toLowerCase()});
|
|
382
409
|
}
|
|
383
410
|
}
|
|
384
411
|
|
|
@@ -392,27 +419,43 @@ export class DeviceModeToolbar {
|
|
|
392
419
|
return 'Continuous';
|
|
393
420
|
}
|
|
394
421
|
|
|
395
|
-
private
|
|
422
|
+
private getScaleOptions(): Array<{title: string, value: number, selected: boolean, jslogContext: string}> {
|
|
423
|
+
const values = [0.5, 0.75, 1, 1.25, 1.5, 2];
|
|
424
|
+
let fitValue: number|null = null;
|
|
396
425
|
if (this.model.type() === EmulationModel.DeviceModeModel.Type.Device) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
426
|
+
fitValue = this.model.fitScale();
|
|
427
|
+
const fitValuePct = (fitValue * 100).toFixed(0);
|
|
428
|
+
let found = false;
|
|
429
|
+
for (let i = 0; i < values.length; ++i) {
|
|
430
|
+
if ((values[i] * 100).toFixed(0) === fitValuePct) {
|
|
431
|
+
found = true;
|
|
432
|
+
values[i] = fitValue;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!found) {
|
|
437
|
+
values.push(fitValue);
|
|
438
|
+
values.sort((a, b) => a - b);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const currentScale = this.model.scaleSetting().get();
|
|
443
|
+
return values.map(value => {
|
|
444
|
+
let title = (value * 100).toFixed(0) + '%';
|
|
445
|
+
let jslogContext = title;
|
|
446
|
+
if (value === fitValue) {
|
|
447
|
+
title = i18nString(UIStrings.fitToWindowPercentage, {PH1: (value * 100).toFixed(0)});
|
|
448
|
+
jslogContext = 'fit-to-window';
|
|
449
|
+
}
|
|
450
|
+
return {title, value, selected: currentScale === value, jslogContext};
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private appendScaleMenuItems(contextMenu: UI.ContextMenu.ContextMenu): void {
|
|
455
|
+
for (const option of this.getScaleOptions()) {
|
|
413
456
|
contextMenu.defaultSection().appendCheckboxItem(
|
|
414
|
-
title, this.onScaleMenuChanged.bind(this, value),
|
|
415
|
-
{checked:
|
|
457
|
+
option.title, this.onScaleMenuChanged.bind(this, option.value),
|
|
458
|
+
{checked: option.selected, jslogContext: option.jslogContext});
|
|
416
459
|
}
|
|
417
460
|
}
|
|
418
461
|
|
|
@@ -420,41 +463,68 @@ export class DeviceModeToolbar {
|
|
|
420
463
|
this.model.scaleSetting().set(value);
|
|
421
464
|
}
|
|
422
465
|
|
|
423
|
-
private
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private appendDeviceScaleMenuItems(contextMenu: UI.ContextMenu.ContextMenu): void {
|
|
466
|
+
private getDeviceScaleFactorOptions():
|
|
467
|
+
Array<{title: string, value: number, selected: boolean, jslogContext: string}> {
|
|
428
468
|
const deviceScaleFactorSetting = this.model.deviceScaleFactorSetting();
|
|
429
469
|
const defaultValue = this.model.uaSetting().get() === EmulationModel.DeviceModeModel.UA.MOBILE ||
|
|
430
470
|
this.model.uaSetting().get() === EmulationModel.DeviceModeModel.UA.MOBILE_NO_TOUCH ?
|
|
431
471
|
EmulationModel.DeviceModeModel.defaultMobileScaleFactor :
|
|
432
472
|
window.devicePixelRatio;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
473
|
+
const values = [1, 2, 3];
|
|
474
|
+
if (!values.includes(defaultValue)) {
|
|
475
|
+
values.push(defaultValue);
|
|
476
|
+
values.sort((a, b) => a - b);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const currentDPR = deviceScaleFactorSetting.get();
|
|
480
|
+
return values.map(value => {
|
|
481
|
+
let title = String(value);
|
|
482
|
+
let jslogContext = `dpr-${value}`;
|
|
483
|
+
if (value === defaultValue) {
|
|
484
|
+
title = i18nString(UIStrings.defaultF, {PH1: value});
|
|
485
|
+
jslogContext = 'dpr-default';
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
title,
|
|
489
|
+
value: value === defaultValue ? 0 : value,
|
|
490
|
+
selected: currentDPR === value || (value === defaultValue && currentDPR === 0),
|
|
491
|
+
jslogContext
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
}
|
|
438
495
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
496
|
+
private appendDeviceScaleMenuItems(contextMenu: UI.ContextMenu.ContextMenu): void {
|
|
497
|
+
const deviceScaleFactorSetting = this.model.deviceScaleFactorSetting();
|
|
498
|
+
for (const option of this.getDeviceScaleFactorOptions()) {
|
|
499
|
+
contextMenu.defaultSection().appendCheckboxItem(
|
|
500
|
+
option.title, deviceScaleFactorSetting.set.bind(deviceScaleFactorSetting, option.value),
|
|
501
|
+
{checked: option.selected, jslogContext: option.jslogContext});
|
|
444
502
|
}
|
|
445
503
|
}
|
|
446
504
|
|
|
447
|
-
private
|
|
505
|
+
private getUserAgentOptions():
|
|
506
|
+
Array<{title: string, value: EmulationModel.DeviceModeModel.UA, selected: boolean, jslogContext: string}> {
|
|
448
507
|
const uaSetting = this.model.uaSetting();
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
508
|
+
const currentUserAgent = uaSetting.get();
|
|
509
|
+
return [
|
|
510
|
+
EmulationModel.DeviceModeModel.UA.MOBILE,
|
|
511
|
+
EmulationModel.DeviceModeModel.UA.MOBILE_NO_TOUCH,
|
|
512
|
+
EmulationModel.DeviceModeModel.UA.DESKTOP,
|
|
513
|
+
EmulationModel.DeviceModeModel.UA.DESKTOP_TOUCH,
|
|
514
|
+
].map(value => ({
|
|
515
|
+
title: value,
|
|
516
|
+
value,
|
|
517
|
+
selected: currentUserAgent === value,
|
|
518
|
+
jslogContext: Platform.StringUtilities.toKebabCase(value)
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
453
521
|
|
|
454
|
-
|
|
522
|
+
private appendUserAgentMenuItems(contextMenu: UI.ContextMenu.ContextMenu): void {
|
|
523
|
+
const uaSetting = this.model.uaSetting();
|
|
524
|
+
for (const option of this.getUserAgentOptions()) {
|
|
455
525
|
contextMenu.defaultSection().appendCheckboxItem(
|
|
456
|
-
title, uaSetting.set.bind(uaSetting, value),
|
|
457
|
-
{checked:
|
|
526
|
+
option.title, uaSetting.set.bind(uaSetting, option.value),
|
|
527
|
+
{checked: option.selected, jslogContext: option.jslogContext});
|
|
458
528
|
}
|
|
459
529
|
}
|
|
460
530
|
|
|
@@ -539,25 +609,75 @@ export class DeviceModeToolbar {
|
|
|
539
609
|
return this.standardDevices().concat(this.customDevices());
|
|
540
610
|
}
|
|
541
611
|
|
|
612
|
+
private getDeviceModeOptions(): {
|
|
613
|
+
responsive: {title: string, selected: boolean, jslogContext: string},
|
|
614
|
+
standard: Array<{
|
|
615
|
+
device: EmulationModel.EmulatedDevices.EmulatedDevice,
|
|
616
|
+
title: string,
|
|
617
|
+
selected: boolean,
|
|
618
|
+
jslogContext: string,
|
|
619
|
+
}>,
|
|
620
|
+
custom: Array<{
|
|
621
|
+
device: EmulationModel.EmulatedDevices.EmulatedDevice,
|
|
622
|
+
title: string,
|
|
623
|
+
selected: boolean,
|
|
624
|
+
jslogContext: string,
|
|
625
|
+
}>,
|
|
626
|
+
edit: {title: string, jslogContext: string},
|
|
627
|
+
} {
|
|
628
|
+
return {
|
|
629
|
+
responsive: {
|
|
630
|
+
title: i18nString(UIStrings.responsive),
|
|
631
|
+
selected: this.model.type() === EmulationModel.DeviceModeModel.Type.Responsive,
|
|
632
|
+
jslogContext: 'responsive'
|
|
633
|
+
},
|
|
634
|
+
standard: this.standardDevices().map(device => ({
|
|
635
|
+
device,
|
|
636
|
+
title: device.title,
|
|
637
|
+
selected: this.model.device() === device,
|
|
638
|
+
jslogContext: Platform.StringUtilities.toKebabCase(device.title)
|
|
639
|
+
})),
|
|
640
|
+
custom: this.customDevices().map(device => ({
|
|
641
|
+
device,
|
|
642
|
+
title: device.title,
|
|
643
|
+
selected: this.model.device() === device,
|
|
644
|
+
jslogContext: Platform.StringUtilities.toKebabCase(device.title)
|
|
645
|
+
})),
|
|
646
|
+
edit: {
|
|
647
|
+
title: i18nString(UIStrings.edit),
|
|
648
|
+
jslogContext: 'edit',
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
542
653
|
private appendDeviceMenuItems(contextMenu: UI.ContextMenu.ContextMenu): void {
|
|
654
|
+
const options = this.getDeviceModeOptions();
|
|
655
|
+
|
|
543
656
|
contextMenu.headerSection().appendCheckboxItem(
|
|
544
|
-
|
|
545
|
-
{checked:
|
|
546
|
-
|
|
547
|
-
appendGroup.call(this,
|
|
548
|
-
|
|
549
|
-
i18nString(UIStrings.edit), this.emulatedDevicesList.revealCustomSetting.bind(this.emulatedDevicesList),
|
|
550
|
-
{jslogContext: 'edit'});
|
|
657
|
+
options.responsive.title, this.switchToResponsive.bind(this),
|
|
658
|
+
{checked: options.responsive.selected, jslogContext: options.responsive.jslogContext});
|
|
659
|
+
|
|
660
|
+
appendGroup.call(this, options.standard);
|
|
661
|
+
appendGroup.call(this, options.custom);
|
|
551
662
|
|
|
552
|
-
|
|
553
|
-
|
|
663
|
+
contextMenu.footerSection().appendItem(
|
|
664
|
+
options.edit.title, this.emulatedDevicesList.revealCustomSetting.bind(this.emulatedDevicesList),
|
|
665
|
+
{jslogContext: options.edit.jslogContext});
|
|
666
|
+
|
|
667
|
+
function appendGroup(this: DeviceModeToolbar, group: Array<{
|
|
668
|
+
device: EmulationModel.EmulatedDevices.EmulatedDevice,
|
|
669
|
+
title: string,
|
|
670
|
+
selected: boolean,
|
|
671
|
+
jslogContext: string,
|
|
672
|
+
}>): void {
|
|
673
|
+
if (!group.length) {
|
|
554
674
|
return;
|
|
555
675
|
}
|
|
556
676
|
const section = contextMenu.section();
|
|
557
|
-
for (const
|
|
558
|
-
section.appendCheckboxItem(
|
|
559
|
-
checked:
|
|
560
|
-
jslogContext:
|
|
677
|
+
for (const item of group) {
|
|
678
|
+
section.appendCheckboxItem(item.title, this.emulateDevice.bind(this, item.device), {
|
|
679
|
+
checked: item.selected,
|
|
680
|
+
jslogContext: item.jslogContext,
|
|
561
681
|
});
|
|
562
682
|
}
|
|
563
683
|
}
|
|
@@ -617,6 +737,9 @@ export class DeviceModeToolbar {
|
|
|
617
737
|
private modeMenuClicked(event: {
|
|
618
738
|
data: Event,
|
|
619
739
|
}): void {
|
|
740
|
+
if (this.model.isScreenOrientationLocked()) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
620
743
|
const device = this.model.device();
|
|
621
744
|
const model = this.model;
|
|
622
745
|
const autoAdjustScaleSetting = this.autoAdjustScaleSetting;
|
|
@@ -737,7 +860,7 @@ export class DeviceModeToolbar {
|
|
|
737
860
|
|
|
738
861
|
const deviceScale = this.model.appliedDeviceScaleFactor();
|
|
739
862
|
if (deviceScale !== this.cachedDeviceScale) {
|
|
740
|
-
this.deviceScaleItem.setText(
|
|
863
|
+
this.deviceScaleItem.setText(`${deviceScale.toFixed(1)}`);
|
|
741
864
|
this.cachedDeviceScale = deviceScale;
|
|
742
865
|
}
|
|
743
866
|
|
|
@@ -755,7 +878,7 @@ export class DeviceModeToolbar {
|
|
|
755
878
|
if (this.model.type() === EmulationModel.DeviceModeModel.Type.Device && device) {
|
|
756
879
|
deviceItemTitle = device.title;
|
|
757
880
|
}
|
|
758
|
-
this.deviceSelectItem.setText(
|
|
881
|
+
this.deviceSelectItem.setText(deviceItemTitle);
|
|
759
882
|
|
|
760
883
|
if (this.model.device() !== this.cachedModelDevice) {
|
|
761
884
|
const device = this.model.device();
|
|
@@ -804,6 +927,23 @@ export class DeviceModeToolbar {
|
|
|
804
927
|
}
|
|
805
928
|
this.persistenceSetting.set(value);
|
|
806
929
|
}
|
|
930
|
+
|
|
931
|
+
// When screen orientation is locked by the page (via screen.orientation.lock()),
|
|
932
|
+
// disable the rotate button to prevent user-initiated rotation.
|
|
933
|
+
// When unlocked, restore the button to its normal state.
|
|
934
|
+
if (this.model.isScreenOrientationLocked()) {
|
|
935
|
+
this.modeButton.setEnabled(false);
|
|
936
|
+
setTitleForButton(this.modeButton, i18nString(UIStrings.screenOrientationLocked));
|
|
937
|
+
} else if (this.cachedModelDevice) {
|
|
938
|
+
const modeCount = this.cachedModelDevice.modes.length;
|
|
939
|
+
this.modeButton.setEnabled(modeCount >= 2);
|
|
940
|
+
setTitleForButton(
|
|
941
|
+
this.modeButton,
|
|
942
|
+
modeCount === 2 ? i18nString(UIStrings.rotate) : i18nString(UIStrings.screenOrientationOptions));
|
|
943
|
+
} else if (this.model.type() === EmulationModel.DeviceModeModel.Type.Responsive) {
|
|
944
|
+
this.modeButton.setEnabled(true);
|
|
945
|
+
setTitleForButton(this.modeButton, i18nString(UIStrings.rotate));
|
|
946
|
+
}
|
|
807
947
|
}
|
|
808
948
|
|
|
809
949
|
restore(): void {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright 2026 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
|
|
5
|
+
import * as i18n from '../../core/i18n/i18n.js';
|
|
6
|
+
import type * as Platform from '../../core/platform/platform.js';
|
|
7
|
+
import type * as SDK from '../../core/sdk/sdk.js';
|
|
8
|
+
import type * as Protocol from '../../generated/protocol.js';
|
|
9
|
+
import * as Bindings from '../../models/bindings/bindings.js';
|
|
10
|
+
import type * as IssuesManager from '../../models/issues_manager/issues_manager.js';
|
|
11
|
+
import * as Components from '../../ui/legacy/components/utils/utils.js';
|
|
12
|
+
import * as UI from '../../ui/legacy/legacy.js';
|
|
13
|
+
import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
|
|
14
|
+
|
|
15
|
+
import {AffectedResourcesView} from './AffectedResourcesView.js';
|
|
16
|
+
|
|
17
|
+
const UIStrings = {
|
|
18
|
+
/**
|
|
19
|
+
* @description Label for number of affected resources indication in issue view
|
|
20
|
+
*/
|
|
21
|
+
nViolations: '{n, plural, =1 {# violation} other {# violations}}',
|
|
22
|
+
/**
|
|
23
|
+
* @description Title for the API column in the Selective Permissions Intervention affected resources list
|
|
24
|
+
*/
|
|
25
|
+
api: 'API',
|
|
26
|
+
/**
|
|
27
|
+
* @description Title for the Script column in the Selective Permissions Intervention affected resources list
|
|
28
|
+
*/
|
|
29
|
+
script: 'Script',
|
|
30
|
+
/**
|
|
31
|
+
* @description Title for the Ad Ancestry column in the Selective Permissions Intervention affected resources list
|
|
32
|
+
*/
|
|
33
|
+
adAncestry: 'Ad Ancestry',
|
|
34
|
+
/**
|
|
35
|
+
* @description Text for unknown value
|
|
36
|
+
*/
|
|
37
|
+
unknown: 'unknown',
|
|
38
|
+
/**
|
|
39
|
+
* @description Text for loading state
|
|
40
|
+
*/
|
|
41
|
+
loading: 'loading…',
|
|
42
|
+
} as const;
|
|
43
|
+
const str_ = i18n.i18n.registerUIStrings('panels/issues/AffectedSelectivePermissionsInterventionView.ts', UIStrings);
|
|
44
|
+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
45
|
+
|
|
46
|
+
export class AffectedSelectivePermissionsInterventionView extends AffectedResourcesView {
|
|
47
|
+
readonly #linkifier = new Components.Linkifier.Linkifier();
|
|
48
|
+
|
|
49
|
+
protected getResourceNameWithCount(count: number): string {
|
|
50
|
+
return i18nString(UIStrings.nViolations, {n: count});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#render(): void {
|
|
54
|
+
const issues = Array.from(this.issue.getSelectivePermissionsInterventionIssues());
|
|
55
|
+
// eslint-disable-next-line @devtools/no-lit-render-outside-of-view
|
|
56
|
+
render(
|
|
57
|
+
html`
|
|
58
|
+
<tr>
|
|
59
|
+
<td class="affected-resource-header">${i18nString(UIStrings.api)}</td>
|
|
60
|
+
<td class="affected-resource-header">${i18nString(UIStrings.script)}</td>
|
|
61
|
+
<td class="affected-resource-header">${i18nString(UIStrings.adAncestry)}</td>
|
|
62
|
+
</tr>
|
|
63
|
+
${issues.map(issue => this.#renderDetail(issue))}
|
|
64
|
+
`,
|
|
65
|
+
this.affectedResources, {host: this});
|
|
66
|
+
this.updateAffectedResourceCount(issues.length);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#renderDetail(issue: IssuesManager.SelectivePermissionsInterventionIssue.SelectivePermissionsInterventionIssue):
|
|
70
|
+
LitTemplate {
|
|
71
|
+
const details = issue.details();
|
|
72
|
+
const issuesModel = issue.model();
|
|
73
|
+
|
|
74
|
+
const stackTracePromise = (details.stackTrace && issuesModel) ?
|
|
75
|
+
this.#resolveStackTrace(details.stackTrace, issuesModel) :
|
|
76
|
+
Promise.resolve(html`<span>${i18nString(UIStrings.unknown)}</span>`);
|
|
77
|
+
|
|
78
|
+
const target = issuesModel ? issuesModel.target() : null;
|
|
79
|
+
|
|
80
|
+
return html`
|
|
81
|
+
<tr class="affected-resource-directive">
|
|
82
|
+
<td>${details.apiName}</td>
|
|
83
|
+
<td>${Directives.until(stackTracePromise, html`<span>${i18nString(UIStrings.loading)}</span>`)}</td>
|
|
84
|
+
<td class="affected-resource-cell">
|
|
85
|
+
<div class="ad-ancestry-list">
|
|
86
|
+
${(details.adAncestry?.adAncestryChain || []).map(script => {
|
|
87
|
+
const link = this.#linkifier.linkifyScriptLocation(
|
|
88
|
+
target, script.scriptId, script.name as Platform.DevToolsPath.UrlString, 0);
|
|
89
|
+
return html`<div>${link}</div>`;
|
|
90
|
+
})}
|
|
91
|
+
${
|
|
92
|
+
details.adAncestry?.rootScriptFilterlistRule ?
|
|
93
|
+
html`<div>Rule: ${details.adAncestry.rootScriptFilterlistRule}</div>` :
|
|
94
|
+
nothing}
|
|
95
|
+
</div>
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async #resolveStackTrace(stackTrace: Protocol.Runtime.StackTrace, issuesModel: SDK.IssuesModel.IssuesModel):
|
|
102
|
+
Promise<LitTemplate> {
|
|
103
|
+
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
|
|
104
|
+
const stackTraceTranslated =
|
|
105
|
+
await debuggerWorkspaceBinding.createStackTraceFromProtocolRuntime(stackTrace, issuesModel.target());
|
|
106
|
+
return html`
|
|
107
|
+
<devtools-widget .widgetConfig=${UI.Widget.widgetConfig(Components.JSPresentationUtils.StackTracePreviewContent, {
|
|
108
|
+
stackTrace: stackTraceTranslated,
|
|
109
|
+
options: {expandable: true},
|
|
110
|
+
})}>
|
|
111
|
+
</devtools-widget>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
update(): void {
|
|
116
|
+
this.requestResolver.clear();
|
|
117
|
+
this.#linkifier.reset();
|
|
118
|
+
this.#render();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -66,7 +66,7 @@ export class HiddenIssuesRow extends UI.TreeOutline.TreeElement {
|
|
|
66
66
|
#view: View;
|
|
67
67
|
|
|
68
68
|
constructor(view: View = DEFAULT_VIEW) {
|
|
69
|
-
super(undefined, true);
|
|
69
|
+
super(undefined, true, 'hidden-issues');
|
|
70
70
|
this.#view = view;
|
|
71
71
|
this.toggleOnClick = true;
|
|
72
72
|
this.listItemElement.classList.add('issue-category', 'hidden-issues');
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import * as Common from '../../core/common/common.js';
|
|
7
7
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
8
|
+
import * as Platform from '../../core/platform/platform.js';
|
|
8
9
|
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
|
|
9
10
|
import * as Adorners from '../../ui/components/adorners/adorners.js';
|
|
10
11
|
import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js';
|
|
@@ -64,7 +65,7 @@ export class IssueKindView extends UI.TreeOutline.TreeElement {
|
|
|
64
65
|
#issueCount: HTMLElement;
|
|
65
66
|
|
|
66
67
|
constructor(kind: IssuesManager.Issue.IssueKind) {
|
|
67
|
-
super(undefined, true);
|
|
68
|
+
super(undefined, true, Platform.StringUtilities.toKebabCase(kind));
|
|
68
69
|
this.#kind = kind;
|
|
69
70
|
this.#issueCount = document.createElement('span');
|
|
70
71
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as Common from '../../core/common/common.js';
|
|
7
7
|
import * as Host from '../../core/host/host.js';
|
|
8
8
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
9
|
-
import
|
|
9
|
+
import * as Platform from '../../core/platform/platform.js';
|
|
10
10
|
import * as Protocol from '../../generated/protocol.js';
|
|
11
11
|
import type {AggregatedIssue} from '../../models/issues_manager/IssueAggregator.js';
|
|
12
12
|
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
|
|
@@ -24,12 +24,12 @@ import {AffectedDescendantsWithinSelectElementView} from './AffectedDescendantsW
|
|
|
24
24
|
import {AffectedDirectivesView} from './AffectedDirectivesView.js';
|
|
25
25
|
import {AffectedDocumentsInQuirksModeView} from './AffectedDocumentsInQuirksModeView.js';
|
|
26
26
|
import {AffectedElementsView} from './AffectedElementsView.js';
|
|
27
|
-
import {AffectedElementsWithLowContrastView} from './AffectedElementsWithLowContrastView.js';
|
|
28
27
|
import {AffectedHeavyAdView} from './AffectedHeavyAdView.js';
|
|
29
28
|
import {AffectedMetadataAllowedSitesView} from './AffectedMetadataAllowedSitesView.js';
|
|
30
29
|
import {AffectedPartitioningBlobURLView} from './AffectedPartitioningBlobURLView.js';
|
|
31
30
|
import {AffectedPermissionElementsView} from './AffectedPermissionElementsView.js';
|
|
32
31
|
import {AffectedItem, AffectedResourcesView, extractShortPath} from './AffectedResourcesView.js';
|
|
32
|
+
import {AffectedSelectivePermissionsInterventionView} from './AffectedSelectivePermissionsInterventionView.js';
|
|
33
33
|
import {AffectedSharedArrayBufferIssueDetailsView} from './AffectedSharedArrayBufferIssueDetailsView.js';
|
|
34
34
|
import {AffectedSourcesView} from './AffectedSourcesView.js';
|
|
35
35
|
import {AffectedTrackingSitesView} from './AffectedTrackingSitesView.js';
|
|
@@ -230,7 +230,7 @@ export class IssueView extends UI.TreeOutline.TreeElement {
|
|
|
230
230
|
#contentCreated = false;
|
|
231
231
|
|
|
232
232
|
constructor(issue: AggregatedIssue, description: IssuesManager.MarkdownIssueDescription.IssueDescription) {
|
|
233
|
-
super();
|
|
233
|
+
super(undefined, undefined, Platform.StringUtilities.toKebabCase(issue.getCategory()));
|
|
234
234
|
this.#issue = issue;
|
|
235
235
|
this.#description = description;
|
|
236
236
|
this.#throttle = new Common.Throttler.Throttler(250);
|
|
@@ -251,7 +251,6 @@ export class IssueView extends UI.TreeOutline.TreeElement {
|
|
|
251
251
|
new AffectedDirectivesView(this, this.#issue, 'directives-details'),
|
|
252
252
|
new AffectedBlockedByResponseView(this, this.#issue, 'blocked-by-response-details'),
|
|
253
253
|
new AffectedSharedArrayBufferIssueDetailsView(this, this.#issue, 'sab-details'),
|
|
254
|
-
new AffectedElementsWithLowContrastView(this, this.#issue, 'low-contrast-details'),
|
|
255
254
|
new CorsIssueDetailsView(this, this.#issue, 'cors-details'),
|
|
256
255
|
new GenericIssueDetailsView(this, this.#issue, 'generic-details'),
|
|
257
256
|
new AffectedDocumentsInQuirksModeView(this, this.#issue, 'affected-documents'),
|
|
@@ -262,6 +261,7 @@ export class IssueView extends UI.TreeOutline.TreeElement {
|
|
|
262
261
|
new AffectedDescendantsWithinSelectElementView(this, this.#issue, 'disallowed-select-descendants-details'),
|
|
263
262
|
new AffectedPartitioningBlobURLView(this, this.#issue, 'partitioning-blob-url-details'),
|
|
264
263
|
new AffectedPermissionElementsView(this, this.#issue, 'permission-element-elements'),
|
|
264
|
+
new AffectedSelectivePermissionsInterventionView(this, this.#issue, 'selective-permissions-intervention-details'),
|
|
265
265
|
];
|
|
266
266
|
this.#hiddenIssuesMenu = new Components.HideIssuesMenu.HideIssuesMenu();
|
|
267
267
|
this.#aggregatedIssuesCount = null;
|
|
@@ -7,7 +7,7 @@ import '../../ui/legacy/legacy.js';
|
|
|
7
7
|
|
|
8
8
|
import * as Common from '../../core/common/common.js';
|
|
9
9
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
10
|
-
import
|
|
10
|
+
import * as Platform from '../../core/platform/platform.js';
|
|
11
11
|
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
|
|
12
12
|
import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js';
|
|
13
13
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
@@ -110,6 +110,10 @@ const UIStrings = {
|
|
|
110
110
|
* @description Category title for a group of permission element issues
|
|
111
111
|
*/
|
|
112
112
|
permissionElement: 'PEPC Element',
|
|
113
|
+
/**
|
|
114
|
+
* @description Category title for the different 'Selective Permissions Intervention' issues.
|
|
115
|
+
*/
|
|
116
|
+
selectivePermissionsIntervention: 'Selective Permissions Intervention',
|
|
113
117
|
} as const;
|
|
114
118
|
const str_ = i18n.i18n.registerUIStrings('panels/issues/IssuesPane.ts', UIStrings);
|
|
115
119
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
@@ -121,7 +125,7 @@ class IssueCategoryView extends UI.TreeOutline.TreeElement {
|
|
|
121
125
|
#category: IssuesManager.Issue.IssueCategory;
|
|
122
126
|
|
|
123
127
|
constructor(category: IssuesManager.Issue.IssueCategory) {
|
|
124
|
-
super();
|
|
128
|
+
super(undefined, undefined, Platform.StringUtilities.toKebabCase(category));
|
|
125
129
|
this.#category = category;
|
|
126
130
|
|
|
127
131
|
this.toggleOnClick = true;
|
|
@@ -153,6 +157,8 @@ class IssueCategoryView extends UI.TreeOutline.TreeElement {
|
|
|
153
157
|
return i18nString(UIStrings.generic);
|
|
154
158
|
case IssuesManager.Issue.IssueCategory.PERMISSION_ELEMENT:
|
|
155
159
|
return i18nString(UIStrings.permissionElement);
|
|
160
|
+
case IssuesManager.Issue.IssueCategory.SELECTIVE_PERMISSIONS_INTERVENTION:
|
|
161
|
+
return i18nString(UIStrings.selectivePermissionsIntervention);
|
|
156
162
|
case IssuesManager.Issue.IssueCategory.OTHER:
|
|
157
163
|
return i18nString(UIStrings.other);
|
|
158
164
|
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// Use of this source code is governed by a BSD-style license that can be
|
|
3
3
|
// found in the LICENSE file.
|
|
4
4
|
|
|
5
|
+
import * as AffectedSelectivePermissionsInterventionView from './AffectedSelectivePermissionsInterventionView.js';
|
|
5
6
|
import * as IssueRevealer from './IssueRevealer.js';
|
|
6
7
|
import * as IssuesPane from './IssuesPane.js';
|
|
7
8
|
import * as IssueView from './IssueView.js';
|
|
8
9
|
|
|
9
10
|
export {
|
|
11
|
+
AffectedSelectivePermissionsInterventionView,
|
|
10
12
|
IssueRevealer,
|
|
11
13
|
IssuesPane,
|
|
12
14
|
IssueView,
|