Skip to content

Commit ef458e4

Browse files
hjanuschkadevtools-frontend-scoped@luci-project-accounts.iam.gserviceaccount.com
authored andcommitted
Extract presenter/view pattern for ProfileLauncherView
Migrate ProfileLauncherView from imperative DOM construction to the declarative presenter/view pattern. Bug: 400353541 Change-Id: Ic0fa6407ab23e46905e774d142d7836dd4415e0a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7666090 Reviewed-by: Wolfgang Beyer <wolfi@chromium.org> Reviewed-by: Simon Zünd <szuend@chromium.org> Commit-Queue: Helmut Januschka <helmut@januschka.com>
1 parent f0dea6a commit ef458e4

3 files changed

Lines changed: 199 additions & 131 deletions

File tree

front_end/panels/profiler/IsolateSelector.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ export class IsolateSelector extends UI.Widget.VBox implements UI.ListControl.Li
6666
totalValueDiv: HTMLElement;
6767
readonly totalTrendDiv: HTMLElement;
6868

69-
constructor() {
70-
super();
69+
// `devtools-widget` passes its host element into widget constructors.
70+
// Accept and forward it so this widget attaches to that host element.
71+
constructor(element?: HTMLElement) {
72+
super(element);
7173

7274
this.items = new UI.ListModel.ListModel();
7375
this.list = new UI.ListControl.ListControl(this.items, this, UI.ListControl.ListMode.NonViewport);

front_end/panels/profiler/ProfileLauncherView.ts

Lines changed: 194 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// Copyright 2011 The Chromium Authors
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
/* eslint-disable @devtools/no-imperative-dom-api */
54

65
import * as Common from '../../core/common/common.js';
76
import * as i18n from '../../core/i18n/i18n.js';
87
import * as Buttons from '../../ui/components/buttons/buttons.js';
98
import * as UI from '../../ui/legacy/legacy.js';
9+
import {html, nothing, render} from '../../ui/lit/lit.js';
10+
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
1011

1112
import {IsolateSelector} from './IsolateSelector.js';
1213
import type {ProfileType} from './ProfileHeader.js';
@@ -41,166 +42,233 @@ const UIStrings = {
4142
} as const;
4243
const str_ = i18n.i18n.registerUIStrings('panels/profiler/ProfileLauncherView.ts', UIStrings);
4344
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
45+
const {widget, widgetRef} = UI.Widget;
46+
47+
interface ProfileTypeEntry {
48+
profileType: ProfileType;
49+
selected: boolean;
50+
customContent: Element|null;
51+
}
52+
53+
export interface ViewInput {
54+
headerText: string;
55+
profileTypes: ProfileTypeEntry[];
56+
controlButtonText: string;
57+
controlButtonDisabled: boolean;
58+
controlButtonTooltip: string;
59+
isProfiling: boolean;
60+
isolateSelector: IsolateSelector|null;
61+
onControlClick: () => void;
62+
onLoadClick: () => void;
63+
onProfileTypeChange: (profileType: ProfileType) => void;
64+
}
65+
66+
export interface ViewOutput {
67+
isolateSelector: IsolateSelector;
68+
}
69+
70+
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
71+
72+
// clang-format off
73+
export const DEFAULT_VIEW: View = (input, output, target) => {
74+
render(html`
75+
<style>${profileLauncherViewStyles}</style>
76+
<div class="profile-launcher-view-content vbox">
77+
<div class="vbox">
78+
<h1>${input.headerText}</h1>
79+
<form role="radiogroup" aria-label=${input.headerText}>
80+
${input.profileTypes.map(entry => {
81+
const radioId = `profile-type-${entry.profileType.id}`;
82+
const customContent = entry.customContent;
83+
return html`
84+
<input id=${radioId} type="radio" name="profile-type"
85+
.checked=${entry.selected}
86+
?disabled=${input.isProfiling}
87+
@change=${() => input.onProfileTypeChange(entry.profileType)}
88+
jslog=${VisualLogging.toggle().track({change: true}).context('profiler.profile-type')}
89+
/>
90+
<label for=${radioId}>${entry.profileType.name}</label>
91+
<p>${entry.profileType.description}</p>
92+
${customContent ? html`
93+
<p>
94+
<span role="group" aria-labelledby=${radioId}>
95+
${customContent}
96+
</span>
97+
</p>
98+
` : nothing}
99+
`;
100+
})}
101+
</form>
102+
</div>
103+
<div class="vbox profile-isolate-selector-block">
104+
<h1>${i18nString(UIStrings.selectJavascriptVmInstance)}</h1>
105+
<div class="vbox profile-launcher-target-list profile-launcher-target-list-container">
106+
<devtools-widget
107+
${widget(IsolateSelector)}
108+
${widgetRef(IsolateSelector, e => {output.isolateSelector = e;})}
109+
></devtools-widget>
110+
</div>
111+
${input.isolateSelector?.totalMemoryElement() ?? nothing}
112+
</div>
113+
<div class="hbox profile-launcher-buttons">
114+
<devtools-button
115+
.variant=${Buttons.Button.Variant.OUTLINED}
116+
.iconName=${'import'}
117+
@click=${input.onLoadClick}
118+
.jslogContext=${'profiler.load-from-file'}
119+
>${i18nString(UIStrings.load)}</devtools-button>
120+
<devtools-button
121+
.variant=${Buttons.Button.Variant.PRIMARY}
122+
?disabled=${input.controlButtonDisabled}
123+
title=${input.controlButtonTooltip}
124+
@click=${input.onControlClick}
125+
.jslogContext=${'profiler.heap-toggle-recording'}
126+
>${input.controlButtonText}</devtools-button>
127+
</div>
128+
</div>
129+
`, target);
130+
};
131+
// clang-format on
132+
44133
export class ProfileLauncherView extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(
45134
UI.Widget.VBox) {
46135
readonly panel: ProfilesPanel;
47-
#contentElement: HTMLElement;
48136
readonly selectedProfileTypeSetting: Common.Settings.Setting<string>;
49-
profileTypeHeaderElement: HTMLElement;
50-
readonly profileTypeSelectorForm: HTMLElement;
51-
controlButton: Buttons.Button.Button;
52-
readonly loadButton: Buttons.Button.Button;
53-
recordButtonEnabled: boolean;
54-
typeIdToOptionElementAndProfileType: Map<string, {
55-
optionElement: HTMLInputElement,
56-
profileType: ProfileType,
57-
}>;
58-
isProfiling?: boolean;
59-
isInstantProfile?: boolean;
60-
isEnabled?: boolean;
61-
62-
constructor(profilesPanel: ProfilesPanel) {
63-
super();
64-
this.registerRequiredCSS(profileLauncherViewStyles);
137+
readonly #view: View;
138+
#isolateSelector: IsolateSelector|null = null;
139+
#profileTypes = new Map<string, ProfileType>();
140+
#isProfiling = false;
141+
#isInstantProfile = false;
142+
#isEnabled = false;
143+
#recordButtonEnabled = true;
144+
#selectedTypeId = '';
65145

66-
this.panel = profilesPanel;
67-
this.element.classList.add('profile-launcher-view');
68-
this.#contentElement = this.element.createChild('div', 'profile-launcher-view-content vbox');
146+
constructor(profilesPanel: ProfilesPanel, view: View = DEFAULT_VIEW) {
147+
super({classes: ['profile-launcher-view']});
69148

70-
const profileTypeSelectorElement = this.#contentElement.createChild('div', 'vbox');
149+
this.#view = view;
150+
this.panel = profilesPanel;
71151
this.selectedProfileTypeSetting = Common.Settings.Settings.instance().createSetting('selected-profile-type', 'CPU');
72-
this.profileTypeHeaderElement = profileTypeSelectorElement.createChild('h1');
73-
this.profileTypeSelectorForm = profileTypeSelectorElement.createChild('form');
74-
UI.ARIAUtils.markAsRadioGroup(this.profileTypeSelectorForm);
75-
76-
const isolateSelectorElement = this.#contentElement.createChild('div', 'vbox profile-isolate-selector-block');
77-
isolateSelectorElement.createChild('h1').textContent = i18nString(UIStrings.selectJavascriptVmInstance);
78-
const isolateSelector = new IsolateSelector();
79-
const isolateSelectorElementChild = isolateSelectorElement.createChild('div', 'vbox profile-launcher-target-list');
80-
isolateSelectorElementChild.classList.add('profile-launcher-target-list-container');
81-
isolateSelector.show(isolateSelectorElementChild);
82-
isolateSelectorElement.appendChild(isolateSelector.totalMemoryElement());
83-
84-
const buttonsDiv = this.#contentElement.createChild('div', 'hbox profile-launcher-buttons');
85-
this.controlButton = UI.UIUtils.createTextButton('', this.controlButtonClicked.bind(this), {
86-
jslogContext: 'profiler.heap-toggle-recording',
87-
variant: Buttons.Button.Variant.PRIMARY,
88-
});
89-
this.loadButton = new Buttons.Button.Button();
90-
this.loadButton
91-
.data = {iconName: 'import', variant: Buttons.Button.Variant.OUTLINED, jslogContext: 'profiler.load-from-file'};
92-
this.loadButton.textContent = i18nString(UIStrings.load);
93-
this.loadButton.addEventListener('click', this.loadButtonClicked.bind(this));
94-
buttonsDiv.appendChild(this.loadButton);
95-
buttonsDiv.appendChild(this.controlButton);
96-
this.recordButtonEnabled = true;
97-
98-
this.typeIdToOptionElementAndProfileType = new Map();
99152
}
100153

101-
loadButtonClicked(): void {
102-
const loadFromFileAction = UI.ActionRegistry.ActionRegistry.instance().getAction('profiler.load-from-file');
103-
void loadFromFileAction.execute();
154+
override wasShown(): void {
155+
super.wasShown();
156+
this.requestUpdate();
104157
}
105158

106-
updateControls(): void {
107-
if (this.isEnabled && this.recordButtonEnabled) {
108-
this.controlButton.removeAttribute('disabled');
109-
} else {
110-
this.controlButton.setAttribute('disabled', '');
111-
}
112-
UI.Tooltip.Tooltip.install(
113-
this.controlButton, this.recordButtonEnabled ? '' : UI.UIUtils.anotherProfilerActiveLabel());
114-
if (this.isInstantProfile) {
115-
this.controlButton.classList.remove('running');
116-
this.controlButton.textContent = i18nString(UIStrings.takeSnapshot);
117-
} else if (this.isProfiling) {
118-
this.controlButton.classList.add('running');
119-
this.controlButton.textContent = i18nString(UIStrings.stop);
120-
} else {
121-
this.controlButton.classList.remove('running');
122-
this.controlButton.textContent = i18nString(UIStrings.start);
123-
}
124-
for (const {optionElement} of this.typeIdToOptionElementAndProfileType.values()) {
125-
optionElement.disabled = Boolean(this.isProfiling);
126-
}
159+
#getHeaderText(): string {
160+
return this.#profileTypes.size > 1 ? i18nString(UIStrings.selectProfilingType) :
161+
(this.#profileTypes.values().next().value?.name ?? '');
127162
}
128163

129164
profileStarted(): void {
130-
this.isProfiling = true;
131-
this.updateControls();
165+
this.#isProfiling = true;
166+
this.requestUpdate();
132167
}
133168

134169
profileFinished(): void {
135-
this.isProfiling = false;
136-
this.updateControls();
170+
this.#isProfiling = false;
171+
this.requestUpdate();
137172
}
138173

139174
updateProfileType(profileType: ProfileType, recordButtonEnabled: boolean): void {
140-
this.isInstantProfile = profileType.isInstantProfile();
141-
this.recordButtonEnabled = recordButtonEnabled;
142-
this.isEnabled = profileType.isEnabled();
143-
this.updateControls();
175+
this.#isInstantProfile = profileType.isInstantProfile();
176+
this.#recordButtonEnabled = recordButtonEnabled;
177+
this.#isEnabled = profileType.isEnabled();
178+
this.requestUpdate();
144179
}
145180

146181
addProfileType(profileType: ProfileType): void {
147-
const {radio, label} = UI.UIUtils.createRadioButton('profile-type', profileType.name, 'profiler.profile-type');
148-
this.profileTypeSelectorForm.appendChild(label);
149-
this.typeIdToOptionElementAndProfileType.set(profileType.id, {optionElement: radio, profileType});
150-
radio.addEventListener('change', this.profileTypeChanged.bind(this, profileType), false);
151-
const descriptionElement = this.profileTypeSelectorForm.createChild('p');
152-
descriptionElement.textContent = profileType.description;
153-
UI.ARIAUtils.setDescription(radio, profileType.description);
154-
const customContent = profileType.customContent();
155-
if (customContent) {
156-
customContent.setAttribute('role', 'group');
157-
customContent.setAttribute('aria-labelledby', `${radio.id}`);
158-
this.profileTypeSelectorForm.createChild('p').appendChild(customContent);
159-
profileType.setCustomContentEnabled(false);
160-
}
161-
const headerText = this.typeIdToOptionElementAndProfileType.size > 1 ? i18nString(UIStrings.selectProfilingType) :
162-
profileType.name;
163-
this.profileTypeHeaderElement.textContent = headerText;
164-
UI.ARIAUtils.setLabel(this.profileTypeSelectorForm, headerText);
182+
this.#profileTypes.set(profileType.id, profileType);
183+
profileType.setCustomContentEnabled(false);
184+
this.requestUpdate();
165185
}
166186

167187
restoreSelectedProfileType(): void {
168188
let typeId = this.selectedProfileTypeSetting.get();
169-
if (!this.typeIdToOptionElementAndProfileType.has(typeId)) {
170-
typeId = this.typeIdToOptionElementAndProfileType.keys().next().value as string;
189+
if (!this.#profileTypes.has(typeId)) {
190+
typeId = this.#profileTypes.keys().next().value as string;
171191
this.selectedProfileTypeSetting.set(typeId);
172192
}
173193

174-
const optionElementAndProfileType = (this.typeIdToOptionElementAndProfileType.get(typeId) as {
175-
optionElement: HTMLInputElement,
176-
profileType: ProfileType,
177-
});
178-
optionElementAndProfileType.optionElement.checked = true;
179-
const type = optionElementAndProfileType.profileType;
180-
for (const [id, {profileType}] of this.typeIdToOptionElementAndProfileType) {
181-
const enabled = (id === typeId);
182-
profileType.setCustomContentEnabled(enabled);
194+
this.#selectedTypeId = typeId;
195+
const selectedType = this.#profileTypes.get(typeId);
196+
if (!selectedType) {
197+
return;
183198
}
184-
this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, type);
185-
}
186-
187-
controlButtonClicked(): void {
188-
this.panel.toggleRecord();
199+
for (const [id, profileType] of this.#profileTypes) {
200+
profileType.setCustomContentEnabled(id === typeId);
201+
}
202+
this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, selectedType);
203+
this.requestUpdate();
189204
}
190205

191-
profileTypeChanged(profileType: ProfileType): void {
192-
const typeId = this.selectedProfileTypeSetting.get();
193-
const type = (this.typeIdToOptionElementAndProfileType.get(typeId) as {
194-
optionElement: HTMLInputElement,
195-
profileType: ProfileType,
196-
}).profileType;
197-
type.setCustomContentEnabled(false);
206+
#profileTypeChanged(profileType: ProfileType): void {
207+
const previousTypeId = this.#selectedTypeId;
208+
const previousType = this.#profileTypes.get(previousTypeId);
209+
if (previousType) {
210+
previousType.setCustomContentEnabled(false);
211+
}
198212
profileType.setCustomContentEnabled(true);
199-
this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, profileType);
200-
this.isInstantProfile = profileType.isInstantProfile();
201-
this.isEnabled = profileType.isEnabled();
202-
this.updateControls();
213+
this.#selectedTypeId = profileType.id;
203214
this.selectedProfileTypeSetting.set(profileType.id);
215+
this.#isInstantProfile = profileType.isInstantProfile();
216+
this.#isEnabled = profileType.isEnabled();
217+
this.dispatchEventToListeners(Events.PROFILE_TYPE_SELECTED, profileType);
218+
this.requestUpdate();
219+
}
220+
221+
override performUpdate(): void {
222+
const profileTypeEntries: ProfileTypeEntry[] = [];
223+
for (const [id, profileType] of this.#profileTypes) {
224+
const selected = id === this.#selectedTypeId;
225+
const customContent = profileType.customContent();
226+
profileType.setCustomContentEnabled(selected);
227+
profileTypeEntries.push({
228+
profileType,
229+
selected,
230+
customContent,
231+
});
232+
}
233+
234+
const controlButtonText = this.#isInstantProfile ?
235+
i18nString(UIStrings.takeSnapshot) :
236+
(this.#isProfiling ? i18nString(UIStrings.stop) : i18nString(UIStrings.start));
237+
const controlButtonDisabled = !(this.#isEnabled && this.#recordButtonEnabled);
238+
const controlButtonTooltip = this.#recordButtonEnabled ? '' : UI.UIUtils.anotherProfilerActiveLabel();
239+
const that = this;
240+
241+
this.#view(
242+
{
243+
headerText: this.#getHeaderText(),
244+
profileTypes: profileTypeEntries,
245+
controlButtonText,
246+
controlButtonDisabled,
247+
controlButtonTooltip,
248+
isProfiling: this.#isProfiling,
249+
isolateSelector: this.#isolateSelector,
250+
onControlClick: () => {
251+
this.panel.toggleRecord();
252+
},
253+
onLoadClick: () => {
254+
const loadFromFileAction = UI.ActionRegistry.ActionRegistry.instance().getAction('profiler.load-from-file');
255+
void loadFromFileAction.execute();
256+
},
257+
onProfileTypeChange: (profileType: ProfileType) => {
258+
this.#profileTypeChanged(profileType);
259+
},
260+
},
261+
{
262+
set isolateSelector(isolateSelector: IsolateSelector) {
263+
if (that.#isolateSelector === isolateSelector) {
264+
return;
265+
}
266+
that.#isolateSelector = isolateSelector;
267+
that.requestUpdate();
268+
},
269+
},
270+
this.contentElement,
271+
);
204272
}
205273
}
206274

0 commit comments

Comments
 (0)