chrome-devtools-frontend 1.0.1512147 → 1.0.1512349
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/front_end/Images/src/ai-explorer-badge.svg +114 -0
- package/front_end/Images/src/code-whisperer-badge.svg +166 -0
- package/front_end/Images/src/devtools-user-badge.svg +129 -0
- package/front_end/Images/src/dom-detective-badge.svg +136 -0
- package/front_end/Images/src/speedster-badge.svg +166 -0
- package/front_end/core/host/GdpClient.ts +38 -2
- package/front_end/core/i18n/NumberFormatter.ts +7 -0
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +3 -19
- package/front_end/models/ai_assistance/ai_assistance.ts +1 -1
- package/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts +7 -6
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +119 -119
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +43 -52
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +100 -100
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +11 -17
- package/front_end/models/ai_assistance/data_formatters/UnitFormatters.ts +151 -0
- package/front_end/models/badges/Badge.ts +7 -4
- package/front_end/models/badges/DOMDetectiveBadge.ts +20 -0
- package/front_end/models/badges/SpeedsterBadge.ts +4 -1
- package/front_end/models/badges/StarterBadge.ts +5 -1
- package/front_end/models/badges/UserBadges.ts +33 -7
- package/front_end/models/trace/ModelImpl.ts +0 -13
- package/front_end/panels/common/BadgeNotification.ts +119 -9
- package/front_end/panels/common/badgeNotification.css +4 -0
- package/front_end/panels/elements/ElementsTreeElement.ts +12 -0
- package/front_end/panels/elements/ElementsTreeOutline.ts +3 -0
- package/front_end/panels/elements/StylePropertiesSection.ts +3 -0
- package/front_end/panels/elements/StylePropertyTreeElement.ts +5 -0
- package/front_end/panels/settings/SettingsScreen.ts +3 -9
- package/front_end/panels/settings/components/SyncSection.ts +6 -2
- package/front_end/panels/timeline/TimelinePanel.ts +21 -9
- package/front_end/panels/timeline/TimelineUIUtils.ts +4 -3
- package/front_end/ui/legacy/filter.css +1 -1
- package/front_end/ui/legacy/inspectorCommon.css +1 -1
- package/front_end/ui/legacy/softDropDownButton.css +1 -1
- package/package.json +1 -1
- package/front_end/models/ai_assistance/data_formatters/Types.ts +0 -9
@@ -0,0 +1,151 @@
|
|
1
|
+
// Copyright 2025 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
|
+
/**
|
6
|
+
* This module contains unit formatters that are only to be used within
|
7
|
+
* the AI models because they do not account for locales other than en-US.
|
8
|
+
*/
|
9
|
+
const defaultTimeFormatterOptions: Intl.NumberFormatOptions = {
|
10
|
+
style: 'unit',
|
11
|
+
unitDisplay: 'narrow',
|
12
|
+
minimumFractionDigits: 0,
|
13
|
+
maximumFractionDigits: 1,
|
14
|
+
} as const;
|
15
|
+
|
16
|
+
const defaultByteFormatterOptions: Intl.NumberFormatOptions = {
|
17
|
+
style: 'unit',
|
18
|
+
unitDisplay: 'narrow',
|
19
|
+
minimumFractionDigits: 0,
|
20
|
+
maximumFractionDigits: 1,
|
21
|
+
} as const;
|
22
|
+
|
23
|
+
const timeFormatters = {
|
24
|
+
milli: new Intl.NumberFormat('en-US', {
|
25
|
+
...defaultTimeFormatterOptions,
|
26
|
+
unit: 'millisecond',
|
27
|
+
}),
|
28
|
+
second: new Intl.NumberFormat('en-US', {
|
29
|
+
...defaultTimeFormatterOptions,
|
30
|
+
unit: 'second',
|
31
|
+
}),
|
32
|
+
micro: new Intl.NumberFormat('en-US', {
|
33
|
+
...defaultTimeFormatterOptions,
|
34
|
+
unit: 'microsecond',
|
35
|
+
}),
|
36
|
+
} as const;
|
37
|
+
|
38
|
+
const byteFormatters = {
|
39
|
+
bytes: new Intl.NumberFormat('en-US', {
|
40
|
+
...defaultByteFormatterOptions,
|
41
|
+
// Don't need as much precision on bytes.
|
42
|
+
minimumFractionDigits: 0,
|
43
|
+
maximumFractionDigits: 0,
|
44
|
+
unit: 'byte',
|
45
|
+
}),
|
46
|
+
kilobytes: new Intl.NumberFormat('en-US', {
|
47
|
+
...defaultByteFormatterOptions,
|
48
|
+
unit: 'kilobyte',
|
49
|
+
}),
|
50
|
+
megabytes: new Intl.NumberFormat('en-US', {
|
51
|
+
...defaultByteFormatterOptions,
|
52
|
+
unit: 'megabyte',
|
53
|
+
}),
|
54
|
+
} as const;
|
55
|
+
|
56
|
+
function numberIsTooLarge(x: number): boolean {
|
57
|
+
return !Number.isFinite(x) || x === Number.MAX_VALUE;
|
58
|
+
}
|
59
|
+
|
60
|
+
export function seconds(x: number): string {
|
61
|
+
if (numberIsTooLarge(x)) {
|
62
|
+
return '-';
|
63
|
+
}
|
64
|
+
if (x === 0) {
|
65
|
+
return formatAndEnsureSpace(timeFormatters.second, x);
|
66
|
+
}
|
67
|
+
|
68
|
+
const asMilli = x * 1_000;
|
69
|
+
|
70
|
+
if (asMilli < 1) {
|
71
|
+
return micros(x * 1_000_000);
|
72
|
+
}
|
73
|
+
|
74
|
+
if (asMilli < 1_000) {
|
75
|
+
return millis(asMilli);
|
76
|
+
}
|
77
|
+
return formatAndEnsureSpace(timeFormatters.second, x);
|
78
|
+
}
|
79
|
+
|
80
|
+
export function millis(x: number): string {
|
81
|
+
if (numberIsTooLarge(x)) {
|
82
|
+
return '-';
|
83
|
+
}
|
84
|
+
return formatAndEnsureSpace(timeFormatters.milli, x);
|
85
|
+
}
|
86
|
+
|
87
|
+
export function micros(x: number): string {
|
88
|
+
if (numberIsTooLarge(x)) {
|
89
|
+
return '-';
|
90
|
+
}
|
91
|
+
|
92
|
+
if (x < 100) {
|
93
|
+
return formatAndEnsureSpace(timeFormatters.micro, x);
|
94
|
+
}
|
95
|
+
|
96
|
+
const asMilli = x / 1_000;
|
97
|
+
return millis(asMilli);
|
98
|
+
}
|
99
|
+
|
100
|
+
export function bytes(x: number): string {
|
101
|
+
if (x < 1_000) {
|
102
|
+
return formatAndEnsureSpace(byteFormatters.bytes, x);
|
103
|
+
}
|
104
|
+
const kilobytes = x / 1_000;
|
105
|
+
if (kilobytes < 1_000) {
|
106
|
+
return formatAndEnsureSpace(byteFormatters.kilobytes, kilobytes);
|
107
|
+
}
|
108
|
+
|
109
|
+
const megabytes = kilobytes / 1_000;
|
110
|
+
return formatAndEnsureSpace(byteFormatters.megabytes, megabytes);
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* When using 'narrow' unitDisplay, many locales exclude the space between the literal and the unit.
|
115
|
+
* We don't like that, so when there is no space literal we inject the provided separator manually.
|
116
|
+
*/
|
117
|
+
function formatAndEnsureSpace(formatter: Intl.NumberFormat, value: number, separator = '\xA0'): string {
|
118
|
+
const parts = formatter.formatToParts(value);
|
119
|
+
|
120
|
+
let hasSpace = false;
|
121
|
+
for (const part of parts) {
|
122
|
+
if (part.type === 'literal') {
|
123
|
+
if (part.value === ' ') {
|
124
|
+
hasSpace = true;
|
125
|
+
part.value = separator;
|
126
|
+
} else if (part.value === separator) {
|
127
|
+
hasSpace = true;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
if (hasSpace) {
|
133
|
+
return parts.map(part => part.value).join('');
|
134
|
+
}
|
135
|
+
|
136
|
+
const unitIndex = parts.findIndex(part => part.type === 'unit');
|
137
|
+
|
138
|
+
// Unexpected for there to be no unit, but just in case, handle that.
|
139
|
+
if (unitIndex === -1) {
|
140
|
+
return parts.map(part => part.value).join('');
|
141
|
+
}
|
142
|
+
|
143
|
+
// For locales where the unit comes first (sw), the space has to come after the unit.
|
144
|
+
if (unitIndex === 0) {
|
145
|
+
return parts[0].value + separator + parts.slice(1).map(part => part.value).join('');
|
146
|
+
}
|
147
|
+
|
148
|
+
// Otherwise, it comes before.
|
149
|
+
return parts.slice(0, unitIndex).map(part => part.value).join('') + separator +
|
150
|
+
parts.slice(unitIndex).map(part => part.value).join('');
|
151
|
+
}
|
@@ -6,29 +6,32 @@ import * as Common from '../../core/common/common.js';
|
|
6
6
|
|
7
7
|
export enum BadgeAction {
|
8
8
|
CSS_RULE_MODIFIED = 'css-rule-modified',
|
9
|
+
DOM_ELEMENT_OR_ATTRIBUTE_EDITED = 'dom-element-or-attribute-edited',
|
10
|
+
MODERN_DOM_BADGE_CLICKED = 'modern-dom-badge-clicked',
|
9
11
|
PERFORMANCE_INSIGHT_CLICKED = 'performance-insight-clicked',
|
10
12
|
}
|
11
13
|
|
12
14
|
export type BadgeActionEvents = Record<BadgeAction, void>;
|
13
15
|
|
14
16
|
export interface BadgeContext {
|
15
|
-
|
17
|
+
onTriggerBadge: (badge: Badge) => void;
|
16
18
|
badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
|
17
19
|
}
|
18
20
|
|
19
21
|
export abstract class Badge {
|
20
|
-
#
|
22
|
+
#onTriggerBadge: (badge: Badge) => void;
|
21
23
|
#badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
|
22
24
|
#eventListeners: Common.EventTarget.EventDescriptor[] = [];
|
23
25
|
#triggeredBefore = false;
|
24
26
|
|
25
27
|
abstract readonly name: string;
|
26
28
|
abstract readonly title: string;
|
29
|
+
abstract readonly imageUri: string;
|
27
30
|
abstract readonly interestedActions: readonly BadgeAction[];
|
28
31
|
readonly isStarterBadge: boolean = false;
|
29
32
|
|
30
33
|
constructor(context: BadgeContext) {
|
31
|
-
this.#
|
34
|
+
this.#onTriggerBadge = context.onTriggerBadge;
|
32
35
|
this.#badgeActionEventTarget = context.badgeActionEventTarget;
|
33
36
|
}
|
34
37
|
|
@@ -40,7 +43,7 @@ export abstract class Badge {
|
|
40
43
|
|
41
44
|
this.#triggeredBefore = true;
|
42
45
|
this.deactivate();
|
43
|
-
this.#
|
46
|
+
this.#onTriggerBadge(this);
|
44
47
|
}
|
45
48
|
|
46
49
|
activate(): void {
|
@@ -0,0 +1,20 @@
|
|
1
|
+
// Copyright 2025 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 {Badge, BadgeAction} from './Badge.js';
|
6
|
+
|
7
|
+
const DOM_DETECTIVE_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
|
8
|
+
export class DOMDetectiveBadge extends Badge {
|
9
|
+
override readonly name = 'awards/dom-detective-badge';
|
10
|
+
override readonly title = 'DOM Detective';
|
11
|
+
override readonly imageUri = DOM_DETECTIVE_BADGE_IMAGE_URI;
|
12
|
+
|
13
|
+
override readonly interestedActions = [
|
14
|
+
BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
|
15
|
+
] as const;
|
16
|
+
|
17
|
+
handleAction(_action: BadgeAction): void {
|
18
|
+
this.trigger();
|
19
|
+
}
|
20
|
+
}
|
@@ -4,10 +4,13 @@
|
|
4
4
|
|
5
5
|
import {Badge, BadgeAction} from './Badge.js';
|
6
6
|
|
7
|
+
const SPEEDSTER_BADGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
|
7
8
|
export class SpeedsterBadge extends Badge {
|
8
|
-
|
9
|
+
// TODO(ergunsh): Update the name to be the actual badge for DevTools.
|
10
|
+
override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Flegacy%2Ftest';
|
9
11
|
override readonly title = 'Speedster';
|
10
12
|
override readonly interestedActions = [BadgeAction.PERFORMANCE_INSIGHT_CLICKED] as const;
|
13
|
+
override readonly imageUri = SPEEDSTER_BADGE_URI;
|
11
14
|
|
12
15
|
handleAction(_action: BadgeAction): void {
|
13
16
|
this.trigger();
|
@@ -4,14 +4,18 @@
|
|
4
4
|
|
5
5
|
import {Badge, BadgeAction} from './Badge.js';
|
6
6
|
|
7
|
+
const STARTER_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
|
7
8
|
export class StarterBadge extends Badge {
|
8
9
|
override readonly isStarterBadge = true;
|
9
|
-
|
10
|
+
// TODO(ergunsh): Update the name to be the actual badge for DevTools.
|
11
|
+
override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Fprofile%2Fcreated-profile';
|
10
12
|
override readonly title = 'Chrome DevTools User';
|
13
|
+
override readonly imageUri = STARTER_BADGE_IMAGE_URI;
|
11
14
|
|
12
15
|
// TODO(ergunsh): Add remaining non-trivial event definitions
|
13
16
|
override readonly interestedActions = [
|
14
17
|
BadgeAction.CSS_RULE_MODIFIED,
|
18
|
+
BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
|
15
19
|
] as const;
|
16
20
|
|
17
21
|
handleAction(_action: BadgeAction): void {
|
@@ -6,6 +6,7 @@ import * as Common from '../../core/common/common.js';
|
|
6
6
|
import * as Host from '../../core/host/host.js';
|
7
7
|
|
8
8
|
import type {Badge, BadgeAction, BadgeActionEvents, BadgeContext} from './Badge.js';
|
9
|
+
import {DOMDetectiveBadge} from './DOMDetectiveBadge.js';
|
9
10
|
import {SpeedsterBadge} from './SpeedsterBadge.js';
|
10
11
|
import {StarterBadge} from './StarterBadge.js';
|
11
12
|
|
@@ -23,12 +24,13 @@ let userBadgesInstance: UserBadges|undefined = undefined;
|
|
23
24
|
export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
24
25
|
readonly #badgeActionEventTarget = new Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>();
|
25
26
|
|
26
|
-
#receiveBadgesSetting
|
27
|
+
#receiveBadgesSetting: Common.Settings.Setting<Boolean>;
|
27
28
|
#allBadges: Badge[];
|
28
29
|
|
29
30
|
static readonly BADGE_REGISTRY: BadgeClass[] = [
|
30
31
|
StarterBadge,
|
31
32
|
SpeedsterBadge,
|
33
|
+
DOMDetectiveBadge,
|
32
34
|
];
|
33
35
|
|
34
36
|
private constructor() {
|
@@ -37,11 +39,10 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
37
39
|
this.#receiveBadgesSetting = Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges');
|
38
40
|
this.#receiveBadgesSetting.addChangeListener(this.#reconcileBadges, this);
|
39
41
|
|
40
|
-
this.#allBadges =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
}));
|
42
|
+
this.#allBadges = UserBadges.BADGE_REGISTRY.map(badgeCtor => new badgeCtor({
|
43
|
+
onTriggerBadge: this.#onTriggerBadge.bind(this),
|
44
|
+
badgeActionEventTarget: this.#badgeActionEventTarget,
|
45
|
+
}));
|
45
46
|
}
|
46
47
|
|
47
48
|
static instance({forceNew}: {forceNew: boolean} = {forceNew: false}): UserBadges {
|
@@ -65,7 +66,28 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
65
66
|
this.#badgeActionEventTarget.dispatchEventToListeners(action);
|
66
67
|
}
|
67
68
|
|
68
|
-
#
|
69
|
+
async #onTriggerBadge(badge: Badge): Promise<void> {
|
70
|
+
let shouldAwardBadge = false;
|
71
|
+
// By default, we award non-starter badges directly when they are triggered.
|
72
|
+
if (!badge.isStarterBadge) {
|
73
|
+
shouldAwardBadge = true;
|
74
|
+
} else {
|
75
|
+
const gdpProfile = await Host.GdpClient.GdpClient.instance().getProfile();
|
76
|
+
const receiveBadgesSettingEnabled = Boolean(this.#receiveBadgesSetting.get());
|
77
|
+
// If there is a GDP profile and the user has enabled receiving badges, we award the starter badge as well.
|
78
|
+
if (gdpProfile && receiveBadgesSettingEnabled) {
|
79
|
+
shouldAwardBadge = true;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// Awarding was needed and not successful, we don't show the notification
|
84
|
+
if (shouldAwardBadge) {
|
85
|
+
const result = await Host.GdpClient.GdpClient.instance().createAward({name: badge.name});
|
86
|
+
if (!result) {
|
87
|
+
return;
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
69
91
|
this.dispatchEventToListeners(Events.BADGE_TRIGGERED, badge);
|
70
92
|
}
|
71
93
|
|
@@ -115,4 +137,8 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
115
137
|
|
116
138
|
reconcileBadgesFinishedForTest(): void {
|
117
139
|
}
|
140
|
+
|
141
|
+
isReceiveBadgesSettingEnabled(): boolean {
|
142
|
+
return Boolean(this.#receiveBadgesSetting?.get());
|
143
|
+
}
|
118
144
|
}
|
@@ -118,19 +118,6 @@ export class Model extends EventTarget {
|
|
118
118
|
metadata,
|
119
119
|
resolveSourceMap: config?.resolveSourceMap,
|
120
120
|
};
|
121
|
-
if (!parseConfig.logger &&
|
122
|
-
(window.location.href.includes('devtools/bundled') || window.location.search.includes('debugFrontend'))) {
|
123
|
-
// Someone is debugging DevTools, enable the logger.
|
124
|
-
const times: Record<string, number> = {};
|
125
|
-
parseConfig.logger = {
|
126
|
-
start(id) {
|
127
|
-
times[id] = performance.now();
|
128
|
-
},
|
129
|
-
end(id) {
|
130
|
-
performance.measure(id, {start: times[id]});
|
131
|
-
},
|
132
|
-
};
|
133
|
-
}
|
134
121
|
await this.#processor.parse(traceEvents, parseConfig);
|
135
122
|
this.#storeParsedFileData(file, this.#processor.parsedTrace, this.#processor.insights);
|
136
123
|
// We only push the file onto this.#traces here once we know it's valid
|
@@ -2,7 +2,9 @@
|
|
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 Host from '../../core/host/host.js';
|
5
6
|
import * as i18n from '../../core/i18n/i18n.js';
|
7
|
+
import * as Badges from '../../models/badges/badges.js';
|
6
8
|
import * as Buttons from '../../ui/components/buttons/buttons.js';
|
7
9
|
import * as UI from '../../ui/legacy/legacy.js';
|
8
10
|
import * as Lit from '../../ui/lit/lit.js';
|
@@ -17,10 +19,50 @@ const UIStrings = {
|
|
17
19
|
* @description Title for close button
|
18
20
|
*/
|
19
21
|
dismiss: 'Dismiss',
|
22
|
+
/**
|
23
|
+
* @description Activity based badge award notification text
|
24
|
+
* @example {Badge Title} PH1
|
25
|
+
*/
|
26
|
+
activityBasedBadgeAwardMessage: 'You earned the {PH1} badge! It has been added to your Developer Profile.',
|
27
|
+
/**
|
28
|
+
* @description Action title for navigating to the badge settings in Google Developer Profile section
|
29
|
+
*/
|
30
|
+
badgeSettings: 'Badge settings',
|
31
|
+
/**
|
32
|
+
* @description Action title for opening the Google Developer Program profile page of the user in a new tab
|
33
|
+
*/
|
34
|
+
viewProfile: 'View profile',
|
35
|
+
/**
|
36
|
+
* @description Starter badge award notification text when the user has a Google Developer Program profile but did not enable receiving badges in DevTools yet
|
37
|
+
* @example {Badge Title} PH1
|
38
|
+
* @example {Google Developer Program link} PH2
|
39
|
+
*/
|
40
|
+
starterBadgeAwardMessageSettingDisabled: 'You earned the {PH1} badge for the {PH2}! Turn on badges to claim it.',
|
41
|
+
/**
|
42
|
+
* @description Starter badge award notification text when the user does not have a Google Developer Program profile.
|
43
|
+
* @example {Badge Title} PH1
|
44
|
+
* @example {Google Developer Program link} PH2
|
45
|
+
*/
|
46
|
+
starterBadgeAwardMessageNoGdpProfile:
|
47
|
+
'You earned the {PH1} badge for the {PH2}! Create a profile to claim your badge.',
|
48
|
+
/**
|
49
|
+
* @description Action title for snoozing the starter badge.
|
50
|
+
*/
|
51
|
+
remindMeLater: 'Remind me later',
|
52
|
+
/**
|
53
|
+
* @description Action title for enabling the "Receive badges" setting
|
54
|
+
*/
|
55
|
+
receiveBadges: 'Receive badges',
|
56
|
+
/**
|
57
|
+
* @description Action title for creating a Google Developer Program profle
|
58
|
+
*/
|
59
|
+
createProfile: 'Create profile',
|
20
60
|
} as const;
|
21
61
|
|
22
62
|
const str_ = i18n.i18n.registerUIStrings('panels/common/BadgeNotification.ts', UIStrings);
|
23
63
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
64
|
+
const i18nFormatString = i18n.i18n.getFormatLocalizedString.bind(undefined, str_);
|
65
|
+
const lockedString = i18n.i18n.lockedString;
|
24
66
|
|
25
67
|
export interface BadgeNotificationAction {
|
26
68
|
label: string;
|
@@ -30,7 +72,7 @@ export interface BadgeNotificationAction {
|
|
30
72
|
}
|
31
73
|
|
32
74
|
export interface BadgeNotificationProperties {
|
33
|
-
message:
|
75
|
+
message: HTMLElement|string;
|
34
76
|
imageUri: string;
|
35
77
|
actions: BadgeNotificationAction[];
|
36
78
|
}
|
@@ -80,7 +122,7 @@ const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement)
|
|
80
122
|
type View = typeof DEFAULT_VIEW;
|
81
123
|
|
82
124
|
export class BadgeNotification extends UI.Widget.Widget {
|
83
|
-
message:
|
125
|
+
message: HTMLElement|string = '';
|
84
126
|
imageUri = '';
|
85
127
|
actions: BadgeNotificationAction[] = [];
|
86
128
|
|
@@ -91,13 +133,81 @@ export class BadgeNotification extends UI.Widget.Widget {
|
|
91
133
|
this.#view = view;
|
92
134
|
}
|
93
135
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
136
|
+
async present(badge: Badges.Badge): Promise<void> {
|
137
|
+
if (badge.isStarterBadge) {
|
138
|
+
await this.#presentStarterBadge(badge);
|
139
|
+
} else {
|
140
|
+
this.#presentActivityBasedBadge(badge);
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
#show(properties: BadgeNotificationProperties): void {
|
145
|
+
this.message = properties.message;
|
146
|
+
this.imageUri = properties.imageUri;
|
147
|
+
this.actions = properties.actions;
|
148
|
+
this.requestUpdate();
|
149
|
+
this.show(UI.InspectorView.InspectorView.instance().element);
|
150
|
+
}
|
151
|
+
|
152
|
+
async #presentStarterBadge(badge: Badges.Badge): Promise<void> {
|
153
|
+
const gdpProfile = await Host.GdpClient.GdpClient.instance().getProfile();
|
154
|
+
const receiveBadgesSettingEnabled = Badges.UserBadges.instance().isReceiveBadgesSettingEnabled();
|
155
|
+
const googleDeveloperProgramLink = UI.XLink.XLink.create(
|
156
|
+
'https://developers.google.com/program', lockedString('Google Developer Program'), 'badge-link', undefined,
|
157
|
+
'gdp.program-link');
|
158
|
+
|
159
|
+
// If the user already has a GDP profile and the receive badges setting enabled,
|
160
|
+
// starter badge behaves as if it's an activity based badge.
|
161
|
+
if (gdpProfile && receiveBadgesSettingEnabled) {
|
162
|
+
this.#presentActivityBasedBadge(badge);
|
163
|
+
return;
|
164
|
+
}
|
165
|
+
|
166
|
+
// If the user already has a GDP profile and the receive badges setting disabled,
|
167
|
+
// starter badge behaves as a nudge for opting into receiving badges.
|
168
|
+
if (gdpProfile && !receiveBadgesSettingEnabled) {
|
169
|
+
this.#show({
|
170
|
+
message: i18nFormatString(
|
171
|
+
UIStrings.starterBadgeAwardMessageSettingDisabled, {PH1: badge.title, PH2: googleDeveloperProgramLink}),
|
172
|
+
actions: [
|
173
|
+
{
|
174
|
+
label: i18nString(UIStrings.remindMeLater),
|
175
|
+
onClick: () => {/* To implement */},
|
176
|
+
},
|
177
|
+
{label: i18nString(UIStrings.receiveBadges), onClick: () => { /* To implement */ }}
|
178
|
+
],
|
179
|
+
imageUri: badge.imageUri,
|
180
|
+
});
|
181
|
+
return;
|
182
|
+
}
|
183
|
+
|
184
|
+
// The user does not have a GDP profile, starter badge acts as a nudge for creating a GDP profile.
|
185
|
+
this.#show({
|
186
|
+
message: i18nFormatString(
|
187
|
+
UIStrings.starterBadgeAwardMessageNoGdpProfile, {PH1: badge.title, PH2: googleDeveloperProgramLink}),
|
188
|
+
actions: [
|
189
|
+
{
|
190
|
+
label: i18nString(UIStrings.remindMeLater),
|
191
|
+
onClick: () => {/* TODO(ergunsh): Implement */},
|
192
|
+
},
|
193
|
+
{label: i18nString(UIStrings.createProfile), onClick: () => { /* TODO(ergunsh): Implement */ }}
|
194
|
+
],
|
195
|
+
imageUri: badge.imageUri,
|
196
|
+
});
|
197
|
+
}
|
198
|
+
|
199
|
+
#presentActivityBasedBadge(badge: Badges.Badge): void {
|
200
|
+
this.#show({
|
201
|
+
message: i18nString(UIStrings.activityBasedBadgeAwardMessage, {PH1: badge.title}),
|
202
|
+
actions: [
|
203
|
+
{
|
204
|
+
label: i18nString(UIStrings.badgeSettings),
|
205
|
+
onClick: () => {/* TODO(ergunsh): Implement */},
|
206
|
+
},
|
207
|
+
{label: i18nString(UIStrings.viewProfile), onClick: () => { /* TODO(ergunsh): Implement */ }}
|
208
|
+
],
|
209
|
+
imageUri: badge.imageUri,
|
210
|
+
});
|
101
211
|
}
|
102
212
|
|
103
213
|
#close = (): void => {
|
@@ -40,6 +40,7 @@ import * as Platform from '../../core/platform/platform.js';
|
|
40
40
|
import * as Root from '../../core/root/root.js';
|
41
41
|
import * as SDK from '../../core/sdk/sdk.js';
|
42
42
|
import * as Protocol from '../../generated/protocol.js';
|
43
|
+
import * as Badges from '../../models/badges/badges.js';
|
43
44
|
import type * as Elements from '../../models/elements/elements.js';
|
44
45
|
import type * as IssuesManager from '../../models/issues_manager/issues_manager.js';
|
45
46
|
import * as TextUtils from '../../models/text_utils/text_utils.js';
|
@@ -1603,6 +1604,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
|
1603
1604
|
|
1604
1605
|
if (attributeName !== null && (attributeName.trim() || newText.trim()) && oldText !== newText) {
|
1605
1606
|
this.nodeInternal.setAttribute(attributeName, newText, moveToNextAttributeIfNeeded.bind(this));
|
1607
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
|
1606
1608
|
return;
|
1607
1609
|
}
|
1608
1610
|
|
@@ -1661,6 +1663,8 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
|
1661
1663
|
if (!treeOutline) {
|
1662
1664
|
return;
|
1663
1665
|
}
|
1666
|
+
|
1667
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
|
1664
1668
|
const newTreeItem = treeOutline.selectNodeAfterEdit(wasExpanded, error, newNode);
|
1665
1669
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
1666
1670
|
// @ts-expect-error
|
@@ -2581,6 +2585,9 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
|
2581
2585
|
const adorner = this.adorn(config);
|
2582
2586
|
const onClick = async(): Promise<void> => {
|
2583
2587
|
const {nodeIds} = await node.domModel().agent.invoke_forceShowPopover({nodeId, enable: adorner.isActive()});
|
2588
|
+
if (adorner.isActive()) {
|
2589
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
|
2590
|
+
}
|
2584
2591
|
for (const closedPopoverNodeId of nodeIds) {
|
2585
2592
|
const node = this.node().domModel().nodeForId(closedPopoverNodeId);
|
2586
2593
|
const treeElement = node && this.treeOutline?.treeElementByNode.get(node);
|
@@ -2617,6 +2624,9 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
|
2617
2624
|
const onClick = ((() => {
|
2618
2625
|
if (adorner.isActive()) {
|
2619
2626
|
node.domModel().overlayModel().highlightGridInPersistentOverlay(nodeId);
|
2627
|
+
if (isSubgrid) {
|
2628
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
|
2629
|
+
}
|
2620
2630
|
} else {
|
2621
2631
|
node.domModel().overlayModel().hideGridInPersistentOverlay(nodeId);
|
2622
2632
|
}
|
@@ -2658,6 +2668,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
|
2658
2668
|
const onClick = ((() => {
|
2659
2669
|
if (adorner.isActive()) {
|
2660
2670
|
node.domModel().overlayModel().highlightGridInPersistentOverlay(nodeId);
|
2671
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
|
2661
2672
|
} else {
|
2662
2673
|
node.domModel().overlayModel().hideGridInPersistentOverlay(nodeId);
|
2663
2674
|
}
|
@@ -2786,6 +2797,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
|
|
2786
2797
|
const model = node.domModel().overlayModel();
|
2787
2798
|
if (adorner.isActive()) {
|
2788
2799
|
model.highlightContainerQueryInPersistentOverlay(nodeId);
|
2800
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.MODERN_DOM_BADGE_CLICKED);
|
2789
2801
|
} else {
|
2790
2802
|
model.hideContainerQueryInPersistentOverlay(nodeId);
|
2791
2803
|
}
|
@@ -38,6 +38,7 @@ import * as Common from '../../core/common/common.js';
|
|
38
38
|
import * as i18n from '../../core/i18n/i18n.js';
|
39
39
|
import * as Root from '../../core/root/root.js';
|
40
40
|
import * as SDK from '../../core/sdk/sdk.js';
|
41
|
+
import * as Badges from '../../models/badges/badges.js';
|
41
42
|
import * as Elements from '../../models/elements/elements.js';
|
42
43
|
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
|
43
44
|
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
|
@@ -1332,6 +1333,8 @@ export class ElementsTreeOutline extends
|
|
1332
1333
|
return;
|
1333
1334
|
}
|
1334
1335
|
|
1336
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
|
1337
|
+
|
1335
1338
|
// Select it and expand if necessary. We force tree update so that it processes dom events and is up to date.
|
1336
1339
|
this.runPendingUpdates();
|
1337
1340
|
|
@@ -41,6 +41,7 @@ import * as Platform from '../../core/platform/platform.js';
|
|
41
41
|
import * as Root from '../../core/root/root.js';
|
42
42
|
import * as SDK from '../../core/sdk/sdk.js';
|
43
43
|
import * as Protocol from '../../generated/protocol.js';
|
44
|
+
import * as Badges from '../../models/badges/badges.js';
|
44
45
|
import * as Bindings from '../../models/bindings/bindings.js';
|
45
46
|
import * as TextUtils from '../../models/text_utils/text_utils.js';
|
46
47
|
import * as Buttons from '../../ui/components/buttons/buttons.js';
|
@@ -1546,6 +1547,8 @@ export class StylePropertiesSection {
|
|
1546
1547
|
if (!success) {
|
1547
1548
|
return Promise.resolve();
|
1548
1549
|
}
|
1550
|
+
|
1551
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.CSS_RULE_MODIFIED);
|
1549
1552
|
return this.matchedStyles.recomputeMatchingSelectors(rule).then(updateSourceRanges.bind(this, rule));
|
1550
1553
|
}
|
1551
1554
|
|
@@ -10,6 +10,7 @@ import * as i18n from '../../core/i18n/i18n.js';
|
|
10
10
|
import * as Platform from '../../core/platform/platform.js';
|
11
11
|
import * as Root from '../../core/root/root.js';
|
12
12
|
import * as SDK from '../../core/sdk/sdk.js';
|
13
|
+
import * as Badges from '../../models/badges/badges.js';
|
13
14
|
import * as Bindings from '../../models/bindings/bindings.js';
|
14
15
|
import * as TextUtils from '../../models/text_utils/text_utils.js';
|
15
16
|
import type * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
|
@@ -3292,6 +3293,10 @@ export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
|
|
3292
3293
|
|
3293
3294
|
const overwriteProperty = !this.newProperty || hasBeenEditedIncrementally;
|
3294
3295
|
let success: boolean = await this.property.setText(styleText, majorChange, overwriteProperty);
|
3296
|
+
if (success && majorChange) {
|
3297
|
+
Badges.UserBadges.instance().recordAction(Badges.BadgeAction.CSS_RULE_MODIFIED);
|
3298
|
+
}
|
3299
|
+
|
3295
3300
|
// Revert to the original text if applying the new text failed
|
3296
3301
|
if (hasBeenEditedIncrementally && majorChange && !success) {
|
3297
3302
|
majorChange = false;
|
@@ -336,19 +336,13 @@ export class GenericSettingsTab extends UI.Widget.VBox implements SettingsTab {
|
|
336
336
|
this.#updateSyncSectionTimerId = -1;
|
337
337
|
}
|
338
338
|
|
339
|
-
void Promise
|
340
|
-
.
|
341
|
-
|
342
|
-
resolve => Host.InspectorFrontendHost.InspectorFrontendHostInstance.getSyncInformation(resolve)),
|
343
|
-
Root.Runtime.hostConfig.devToolsGdpProfiles?.enabled ? Host.GdpClient.GdpClient.instance().getProfile() :
|
344
|
-
Promise.resolve(undefined),
|
345
|
-
])
|
346
|
-
.then(([syncInfo, gdpProfile]) => {
|
339
|
+
void new Promise<Host.InspectorFrontendHostAPI.SyncInformation>(
|
340
|
+
resolve => Host.InspectorFrontendHost.InspectorFrontendHostInstance.getSyncInformation(resolve))
|
341
|
+
.then(syncInfo => {
|
347
342
|
this.syncSection.data = {
|
348
343
|
syncInfo,
|
349
344
|
syncSetting: Common.Settings.moduleSetting('sync-preferences') as Common.Settings.Setting<boolean>,
|
350
345
|
receiveBadgesSetting: Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges'),
|
351
|
-
gdpProfile: gdpProfile ?? undefined,
|
352
346
|
};
|
353
347
|
if (!syncInfo.isSyncActive || !syncInfo.arePreferencesSynced) {
|
354
348
|
this.#updateSyncSectionTimerId = window.setTimeout(this.updateSyncSection.bind(this), 500);
|