Files

217 lines
7.4 KiB
TypeScript

// Copyright (c) Meta Platforms, Inc. and affiliates.
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Logs from '../../models/logs/logs.js';
import * as Workspace from '../../models/workspace/workspace.js';
import type * as ReactDevToolsTypes from '../../third_party/react-devtools/react-devtools.js';
import * as ReactDevTools from '../../third_party/react-devtools/react-devtools.js';
import * as UI from '../../ui/legacy/legacy.js';
import {
Events as ReactDevToolsModelEvents,
type EventTypes as ReactDevToolsModelEventTypes,
ReactDevToolsModel
} from './ReactDevToolsModel.js';
const UIStrings = {
/**
* @description Label of the FB-only 'send feedback' button.
*/
sendFeedback: '[FB-only] Send feedback',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/react_devtools/ReactDevToolsViewBase.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
type ReactDevToolsInitializationFailedEvent =
Common.EventTarget.EventTargetEvent<ReactDevToolsModelEventTypes[ReactDevToolsModelEvents.INITIALIZATION_FAILED]>;
// Based on ExtensionServer.onOpenResource
async function openResource(
url: Platform.DevToolsPath.UrlString,
lineNumber: number, // 0-based
columnNumber: number, // 0-based
): Promise<void> {
const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url);
if (uiSourceCode) {
// Unlike the Extension API's version of openResource, we want to normalize the location
// so that source maps (if any) are applied.
const normalizedUiLocation = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().normalizeUILocation(uiSourceCode.uiLocation(lineNumber, columnNumber));
void Common.Revealer.reveal(normalizedUiLocation);
return;
}
const resource = Bindings.ResourceUtils.resourceForURL(url);
if (resource) {
void Common.Revealer.reveal(resource);
return;
}
const request = Logs.NetworkLog.NetworkLog.instance().requestForURL(url);
if (request) {
void Common.Revealer.reveal(request);
return;
}
throw new Error('Could not find resource for ' + url);
}
function viewElementSourceFunction(source: ReactDevToolsTypes.Source, symbolicatedSource: ReactDevToolsTypes.Source | null): void {
const {sourceURL, line, column} = symbolicatedSource
? symbolicatedSource
: source;
// We use 1-based line and column, Chrome expects them 0-based.
void openResource(sourceURL as Platform.DevToolsPath.UrlString, line - 1, column - 1);
}
export class ReactDevToolsViewBase extends UI.View.SimpleView implements
SDK.TargetManager.SDKModelObserver<ReactDevToolsModel> {
readonly #tab: string;
#model: ReactDevToolsModel | null = null;
constructor(
tab: 'components' | 'profiler',
title: Platform.UIString.LocalizedString,
) {
super(title, true);
this.registerRequiredCSS(ReactDevTools.CSS);
this.#tab = tab;
this.#renderLoader();
SDK.TargetManager.TargetManager.instance().observeModels(ReactDevToolsModel, this);
this.element.style.userSelect = 'text';
}
modelAdded(model: ReactDevToolsModel): void {
this.#model = model;
model.addEventListener(
ReactDevToolsModelEvents.INITIALIZATION_COMPLETED,
this.#handleInitializationCompleted,
this,
);
model.addEventListener(
ReactDevToolsModelEvents.INITIALIZATION_FAILED,
this.#handleInitializationFailed,
this,
);
model.addEventListener(
ReactDevToolsModelEvents.DESTROYED,
this.#handleBackendDestroyed,
this,
);
if (model.isInitialized()) {
// Already initialized from another rendered React DevTools panel - render
// from initialized state
this.#renderDevToolsView();
} else {
// Once initialized, it will emit InitializationCompleted event
model.ensureInitialized();
}
}
modelRemoved(model: ReactDevToolsModel): void {
model.removeEventListener(
ReactDevToolsModelEvents.INITIALIZATION_COMPLETED,
this.#handleInitializationCompleted,
this,
);
model.removeEventListener(
ReactDevToolsModelEvents.INITIALIZATION_FAILED,
this.#handleInitializationFailed,
this,
);
model.removeEventListener(
ReactDevToolsModelEvents.DESTROYED,
this.#handleBackendDestroyed,
this,
);
}
#handleInitializationCompleted(): void {
this.#renderDevToolsView();
}
#handleInitializationFailed({data: errorMessage}: ReactDevToolsInitializationFailedEvent): void {
this.#renderErrorView(errorMessage);
}
#handleBackendDestroyed(): void {
this.#renderLoader();
}
#renderDevToolsView(): void {
this.#clearView();
const model = this.#model;
if (model === null) {
throw new Error('Attempted to render React DevTools panel, but the model was null');
}
const usingDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initializeFn = this.#tab === 'components' ? ReactDevTools.initializeComponents : ReactDevTools.initializeProfiler;
initializeFn(this.contentElement, {
bridge: model.getBridgeOrThrow(),
store: model.getStoreOrThrow(),
theme: usingDarkTheme ? 'dark' : 'light',
canViewElementSourceFunction: () => true,
viewElementSourceFunction,
});
}
#renderLoader(): void {
this.#clearView();
const loaderContainer = document.createElement('div');
loaderContainer.setAttribute('style', 'display: flex; flex: 1; justify-content: center; align-items: center');
const loader = document.createElement('span');
loader.classList.add('spinner');
loaderContainer.appendChild(loader);
this.contentElement.appendChild(loaderContainer);
}
#renderErrorView(errorMessage: string): void {
this.#clearView();
const errorContainer = document.createElement('div');
errorContainer.setAttribute('style', 'display: flex; flex: 1; flex-direction: column; justify-content: center; align-items: center');
const errorIconView = document.createElement('div');
errorIconView.setAttribute('style', 'font-size: 3rem');
errorIconView.innerHTML = '❗';
const errorMessageParagraph = document.createElement('p');
errorMessageParagraph.setAttribute('style', 'user-select: all');
errorMessageParagraph.innerHTML = errorMessage;
errorContainer.appendChild(errorIconView);
errorContainer.appendChild(errorMessageParagraph);
this.contentElement.appendChild(errorContainer);
if (globalThis.FB_ONLY__reactNativeFeedbackLink) {
const feedbackLink = globalThis.FB_ONLY__reactNativeFeedbackLink as Platform.DevToolsPath.UrlString;
const feedbackButton = UI.UIUtils.createTextButton(i18nString(UIStrings.sendFeedback), () => {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(feedbackLink);
}, {className: 'primary-button', jslogContext: 'sendFeedback'});
errorContainer.appendChild(feedbackButton);
}
}
#clearView(): void {
this.contentElement.removeChildren();
}
}