Merge branch 'main' of github.com:facebook/flipper into universalBuild
This commit is contained in:
@@ -143,7 +143,7 @@ async function getFlipperServer(
|
||||
const {readyForIncomingConnections} = await startServer(
|
||||
{
|
||||
staticPath,
|
||||
entry: 'index.web.dev.html',
|
||||
entry: 'index.web.html',
|
||||
port,
|
||||
},
|
||||
environmentInfo,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export const getIdbInstallationInstructions = (idbPath: string) =>
|
||||
`IDB is required to use Flipper with iOS devices. It can be installed from https://github.com/facebook/idb and configured in Flipper settings. You can also disable physical iOS device support in settings. Current setting: ${idbPath} isn't a valid IDB installation.`;
|
||||
26
desktop/doctor/src/fb-stubs/messages.tsx
Normal file
26
desktop/doctor/src/fb-stubs/messages.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export const getIdbInstallationInstructions = (
|
||||
idbPath: string,
|
||||
): {message: string; commands: {title: string; command: string}[]} => ({
|
||||
message: `IDB is required to use Flipper with iOS devices. It can be installed from https://github.com/facebook/idb and configured in Flipper settings. You can also disable physical iOS device support in settings. Current setting: ${idbPath} isn't a valid IDB installation.`,
|
||||
commands: [],
|
||||
});
|
||||
|
||||
export const installXcode =
|
||||
'Install Xcode from the App Store or download it from https://developer.apple.com';
|
||||
|
||||
export const installSDK =
|
||||
'You can install it using Xcode (https://developer.apple.com/xcode/). Once installed, restart flipper.';
|
||||
|
||||
export const installAndroidStudio = [
|
||||
'Android Studio is not installed.',
|
||||
'Install Android Studio from https://developer.android.com/studio',
|
||||
].join('\n');
|
||||
@@ -17,7 +17,12 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type {FlipperDoctor} from 'flipper-common';
|
||||
import * as fs_extra from 'fs-extra';
|
||||
import {getIdbInstallationInstructions} from './fb-stubs/idbInstallationInstructions';
|
||||
import {
|
||||
getIdbInstallationInstructions,
|
||||
installXcode,
|
||||
installSDK,
|
||||
installAndroidStudio,
|
||||
} from './fb-stubs/messages';
|
||||
import {validateSelectedXcodeVersion} from './fb-stubs/validateSelectedXcodeVersion';
|
||||
|
||||
export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
@@ -62,6 +67,29 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
isRequired: false,
|
||||
isSkipped: false,
|
||||
healthchecks: [
|
||||
...(process.platform === 'darwin'
|
||||
? [
|
||||
{
|
||||
key: 'android.android-studio',
|
||||
label: 'Android Studio Installed',
|
||||
isRequired: false,
|
||||
run: async (_: FlipperDoctor.EnvironmentInfo) => {
|
||||
const hasProblem = !fs.existsSync(
|
||||
'/Applications/Android Studio.app',
|
||||
);
|
||||
|
||||
const message = hasProblem
|
||||
? installAndroidStudio
|
||||
: `Android Studio is installed.`;
|
||||
|
||||
return {
|
||||
hasProblem,
|
||||
message,
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'android.sdk',
|
||||
label: 'SDK Installed',
|
||||
@@ -74,12 +102,12 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
if (!androidHome) {
|
||||
androidHomeResult = {
|
||||
hasProblem: true,
|
||||
message: `ANDROID_HOME is not defined. You can use Flipper Settings (File > Preferences) to point to its location.`,
|
||||
message: `ANDROID_HOME is not defined. You can use Flipper Settings (More > Settings) to point to its location.`,
|
||||
};
|
||||
} else if (!fs.existsSync(androidHome)) {
|
||||
androidHomeResult = {
|
||||
hasProblem: true,
|
||||
message: `ANDROID_HOME point to a folder which does not exist: ${androidHome}. You can use Flipper Settings (File > Preferences) to point to a different location.`,
|
||||
message: `ANDROID_HOME point to a folder which does not exist: ${androidHome}. You can use Flipper Settings (More > Settings) to point to a different location.`,
|
||||
};
|
||||
} else {
|
||||
const platformToolsDir = path.join(androidHome, 'platform-tools');
|
||||
@@ -102,12 +130,12 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
if (!androidSdkRoot) {
|
||||
androidSdkRootResult = {
|
||||
hasProblem: true,
|
||||
message: `ANDROID_SDK_ROOT is not defined. You can use Flipper Settings (File > Preferences) to point to its location.`,
|
||||
message: `ANDROID_SDK_ROOT is not defined. You can use Flipper Settings (More > Settings) to point to its location.`,
|
||||
};
|
||||
} else if (!fs.existsSync(androidSdkRoot)) {
|
||||
androidSdkRootResult = {
|
||||
hasProblem: true,
|
||||
message: `ANDROID_SDK_ROOT point to a folder which does not exist: ${androidSdkRoot}. You can use Flipper Settings (File > Preferences) to point to a different location.`,
|
||||
message: `ANDROID_SDK_ROOT point to a folder which does not exist: ${androidSdkRoot}. You can use Flipper Settings (More > Settings) to point to a different location.`,
|
||||
};
|
||||
} else {
|
||||
const platformToolsDir = path.join(
|
||||
@@ -137,26 +165,6 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
isRequired: false,
|
||||
isSkipped: false,
|
||||
healthchecks: [
|
||||
{
|
||||
key: 'ios.sdk',
|
||||
label: 'SDK Installed',
|
||||
isRequired: true,
|
||||
run: async (e: FlipperDoctor.EnvironmentInfo) => {
|
||||
const hasProblem =
|
||||
!e.SDKs['iOS SDK'] ||
|
||||
!e.SDKs['iOS SDK'].Platforms ||
|
||||
!e.SDKs['iOS SDK'].Platforms.length;
|
||||
const message = hasProblem
|
||||
? 'iOS SDK is not installed. You can install it using Xcode (https://developer.apple.com/xcode/).'
|
||||
: `iOS SDK is installed for the following platforms: ${JSON.stringify(
|
||||
e.SDKs['iOS SDK'].Platforms,
|
||||
)}.`;
|
||||
return {
|
||||
hasProblem,
|
||||
message,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ios.xcode',
|
||||
label: 'XCode Installed',
|
||||
@@ -164,7 +172,7 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
run: async (e: FlipperDoctor.EnvironmentInfo) => {
|
||||
const hasProblem = e.IDEs == null || e.IDEs.Xcode == null;
|
||||
const message = hasProblem
|
||||
? 'Xcode (https://developer.apple.com/xcode/) is not installed.'
|
||||
? `Xcode is not installed.\n${installXcode}.`
|
||||
: `Xcode version ${e.IDEs.Xcode.version} is installed at "${e.IDEs.Xcode.path}".`;
|
||||
return {
|
||||
hasProblem,
|
||||
@@ -217,6 +225,26 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ios.sdk',
|
||||
label: 'SDK Installed',
|
||||
isRequired: true,
|
||||
run: async (e: FlipperDoctor.EnvironmentInfo) => {
|
||||
const hasProblem =
|
||||
!e.SDKs['iOS SDK'] ||
|
||||
!e.SDKs['iOS SDK'].Platforms ||
|
||||
!e.SDKs['iOS SDK'].Platforms.length;
|
||||
const message = hasProblem
|
||||
? `iOS SDK is not installed. ${installSDK}`
|
||||
: `iOS SDK is installed for the following platforms: ${JSON.stringify(
|
||||
e.SDKs['iOS SDK'].Platforms,
|
||||
)}.`;
|
||||
return {
|
||||
hasProblem,
|
||||
message,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ios.xctrace',
|
||||
label: 'xctrace exists',
|
||||
@@ -260,13 +288,17 @@ export function getHealthchecks(): FlipperDoctor.Healthchecks {
|
||||
const result = await tryExecuteCommand(
|
||||
`${settings?.idbPath} --help`,
|
||||
);
|
||||
const hasProblem = result.hasProblem;
|
||||
const message = hasProblem
|
||||
? getIdbInstallationInstructions(settings.idbPath)
|
||||
: 'Flipper is configured to use your IDB installation.';
|
||||
if (result.hasProblem) {
|
||||
return {
|
||||
hasProblem: true,
|
||||
...getIdbInstallationInstructions(settings.idbPath),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasProblem,
|
||||
message,
|
||||
hasProblem: false,
|
||||
message:
|
||||
'Flipper is configured to use your IDB installation.',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -82,6 +82,7 @@ export type ActivatablePluginDetails = InstalledPluginDetails;
|
||||
// Describes plugin available for downloading. Until downloaded to the disk it is not available for activation in Flipper.
|
||||
export interface DownloadablePluginDetails extends ConcretePluginDetails {
|
||||
isActivatable: false;
|
||||
buildId: string;
|
||||
downloadUrl: string;
|
||||
lastUpdated: Date;
|
||||
// Indicates whether plugin should be enabled by default for new users
|
||||
|
||||
@@ -54,6 +54,7 @@ export {
|
||||
isConnectivityOrAuthError,
|
||||
isError,
|
||||
isAuthError,
|
||||
FlipperServerDisconnectedError,
|
||||
getStringFromErrorLike,
|
||||
getErrorFromErrorLike,
|
||||
deserializeRemoteError,
|
||||
|
||||
@@ -168,6 +168,7 @@ export type FlipperServerEvents = {
|
||||
'plugins-server-add-on-message': ExecuteMessage;
|
||||
'download-file-update': DownloadFileUpdate;
|
||||
'server-log': LoggerInfo;
|
||||
'browser-connection-created': {};
|
||||
};
|
||||
|
||||
export type OS =
|
||||
@@ -287,6 +288,7 @@ export type FlipperServerCommands = {
|
||||
serial: string,
|
||||
appBundlePath: string,
|
||||
) => Promise<void>;
|
||||
'device-open-app': (serial: string, name: string) => Promise<void>;
|
||||
'device-forward-port': (
|
||||
serial: string,
|
||||
local: string,
|
||||
@@ -371,6 +373,7 @@ export type FlipperServerCommands = {
|
||||
timeout?: number;
|
||||
internGraphUrl?: string;
|
||||
headers?: Record<string, string | number | boolean>;
|
||||
vpnMode?: 'vpn' | 'vpnless';
|
||||
},
|
||||
) => Promise<GraphResponse>;
|
||||
'intern-upload-scribe-logs': (
|
||||
@@ -381,6 +384,7 @@ export type FlipperServerCommands = {
|
||||
'is-logged-in': () => Promise<boolean>;
|
||||
'environment-info': () => Promise<EnvironmentInfo>;
|
||||
'move-pwa': () => Promise<void>;
|
||||
'fetch-new-version': (version: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export type GraphResponse = {
|
||||
|
||||
@@ -96,6 +96,12 @@ export class NoLongerConnectedToClientError extends Error {
|
||||
name: 'NoLongerConnectedToClientError';
|
||||
}
|
||||
|
||||
export class FlipperServerDisconnectedError extends Error {
|
||||
constructor(public readonly reason: 'ws-close') {
|
||||
super(`Flipper Server disconnected. Reason: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Error {
|
||||
interaction?: unknown;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
InstalledPluginDetails,
|
||||
tryCatchReportPluginFailuresAsync,
|
||||
notNull,
|
||||
FlipperServerDisconnectedError,
|
||||
} from 'flipper-common';
|
||||
import {ActivatablePluginDetails, ConcretePluginDetails} from 'flipper-common';
|
||||
import {reportUsage} from 'flipper-common';
|
||||
@@ -229,7 +230,15 @@ export const createRequirePluginFunction =
|
||||
return pluginDefinition;
|
||||
} catch (e) {
|
||||
failedPlugins.push([pluginDetails, e.message]);
|
||||
console.error(`Plugin ${pluginDetails.id} failed to load`, e);
|
||||
|
||||
let severity: 'error' | 'warn' = 'error';
|
||||
if (
|
||||
e instanceof FlipperServerDisconnectedError &&
|
||||
e.reason === 'ws-close'
|
||||
) {
|
||||
severity = 'warn';
|
||||
}
|
||||
console[severity](`Plugin ${pluginDetails.id} failed to load`, e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import sortedIndexBy from 'lodash/sortedIndexBy';
|
||||
import sortedLastIndexBy from 'lodash/sortedLastIndexBy';
|
||||
import property from 'lodash/property';
|
||||
import lodashSort from 'lodash/sortBy';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
// If the dataSource becomes to large, after how many records will we start to drop items?
|
||||
const dropFactor = 0.1;
|
||||
@@ -45,12 +46,23 @@ type ShiftEvent<T> = {
|
||||
entries: Entry<T>[];
|
||||
amount: number;
|
||||
};
|
||||
type SINewIndexValueEvent<T> = {
|
||||
type: 'siNewIndexValue';
|
||||
indexKey: string;
|
||||
value: T;
|
||||
firstOfKind: boolean;
|
||||
};
|
||||
type ClearEvent = {
|
||||
type: 'clear';
|
||||
};
|
||||
|
||||
type DataEvent<T> =
|
||||
| AppendEvent<T>
|
||||
| UpdateEvent<T>
|
||||
| RemoveEvent<T>
|
||||
| ShiftEvent<T>;
|
||||
| ShiftEvent<T>
|
||||
| SINewIndexValueEvent<T>
|
||||
| ClearEvent;
|
||||
|
||||
type Entry<T> = {
|
||||
value: T;
|
||||
@@ -180,6 +192,8 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
[viewId: string]: DataSourceView<T, KeyType>;
|
||||
};
|
||||
|
||||
private readonly outputEventEmitter = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
keyAttribute: keyof T | undefined,
|
||||
secondaryIndices: IndexDefinition<T>[] = [],
|
||||
@@ -259,6 +273,10 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
};
|
||||
}
|
||||
|
||||
public secondaryIndicesKeys(): string[] {
|
||||
return [...this._secondaryIndices.keys()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of a specific key in the *records* set.
|
||||
* Returns -1 if the record wansn't found
|
||||
@@ -466,6 +484,7 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
this.shiftOffset = 0;
|
||||
this.idToIndex.clear();
|
||||
this.rebuild();
|
||||
this.emitDataEvent({type: 'clear'});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -519,6 +538,16 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
}
|
||||
}
|
||||
|
||||
public addDataListener<E extends DataEvent<T>['type']>(
|
||||
event: E,
|
||||
cb: (data: Extract<DataEvent<T>, {type: E}>) => void,
|
||||
) {
|
||||
this.outputEventEmitter.addListener(event, cb);
|
||||
return () => {
|
||||
this.outputEventEmitter.removeListener(event, cb);
|
||||
};
|
||||
}
|
||||
|
||||
private assertKeySet() {
|
||||
if (!this.keyAttribute) {
|
||||
throw new Error(
|
||||
@@ -550,6 +579,7 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
Object.entries(this.additionalViews).forEach(([, dataView]) => {
|
||||
dataView.processEvent(event);
|
||||
});
|
||||
this.outputEventEmitter.emit(event.type, event);
|
||||
}
|
||||
|
||||
private storeSecondaryIndices(value: T) {
|
||||
@@ -567,6 +597,12 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
} else {
|
||||
a.push(value);
|
||||
}
|
||||
this.emitDataEvent({
|
||||
type: 'siNewIndexValue',
|
||||
indexKey: indexValue,
|
||||
value,
|
||||
firstOfKind: !a,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,11 +663,21 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
return this.getAllRecordsByIndex(indexQuery)[0];
|
||||
}
|
||||
|
||||
public getAllIndexValues(index: IndexDefinition<T>) {
|
||||
const sortedKeys = index.slice().sort();
|
||||
const indexKey = sortedKeys.join(':');
|
||||
const recordsByIndex = this._recordsBySecondaryIndex.get(indexKey);
|
||||
if (!recordsByIndex) {
|
||||
return;
|
||||
}
|
||||
return [...recordsByIndex.keys()];
|
||||
}
|
||||
|
||||
private getSecondaryIndexValueFromRecord(
|
||||
record: T,
|
||||
// assumes keys is already ordered
|
||||
keys: IndexDefinition<T>,
|
||||
): any {
|
||||
): string {
|
||||
return JSON.stringify(
|
||||
Object.fromEntries(keys.map((k) => [k, String(record[k])])),
|
||||
);
|
||||
@@ -989,6 +1035,10 @@ export class DataSourceView<T, KeyType> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'clear':
|
||||
case 'siNewIndexValue': {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('unknown event type');
|
||||
}
|
||||
|
||||
@@ -912,6 +912,13 @@ test('secondary keys - lookup by single key', () => {
|
||||
indices: [['id'], ['title'], ['done']],
|
||||
});
|
||||
|
||||
expect(ds.secondaryIndicesKeys()).toEqual(['id', 'title', 'done']);
|
||||
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||
JSON.stringify({id: 'cookie'}),
|
||||
JSON.stringify({id: 'coffee'}),
|
||||
JSON.stringify({id: 'bug'}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
ds.getAllRecordsByIndex({
|
||||
title: 'eat a cookie',
|
||||
@@ -938,6 +945,12 @@ test('secondary keys - lookup by single key', () => {
|
||||
}),
|
||||
).toEqual(submitBug);
|
||||
|
||||
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||
JSON.stringify({id: 'cookie'}),
|
||||
JSON.stringify({id: 'coffee'}),
|
||||
JSON.stringify({id: 'bug'}),
|
||||
]);
|
||||
|
||||
ds.delete(0); // eat Cookie
|
||||
expect(
|
||||
ds.getAllRecordsByIndex({
|
||||
@@ -945,6 +958,13 @@ test('secondary keys - lookup by single key', () => {
|
||||
}),
|
||||
).toEqual([cookie2]);
|
||||
|
||||
// We do not remove empty index values (for now)
|
||||
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||
JSON.stringify({id: 'cookie'}),
|
||||
JSON.stringify({id: 'coffee'}),
|
||||
JSON.stringify({id: 'bug'}),
|
||||
]);
|
||||
|
||||
// replace submit Bug
|
||||
const n = {
|
||||
id: 'bug',
|
||||
@@ -972,6 +992,12 @@ test('secondary keys - lookup by single key', () => {
|
||||
title: 'eat a cookie',
|
||||
}),
|
||||
).toEqual([cookie2]);
|
||||
|
||||
expect(ds.getAllIndexValues(['id'])).toEqual([
|
||||
JSON.stringify({id: 'cookie'}),
|
||||
JSON.stringify({id: 'coffee'}),
|
||||
JSON.stringify({id: 'bug'}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('secondary keys - lookup by combined keys', () => {
|
||||
@@ -983,6 +1009,13 @@ test('secondary keys - lookup by combined keys', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(ds.secondaryIndicesKeys()).toEqual(['id:title', 'done:title']);
|
||||
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||
JSON.stringify({id: 'cookie', title: 'eat a cookie'}),
|
||||
JSON.stringify({id: 'coffee', title: 'drink coffee'}),
|
||||
JSON.stringify({id: 'bug', title: 'submit a bug'}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
ds.getAllRecordsByIndex({
|
||||
id: 'cookie',
|
||||
@@ -1014,6 +1047,13 @@ test('secondary keys - lookup by combined keys', () => {
|
||||
}),
|
||||
).toEqual([eatCookie, cookie2]);
|
||||
|
||||
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||
JSON.stringify({id: 'cookie', title: 'eat a cookie'}),
|
||||
JSON.stringify({id: 'coffee', title: 'drink coffee'}),
|
||||
JSON.stringify({id: 'bug', title: 'submit a bug'}),
|
||||
JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||
]);
|
||||
|
||||
const upsertedCookie = {
|
||||
id: 'cookie',
|
||||
title: 'eat a cookie',
|
||||
@@ -1041,6 +1081,16 @@ test('secondary keys - lookup by combined keys', () => {
|
||||
}),
|
||||
).toEqual(undefined);
|
||||
|
||||
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||
JSON.stringify({id: 'cookie', title: 'eat a cookie'}),
|
||||
JSON.stringify({id: 'coffee', title: 'drink coffee'}),
|
||||
JSON.stringify({id: 'bug', title: 'submit a bug'}),
|
||||
JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||
]);
|
||||
|
||||
const clearSub = jest.fn();
|
||||
ds.addDataListener('clear', clearSub);
|
||||
|
||||
ds.clear();
|
||||
expect(
|
||||
ds.getAllRecordsByIndex({
|
||||
@@ -1049,6 +1099,12 @@ test('secondary keys - lookup by combined keys', () => {
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([]);
|
||||
expect(clearSub).toBeCalledTimes(1);
|
||||
|
||||
const newIndexValueSub = jest.fn();
|
||||
ds.addDataListener('siNewIndexValue', newIndexValueSub);
|
||||
|
||||
ds.append(cookie2);
|
||||
expect(
|
||||
ds.getAllRecordsByIndex({
|
||||
@@ -1056,4 +1112,23 @@ test('secondary keys - lookup by combined keys', () => {
|
||||
title: 'eat a cookie',
|
||||
}),
|
||||
).toEqual([cookie2]);
|
||||
|
||||
expect(ds.getAllIndexValues(['id', 'title'])).toEqual([
|
||||
JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||
]);
|
||||
|
||||
// Because we have 2 indecies
|
||||
expect(newIndexValueSub).toBeCalledTimes(2);
|
||||
expect(newIndexValueSub).toBeCalledWith({
|
||||
type: 'siNewIndexValue',
|
||||
indexKey: JSON.stringify({id: 'cookie2', title: 'eat a cookie'}),
|
||||
firstOfKind: true,
|
||||
value: cookie2,
|
||||
});
|
||||
expect(newIndexValueSub).toBeCalledWith({
|
||||
type: 'siNewIndexValue',
|
||||
indexKey: JSON.stringify({done: 'true', title: 'eat a cookie'}),
|
||||
firstOfKind: true,
|
||||
value: cookie2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,6 +121,7 @@ test('Correct top level API exposed', () => {
|
||||
"ElementSearchResultSet",
|
||||
"ElementsInspectorElement",
|
||||
"ElementsInspectorProps",
|
||||
"EnumLabels",
|
||||
"FieldConfig",
|
||||
"FileDescriptor",
|
||||
"FileEncoding",
|
||||
|
||||
@@ -38,9 +38,9 @@ export {Sidebar as _Sidebar} from './ui/Sidebar';
|
||||
export {DetailSidebar} from './ui/DetailSidebar';
|
||||
export {Toolbar} from './ui/Toolbar';
|
||||
|
||||
export {MasterDetail} from './ui/MasterDetail';
|
||||
export {MasterDetail as MasterDetailLegacy} from './ui/MasterDetail';
|
||||
export {MasterDetailWithPowerSearch as MasterDetail} from './ui/MasterDetailWithPowerSearch';
|
||||
export {MasterDetailWithPowerSearch as _MasterDetailWithPowerSearch} from './ui/MasterDetailWithPowerSearch';
|
||||
export {MasterDetail as MasterDetailLegacy} from './ui/MasterDetail';
|
||||
export {CodeBlock} from './ui/CodeBlock';
|
||||
|
||||
export {renderReactRoot, _PortalsManager} from './utils/renderReactRoot';
|
||||
@@ -59,19 +59,22 @@ export {DataFormatter} from './ui/DataFormatter';
|
||||
|
||||
export {useLogger, _LoggerContext} from './utils/useLogger';
|
||||
|
||||
export {DataTable, DataTableColumn} from './ui/data-table/DataTable';
|
||||
export {
|
||||
DataTable as DataTableLegacy,
|
||||
DataTableColumn as DataTableColumnLegacy,
|
||||
} from './ui/data-table/DataTable';
|
||||
export {DataTableManager} from './ui/data-table/DataTableManager';
|
||||
export {DataTableManager as DataTableManagerLegacy} from './ui/data-table/DataTableManager';
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
} from './ui/data-table/DataTableWithPowerSearch';
|
||||
export {
|
||||
DataTable as _DataTableWithPowerSearch,
|
||||
DataTableColumn as _DataTableColumnWithPowerSearch,
|
||||
} from './ui/data-table/DataTableWithPowerSearch';
|
||||
export {dataTablePowerSearchOperators} from './ui/data-table/DataTableDefaultPowerSearchOperators';
|
||||
export {
|
||||
DataTable as DataTableLegacy,
|
||||
DataTableColumn as DataTableColumnLegacy,
|
||||
} from './ui/data-table/DataTable';
|
||||
export {DataTableManager} from './ui/data-table/DataTableWithPowerSearchManager';
|
||||
export {DataTableManager as _DataTableWithPowerSearchManager} from './ui/data-table/DataTableWithPowerSearchManager';
|
||||
export {DataTableManager as DataTableManagerLegacy} from './ui/data-table/DataTableManager';
|
||||
export {dataTablePowerSearchOperators} from './ui/data-table/DataTableDefaultPowerSearchOperators';
|
||||
export {DataList} from './ui/DataList';
|
||||
export {Spinner} from './ui/Spinner';
|
||||
export * from './ui/PowerSearch';
|
||||
|
||||
@@ -213,7 +213,6 @@ export function MasterDetailWithPowerSearch<T extends object>({
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
style={{height: '100%'}}
|
||||
title={`Click to ${pausedState ? 'resume' : 'pause'} the stream`}
|
||||
danger={pausedState}
|
||||
onClick={handleTogglePause}>
|
||||
@@ -225,8 +224,7 @@ export function MasterDetailWithPowerSearch<T extends object>({
|
||||
size="small"
|
||||
type="text"
|
||||
title="Clear records"
|
||||
onClick={handleClear}
|
||||
style={{height: '100%'}}>
|
||||
onClick={handleClear}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -33,7 +33,6 @@ export type StringOperatorConfig = {
|
||||
valueType: StringFilterValueType;
|
||||
key: string;
|
||||
label: string;
|
||||
handleUnknownValues?: boolean;
|
||||
};
|
||||
|
||||
export type StringSetOperatorConfig = {
|
||||
@@ -55,11 +54,17 @@ export type FloatOperatorConfig = {
|
||||
precision?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* { value: label }
|
||||
*/
|
||||
export type EnumLabels = {[key: string | number]: string | number};
|
||||
|
||||
export type EnumOperatorConfig = {
|
||||
valueType: EnumFilterValueType;
|
||||
key: string;
|
||||
label: string;
|
||||
enumLabels: {[key: string]: string};
|
||||
enumLabels: EnumLabels;
|
||||
allowFreeform?: boolean;
|
||||
};
|
||||
|
||||
export type AbsoluteDateOperatorConfig = {
|
||||
|
||||
@@ -12,14 +12,16 @@ import {css} from '@emotion/css';
|
||||
import {theme} from '../theme';
|
||||
|
||||
const containerStyle = css`
|
||||
flex: 1 0 auto;
|
||||
flex: 1 1 auto;
|
||||
background-color: ${theme.backgroundDefault};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
border-radius: ${theme.borderRadius};
|
||||
border: 1px solid ${theme.borderColor};
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
padding: 0 ${theme.space.tiny}px;
|
||||
padding: ${theme.space.tiny / 2}px;
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
|
||||
import {Select} from 'antd';
|
||||
import React from 'react';
|
||||
import {EnumLabels} from './PowerSearchConfig';
|
||||
|
||||
type PowerSearchEnumSetTermProps = {
|
||||
onCancel: () => void;
|
||||
onChange: (value: string[]) => void;
|
||||
enumLabels: {[key: string]: string};
|
||||
enumLabels: EnumLabels;
|
||||
defaultValue?: string[];
|
||||
allowFreeform?: boolean;
|
||||
};
|
||||
|
||||
export const PowerSearchEnumSetTerm: React.FC<PowerSearchEnumSetTermProps> = ({
|
||||
@@ -22,6 +24,7 @@ export const PowerSearchEnumSetTerm: React.FC<PowerSearchEnumSetTermProps> = ({
|
||||
onChange,
|
||||
enumLabels,
|
||||
defaultValue,
|
||||
allowFreeform,
|
||||
}) => {
|
||||
const options = React.useMemo(() => {
|
||||
return Object.entries(enumLabels).map(([key, label]) => ({
|
||||
@@ -37,13 +40,14 @@ export const PowerSearchEnumSetTerm: React.FC<PowerSearchEnumSetTermProps> = ({
|
||||
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
mode={allowFreeform ? 'tags' : 'multiple'}
|
||||
autoFocus={!defaultValue}
|
||||
style={{minWidth: 100}}
|
||||
placeholder="..."
|
||||
options={options}
|
||||
defaultOpen={!defaultValue}
|
||||
defaultValue={defaultValue}
|
||||
dropdownMatchSelectWidth={false}
|
||||
onBlur={() => {
|
||||
if (!selectValueRef.current?.length) {
|
||||
onCancel();
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
|
||||
import {Button, Select} from 'antd';
|
||||
import React from 'react';
|
||||
import {EnumLabels} from './PowerSearchConfig';
|
||||
|
||||
type PowerSearchEnumTermProps = {
|
||||
onCancel: () => void;
|
||||
onChange: (value: string) => void;
|
||||
enumLabels: {[key: string]: string};
|
||||
enumLabels: EnumLabels;
|
||||
defaultValue?: string;
|
||||
allowFreeform?: boolean;
|
||||
};
|
||||
|
||||
export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
@@ -22,6 +24,7 @@ export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
onChange,
|
||||
enumLabels,
|
||||
defaultValue,
|
||||
allowFreeform,
|
||||
}) => {
|
||||
const [editing, setEditing] = React.useState(!defaultValue);
|
||||
|
||||
@@ -38,8 +41,8 @@ export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
|
||||
let longestOptionLabelWidth = 0;
|
||||
Object.values(enumLabels).forEach((label) => {
|
||||
if (label.length > longestOptionLabelWidth) {
|
||||
longestOptionLabelWidth = label.length;
|
||||
if (label.toString().length > longestOptionLabelWidth) {
|
||||
longestOptionLabelWidth = label.toString().length;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,6 +74,7 @@ export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
if (editing) {
|
||||
return (
|
||||
<Select
|
||||
mode={allowFreeform ? 'tags' : undefined}
|
||||
autoFocus
|
||||
style={{width}}
|
||||
placeholder="..."
|
||||
@@ -99,7 +103,7 @@ export const PowerSearchEnumTerm: React.FC<PowerSearchEnumTermProps> = ({
|
||||
return (
|
||||
<Button onClick={() => setEditing(true)}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{enumLabels[defaultValue!]}
|
||||
{enumLabels[defaultValue!] ?? defaultValue}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import {CloseOutlined} from '@ant-design/icons';
|
||||
import {Button, Space} from 'antd';
|
||||
import * as React from 'react';
|
||||
import {theme} from '../theme';
|
||||
import {PowerSearchAbsoluteDateTerm} from './PowerSearchAbsoluteDateTerm';
|
||||
import {OperatorConfig} from './PowerSearchConfig';
|
||||
import {PowerSearchEnumSetTerm} from './PowerSearchEnumSetTerm';
|
||||
@@ -115,6 +116,7 @@ export const PowerSearchTerm: React.FC<PowerSearchTermProps> = ({
|
||||
});
|
||||
}}
|
||||
enumLabels={searchTerm.operator.enumLabels}
|
||||
allowFreeform={searchTerm.operator.allowFreeform}
|
||||
defaultValue={searchTerm.searchValue}
|
||||
/>
|
||||
);
|
||||
@@ -131,6 +133,7 @@ export const PowerSearchTerm: React.FC<PowerSearchTermProps> = ({
|
||||
});
|
||||
}}
|
||||
enumLabels={searchTerm.operator.enumLabels}
|
||||
allowFreeform={searchTerm.operator.allowFreeform}
|
||||
defaultValue={searchTerm.searchValue}
|
||||
/>
|
||||
);
|
||||
@@ -166,7 +169,7 @@ export const PowerSearchTerm: React.FC<PowerSearchTermProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Space.Compact block size="small">
|
||||
<Space.Compact size="small" style={{margin: theme.space.tiny / 2}}>
|
||||
<Button tabIndex={-1} style={{pointerEvents: 'none'}}>
|
||||
{searchTerm.field.label}
|
||||
</Button>
|
||||
|
||||
@@ -67,6 +67,7 @@ export const PowerSearchTermFinder = React.forwardRef<
|
||||
setSearchTermFinderValue(null);
|
||||
}}>
|
||||
<Input
|
||||
size="small"
|
||||
bordered={false}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Space} from 'antd';
|
||||
import {
|
||||
PowerSearchConfig,
|
||||
FieldConfig,
|
||||
OperatorConfig,
|
||||
EnumLabels,
|
||||
} from './PowerSearchConfig';
|
||||
import {PowerSearchContainer} from './PowerSearchContainer';
|
||||
import {
|
||||
@@ -31,7 +31,13 @@ import {theme} from '../theme';
|
||||
import {SearchOutlined} from '@ant-design/icons';
|
||||
import {getFlipperLib} from 'flipper-plugin-core';
|
||||
|
||||
export {PowerSearchConfig, OperatorConfig, FieldConfig, SearchExpressionTerm};
|
||||
export {
|
||||
PowerSearchConfig,
|
||||
EnumLabels,
|
||||
OperatorConfig,
|
||||
FieldConfig,
|
||||
SearchExpressionTerm,
|
||||
};
|
||||
|
||||
type PowerSearchProps = {
|
||||
config: PowerSearchConfig;
|
||||
@@ -122,44 +128,41 @@ export const PowerSearch: React.FC<PowerSearchProps> = ({
|
||||
|
||||
return (
|
||||
<PowerSearchContainer>
|
||||
<Space size={[theme.space.tiny, 0]}>
|
||||
<SearchOutlined
|
||||
style={{
|
||||
marginLeft: theme.space.tiny,
|
||||
marginRight: theme.space.tiny,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
/>
|
||||
{searchExpression.map((searchTerm, i) => {
|
||||
return (
|
||||
<PowerSearchTerm
|
||||
key={JSON.stringify(searchTerm)}
|
||||
searchTerm={searchTerm}
|
||||
onCancel={() => {
|
||||
setSearchExpression((prevSearchExpression) => {
|
||||
if (prevSearchExpression[i]) {
|
||||
return [
|
||||
...prevSearchExpression.slice(0, i),
|
||||
...prevSearchExpression.slice(i + 1),
|
||||
];
|
||||
}
|
||||
return prevSearchExpression;
|
||||
});
|
||||
}}
|
||||
onFinalize={(finalSearchTerm) => {
|
||||
setSearchExpression((prevSearchExpression) => {
|
||||
<SearchOutlined
|
||||
style={{
|
||||
margin: theme.space.tiny,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
/>
|
||||
{searchExpression.map((searchTerm, i) => {
|
||||
return (
|
||||
<PowerSearchTerm
|
||||
key={JSON.stringify(searchTerm)}
|
||||
searchTerm={searchTerm}
|
||||
onCancel={() => {
|
||||
setSearchExpression((prevSearchExpression) => {
|
||||
if (prevSearchExpression[i]) {
|
||||
return [
|
||||
...prevSearchExpression.slice(0, i),
|
||||
finalSearchTerm,
|
||||
...prevSearchExpression.slice(i + 1),
|
||||
];
|
||||
});
|
||||
searchTermFinderRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
}
|
||||
return prevSearchExpression;
|
||||
});
|
||||
}}
|
||||
onFinalize={(finalSearchTerm) => {
|
||||
setSearchExpression((prevSearchExpression) => {
|
||||
return [
|
||||
...prevSearchExpression.slice(0, i),
|
||||
finalSearchTerm,
|
||||
...prevSearchExpression.slice(i + 1),
|
||||
];
|
||||
});
|
||||
searchTermFinderRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<PowerSearchTermFinder
|
||||
ref={searchTermFinderRef}
|
||||
options={options}
|
||||
|
||||
@@ -168,7 +168,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||
if (horizontal) {
|
||||
width = width == null ? 200 : width;
|
||||
minWidth = (minWidth == null ? 100 : minWidth) + gutterWidth;
|
||||
maxWidth = maxWidth == null ? 600 : maxWidth;
|
||||
maxWidth = maxWidth == null ? 1200 : maxWidth;
|
||||
} else {
|
||||
height = height == null ? 200 : height;
|
||||
minHeight = minHeight == null ? 100 : minHeight;
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import {getFlipperLib} from 'flipper-plugin-core';
|
||||
import {OperatorConfig} from '../PowerSearch';
|
||||
import {
|
||||
EnumLabels,
|
||||
FloatOperatorConfig,
|
||||
StringOperatorConfig,
|
||||
} from '../PowerSearch/PowerSearchConfig';
|
||||
|
||||
export type PowerSearchOperatorProcessor = (
|
||||
@@ -22,35 +21,41 @@ export type PowerSearchOperatorProcessor = (
|
||||
) => boolean;
|
||||
|
||||
export const dataTablePowerSearchOperators = {
|
||||
string_contains: (handleUnknownValues?: boolean) => ({
|
||||
string_matches_regex: () => ({
|
||||
label: 'matches regex',
|
||||
key: 'string_matches_regex',
|
||||
valueType: 'STRING',
|
||||
}),
|
||||
string_contains: () => ({
|
||||
label: 'contains',
|
||||
key: 'string_contains',
|
||||
valueType: 'STRING',
|
||||
handleUnknownValues,
|
||||
}),
|
||||
string_not_contains: (handleUnknownValues?: boolean) => ({
|
||||
string_not_contains: () => ({
|
||||
label: 'does not contain',
|
||||
key: 'string_not_contains',
|
||||
valueType: 'STRING',
|
||||
handleUnknownValues,
|
||||
}),
|
||||
string_matches_exactly: (handleUnknownValues?: boolean) => ({
|
||||
string_matches_exactly: () => ({
|
||||
label: 'is',
|
||||
key: 'string_matches_exactly',
|
||||
valueType: 'STRING',
|
||||
handleUnknownValues,
|
||||
}),
|
||||
string_not_matches_exactly: (handleUnknownValues?: boolean) => ({
|
||||
string_not_matches_exactly: () => ({
|
||||
label: 'is not',
|
||||
key: 'string_not_matches_exactly',
|
||||
valueType: 'STRING',
|
||||
handleUnknownValues,
|
||||
}),
|
||||
searializable_object_contains: () => ({
|
||||
label: 'contains',
|
||||
key: 'searializable_object_contains',
|
||||
valueType: 'STRING',
|
||||
}),
|
||||
searializable_object_matches_regex: () => ({
|
||||
label: 'matches regex',
|
||||
key: 'searializable_object_matches_regex',
|
||||
valueType: 'STRING',
|
||||
}),
|
||||
searializable_object_not_contains: () => ({
|
||||
label: 'does not contain',
|
||||
key: 'searializable_object_not_contains',
|
||||
@@ -118,42 +123,51 @@ export const dataTablePowerSearchOperators = {
|
||||
valueType: 'FLOAT',
|
||||
}),
|
||||
// { [enumValue]: enumLabel }
|
||||
enum_is: (enumLabels: Record<string, string>) => ({
|
||||
enum_is: (enumLabels: EnumLabels, allowFreeform?: boolean) => ({
|
||||
label: 'is',
|
||||
key: 'enum_is',
|
||||
valueType: 'ENUM',
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
}),
|
||||
enum_is_nullish_or: (enumLabels: Record<string, string>) => ({
|
||||
enum_is_nullish_or: (enumLabels: EnumLabels, allowFreeform?: boolean) => ({
|
||||
label: 'is nullish or',
|
||||
key: 'enum_is_nullish_or',
|
||||
valueType: 'ENUM',
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
}),
|
||||
enum_is_not: (enumLabels: Record<string, string>) => ({
|
||||
enum_is_not: (enumLabels: EnumLabels, allowFreeform?: boolean) => ({
|
||||
label: 'is not',
|
||||
key: 'enum_is_not',
|
||||
valueType: 'ENUM',
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
}),
|
||||
// TODO: Support logical operations (AND, OR, NOT) to combine primitive operators instead of adding new complex operators!
|
||||
enum_set_is_nullish_or_any_of: (enumLabels: Record<string, string>) => ({
|
||||
enum_set_is_nullish_or_any_of: (
|
||||
enumLabels: EnumLabels,
|
||||
allowFreeform?: boolean,
|
||||
) => ({
|
||||
label: 'is nullish or any of',
|
||||
key: 'enum_set_is_nullish_or_any_of',
|
||||
valueType: 'ENUM_SET',
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
}),
|
||||
enum_set_is_any_of: (enumLabels: Record<string, string>) => ({
|
||||
enum_set_is_any_of: (enumLabels: EnumLabels, allowFreeform?: boolean) => ({
|
||||
label: 'is any of',
|
||||
key: 'enum_set_is_any_of',
|
||||
valueType: 'ENUM_SET',
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
}),
|
||||
enum_set_is_none_of: (enumLabels: Record<string, string>) => ({
|
||||
enum_set_is_none_of: (enumLabels: EnumLabels, allowFreeform?: boolean) => ({
|
||||
label: 'is none of',
|
||||
key: 'enum_set_is_none_of',
|
||||
valueType: 'ENUM_SET',
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
}),
|
||||
is_nullish: () => ({
|
||||
label: 'is nullish',
|
||||
@@ -222,25 +236,53 @@ const tryConvertingUnknownToString = (value: unknown): string | null => {
|
||||
}
|
||||
};
|
||||
|
||||
const regexCache: Record<string, RegExp> = {};
|
||||
function safeCreateRegExp(source: string): RegExp | undefined {
|
||||
try {
|
||||
if (!regexCache[source]) {
|
||||
regexCache[source] = new RegExp(source);
|
||||
}
|
||||
return regexCache[source];
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const enumPredicateForWhenValueCouldBeAStringifiedNullish = (
|
||||
// searchValue is typed as a string here, but originally it could have been an undefined or a null and we stringified them during inference (search for `inferEnumOptionsFromData`)
|
||||
searchValue: string,
|
||||
value: string | null | undefined,
|
||||
): boolean => {
|
||||
if (searchValue === value) {
|
||||
return true;
|
||||
}
|
||||
if (value === null && searchValue === 'null') {
|
||||
return true;
|
||||
}
|
||||
if (value === undefined && searchValue === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const dataTablePowerSearchOperatorProcessorConfig = {
|
||||
string_contains: (operator, searchValue: string, value: string) =>
|
||||
!!(
|
||||
(operator as StringOperatorConfig).handleUnknownValues &&
|
||||
getFlipperLib().GK('flipper_power_search_auto_json_stringify')
|
||||
? tryConvertingUnknownToString(value)
|
||||
: value
|
||||
)
|
||||
string_matches_regex: (_operator, searchValue: string, value: string) =>
|
||||
!!safeCreateRegExp(searchValue)?.test(
|
||||
tryConvertingUnknownToString(value) ?? '',
|
||||
),
|
||||
string_contains: (_operator, searchValue: string, value: string) =>
|
||||
!!tryConvertingUnknownToString(value)
|
||||
?.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()),
|
||||
string_not_contains: (operator, searchValue: string, value: string) =>
|
||||
!(
|
||||
(operator as StringOperatorConfig).handleUnknownValues &&
|
||||
getFlipperLib().GK('flipper_power_search_auto_json_stringify')
|
||||
? tryConvertingUnknownToString(value)
|
||||
: value
|
||||
)
|
||||
string_not_contains: (_operator, searchValue: string, value: string) =>
|
||||
!tryConvertingUnknownToString(value)
|
||||
?.toLowerCase()
|
||||
.includes(searchValue.toLowerCase()),
|
||||
searializable_object_matches_regex: (
|
||||
_operator,
|
||||
searchValue: string,
|
||||
value: object,
|
||||
) => !!safeCreateRegExp(searchValue)?.test(JSON.stringify(value)),
|
||||
searializable_object_contains: (
|
||||
_operator,
|
||||
searchValue: string,
|
||||
@@ -251,16 +293,10 @@ export const dataTablePowerSearchOperatorProcessorConfig = {
|
||||
searchValue: string,
|
||||
value: object,
|
||||
) => !JSON.stringify(value).toLowerCase().includes(searchValue.toLowerCase()),
|
||||
string_matches_exactly: (operator, searchValue: string, value: string) =>
|
||||
((operator as StringOperatorConfig).handleUnknownValues &&
|
||||
getFlipperLib().GK('flipper_power_search_auto_json_stringify')
|
||||
? tryConvertingUnknownToString(value)
|
||||
: value) === searchValue,
|
||||
string_not_matches_exactly: (operator, searchValue: string, value: string) =>
|
||||
((operator as StringOperatorConfig).handleUnknownValues &&
|
||||
getFlipperLib().GK('flipper_power_search_auto_json_stringify')
|
||||
? tryConvertingUnknownToString(value)
|
||||
: value) !== searchValue,
|
||||
string_matches_exactly: (_operator, searchValue: string, value: string) =>
|
||||
tryConvertingUnknownToString(value) === searchValue,
|
||||
string_not_matches_exactly: (_operator, searchValue: string, value: string) =>
|
||||
tryConvertingUnknownToString(value) !== searchValue,
|
||||
// See PowerSearchStringSetTerm
|
||||
string_set_contains_any_of: (
|
||||
_operator,
|
||||
@@ -268,7 +304,9 @@ export const dataTablePowerSearchOperatorProcessorConfig = {
|
||||
value: string,
|
||||
) =>
|
||||
searchValue.some((item) =>
|
||||
value.toLowerCase().includes(item.toLowerCase()),
|
||||
tryConvertingUnknownToString(value)
|
||||
?.toLowerCase()
|
||||
.includes(item.toLowerCase()),
|
||||
),
|
||||
string_set_contains_none_of: (
|
||||
_operator,
|
||||
@@ -276,7 +314,9 @@ export const dataTablePowerSearchOperatorProcessorConfig = {
|
||||
value: string,
|
||||
) =>
|
||||
!searchValue.some((item) =>
|
||||
value.toLowerCase().includes(item.toLowerCase()),
|
||||
tryConvertingUnknownToString(value)
|
||||
?.toLowerCase()
|
||||
.includes(item.toLowerCase()),
|
||||
),
|
||||
int_equals: (_operator, searchValue: number, value: number) =>
|
||||
value === searchValue,
|
||||
@@ -301,20 +341,29 @@ export const dataTablePowerSearchOperatorProcessorConfig = {
|
||||
float_less_or_equal: (_operator, searchValue: number, value: number) =>
|
||||
value <= searchValue,
|
||||
enum_is: (_operator, searchValue: string, value: string) =>
|
||||
searchValue === value,
|
||||
enumPredicateForWhenValueCouldBeAStringifiedNullish(searchValue, value),
|
||||
enum_is_nullish_or: (_operator, searchValue: string, value?: string | null) =>
|
||||
value == null || searchValue === value,
|
||||
value == null ||
|
||||
enumPredicateForWhenValueCouldBeAStringifiedNullish(searchValue, value),
|
||||
enum_is_not: (_operator, searchValue: string, value: string) =>
|
||||
searchValue !== value,
|
||||
!enumPredicateForWhenValueCouldBeAStringifiedNullish(searchValue, value),
|
||||
enum_set_is_nullish_or_any_of: (
|
||||
_operator,
|
||||
searchValue: string[],
|
||||
value?: string | null,
|
||||
) => value == null || searchValue.some((item) => value === item),
|
||||
) =>
|
||||
value == null ||
|
||||
searchValue.some((item) =>
|
||||
enumPredicateForWhenValueCouldBeAStringifiedNullish(item, value),
|
||||
),
|
||||
enum_set_is_any_of: (_operator, searchValue: string[], value: string) =>
|
||||
searchValue.some((item) => value === item),
|
||||
searchValue.some((item) =>
|
||||
enumPredicateForWhenValueCouldBeAStringifiedNullish(item, value),
|
||||
),
|
||||
enum_set_is_none_of: (_operator, searchValue: string[], value: string) =>
|
||||
!searchValue.some((item) => value === item),
|
||||
!searchValue.some((item) =>
|
||||
enumPredicateForWhenValueCouldBeAStringifiedNullish(item, value),
|
||||
),
|
||||
is_nullish: (_operator, _searchValue, value) => value == null,
|
||||
// See PowerSearchAbsoluteDateTerm
|
||||
newer_than_absolute_date: (_operator, searchValue: Date, value: any) => {
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
FieldConfig,
|
||||
OperatorConfig,
|
||||
SearchExpressionTerm,
|
||||
EnumLabels,
|
||||
} from '../PowerSearch';
|
||||
import {
|
||||
dataTablePowerSearchOperatorProcessorConfig,
|
||||
@@ -104,6 +105,10 @@ type DataTableBaseProps<T = any> = {
|
||||
* @default true
|
||||
*/
|
||||
enablePowerSearchWholeRowSearch?: boolean;
|
||||
/** If set to `true` and row[columnKey] is undefined, then it is going to pass filtering (search).
|
||||
* @default false
|
||||
*/
|
||||
treatUndefinedValuesAsMatchingFiltering?: boolean;
|
||||
};
|
||||
|
||||
const powerSearchConfigEntireRow: FieldConfig = {
|
||||
@@ -114,6 +119,8 @@ const powerSearchConfigEntireRow: FieldConfig = {
|
||||
dataTablePowerSearchOperators.searializable_object_contains(),
|
||||
searializable_object_not_contains:
|
||||
dataTablePowerSearchOperators.searializable_object_not_contains(),
|
||||
searializable_object_matches_regex:
|
||||
dataTablePowerSearchOperators.searializable_object_matches_regex(),
|
||||
},
|
||||
useWholeRow: true,
|
||||
};
|
||||
@@ -138,6 +145,64 @@ type DataTableInput<T = any> =
|
||||
dataSource?: undefined;
|
||||
};
|
||||
|
||||
type PowerSearchSimplifiedConfig =
|
||||
| {
|
||||
type: 'enum';
|
||||
enumLabels: EnumLabels;
|
||||
inferEnumOptionsFromData?: false;
|
||||
allowFreeform?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'enum';
|
||||
enumLabels?: never;
|
||||
inferEnumOptionsFromData: true;
|
||||
allowFreeform?: boolean;
|
||||
}
|
||||
| {type: 'int'}
|
||||
| {type: 'float'}
|
||||
| {type: 'string'}
|
||||
| {type: 'date'}
|
||||
| {type: 'dateTime'}
|
||||
| {type: 'object'};
|
||||
type PowerSearchExtendedConfig = {
|
||||
operators: OperatorConfig[];
|
||||
useWholeRow?: boolean;
|
||||
/**
|
||||
* Auto-generate enum options based on the data.
|
||||
* Requires the column to be set as a secondary "index" (single column, not a compound multi-column index).
|
||||
* See https://fburl.com/code/0waicx6p
|
||||
*/
|
||||
inferEnumOptionsFromData?: boolean;
|
||||
/**
|
||||
* Allows freeform entries for enum column types. Makes most sense together with `inferEnumOptionsFromData`.
|
||||
* If `inferEnumOptionsFromData=true`, then it is `true` by default.
|
||||
* See use-case https://fburl.com/workplace/0kx6fkhm
|
||||
*/
|
||||
allowFreeform?: boolean;
|
||||
};
|
||||
|
||||
const powerSearchConfigIsExtendedConfig = (
|
||||
powerSearchConfig:
|
||||
| undefined
|
||||
| PowerSearchSimplifiedConfig
|
||||
| OperatorConfig[]
|
||||
| false
|
||||
| PowerSearchExtendedConfig,
|
||||
): powerSearchConfig is PowerSearchExtendedConfig =>
|
||||
!!powerSearchConfig &&
|
||||
Array.isArray((powerSearchConfig as PowerSearchExtendedConfig).operators);
|
||||
|
||||
const powerSearchConfigIsSimplifiedConfig = (
|
||||
powerSearchConfig:
|
||||
| undefined
|
||||
| PowerSearchSimplifiedConfig
|
||||
| OperatorConfig[]
|
||||
| false
|
||||
| PowerSearchExtendedConfig,
|
||||
): powerSearchConfig is PowerSearchSimplifiedConfig =>
|
||||
!!powerSearchConfig &&
|
||||
typeof (powerSearchConfig as PowerSearchSimplifiedConfig).type === 'string';
|
||||
|
||||
export type DataTableColumn<T = any> = {
|
||||
//this can be a dotted path into a nest objects. e.g foo.bar
|
||||
key: keyof T & string;
|
||||
@@ -152,9 +217,10 @@ export type DataTableColumn<T = any> = {
|
||||
inversed?: boolean;
|
||||
sortable?: boolean;
|
||||
powerSearchConfig?:
|
||||
| PowerSearchSimplifiedConfig
|
||||
| OperatorConfig[]
|
||||
| false
|
||||
| {operators: OperatorConfig[]; useWholeRow?: boolean};
|
||||
| PowerSearchExtendedConfig;
|
||||
};
|
||||
|
||||
export interface TableRowRenderContext<T = any> {
|
||||
@@ -263,6 +329,74 @@ export function DataTable<T extends object>(
|
||||
[columns],
|
||||
);
|
||||
|
||||
// Collecting a hashmap of unique values for every column we infer the power search enum labels for (hashmap of hashmaps).
|
||||
// It could be a hashmap of sets, but then we would need to convert a set to a hashpmap when rendering enum power search term, so it is just more convenient to make it a hashmap of hashmaps
|
||||
const [inferredPowerSearchEnumLabels, setInferredPowerSearchEnumLabels] =
|
||||
React.useState<Record<DataTableColumn['key'], EnumLabels>>({});
|
||||
React.useEffect(() => {
|
||||
const columnKeysToInferOptionsFor: string[] = [];
|
||||
const secondaryIndeciesKeys = new Set(dataSource.secondaryIndicesKeys());
|
||||
|
||||
for (const column of columns) {
|
||||
if (
|
||||
(powerSearchConfigIsExtendedConfig(column.powerSearchConfig) ||
|
||||
(powerSearchConfigIsSimplifiedConfig(column.powerSearchConfig) &&
|
||||
column.powerSearchConfig.type === 'enum')) &&
|
||||
column.powerSearchConfig.inferEnumOptionsFromData
|
||||
) {
|
||||
if (!secondaryIndeciesKeys.has(column.key)) {
|
||||
console.warn(
|
||||
'inferEnumOptionsFromData work only if the same column key is specified as a DataSource secondary index! See https://fburl.com/code/0waicx6p. Missing index definition!',
|
||||
column.key,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
columnKeysToInferOptionsFor.push(column.key);
|
||||
}
|
||||
}
|
||||
|
||||
if (columnKeysToInferOptionsFor.length > 0) {
|
||||
const getInferredLabels = () => {
|
||||
const newInferredLabels: Record<DataTableColumn['key'], EnumLabels> =
|
||||
{};
|
||||
|
||||
for (const key of columnKeysToInferOptionsFor) {
|
||||
newInferredLabels[key] = {};
|
||||
for (const indexValue of dataSource.getAllIndexValues([
|
||||
key as keyof T,
|
||||
]) ?? []) {
|
||||
// `indexValue` is a stringified JSON in a format of { key: value }
|
||||
const value = Object.values(JSON.parse(indexValue))[0] as string;
|
||||
newInferredLabels[key][value] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return newInferredLabels;
|
||||
};
|
||||
setInferredPowerSearchEnumLabels(getInferredLabels());
|
||||
|
||||
const unsubscribeIndexUpdates = dataSource.addDataListener(
|
||||
'siNewIndexValue',
|
||||
({firstOfKind}) => {
|
||||
if (firstOfKind) {
|
||||
setInferredPowerSearchEnumLabels(getInferredLabels());
|
||||
}
|
||||
},
|
||||
);
|
||||
const unsubscribeDataSourceClear = dataSource.addDataListener(
|
||||
'clear',
|
||||
() => {
|
||||
setInferredPowerSearchEnumLabels(getInferredLabels());
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeIndexUpdates();
|
||||
unsubscribeDataSourceClear();
|
||||
};
|
||||
}
|
||||
}, [columns, dataSource]);
|
||||
|
||||
const powerSearchConfig: PowerSearchConfig = useMemo(() => {
|
||||
const res: PowerSearchConfig = {fields: {}};
|
||||
|
||||
@@ -280,16 +414,128 @@ export function DataTable<T extends object>(
|
||||
// If no power search config provided we treat every input as a string
|
||||
if (!column.powerSearchConfig) {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.string_contains(true),
|
||||
dataTablePowerSearchOperators.string_not_contains(true),
|
||||
dataTablePowerSearchOperators.string_matches_exactly(true),
|
||||
dataTablePowerSearchOperators.string_not_matches_exactly(true),
|
||||
dataTablePowerSearchOperators.string_contains(),
|
||||
dataTablePowerSearchOperators.string_not_contains(),
|
||||
dataTablePowerSearchOperators.string_matches_exactly(),
|
||||
dataTablePowerSearchOperators.string_not_matches_exactly(),
|
||||
dataTablePowerSearchOperators.string_set_contains_any_of(),
|
||||
dataTablePowerSearchOperators.string_set_contains_none_of(),
|
||||
dataTablePowerSearchOperators.string_matches_regex(),
|
||||
];
|
||||
} else if (Array.isArray(column.powerSearchConfig)) {
|
||||
columnPowerSearchOperators = column.powerSearchConfig;
|
||||
} else {
|
||||
} else if (powerSearchConfigIsExtendedConfig(column.powerSearchConfig)) {
|
||||
columnPowerSearchOperators = column.powerSearchConfig.operators;
|
||||
useWholeRow = !!column.powerSearchConfig.useWholeRow;
|
||||
|
||||
const inferredPowerSearchEnumLabelsForColumn =
|
||||
inferredPowerSearchEnumLabels[column.key];
|
||||
if (
|
||||
inferredPowerSearchEnumLabelsForColumn &&
|
||||
column.powerSearchConfig.inferEnumOptionsFromData
|
||||
) {
|
||||
const allowFreeform = column.powerSearchConfig.allowFreeform ?? true;
|
||||
columnPowerSearchOperators = columnPowerSearchOperators.map(
|
||||
(operator) => ({
|
||||
...operator,
|
||||
enumLabels: inferredPowerSearchEnumLabelsForColumn,
|
||||
allowFreeform,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
switch (column.powerSearchConfig.type) {
|
||||
case 'date': {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.same_as_absolute_date_no_time(),
|
||||
dataTablePowerSearchOperators.older_than_absolute_date_no_time(),
|
||||
dataTablePowerSearchOperators.newer_than_absolute_date_no_time(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'dateTime': {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.older_than_absolute_date(),
|
||||
dataTablePowerSearchOperators.newer_than_absolute_date(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.string_matches_exactly(),
|
||||
dataTablePowerSearchOperators.string_not_matches_exactly(),
|
||||
dataTablePowerSearchOperators.string_set_contains_any_of(),
|
||||
dataTablePowerSearchOperators.string_set_contains_none_of(),
|
||||
dataTablePowerSearchOperators.string_matches_regex(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'int': {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.int_equals(),
|
||||
dataTablePowerSearchOperators.int_greater_or_equal(),
|
||||
dataTablePowerSearchOperators.int_greater_than(),
|
||||
dataTablePowerSearchOperators.int_less_or_equal(),
|
||||
dataTablePowerSearchOperators.int_less_than(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'float': {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.float_equals(),
|
||||
dataTablePowerSearchOperators.float_greater_or_equal(),
|
||||
dataTablePowerSearchOperators.float_greater_than(),
|
||||
dataTablePowerSearchOperators.float_less_or_equal(),
|
||||
dataTablePowerSearchOperators.float_less_than(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'enum': {
|
||||
let enumLabels: EnumLabels;
|
||||
let allowFreeform = column.powerSearchConfig.allowFreeform;
|
||||
|
||||
if (column.powerSearchConfig.inferEnumOptionsFromData) {
|
||||
enumLabels = inferredPowerSearchEnumLabels[column.key] ?? {};
|
||||
// Fallback to `true` by default when we use inferred labels
|
||||
if (allowFreeform === undefined) {
|
||||
allowFreeform = true;
|
||||
}
|
||||
} else {
|
||||
enumLabels = column.powerSearchConfig.enumLabels;
|
||||
}
|
||||
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.enum_set_is_any_of(
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
),
|
||||
dataTablePowerSearchOperators.enum_set_is_none_of(
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
),
|
||||
dataTablePowerSearchOperators.enum_set_is_nullish_or_any_of(
|
||||
enumLabels,
|
||||
allowFreeform,
|
||||
),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'object': {
|
||||
columnPowerSearchOperators = [
|
||||
dataTablePowerSearchOperators.searializable_object_contains(),
|
||||
dataTablePowerSearchOperators.searializable_object_not_contains(),
|
||||
dataTablePowerSearchOperators.searializable_object_matches_regex(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unknown power search config type ${JSON.stringify(
|
||||
column.powerSearchConfig,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columnFieldConfig: FieldConfig = {
|
||||
@@ -305,7 +551,11 @@ export function DataTable<T extends object>(
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [columns, props.enablePowerSearchWholeRowSearch]);
|
||||
}, [
|
||||
columns,
|
||||
props.enablePowerSearchWholeRowSearch,
|
||||
inferredPowerSearchEnumLabels,
|
||||
]);
|
||||
|
||||
const renderingConfig = useMemo<TableRowRenderContext<T>>(() => {
|
||||
let startIndex = 0;
|
||||
@@ -461,6 +711,7 @@ export function DataTable<T extends object>(
|
||||
computeDataTableFilter(
|
||||
tableState.searchExpression,
|
||||
dataTablePowerSearchOperatorProcessorConfig,
|
||||
props.treatUndefinedValuesAsMatchingFiltering,
|
||||
),
|
||||
);
|
||||
dataView.setFilterExpections(
|
||||
@@ -670,7 +921,7 @@ export function DataTable<T extends object>(
|
||||
<Layout.Container>
|
||||
{props.actionsTop ? <Searchbar gap>{props.actionsTop}</Searchbar> : null}
|
||||
{props.enableSearchbar && (
|
||||
<Searchbar gap>
|
||||
<Searchbar grow shrink gap style={{alignItems: 'baseline'}}>
|
||||
<PowerSearch
|
||||
config={powerSearchConfig}
|
||||
searchExpression={searchExpression}
|
||||
@@ -690,7 +941,7 @@ export function DataTable<T extends object>(
|
||||
/>
|
||||
{contexMenu && (
|
||||
<Dropdown overlay={contexMenu} placement="bottomRight">
|
||||
<Button type="text" size="small" style={{height: '100%'}}>
|
||||
<Button type="text" size="small">
|
||||
<MenuOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
@@ -819,6 +1070,7 @@ DataTable.defaultProps = {
|
||||
enablePersistSettings: true,
|
||||
onRenderEmpty: undefined,
|
||||
enablePowerSearchWholeRowSearch: true,
|
||||
treatUndefinedValuesAsMatchingFiltering: false,
|
||||
} as Partial<DataTableProps<any>>;
|
||||
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
|
||||
@@ -14,7 +14,10 @@ import {DataSourceVirtualizer} from '../../data-source/index';
|
||||
import produce, {castDraft, immerable, original} from 'immer';
|
||||
import {DataSource, getFlipperLib, _DataSourceView} from 'flipper-plugin-core';
|
||||
import {SearchExpressionTerm} from '../PowerSearch';
|
||||
import {PowerSearchOperatorProcessorConfig} from './DataTableDefaultPowerSearchOperators';
|
||||
import {
|
||||
dataTablePowerSearchOperators,
|
||||
PowerSearchOperatorProcessorConfig,
|
||||
} from './DataTableDefaultPowerSearchOperators';
|
||||
import {DataTableManager as DataTableManagerLegacy} from './DataTableManager';
|
||||
|
||||
export type OnColumnResize = (id: string, size: number | Percentage) => void;
|
||||
@@ -38,7 +41,7 @@ const emptySelection: Selection = {
|
||||
|
||||
type PersistedState = {
|
||||
/** Active search value */
|
||||
searchExpression?: SearchExpressionTerm[];
|
||||
searchExpression: SearchExpressionTerm[];
|
||||
/** current selection, describes the index index in the datasources's current output (not window!) */
|
||||
selection: {current: number; items: number[]};
|
||||
/** The currently applicable sorting, if any */
|
||||
@@ -87,6 +90,7 @@ type DataManagerActions<T> =
|
||||
}
|
||||
>
|
||||
| Action<'clearSelection', {}>
|
||||
| Action<'setSearchExpressionFromSelection', {column: DataTableColumn<T>}>
|
||||
| Action<'setFilterExceptions', {exceptions: string[] | undefined}>
|
||||
| Action<'appliedInitialScroll'>
|
||||
| Action<'toggleAutoScroll'>
|
||||
@@ -116,7 +120,7 @@ export type DataManagerState<T> = {
|
||||
sorting: Sorting<T> | undefined;
|
||||
selection: Selection;
|
||||
autoScroll: boolean;
|
||||
searchExpression?: SearchExpressionTerm[];
|
||||
searchExpression: SearchExpressionTerm[];
|
||||
filterExceptions: string[] | undefined;
|
||||
sideBySide: boolean;
|
||||
};
|
||||
@@ -136,13 +140,13 @@ export const dataTableManagerReducer = produce<
|
||||
case 'reset': {
|
||||
draft.columns = computeInitialColumns(config.defaultColumns);
|
||||
draft.sorting = undefined;
|
||||
draft.searchExpression = undefined;
|
||||
draft.searchExpression = [];
|
||||
draft.selection = castDraft(emptySelection);
|
||||
draft.filterExceptions = undefined;
|
||||
break;
|
||||
}
|
||||
case 'resetFilters': {
|
||||
draft.searchExpression = undefined;
|
||||
draft.searchExpression = [];
|
||||
draft.filterExceptions = undefined;
|
||||
break;
|
||||
}
|
||||
@@ -169,7 +173,35 @@ export const dataTableManagerReducer = produce<
|
||||
}
|
||||
case 'setSearchExpression': {
|
||||
getFlipperLib().logger.track('usage', 'data-table:filter:power-search');
|
||||
draft.searchExpression = action.searchExpression;
|
||||
draft.searchExpression = action.searchExpression ?? [];
|
||||
draft.filterExceptions = undefined;
|
||||
break;
|
||||
}
|
||||
case 'setSearchExpressionFromSelection': {
|
||||
getFlipperLib().logger.track(
|
||||
'usage',
|
||||
'data-table:filter:power-search-from-selection',
|
||||
);
|
||||
draft.filterExceptions = undefined;
|
||||
const items = getSelectedItems(
|
||||
config.dataView as _DataSourceView<any, any>,
|
||||
draft.selection,
|
||||
);
|
||||
|
||||
const searchExpressionFromSelection: SearchExpressionTerm[] = [
|
||||
{
|
||||
field: {
|
||||
key: action.column.key,
|
||||
label: action.column.title ?? action.column.key,
|
||||
},
|
||||
operator: dataTablePowerSearchOperators.enum_set_is_any_of({}),
|
||||
searchValue: items.map((item) =>
|
||||
getValueAtPath(item, action.column.key),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
draft.searchExpression = searchExpressionFromSelection;
|
||||
draft.filterExceptions = undefined;
|
||||
break;
|
||||
}
|
||||
@@ -374,7 +406,7 @@ export function createInitialState<T>(
|
||||
});
|
||||
}
|
||||
|
||||
let searchExpression = config.initialSearchExpression;
|
||||
let searchExpression = config.initialSearchExpression ?? [];
|
||||
if (prefs?.searchExpression?.length) {
|
||||
searchExpression = prefs.searchExpression;
|
||||
}
|
||||
@@ -506,25 +538,18 @@ export function getValueAtPath(obj: Record<string, any>, keyPath: string): any {
|
||||
}
|
||||
|
||||
export function computeDataTableFilter(
|
||||
searchExpression: SearchExpressionTerm[] | undefined,
|
||||
searchExpression: SearchExpressionTerm[],
|
||||
powerSearchProcessors: PowerSearchOperatorProcessorConfig,
|
||||
treatUndefinedValuesAsMatchingFiltering: boolean = false,
|
||||
) {
|
||||
return function dataTableFilter(item: any) {
|
||||
if (!searchExpression || !searchExpression.length) {
|
||||
if (!searchExpression.length) {
|
||||
return true;
|
||||
}
|
||||
return searchExpression.every((searchTerm) => {
|
||||
const value = searchTerm.field.useWholeRow
|
||||
? item
|
||||
: getValueAtPath(item, searchTerm.field.key);
|
||||
if (!value) {
|
||||
console.warn(
|
||||
'computeDataTableFilter -> value at searchTerm.field.key is not recognized',
|
||||
searchTerm,
|
||||
item,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const processor =
|
||||
powerSearchProcessors[
|
||||
@@ -539,7 +564,21 @@ export function computeDataTableFilter(
|
||||
return true;
|
||||
}
|
||||
|
||||
return processor(searchTerm.operator, searchTerm.searchValue, value);
|
||||
try {
|
||||
const res = processor(
|
||||
searchTerm.operator,
|
||||
searchTerm.searchValue,
|
||||
value,
|
||||
);
|
||||
|
||||
if (!res && !value) {
|
||||
return treatUndefinedValuesAsMatchingFiltering;
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch {
|
||||
return treatUndefinedValuesAsMatchingFiltering;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getSelectedItems,
|
||||
getValueAtPath,
|
||||
Selection,
|
||||
} from './DataTableManager';
|
||||
} from './DataTableWithPowerSearchManager';
|
||||
import React from 'react';
|
||||
import {
|
||||
_tryGetFlipperLibImplementation,
|
||||
@@ -65,8 +65,8 @@ export function tableContextMenuFactory<T extends object>(
|
||||
key={column.key ?? idx}
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'setColumnFilterFromSelection',
|
||||
column: column.key,
|
||||
type: 'setSearchExpressionFromSelection',
|
||||
column,
|
||||
});
|
||||
}}>
|
||||
{friendlyColumnTitle(column)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FlipperServerCommands,
|
||||
FlipperServerExecOptions,
|
||||
ServerWebSocketMessage,
|
||||
FlipperServerDisconnectedError,
|
||||
} from 'flipper-common';
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
|
||||
@@ -30,11 +31,11 @@ export type {FlipperServer, FlipperServerCommands, FlipperServerExecOptions};
|
||||
export function createFlipperServer(
|
||||
host: string,
|
||||
port: number,
|
||||
tokenProvider: () => Promise<string | null | undefined>,
|
||||
tokenProvider: () => string | null | undefined,
|
||||
onStateChange: (state: FlipperServerState) => void,
|
||||
): Promise<FlipperServer> {
|
||||
const URLProvider = async () => {
|
||||
const token = await tokenProvider();
|
||||
const URLProvider = () => {
|
||||
const token = tokenProvider();
|
||||
return `ws://${host}:${port}?token=${token}`;
|
||||
};
|
||||
|
||||
@@ -90,7 +91,7 @@ export function createFlipperServerWithSocket(
|
||||
onStateChange(FlipperServerState.DISCONNECTED);
|
||||
|
||||
pendingRequests.forEach((r) =>
|
||||
r.reject(new Error('flipper-server disconnected')),
|
||||
r.reject(new FlipperServerDisconnectedError('ws-close')),
|
||||
);
|
||||
pendingRequests.clear();
|
||||
});
|
||||
|
||||
@@ -59,6 +59,8 @@ import {DebuggableDevice} from './devices/DebuggableDevice';
|
||||
import {jfUpload} from './fb-stubs/jf';
|
||||
import path from 'path';
|
||||
import {movePWA} from './utils/findInstallation';
|
||||
import GK from './fb-stubs/GK';
|
||||
import {fetchNewVersion} from './fb-stubs/fetchNewVersion';
|
||||
|
||||
const {access, copyFile, mkdir, unlink, stat, readlink, readFile, writeFile} =
|
||||
promises;
|
||||
@@ -75,10 +77,8 @@ function setProcessState(settings: Settings) {
|
||||
const androidHome = settings.androidHome;
|
||||
const idbPath = settings.idbPath;
|
||||
|
||||
if (!process.env.ANDROID_HOME && !process.env.ANDROID_SDK_ROOT) {
|
||||
process.env.ANDROID_HOME = androidHome;
|
||||
process.env.ANDROID_SDK_ROOT = androidHome;
|
||||
}
|
||||
process.env.ANDROID_HOME = androidHome;
|
||||
process.env.ANDROID_SDK_ROOT = androidHome;
|
||||
|
||||
// emulator/emulator is more reliable than tools/emulator, so prefer it if
|
||||
// it exists
|
||||
@@ -111,6 +111,7 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
keytarManager: KeytarManager;
|
||||
pluginManager: PluginManager;
|
||||
unresponsiveClients: Set<string> = new Set();
|
||||
private acceptingNewConections = true;
|
||||
|
||||
constructor(
|
||||
public config: FlipperServerConfig,
|
||||
@@ -118,9 +119,7 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
keytarModule?: KeytarModule,
|
||||
) {
|
||||
setFlipperServerConfig(config);
|
||||
console.log(
|
||||
'Loaded flipper config, paths: ' + JSON.stringify(config.paths, null, 2),
|
||||
);
|
||||
console.info('Loaded flipper config: ' + JSON.stringify(config, null, 2));
|
||||
|
||||
setProcessState(config.settings);
|
||||
const server = (this.server = new ServerController(this));
|
||||
@@ -179,6 +178,35 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
);
|
||||
}
|
||||
|
||||
startAcceptingNewConections() {
|
||||
if (!GK.get('flipper_disconnect_device_when_ui_offline')) {
|
||||
return;
|
||||
}
|
||||
if (this.acceptingNewConections) {
|
||||
return;
|
||||
}
|
||||
this.acceptingNewConections = true;
|
||||
|
||||
this.server.insecureServer?.startAcceptingNewConections();
|
||||
this.server.altInsecureServer?.startAcceptingNewConections();
|
||||
this.server.secureServer?.startAcceptingNewConections();
|
||||
this.server.altSecureServer?.startAcceptingNewConections();
|
||||
this.server.browserServer?.startAcceptingNewConections();
|
||||
}
|
||||
|
||||
stopAcceptingNewConections() {
|
||||
if (!GK.get('flipper_disconnect_device_when_ui_offline')) {
|
||||
return;
|
||||
}
|
||||
this.acceptingNewConections = false;
|
||||
|
||||
this.server.insecureServer?.stopAcceptingNewConections();
|
||||
this.server.altInsecureServer?.stopAcceptingNewConections();
|
||||
this.server.secureServer?.stopAcceptingNewConections();
|
||||
this.server.altSecureServer?.stopAcceptingNewConections();
|
||||
this.server.browserServer?.stopAcceptingNewConections();
|
||||
}
|
||||
|
||||
setServerState(state: FlipperServerState, error?: Error) {
|
||||
this.state = state;
|
||||
this.stateError = '' + error;
|
||||
@@ -369,6 +397,9 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
'device-install-app': async (serial, bundlePath) => {
|
||||
return this.devices.get(serial)?.installApp(bundlePath);
|
||||
},
|
||||
'device-open-app': async (serial, name) => {
|
||||
return this.devices.get(serial)?.openApp(name);
|
||||
},
|
||||
'get-server-state': async () => ({
|
||||
state: this.state,
|
||||
error: this.stateError,
|
||||
@@ -585,6 +616,7 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
return uploadRes;
|
||||
},
|
||||
shutdown: async () => {
|
||||
// Do not use processExit helper. We want to server immediatelly quit when this call is triggerred
|
||||
process.exit(0);
|
||||
},
|
||||
'is-logged-in': async () => {
|
||||
@@ -601,6 +633,7 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
'move-pwa': async () => {
|
||||
await movePWA();
|
||||
},
|
||||
'fetch-new-version': fetchNewVersion,
|
||||
};
|
||||
|
||||
registerDevice(device: ServerDevice) {
|
||||
|
||||
@@ -148,6 +148,10 @@ class BrowserServerWebSocket extends SecureServerWebSocket {
|
||||
|
||||
protected verifyClient(): ws.VerifyClientCallbackSync {
|
||||
return (info: {origin: string; req: IncomingMessage; secure: boolean}) => {
|
||||
if (!this.acceptingNewConections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFBBuild) {
|
||||
try {
|
||||
const urlObj = new URL(info.origin);
|
||||
|
||||
@@ -293,6 +293,11 @@ class ServerRSocket extends ServerWebSocketBase {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
protected stopAcceptingNewConectionsImpl(): void {
|
||||
// Did not find a straightforard way to iterate through RSocket open connections and close them.
|
||||
// We probably should not care and invest in it anyway as we are going to remove RScokets.
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerRSocket;
|
||||
|
||||
@@ -294,11 +294,20 @@ class ServerWebSocket extends ServerWebSocketBase {
|
||||
*/
|
||||
protected verifyClient(): VerifyClientCallbackSync {
|
||||
return (_info: {origin: string; req: IncomingMessage; secure: boolean}) => {
|
||||
if (!this.acceptingNewConections) {
|
||||
return false;
|
||||
}
|
||||
// Client verification is not necessary. The connected client has
|
||||
// already been verified using its certificate signed by the server.
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
protected stopAcceptingNewConectionsImpl(): void {
|
||||
this.wsServer?.clients.forEach((client) =>
|
||||
client.close(WSCloseCode.GoingAway),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ServerWebSocket;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SignCertificateMessage,
|
||||
} from 'flipper-common';
|
||||
import {SecureServerConfig} from './certificate-exchange/certificate-utils';
|
||||
import GK from '../fb-stubs/GK';
|
||||
|
||||
/**
|
||||
* Defines an interface for events triggered by a running server interacting
|
||||
@@ -98,6 +99,8 @@ export interface ServerEventsListener {
|
||||
* RSocket, WebSocket, etc.
|
||||
*/
|
||||
abstract class ServerWebSocketBase {
|
||||
protected acceptingNewConections = true;
|
||||
|
||||
constructor(protected listener: ServerEventsListener) {}
|
||||
|
||||
/**
|
||||
@@ -169,6 +172,23 @@ abstract class ServerWebSocketBase {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
startAcceptingNewConections() {
|
||||
if (!GK.get('flipper_disconnect_device_when_ui_offline')) {
|
||||
return;
|
||||
}
|
||||
this.acceptingNewConections = true;
|
||||
}
|
||||
|
||||
stopAcceptingNewConections() {
|
||||
if (!GK.get('flipper_disconnect_device_when_ui_offline')) {
|
||||
return;
|
||||
}
|
||||
this.acceptingNewConections = false;
|
||||
this.stopAcceptingNewConectionsImpl();
|
||||
}
|
||||
|
||||
protected abstract stopAcceptingNewConectionsImpl(): void;
|
||||
}
|
||||
|
||||
export default ServerWebSocketBase;
|
||||
|
||||
@@ -16,11 +16,10 @@ import {
|
||||
} from './openssl-wrapper-with-promises';
|
||||
import path from 'path';
|
||||
import tmp, {FileOptions} from 'tmp';
|
||||
import {FlipperServerConfig, reportPlatformFailures} from 'flipper-common';
|
||||
import {reportPlatformFailures} from 'flipper-common';
|
||||
import {isTest} from 'flipper-common';
|
||||
import {flipperDataFolder} from '../../utils/paths';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import {getFlipperServerConfig} from '../../FlipperServerConfig';
|
||||
import {Mutex} from 'async-mutex';
|
||||
import {createSecureContext} from 'tls';
|
||||
|
||||
@@ -288,45 +287,6 @@ const writeToTempFile = async (content: string): Promise<string> => {
|
||||
await fs.writeFile(path, content);
|
||||
return path;
|
||||
};
|
||||
|
||||
const manifestFilename = 'manifest.json';
|
||||
const getManifestPath = (config: FlipperServerConfig): string => {
|
||||
return path.resolve(config.paths.staticPath, manifestFilename);
|
||||
};
|
||||
|
||||
const exportTokenToManifest = async (token: string) => {
|
||||
console.info('Export token to manifest');
|
||||
let config: FlipperServerConfig | undefined;
|
||||
try {
|
||||
config = getFlipperServerConfig();
|
||||
} catch {
|
||||
console.warn(
|
||||
'Unable to obtain server configuration whilst exporting token to manifest',
|
||||
);
|
||||
}
|
||||
|
||||
if (!config || !config.environmentInfo.isHeadlessBuild) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifestPath = getManifestPath(config);
|
||||
try {
|
||||
const manifestData = await fs.readFile(manifestPath, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const manifest = JSON.parse(manifestData);
|
||||
manifest.token = token;
|
||||
|
||||
const newManifestData = JSON.stringify(manifest, null, 4);
|
||||
|
||||
await fs.writeFile(manifestPath, newManifestData);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Unable to export authentication token to manifest, may be non existent.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const generateAuthToken = async () => {
|
||||
console.info('Generate client authentication token');
|
||||
|
||||
@@ -340,8 +300,6 @@ export const generateAuthToken = async () => {
|
||||
|
||||
await fs.writeFile(serverAuthToken, token);
|
||||
|
||||
await exportTokenToManifest(token);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
@@ -375,8 +333,6 @@ export const getAuthToken = async (): Promise<string> => {
|
||||
return generateAuthToken();
|
||||
}
|
||||
|
||||
await exportTokenToManifest(token);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
|
||||
@@ -82,4 +82,8 @@ export abstract class ServerDevice {
|
||||
async installApp(_appBundlePath: string): Promise<void> {
|
||||
throw new Error('installApp not implemented');
|
||||
}
|
||||
|
||||
async openApp(_name: string): Promise<void> {
|
||||
throw new Error('openApp not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface IOSBridge {
|
||||
ipaPath: string,
|
||||
tempPath: string,
|
||||
) => Promise<void>;
|
||||
openApp: (serial: string, name: string) => Promise<void>;
|
||||
getInstalledApps: (serial: string) => Promise<IOSInstalledAppDescriptor[]>;
|
||||
ls: (serial: string, appBundleId: string, path: string) => Promise<string[]>;
|
||||
pull: (
|
||||
@@ -149,6 +150,11 @@ export class IDBBridge implements IOSBridge {
|
||||
await this._execIdb(`install ${ipaPath} --udid ${serial}`);
|
||||
}
|
||||
|
||||
async openApp(serial: string, name: string): Promise<void> {
|
||||
console.log(`Opening app via IDB ${name} ${serial}`);
|
||||
await this._execIdb(`launch ${name} --udid ${serial} -f`);
|
||||
}
|
||||
|
||||
async getActiveDevices(bootedOnly: boolean): Promise<DeviceTarget[]> {
|
||||
return iosUtil
|
||||
.targets(this.idbPath, this.enablePhysicalDevices, bootedOnly)
|
||||
@@ -217,6 +223,10 @@ export class SimctlBridge implements IOSBridge {
|
||||
);
|
||||
}
|
||||
|
||||
async openApp(): Promise<void> {
|
||||
throw new Error('openApp is not implemented for SimctlBridge');
|
||||
}
|
||||
|
||||
async installApp(
|
||||
serial: string,
|
||||
ipaPath: string,
|
||||
|
||||
@@ -140,6 +140,10 @@ export default class IOSDevice
|
||||
);
|
||||
}
|
||||
|
||||
async openApp(name: string): Promise<void> {
|
||||
return this.iOSBridge.openApp(this.serial, name);
|
||||
}
|
||||
|
||||
async readFlipperFolderForAllApps(): Promise<DeviceDebugData[]> {
|
||||
console.debug('IOSDevice.readFlipperFolderForAllApps', this.info.serial);
|
||||
const installedApps = await this.iOSBridge.getInstalledApps(
|
||||
|
||||
10
desktop/flipper-server-core/src/fb-stubs/fetchNewVersion.tsx
Normal file
10
desktop/flipper-server-core/src/fb-stubs/fetchNewVersion.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export const fetchNewVersion = async (): Promise<void> => {};
|
||||
@@ -13,13 +13,14 @@ export * from './tracker';
|
||||
export {loadLauncherSettings} from './utils/launcherSettings';
|
||||
export {loadProcessConfig} from './utils/processConfig';
|
||||
export {getEnvironmentInfo} from './utils/environmentInfo';
|
||||
export {findInstallation} from './utils/findInstallation';
|
||||
export {processExit, setProcessExitRoutine} from './utils/processExit';
|
||||
export {getGatekeepers} from './gk';
|
||||
export {setupPrefetcher} from './fb-stubs/Prefetcher';
|
||||
export * from './server/attachSocketServer';
|
||||
export * from './server/startFlipperServer';
|
||||
export * from './server/startServer';
|
||||
export * from './server/utilities';
|
||||
export * from './utils/openUI';
|
||||
export {isFBBuild} from './fb-stubs/constants';
|
||||
export {initializeLogger} from './fb-stubs/Logger';
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ import {
|
||||
FlipperServerCompanionEnv,
|
||||
} from 'flipper-server-companion';
|
||||
import {URLSearchParams} from 'url';
|
||||
import {getFlipperServerConfig} from '../FlipperServerConfig';
|
||||
import {tracker} from '../tracker';
|
||||
import {getFlipperServerConfig} from '../FlipperServerConfig';
|
||||
import {performance} from 'perf_hooks';
|
||||
import {processExit} from '../utils/processExit';
|
||||
|
||||
const safe = (f: () => void) => {
|
||||
try {
|
||||
@@ -41,6 +42,7 @@ const safe = (f: () => void) => {
|
||||
};
|
||||
|
||||
let numberOfConnectedClients = 0;
|
||||
let disconnectTimeout: NodeJS.Timeout | undefined;
|
||||
|
||||
/**
|
||||
* Attach and handle incoming messages from clients.
|
||||
@@ -52,15 +54,9 @@ export function attachSocketServer(
|
||||
server: FlipperServerImpl,
|
||||
companionEnv: FlipperServerCompanionEnv,
|
||||
) {
|
||||
const t0 = performance.now();
|
||||
const browserConnectionTimeout = setTimeout(() => {
|
||||
tracker.track('browser-connection-created', {
|
||||
successful: false,
|
||||
timeMS: performance.now() - t0,
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
socket.on('connection', (client, req) => {
|
||||
const t0 = performance.now();
|
||||
|
||||
const clientAddress =
|
||||
(req.socket.remoteAddress &&
|
||||
` ${req.socket.remoteAddress}:${req.socket.remotePort}`) ||
|
||||
@@ -69,13 +65,14 @@ export function attachSocketServer(
|
||||
console.log('Client connected', clientAddress);
|
||||
numberOfConnectedClients++;
|
||||
|
||||
clearTimeout(browserConnectionTimeout);
|
||||
tracker.track('browser-connection-created', {
|
||||
successful: true,
|
||||
timeMS: performance.now() - t0,
|
||||
});
|
||||
if (disconnectTimeout) {
|
||||
clearTimeout(disconnectTimeout);
|
||||
}
|
||||
|
||||
server.emit('browser-connection-created', {});
|
||||
|
||||
let connected = true;
|
||||
server.startAcceptingNewConections();
|
||||
|
||||
let flipperServerCompanion: FlipperServerCompanion | undefined;
|
||||
if (req.url) {
|
||||
@@ -242,7 +239,7 @@ export function attachSocketServer(
|
||||
safe(() => onClientMessage(data));
|
||||
});
|
||||
|
||||
async function onClientClose(closeOnIdle: boolean) {
|
||||
async function onClientClose(code?: number, error?: string) {
|
||||
console.log(`Client disconnected ${clientAddress}`);
|
||||
|
||||
numberOfConnectedClients--;
|
||||
@@ -251,29 +248,39 @@ export function attachSocketServer(
|
||||
server.offAny(onServerEvent);
|
||||
flipperServerCompanion?.destroyAll();
|
||||
|
||||
tracker.track('server-client-close', {
|
||||
code,
|
||||
error,
|
||||
sessionLength: performance.now() - t0,
|
||||
});
|
||||
|
||||
if (numberOfConnectedClients === 0) {
|
||||
server.stopAcceptingNewConections();
|
||||
}
|
||||
|
||||
if (
|
||||
getFlipperServerConfig().environmentInfo.isHeadlessBuild &&
|
||||
closeOnIdle
|
||||
isProduction()
|
||||
) {
|
||||
if (numberOfConnectedClients === 0 && isProduction()) {
|
||||
console.info('Shutdown as no clients are currently connected');
|
||||
process.exit(0);
|
||||
const FIVE_HOURS = 5 * 60 * 60 * 1000;
|
||||
if (disconnectTimeout) {
|
||||
clearTimeout(disconnectTimeout);
|
||||
}
|
||||
|
||||
disconnectTimeout = setTimeout(() => {
|
||||
if (numberOfConnectedClients === 0) {
|
||||
console.info(
|
||||
'[flipper-server] Shutdown as no clients are currently connected',
|
||||
);
|
||||
processExit(0);
|
||||
}
|
||||
}, FIVE_HOURS);
|
||||
}
|
||||
}
|
||||
|
||||
client.on('close', (code, _reason) => {
|
||||
console.info('[flipper-server] Client close with code', code);
|
||||
/**
|
||||
* The socket will close as the endpoint is terminating
|
||||
* the connection. Status code 1000 and 1001 are used for normal
|
||||
* closures. Either the connection is no longer needed or the
|
||||
* endpoint is going away i.e. browser navigating away from the
|
||||
* current page.
|
||||
* WS RFC: https://www.rfc-editor.org/rfc/rfc6455
|
||||
*/
|
||||
const closeOnIdle = code === 1000 || code === 1001;
|
||||
safe(() => onClientClose(closeOnIdle));
|
||||
safe(() => onClientClose(code));
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
@@ -283,7 +290,7 @@ export function attachSocketServer(
|
||||
* do not close on idle as there's a high probability the
|
||||
* client will attempt to connect again.
|
||||
*/
|
||||
onClientClose(false);
|
||||
onClientClose(undefined, error.message);
|
||||
console.error('Client disconnected with error', error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import express, {Express} from 'express';
|
||||
import express, {Express, RequestHandler} from 'express';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
@@ -18,11 +18,18 @@ import exitHook from 'exit-hook';
|
||||
import {attachSocketServer} from './attachSocketServer';
|
||||
import {FlipperServerImpl} from '../FlipperServerImpl';
|
||||
import {FlipperServerCompanionEnv} from 'flipper-server-companion';
|
||||
import {validateAuthToken} from '../app-connectivity/certificate-exchange/certificate-utils';
|
||||
import {
|
||||
getAuthToken,
|
||||
validateAuthToken,
|
||||
} from '../app-connectivity/certificate-exchange/certificate-utils';
|
||||
import {tracker} from '../tracker';
|
||||
import {EnvironmentInfo, isProduction} from 'flipper-common';
|
||||
import {GRAPH_SECRET} from '../fb-stubs/constants';
|
||||
import {sessionId} from '../sessionId';
|
||||
import {UIPreference, openUI} from '../utils/openUI';
|
||||
import {processExit} from '../utils/processExit';
|
||||
|
||||
import util from 'node:util';
|
||||
|
||||
type Config = {
|
||||
port: number;
|
||||
@@ -37,6 +44,7 @@ type ReadyForConnections = (
|
||||
|
||||
const verifyAuthToken = (req: http.IncomingMessage): boolean => {
|
||||
let token: string | null = null;
|
||||
|
||||
if (req.url) {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
token = url.searchParams.get('token');
|
||||
@@ -46,6 +54,10 @@ const verifyAuthToken = (req: http.IncomingMessage): boolean => {
|
||||
token = req.headers['x-access-token'] as string;
|
||||
}
|
||||
|
||||
if (!isProduction()) {
|
||||
console.info('[conn] verifyAuthToken -> token', token);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.warn('[conn] A token is required for authentication');
|
||||
tracker.track('server-auth-token-verification', {
|
||||
@@ -114,7 +126,7 @@ export async function startServer(
|
||||
console.error(
|
||||
`[flipper-server] Unable to become ready within ${timeoutSeconds} seconds, exit`,
|
||||
);
|
||||
process.exit(1);
|
||||
processExit(1);
|
||||
}
|
||||
}, timeoutSeconds * 1000);
|
||||
|
||||
@@ -145,20 +157,34 @@ async function startHTTPServer(
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
const serveRoot: RequestHandler = async (_req, res) => {
|
||||
const resource = isReady
|
||||
? path.join(config.staticPath, config.entry)
|
||||
: path.join(config.staticPath, 'loading.html');
|
||||
const token = await getAuthToken();
|
||||
|
||||
const flipperConfig = {
|
||||
theme: 'light',
|
||||
entryPoint: isProduction()
|
||||
? 'bundle.js'
|
||||
: 'flipper-ui-browser/src/index-fast-refresh.bundle?platform=web&dev=true&minify=false',
|
||||
debug: !isProduction(),
|
||||
graphSecret: GRAPH_SECRET,
|
||||
appVersion: environmentInfo.appVersion,
|
||||
sessionId: sessionId,
|
||||
unixname: environmentInfo.os.unixname,
|
||||
authToken: token,
|
||||
};
|
||||
|
||||
fs.readFile(resource, (_err, content) => {
|
||||
const processedContent = content
|
||||
.toString()
|
||||
.replace('GRAPH_SECRET_REPLACE_ME', GRAPH_SECRET)
|
||||
.replace('FLIPPER_APP_VERSION_REPLACE_ME', environmentInfo.appVersion)
|
||||
.replace('FLIPPER_UNIXNAME_REPLACE_ME', environmentInfo.os.unixname)
|
||||
.replace('FLIPPER_SESSION_ID_REPLACE_ME', sessionId);
|
||||
.replace('FLIPPER_CONFIG_PLACEHOLDER', util.inspect(flipperConfig));
|
||||
res.end(processedContent);
|
||||
});
|
||||
});
|
||||
};
|
||||
app.get('/', serveRoot);
|
||||
app.get('/index.web.html', serveRoot);
|
||||
|
||||
app.get('/ready', (_req, res) => {
|
||||
tracker.track('server-endpoint-hit', {name: 'ready'});
|
||||
@@ -178,6 +204,7 @@ async function startHTTPServer(
|
||||
res.json({success: true});
|
||||
|
||||
// Just exit the process, this will trigger the shutdown hooks.
|
||||
// Do not use prcoessExit util as we want the serve to shutdown immediately
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -186,6 +213,13 @@ async function startHTTPServer(
|
||||
res.end('flipper-ok');
|
||||
});
|
||||
|
||||
app.get('/open-ui', (_req, res) => {
|
||||
tracker.track('server-endpoint-hit', {name: 'open-ui'});
|
||||
const preference = isProduction() ? UIPreference.PWA : UIPreference.Browser;
|
||||
openUI(preference, config.port);
|
||||
res.json({success: true});
|
||||
});
|
||||
|
||||
app.use(express.static(config.staticPath));
|
||||
|
||||
const server = http.createServer(app);
|
||||
@@ -205,7 +239,7 @@ async function startHTTPServer(
|
||||
`[flipper-server] Unable to listen at port: ${config.port}, is already in use`,
|
||||
);
|
||||
tracker.track('server-socket-already-in-use', {});
|
||||
process.exit(1);
|
||||
processExit(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ export async function checkServerRunning(
|
||||
port: number,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/info`);
|
||||
const response = await fetch(`http://localhost:${port}/info`, {
|
||||
timeout: 1000,
|
||||
});
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const environmentInfo: EnvironmentInfo = await response.json();
|
||||
return environmentInfo.appVersion;
|
||||
@@ -74,7 +76,9 @@ export async function checkServerRunning(
|
||||
*/
|
||||
export async function shutdownRunningInstance(port: number): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:${port}/shutdown`);
|
||||
const response = await fetch(`http://localhost:${port}/shutdown`, {
|
||||
timeout: 1000,
|
||||
});
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const json = await response.json();
|
||||
console.info(
|
||||
|
||||
@@ -9,4 +9,9 @@
|
||||
|
||||
import {uuid} from 'flipper-common';
|
||||
|
||||
export const sessionId = uuid();
|
||||
if (process.env.FLIPPER_SESSION_ID) {
|
||||
console.info('Use external session ID', process.env.FLIPPER_SESSION_ID);
|
||||
}
|
||||
export const sessionId = `${
|
||||
process.env.FLIPPER_SESSION_ID ?? 'unset'
|
||||
}::${uuid()}`;
|
||||
|
||||
@@ -48,11 +48,13 @@ type TrackerEvents = {
|
||||
};
|
||||
'server-socket-already-in-use': {};
|
||||
'server-open-ui': {browser: boolean; hasToken: boolean};
|
||||
'server-client-close': {code?: number; error?: string; sessionLength: number};
|
||||
'server-ws-server-error': {port: number; error: string};
|
||||
'server-ready-timeout': {timeout: number};
|
||||
'browser-connection-created': {
|
||||
successful: boolean;
|
||||
timeMS: number;
|
||||
timedOut: boolean;
|
||||
};
|
||||
'app-connection-created': AppConnectionPayload;
|
||||
'app-connection-secure-attempt': AppConnectionPayload;
|
||||
|
||||
59
desktop/flipper-server-core/src/utils/openUI.tsx
Normal file
59
desktop/flipper-server-core/src/utils/openUI.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import open from 'open';
|
||||
import {getAuthToken} from '../app-connectivity/certificate-exchange/certificate-utils';
|
||||
import {findInstallation} from './findInstallation';
|
||||
import {tracker} from '../tracker';
|
||||
|
||||
export enum UIPreference {
|
||||
Browser,
|
||||
PWA,
|
||||
}
|
||||
|
||||
export async function openUI(preference: UIPreference, port: number) {
|
||||
console.info('[flipper-server] Launch UI');
|
||||
|
||||
const token = await getAuthToken();
|
||||
console.info(
|
||||
`[flipper-server] Get authentication token: ${token?.length != 0}`,
|
||||
);
|
||||
|
||||
const openInBrowser = async () => {
|
||||
console.info('[flipper-server] Open in browser');
|
||||
const url = new URL(`http://localhost:${port}`);
|
||||
|
||||
console.info(`[flipper-server] Go to: ${url.toString()}`);
|
||||
|
||||
open(url.toString(), {app: {name: open.apps.chrome}});
|
||||
|
||||
tracker.track('server-open-ui', {
|
||||
browser: true,
|
||||
hasToken: token?.length != 0,
|
||||
});
|
||||
};
|
||||
|
||||
if (preference === UIPreference.Browser) {
|
||||
await openInBrowser();
|
||||
} else {
|
||||
const path = await findInstallation();
|
||||
if (path) {
|
||||
console.info('[flipper-server] Open in PWA. Location:', path);
|
||||
tracker.track('server-open-ui', {
|
||||
browser: false,
|
||||
hasToken: token?.length != 0,
|
||||
});
|
||||
open(path);
|
||||
} else {
|
||||
await openInBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
console.info('[flipper-server] Launch UI completed');
|
||||
}
|
||||
44
desktop/flipper-server-core/src/utils/processExit.tsx
Normal file
44
desktop/flipper-server-core/src/utils/processExit.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
const onBeforeExitFns: (() => void | Promise<void>)[] = [];
|
||||
export const setProcessExitRoutine = (
|
||||
onBeforeExit: () => void | Promise<void>,
|
||||
) => {
|
||||
onBeforeExitFns.push(onBeforeExit);
|
||||
};
|
||||
|
||||
const resIsPromise = (res: void | Promise<void>): res is Promise<void> =>
|
||||
res instanceof Promise;
|
||||
export const processExit = async (code: number) => {
|
||||
console.debug('processExit', code);
|
||||
|
||||
setTimeout(() => {
|
||||
console.error('Process exit routines timed out');
|
||||
process.exit(code);
|
||||
}, 5000);
|
||||
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
await Promise.all(
|
||||
onBeforeExitFns.map(async (fn) => {
|
||||
try {
|
||||
const res = fn();
|
||||
if (resIsPromise(res)) {
|
||||
return res.catch((e) => {
|
||||
console.error('Process exit routine failed', e);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Process exit routine failed', e);
|
||||
}
|
||||
}),
|
||||
).finally(() => {
|
||||
process.exit(code);
|
||||
});
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
import {resolve} from 'path';
|
||||
import {Settings, Tristate} from 'flipper-common';
|
||||
import {readFile, writeFile, pathExists, mkdirp} from 'fs-extra';
|
||||
@@ -18,7 +19,7 @@ export async function loadSettings(
|
||||
): Promise<Settings> {
|
||||
if (settingsString !== '') {
|
||||
try {
|
||||
return replaceDefaultSettings(JSON.parse(settingsString));
|
||||
return await replaceDefaultSettings(JSON.parse(settingsString));
|
||||
} catch (e) {
|
||||
throw new Error("couldn't read the user settingsString");
|
||||
}
|
||||
@@ -48,9 +49,9 @@ function getSettingsFile() {
|
||||
|
||||
export const DEFAULT_ANDROID_SDK_PATH = getDefaultAndroidSdkPath();
|
||||
|
||||
function getDefaultSettings(): Settings {
|
||||
async function getDefaultSettings(): Promise<Settings> {
|
||||
return {
|
||||
androidHome: getDefaultAndroidSdkPath(),
|
||||
androidHome: await getDefaultAndroidSdkPath(),
|
||||
enableAndroid: true,
|
||||
enableIOS: os.platform() === 'darwin',
|
||||
enablePhysicalIOS: os.platform() === 'darwin',
|
||||
@@ -76,14 +77,24 @@ function getDefaultSettings(): Settings {
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultAndroidSdkPath() {
|
||||
return os.platform() === 'win32' ? getWindowsSdkPath() : '/opt/android_sdk';
|
||||
async function getDefaultAndroidSdkPath() {
|
||||
if (os.platform() === 'win32') {
|
||||
return `${os.homedir()}\\AppData\\Local\\android\\sdk`;
|
||||
}
|
||||
|
||||
// non windows platforms
|
||||
|
||||
// created when created a project in Android Studio
|
||||
const androidStudioSdkPath = `${os.homedir()}/Library/Android/sdk`;
|
||||
if (await fs.exists(androidStudioSdkPath)) {
|
||||
return androidStudioSdkPath;
|
||||
}
|
||||
|
||||
return '/opt/android_sdk';
|
||||
}
|
||||
|
||||
function getWindowsSdkPath() {
|
||||
return `${os.homedir()}\\AppData\\Local\\android\\sdk`;
|
||||
}
|
||||
|
||||
function replaceDefaultSettings(userSettings: Partial<Settings>): Settings {
|
||||
return {...getDefaultSettings(), ...userSettings};
|
||||
async function replaceDefaultSettings(
|
||||
userSettings: Partial<Settings>,
|
||||
): Promise<Settings> {
|
||||
return {...(await getDefaultSettings()), ...userSettings};
|
||||
}
|
||||
|
||||
@@ -16,22 +16,23 @@ import {attachDevServer} from './attachDevServer';
|
||||
import {initializeLogger} from './logger';
|
||||
import fs from 'fs-extra';
|
||||
import yargs from 'yargs';
|
||||
import open from 'open';
|
||||
import os from 'os';
|
||||
import {initCompanionEnv} from 'flipper-server-companion';
|
||||
import {
|
||||
UIPreference,
|
||||
checkPortInUse,
|
||||
checkServerRunning,
|
||||
compareServerVersion,
|
||||
getEnvironmentInfo,
|
||||
openUI,
|
||||
setupPrefetcher,
|
||||
shutdownRunningInstance,
|
||||
startFlipperServer,
|
||||
startServer,
|
||||
tracker,
|
||||
processExit,
|
||||
} from 'flipper-server-core';
|
||||
import {addLogTailer, isTest, LoggerFormat} from 'flipper-common';
|
||||
import exitHook from 'exit-hook';
|
||||
import {getAuthToken, findInstallation} from 'flipper-server-core';
|
||||
|
||||
const argv = yargs
|
||||
.usage('yarn flipper-server [args]')
|
||||
@@ -95,9 +96,30 @@ const rootPath = argv.bundler
|
||||
: path.resolve(__dirname, '..'); // In pre-packaged versions of the server, static is copied inside the package.
|
||||
const staticPath = path.join(rootPath, 'static');
|
||||
|
||||
async function start() {
|
||||
const t0 = performance.now();
|
||||
const t0 = performance.now();
|
||||
|
||||
const browserConnectionTimeout = setTimeout(() => {
|
||||
tracker.track('browser-connection-created', {
|
||||
successful: false,
|
||||
timeMS: performance.now() - t0,
|
||||
timedOut: true,
|
||||
});
|
||||
}, 10000);
|
||||
let reported = false;
|
||||
const reportBrowserConnection = (successful: boolean) => {
|
||||
if (reported) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(browserConnectionTimeout);
|
||||
reported = true;
|
||||
tracker.track('browser-connection-created', {
|
||||
successful,
|
||||
timeMS: performance.now() - t0,
|
||||
timedOut: false,
|
||||
});
|
||||
};
|
||||
|
||||
async function start() {
|
||||
const isProduction =
|
||||
process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test';
|
||||
const environmentInfo = await getEnvironmentInfo(
|
||||
@@ -143,30 +165,24 @@ async function start() {
|
||||
`[flipper-server][bootstrap] Keytar loaded (${keytarLoadedMS} ms)`,
|
||||
);
|
||||
|
||||
let launchAndFinish = false;
|
||||
|
||||
console.info('[flipper-server] Check for running instances');
|
||||
const existingRunningInstanceVersion = await checkServerRunning(argv.port);
|
||||
if (existingRunningInstanceVersion) {
|
||||
console.info(
|
||||
`[flipper-server] Running instance found with version: ${existingRunningInstanceVersion}, current version: ${environmentInfo.appVersion}`,
|
||||
);
|
||||
if (
|
||||
compareServerVersion(
|
||||
environmentInfo.appVersion,
|
||||
existingRunningInstanceVersion,
|
||||
) > 0
|
||||
) {
|
||||
console.info(`[flipper-server] Shutdown running instance`);
|
||||
await shutdownRunningInstance(argv.port);
|
||||
} else {
|
||||
launchAndFinish = true;
|
||||
}
|
||||
console.info(`[flipper-server] Shutdown running instance`);
|
||||
const success = await shutdownRunningInstance(argv.port);
|
||||
console.info(
|
||||
`[flipper-server] Shutdown running instance acknowledged: ${success}`,
|
||||
);
|
||||
} else {
|
||||
console.info('[flipper-server] Checking if port is in use (TCP)');
|
||||
if (await checkPortInUse(argv.port)) {
|
||||
console.info(`[flipper-server] Shutdown running instance`);
|
||||
await shutdownRunningInstance(argv.port);
|
||||
const success = await shutdownRunningInstance(argv.port);
|
||||
console.info(
|
||||
`[flipper-server] Shutdown running instance acknowledged: ${success}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,14 +192,10 @@ async function start() {
|
||||
`[flipper-server][bootstrap] Check for running instances completed (${runningInstanceShutdownMS} ms)`,
|
||||
);
|
||||
|
||||
if (launchAndFinish) {
|
||||
return await launch();
|
||||
}
|
||||
|
||||
const {app, server, socket, readyForIncomingConnections} = await startServer(
|
||||
{
|
||||
staticPath,
|
||||
entry: `index.web${argv.bundler ? '.dev' : ''}.html`,
|
||||
entry: `index.web.html`,
|
||||
port: argv.port,
|
||||
},
|
||||
environmentInfo,
|
||||
@@ -206,6 +218,10 @@ async function start() {
|
||||
environmentInfo,
|
||||
);
|
||||
|
||||
flipperServer.once('browser-connection-created', () => {
|
||||
reportBrowserConnection(true);
|
||||
});
|
||||
|
||||
const t5 = performance.now();
|
||||
const serverCreatedMS = t5 - t4;
|
||||
console.info(
|
||||
@@ -244,7 +260,7 @@ async function start() {
|
||||
console.error(
|
||||
'[flipper-server] state changed to error, process will exit.',
|
||||
);
|
||||
process.exit(1);
|
||||
processExit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -277,6 +293,8 @@ async function start() {
|
||||
)} (${serverStartedMS} ms)`,
|
||||
);
|
||||
|
||||
setupPrefetcher(flipperServer.config.settings);
|
||||
|
||||
const startupMS = t10 - t0;
|
||||
|
||||
tracker.track('server-bootstrap-performance', {
|
||||
@@ -295,47 +313,14 @@ async function start() {
|
||||
}
|
||||
|
||||
async function launch() {
|
||||
console.info('[flipper-server] Launch UI');
|
||||
|
||||
const token = await getAuthToken();
|
||||
console.info(
|
||||
`[flipper-server] Get authentication token: ${token?.length != 0}`,
|
||||
);
|
||||
|
||||
if (!argv.open) {
|
||||
console.warn(
|
||||
'[flipper-server] Not opening UI, --open flag was not provided',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const openInBrowser = async () => {
|
||||
console.info('[flipper-server] Open in browser');
|
||||
const url = new URL(`http://localhost:${argv.port}`);
|
||||
|
||||
console.info(`[flipper-server] Go to: ${chalk.blue(url.toString())}`);
|
||||
|
||||
open(url.toString(), {app: {name: open.apps.chrome}});
|
||||
|
||||
tracker.track('server-open-ui', {
|
||||
browser: true,
|
||||
hasToken: token?.length != 0,
|
||||
});
|
||||
};
|
||||
|
||||
if (argv.bundler) {
|
||||
await openInBrowser();
|
||||
} else {
|
||||
const path = await findInstallation();
|
||||
if (path) {
|
||||
tracker.track('server-open-ui', {
|
||||
browser: false,
|
||||
hasToken: token?.length != 0,
|
||||
});
|
||||
open(path);
|
||||
} else {
|
||||
await openInBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
console.info('[flipper-server] Launch UI completed');
|
||||
openUI(UIPreference.PWA, argv.port);
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
@@ -343,7 +328,8 @@ process.on('uncaughtException', (error) => {
|
||||
'[flipper-server] uncaught exception, process will exit.',
|
||||
error,
|
||||
);
|
||||
process.exit(1);
|
||||
reportBrowserConnection(false);
|
||||
processExit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
@@ -355,7 +341,17 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
);
|
||||
});
|
||||
|
||||
start().catch((e) => {
|
||||
console.error(chalk.red('Server startup error: '), e);
|
||||
process.exit(1);
|
||||
});
|
||||
// It has to fit in 32 bit int
|
||||
const MAX_TIMEOUT = 2147483647;
|
||||
// Node.js process never waits for all promises to settle and exits as soon as there is not pending timers or open sockets or tasks in teh macroqueue
|
||||
const runtimeTimeout = setTimeout(() => {}, MAX_TIMEOUT);
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
start()
|
||||
.catch((e) => {
|
||||
console.error(chalk.red('Server startup error: '), e);
|
||||
reportBrowserConnection(false);
|
||||
return processExit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
clearTimeout(runtimeTimeout);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,10 @@ import fsRotator from 'file-stream-rotator';
|
||||
import {ensureFile} from 'fs-extra';
|
||||
import {access} from 'fs/promises';
|
||||
import {constants} from 'fs';
|
||||
import {initializeLogger as initLogger} from 'flipper-server-core';
|
||||
import {
|
||||
initializeLogger as initLogger,
|
||||
setProcessExitRoutine,
|
||||
} from 'flipper-server-core';
|
||||
|
||||
export const loggerOutputFile = 'flipper-server-log.out';
|
||||
|
||||
@@ -64,4 +67,14 @@ export async function initializeLogger(
|
||||
logStream?.write(`${name}: \n${stack}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
const finalizeLogger = async () => {
|
||||
const logStreamToEnd = logStream;
|
||||
// Prevent future writes
|
||||
logStream = undefined;
|
||||
await new Promise<void>((resolve) => {
|
||||
logStreamToEnd?.end(resolve);
|
||||
});
|
||||
};
|
||||
setProcessExitRoutine(finalizeLogger);
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ function showCompileError() {
|
||||
// Symbolicating compile errors is wasted effort
|
||||
// because the stack trace is meaningless:
|
||||
(error as any).preventSymbolication = true;
|
||||
window.flipperShowMessage?.(message);
|
||||
window.flipperShowMessage?.({detail: message});
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ declare global {
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
entryPoint: string;
|
||||
debug: boolean;
|
||||
graphSecret: string;
|
||||
appVersion: string;
|
||||
sessionId: string;
|
||||
unixname: string;
|
||||
authToken: string;
|
||||
};
|
||||
GRAPH_SECRET: string;
|
||||
FLIPPER_APP_VERSION: string;
|
||||
FLIPPER_SESSION_ID: string;
|
||||
FLIPPER_UNIXNAME: string;
|
||||
|
||||
flipperShowMessage?(message: string): void;
|
||||
flipperShowMessage?(message: {title?: string; detail?: string}): void;
|
||||
flipperHideMessage?(): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import {
|
||||
getLogger,
|
||||
getStringFromErrorLike,
|
||||
isProduction,
|
||||
setLoggerInstance,
|
||||
} from 'flipper-common';
|
||||
import {init as initLogger} from './fb-stubs/Logger';
|
||||
@@ -51,28 +52,57 @@ async function start() {
|
||||
|
||||
const params = new URL(location.href).searchParams;
|
||||
|
||||
const tokenProvider = async () => {
|
||||
if (!isProduction()) {
|
||||
let token = params.get('token');
|
||||
if (!token) {
|
||||
token = window.flipperConfig.authToken;
|
||||
}
|
||||
|
||||
const socket = new WebSocket(`ws://${location.host}?token=${token}`);
|
||||
socket.addEventListener('message', ({data: dataRaw}) => {
|
||||
const message = JSON.parse(dataRaw.toString());
|
||||
|
||||
if (typeof message.event === 'string') {
|
||||
switch (message.event) {
|
||||
case 'hasErrors': {
|
||||
console.warn('Error message received', message.payload);
|
||||
break;
|
||||
}
|
||||
case 'plugins-source-updated': {
|
||||
window.postMessage({
|
||||
type: 'plugins-source-updated',
|
||||
data: message.payload,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tokenProvider = () => {
|
||||
const providerParams = new URL(location.href).searchParams;
|
||||
let token = providerParams.get('token');
|
||||
if (!token) {
|
||||
console.info(
|
||||
'[flipper-client][ui-browser] Get token from manifest instead',
|
||||
);
|
||||
try {
|
||||
const manifestResponse = await fetch('manifest.json');
|
||||
const manifest = await manifestResponse.json();
|
||||
token = manifest.token;
|
||||
} catch (e) {
|
||||
console.info('[flipper-client][ui-browser] Get token from HTML instead');
|
||||
token = window.flipperConfig.authToken;
|
||||
if (!token || token === 'FLIPPER_AUTH_TOKEN_REPLACE_ME') {
|
||||
console.warn(
|
||||
'[flipper-client][ui-browser] Failed to get token from manifest. Error:',
|
||||
e.message,
|
||||
'[flipper-client][ui-browser] Failed to get token from HTML',
|
||||
token,
|
||||
);
|
||||
window.flipperShowMessage?.({
|
||||
detail:
|
||||
'[flipper-client][ui-browser] Failed to get token from HTML: ' +
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getLogger().info(
|
||||
'[flipper-client][ui-browser] Token is available: ',
|
||||
token?.length != 0,
|
||||
token?.length === 460,
|
||||
);
|
||||
|
||||
return token;
|
||||
@@ -112,7 +142,7 @@ async function start() {
|
||||
switch (state) {
|
||||
case FlipperServerState.CONNECTING:
|
||||
getLogger().info('[flipper-client] Connecting to server');
|
||||
window.flipperShowMessage?.('Connecting to server...');
|
||||
window.flipperShowMessage?.({title: 'Connecting to server...'});
|
||||
break;
|
||||
case FlipperServerState.CONNECTED:
|
||||
getLogger().info(
|
||||
@@ -122,7 +152,7 @@ async function start() {
|
||||
break;
|
||||
case FlipperServerState.DISCONNECTED:
|
||||
getLogger().info('[flipper-client] Disconnected from server');
|
||||
window.flipperShowMessage?.('Waiting for server...');
|
||||
window.flipperShowMessage?.({title: 'Waiting for server...'});
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -176,7 +206,7 @@ start().catch((e) => {
|
||||
error: getStringFromErrorLike(e),
|
||||
pwa: window.matchMedia('(display-mode: standalone)').matches,
|
||||
});
|
||||
window.flipperShowMessage?.('Failed to start UI with error: ' + e);
|
||||
window.flipperShowMessage?.({detail: 'Failed to start UI with error: ' + e});
|
||||
});
|
||||
|
||||
async function initializePWA() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {notification, Typography} from 'antd';
|
||||
import {Button, notification, Typography} from 'antd';
|
||||
import isProduction from '../utils/isProduction';
|
||||
import {reportPlatformFailures, ReleaseChannel} from 'flipper-common';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
@@ -91,17 +91,29 @@ export default function UpdateIndicator() {
|
||||
isProduction()
|
||||
) {
|
||||
reportPlatformFailures(
|
||||
checkForUpdate(version).then((res) => {
|
||||
if (res.kind === 'error') {
|
||||
console.warn('Version check failure: ', res);
|
||||
checkForUpdate(version)
|
||||
.then((res) => {
|
||||
if (res.kind === 'error') {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
if (res.kind === 'up-to-date') {
|
||||
setVersionCheckResult(res);
|
||||
return;
|
||||
}
|
||||
|
||||
return getRenderHostInstance()
|
||||
.flipperServer.exec('fetch-new-version', res.version)
|
||||
.then(() => {
|
||||
setVersionCheckResult(res);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Version check failure: ', e);
|
||||
setVersionCheckResult({
|
||||
kind: 'error',
|
||||
msg: res.msg,
|
||||
msg: e,
|
||||
});
|
||||
} else {
|
||||
setVersionCheckResult(res);
|
||||
}
|
||||
}),
|
||||
}),
|
||||
'publicVersionCheck',
|
||||
);
|
||||
}
|
||||
@@ -114,18 +126,31 @@ export function getUpdateAvailableMessage(versionCheckResult: {
|
||||
url: string;
|
||||
version: string;
|
||||
}): React.ReactNode {
|
||||
const {launcherSettings} = getRenderHostInstance().serverConfig;
|
||||
|
||||
const shutdownFlipper = () => {
|
||||
getRenderHostInstance().flipperServer.exec('shutdown');
|
||||
window.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
Flipper version {versionCheckResult.version} is now available.
|
||||
{fbConfig.isFBBuild ? (
|
||||
fbConfig.getReleaseChannel() === ReleaseChannel.INSIDERS ? (
|
||||
<> Restart Flipper to update to the latest version.</>
|
||||
fbConfig.getReleaseChannel() === ReleaseChannel.INSIDERS ||
|
||||
launcherSettings.ignoreLocalPin ? (
|
||||
<Button block type="primary" onClick={shutdownFlipper}>
|
||||
Quit Flipper to upgrade
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{' '}
|
||||
Run <code>arc pull</code> (optionally with <code>--latest</code>) in{' '}
|
||||
<code>~/fbsource</code> and restart Flipper to update to the latest
|
||||
version.
|
||||
<code>~/fbsource</code> and{' '}
|
||||
<Button block type="primary" onClick={shutdownFlipper}>
|
||||
Quit Flipper to upgrade
|
||||
</Button>
|
||||
.
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
|
||||
@@ -121,82 +121,49 @@ test('It can render rows', async () => {
|
||||
(await renderer.findByText('unique-string')).parentElement?.parentElement,
|
||||
).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
style="position: absolute; top: 0px; left: 0px; width: 100%; height: 24px; transform: translateY(24px);"
|
||||
>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
00:00:00.000
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
Android Phone
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
FB4A
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
unique-string
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
style="background-color: rgb(255, 245, 102);"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
/>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
/>
|
||||
<div
|
||||
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
|
||||
width="14%"
|
||||
>
|
||||
toClient:send
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
@@ -105,11 +105,11 @@ class UIPluginInitializer extends AbstractPluginInitializer {
|
||||
let uiPluginInitializer: UIPluginInitializer;
|
||||
export default async (store: Store, _logger: Logger) => {
|
||||
let FlipperPlugin = FlipperPluginSDK;
|
||||
if (getRenderHostInstance().GK('flipper_power_search')) {
|
||||
if (!getRenderHostInstance().GK('flipper_power_search')) {
|
||||
FlipperPlugin = {
|
||||
...FlipperPlugin,
|
||||
MasterDetail: FlipperPlugin._MasterDetailWithPowerSearch as any,
|
||||
DataTable: FlipperPlugin._DataTableWithPowerSearch as any,
|
||||
MasterDetail: FlipperPlugin.MasterDetailLegacy as any,
|
||||
DataTable: FlipperPlugin.DataTableLegacy as any,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ import {TroubleshootingGuide} from './appinspect/fb-stubs/TroubleshootingGuide';
|
||||
import {FlipperDevTools} from '../chrome/FlipperDevTools';
|
||||
import {TroubleshootingHub} from '../chrome/TroubleshootingHub';
|
||||
import {Notification} from './notification/Notification';
|
||||
import {SandyRatingButton} from './RatingButton';
|
||||
|
||||
export const Navbar = withTrackingScope(function Navbar() {
|
||||
return (
|
||||
@@ -104,6 +105,7 @@ export const Navbar = withTrackingScope(function Navbar() {
|
||||
|
||||
<NotificationButton />
|
||||
<TroubleshootMenu />
|
||||
<SandyRatingButton />
|
||||
<ExtrasMenu />
|
||||
<RightSidebarToggleButton />
|
||||
{getRenderHostInstance().serverConfig.environmentInfo
|
||||
|
||||
349
desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx
Normal file
349
desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {
|
||||
Component,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {styled, Input, Link, FlexColumn, FlexRow} from '../ui';
|
||||
import * as UserFeedback from '../fb-stubs/UserFeedback';
|
||||
import {FeedbackPrompt} from '../fb-stubs/UserFeedback';
|
||||
import {StarOutlined} from '@ant-design/icons';
|
||||
import {Button, Checkbox, Popover, Rate} from 'antd';
|
||||
import {currentUser} from '../fb-stubs/user';
|
||||
import {theme, useValue} from 'flipper-plugin';
|
||||
import {reportPlatformFailures} from 'flipper-common';
|
||||
import {getRenderHostInstance} from 'flipper-frontend-core';
|
||||
import {NavbarButton} from './Navbar';
|
||||
|
||||
type NextAction = 'select-rating' | 'leave-comment' | 'finished';
|
||||
|
||||
class PredefinedComment extends Component<{
|
||||
comment: string;
|
||||
selected: boolean;
|
||||
onClick: (_: unknown) => unknown;
|
||||
}> {
|
||||
static Container = styled.div<{selected: boolean}>((props) => {
|
||||
return {
|
||||
border: '1px solid #f2f3f5',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 24,
|
||||
backgroundColor: props.selected ? '#ecf3ff' : '#f2f3f5',
|
||||
marginBottom: 4,
|
||||
marginRight: 4,
|
||||
padding: '4px 8px',
|
||||
color: props.selected ? 'rgb(56, 88, 152)' : undefined,
|
||||
borderColor: props.selected ? '#3578e5' : undefined,
|
||||
':hover': {
|
||||
borderColor: '#3578e5',
|
||||
},
|
||||
};
|
||||
});
|
||||
render() {
|
||||
return (
|
||||
<PredefinedComment.Container
|
||||
onClick={this.props.onClick}
|
||||
selected={this.props.selected}>
|
||||
{this.props.comment}
|
||||
</PredefinedComment.Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Row = styled(FlexRow)({
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
color: '#9a9a9a',
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
const DismissRow = styled(Row)({
|
||||
marginBottom: 0,
|
||||
marginTop: 10,
|
||||
});
|
||||
|
||||
const DismissButton = styled.span({
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
});
|
||||
|
||||
const Spacer = styled(FlexColumn)({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
function dismissRow(dismiss: () => void) {
|
||||
return (
|
||||
<DismissRow key="dismiss">
|
||||
<Spacer />
|
||||
<DismissButton onClick={dismiss}>Dismiss</DismissButton>
|
||||
<Spacer />
|
||||
</DismissRow>
|
||||
);
|
||||
}
|
||||
|
||||
type FeedbackComponentState = {
|
||||
rating: number | null;
|
||||
hoveredRating: number;
|
||||
allowUserInfoSharing: boolean;
|
||||
nextAction: NextAction;
|
||||
predefinedComments: {[key: string]: boolean};
|
||||
comment: string;
|
||||
};
|
||||
|
||||
class FeedbackComponent extends Component<
|
||||
{
|
||||
submitRating: (rating: number) => void;
|
||||
submitComment: (
|
||||
rating: number,
|
||||
comment: string,
|
||||
selectedPredefinedComments: Array<string>,
|
||||
allowUserInfoSharing: boolean,
|
||||
) => void;
|
||||
close: () => void;
|
||||
dismiss: () => void;
|
||||
promptData: FeedbackPrompt;
|
||||
},
|
||||
FeedbackComponentState
|
||||
> {
|
||||
state: FeedbackComponentState = {
|
||||
rating: null,
|
||||
hoveredRating: 0,
|
||||
allowUserInfoSharing: true,
|
||||
nextAction: 'select-rating' as NextAction,
|
||||
predefinedComments: this.props.promptData.predefinedComments.reduce(
|
||||
(acc, cv) => ({...acc, [cv]: false}),
|
||||
{},
|
||||
),
|
||||
comment: '',
|
||||
};
|
||||
onSubmitRating(newRating: number) {
|
||||
const nextAction = newRating <= 2 ? 'leave-comment' : 'finished';
|
||||
this.setState({rating: newRating, nextAction: nextAction});
|
||||
this.props.submitRating(newRating);
|
||||
if (nextAction === 'finished') {
|
||||
setTimeout(this.props.close, 5000);
|
||||
}
|
||||
}
|
||||
onCommentSubmitted(comment: string) {
|
||||
this.setState({nextAction: 'finished'});
|
||||
const selectedPredefinedComments: Array<string> = Object.entries(
|
||||
this.state.predefinedComments,
|
||||
)
|
||||
.map((x) => ({comment: x[0], enabled: x[1]}))
|
||||
.filter((x) => x.enabled)
|
||||
.map((x) => x.comment);
|
||||
const currentRating = this.state.rating;
|
||||
if (currentRating) {
|
||||
this.props.submitComment(
|
||||
currentRating,
|
||||
comment,
|
||||
selectedPredefinedComments,
|
||||
this.state.allowUserInfoSharing,
|
||||
);
|
||||
} else {
|
||||
console.error('Illegal state: Submitting comment with no rating set.');
|
||||
}
|
||||
setTimeout(this.props.close, 1000);
|
||||
}
|
||||
onAllowUserSharingChanged(allowed: boolean) {
|
||||
this.setState({allowUserInfoSharing: allowed});
|
||||
}
|
||||
render() {
|
||||
let body: Array<ReactElement>;
|
||||
switch (this.state.nextAction) {
|
||||
case 'select-rating':
|
||||
body = [
|
||||
<Row key="bodyText">{this.props.promptData.bodyText}</Row>,
|
||||
<Row key="stars" style={{margin: 'auto'}}>
|
||||
<Rate onChange={(newRating) => this.onSubmitRating(newRating)} />
|
||||
</Row>,
|
||||
dismissRow(this.props.dismiss),
|
||||
];
|
||||
break;
|
||||
case 'leave-comment':
|
||||
const predefinedComments = Object.entries(
|
||||
this.state.predefinedComments,
|
||||
).map((c: [string, unknown], idx: number) => (
|
||||
<PredefinedComment
|
||||
key={idx}
|
||||
comment={c[0]}
|
||||
selected={Boolean(c[1])}
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
predefinedComments: {
|
||||
...this.state.predefinedComments,
|
||||
[c[0]]: !c[1],
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
));
|
||||
body = [
|
||||
<Row key="predefinedComments">{predefinedComments}</Row>,
|
||||
<Row key="inputRow">
|
||||
<Input
|
||||
style={{height: 30, width: '100%'}}
|
||||
placeholder={this.props.promptData.commentPlaceholder}
|
||||
value={this.state.comment}
|
||||
onChange={(e) => this.setState({comment: e.target.value})}
|
||||
onKeyDown={(e) =>
|
||||
e.key == 'Enter' && this.onCommentSubmitted(this.state.comment)
|
||||
}
|
||||
autoFocus
|
||||
/>
|
||||
</Row>,
|
||||
<Row key="contactCheckbox">
|
||||
<Checkbox
|
||||
checked={this.state.allowUserInfoSharing}
|
||||
onChange={(e) => this.onAllowUserSharingChanged(e.target.checked)}
|
||||
/>
|
||||
{'Tool owner can contact me '}
|
||||
</Row>,
|
||||
<Row key="submit">
|
||||
<Button onClick={() => this.onCommentSubmitted(this.state.comment)}>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>,
|
||||
dismissRow(this.props.dismiss),
|
||||
];
|
||||
break;
|
||||
case 'finished':
|
||||
body = [
|
||||
<Row key="thanks">
|
||||
Thanks for the feedback! You can now help
|
||||
<Link href="https://www.internalfb.com/intern/papercuts/?application=flipper">
|
||||
prioritize bugs and features for Flipper in Papercuts
|
||||
</Link>
|
||||
</Row>,
|
||||
dismissRow(this.props.dismiss),
|
||||
];
|
||||
break;
|
||||
default: {
|
||||
console.error('Illegal state: nextAction: ' + this.state.nextAction);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FlexColumn
|
||||
style={{
|
||||
width: 400,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
}}>
|
||||
<Row key="heading" style={{color: theme.primaryColor, fontSize: 20}}>
|
||||
{this.state.nextAction === 'finished'
|
||||
? this.props.promptData.postSubmitHeading
|
||||
: this.props.promptData.preSubmitHeading}
|
||||
</Row>
|
||||
{body}
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function SandyRatingButton() {
|
||||
const [promptData, setPromptData] =
|
||||
useState<UserFeedback.FeedbackPrompt | null>(null);
|
||||
const [isShown, setIsShown] = useState(false);
|
||||
const [hasTriggered, setHasTriggered] = useState(false);
|
||||
const sessionId = getRenderHostInstance().serverConfig.sessionId;
|
||||
const loggedIn = useValue(currentUser());
|
||||
|
||||
const triggerPopover = useCallback(() => {
|
||||
if (!hasTriggered) {
|
||||
setIsShown(true);
|
||||
setHasTriggered(true);
|
||||
}
|
||||
}, [hasTriggered]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getRenderHostInstance().GK('flipper_enable_star_ratiings') &&
|
||||
!hasTriggered &&
|
||||
loggedIn
|
||||
) {
|
||||
reportPlatformFailures(
|
||||
UserFeedback.getPrompt().then((prompt) => {
|
||||
setPromptData(prompt);
|
||||
setTimeout(triggerPopover, 30000);
|
||||
}),
|
||||
'RatingButton:getPrompt',
|
||||
).catch((e) => {
|
||||
console.warn('Failed to load ratings prompt:', e);
|
||||
});
|
||||
}
|
||||
}, [triggerPopover, hasTriggered, loggedIn]);
|
||||
|
||||
const onClick = () => {
|
||||
const willBeShown = !isShown;
|
||||
setIsShown(willBeShown);
|
||||
setHasTriggered(true);
|
||||
if (!willBeShown) {
|
||||
UserFeedback.dismiss(sessionId);
|
||||
}
|
||||
};
|
||||
|
||||
const submitRating = (rating: number) => {
|
||||
UserFeedback.submitRating(rating, sessionId);
|
||||
};
|
||||
|
||||
const submitComment = (
|
||||
rating: number,
|
||||
comment: string,
|
||||
selectedPredefinedComments: Array<string>,
|
||||
allowUserInfoSharing: boolean,
|
||||
) => {
|
||||
UserFeedback.submitComment(
|
||||
rating,
|
||||
comment,
|
||||
selectedPredefinedComments,
|
||||
allowUserInfoSharing,
|
||||
sessionId,
|
||||
);
|
||||
};
|
||||
|
||||
if (!promptData) {
|
||||
return null;
|
||||
}
|
||||
if (!promptData.shouldPopup || (hasTriggered && !isShown)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
visible={isShown}
|
||||
content={
|
||||
<FeedbackComponent
|
||||
submitRating={submitRating}
|
||||
submitComment={submitComment}
|
||||
close={() => {
|
||||
setIsShown(false);
|
||||
}}
|
||||
dismiss={onClick}
|
||||
promptData={promptData}
|
||||
/>
|
||||
}
|
||||
placement="right"
|
||||
trigger="click">
|
||||
<NavbarButton
|
||||
icon={StarOutlined}
|
||||
label="Rate Flipper"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -224,7 +224,6 @@ const outOfContentsContainer = (
|
||||
|
||||
const MainContainer = styled(Layout.Container)({
|
||||
background: theme.backgroundWash,
|
||||
padding: `0 ${theme.space.large}px ${theme.space.large}px 0`,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
|
||||
@@ -133,7 +133,11 @@ function CollapsableCategory(props: {
|
||||
key={check.key}
|
||||
header={check.label}
|
||||
extra={<CheckIcon status={check.result.status} />}>
|
||||
<Paragraph>{check.result.message}</Paragraph>
|
||||
{check.result.message?.split('\n').map((line, index) => (
|
||||
<Paragraph key={index} style={{marginBottom: 0}}>
|
||||
{line}
|
||||
</Paragraph>
|
||||
))}
|
||||
{check.result.commands && (
|
||||
<List>
|
||||
{check.result.commands.map(({title, command}, i) => (
|
||||
|
||||
@@ -122,11 +122,15 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
||||
'ios-get-simulators',
|
||||
false,
|
||||
);
|
||||
|
||||
const nonPhysical = simulators.filter(
|
||||
(simulator) => simulator.type !== 'physical',
|
||||
);
|
||||
setWaitingForIos(false);
|
||||
setIosEmulators(simulators);
|
||||
setIosEmulators(nonPhysical);
|
||||
} catch (error) {
|
||||
console.warn('Failed to find iOS simulators', error);
|
||||
setiOSMessage(`Error: ${error.message} \nRetrying...`);
|
||||
setiOSMessage(`Error: ${error.message ?? error} \nRetrying...`);
|
||||
setTimeout(getiOSSimulators, 1000);
|
||||
}
|
||||
};
|
||||
@@ -148,7 +152,7 @@ export const LaunchEmulatorDialog = withTrackingScope(
|
||||
setAndroidEmulators(emulators);
|
||||
} catch (error) {
|
||||
console.warn('Failed to find Android emulators', error);
|
||||
setAndroidMessage(`Error: ${error.message} \nRetrying...`);
|
||||
setAndroidMessage(`Error: ${error.message ?? error} \nRetrying...`);
|
||||
setTimeout(getAndroidEmulators, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,8 +24,17 @@ export function createMockDownloadablePluginDetails(
|
||||
lastUpdated?: Date;
|
||||
} = {},
|
||||
): DownloadablePluginDetails {
|
||||
const {id, version, title, flipperEngineVersion, gatekeeper, lastUpdated} = {
|
||||
const {
|
||||
id,
|
||||
buildId,
|
||||
version,
|
||||
title,
|
||||
flipperEngineVersion,
|
||||
gatekeeper,
|
||||
lastUpdated,
|
||||
} = {
|
||||
id: 'test',
|
||||
buildId: '1337',
|
||||
version: '3.0.1',
|
||||
flipperEngineVersion: '0.46.0',
|
||||
lastUpdated: new Date(1591226525 * 1000),
|
||||
@@ -36,6 +45,7 @@ export function createMockDownloadablePluginDetails(
|
||||
const details: DownloadablePluginDetails = {
|
||||
name: name || `flipper-plugin-${lowercasedID}`,
|
||||
id: id,
|
||||
buildId,
|
||||
bugs: {
|
||||
email: 'bugs@localhost',
|
||||
url: 'bugs.localhost',
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
"npm": "use yarn instead",
|
||||
"yarn": "^1.16"
|
||||
},
|
||||
"version": "0.236.0",
|
||||
"version": "0.239.0",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"scripts",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
"dependencies": {
|
||||
"chalk": "^4",
|
||||
"esbuild": "^0.15.7",
|
||||
"esbuild": "^0.15.18",
|
||||
"fb-watchman": "^2.0.2",
|
||||
"flipper-common": "0.0.0",
|
||||
"flipper-plugin-lib": "0.0.0",
|
||||
|
||||
@@ -28,6 +28,40 @@ const resolveFbStubsToFbPlugin: Plugin = {
|
||||
},
|
||||
};
|
||||
|
||||
const workerPlugin: Plugin = {
|
||||
name: 'worker-plugin',
|
||||
setup({onResolve, onLoad}) {
|
||||
onResolve({filter: /\?worker$/}, (args) => {
|
||||
return {
|
||||
path: require.resolve(args.path.slice(0, -7), {
|
||||
paths: [args.resolveDir],
|
||||
}),
|
||||
namespace: 'worker',
|
||||
};
|
||||
});
|
||||
|
||||
onLoad({filter: /.*/, namespace: 'worker'}, async (args) => {
|
||||
// Bundle the worker file
|
||||
const result = await build({
|
||||
entryPoints: [args.path],
|
||||
bundle: true,
|
||||
write: false,
|
||||
format: 'iife',
|
||||
platform: 'browser',
|
||||
});
|
||||
|
||||
const dataUri = `data:text/javascript;base64,${Buffer.from(
|
||||
result.outputFiles[0].text,
|
||||
).toString('base64')}`;
|
||||
|
||||
return {
|
||||
contents: `export default function() { return new Worker("${dataUri}"); }`,
|
||||
loader: 'js',
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
interface RunBuildConfig {
|
||||
pluginDir: string;
|
||||
entry: string;
|
||||
@@ -73,7 +107,7 @@ async function runBuild({
|
||||
],
|
||||
sourcemap: dev ? 'inline' : 'external',
|
||||
minify: !dev,
|
||||
plugins: intern ? [resolveFbStubsToFbPlugin] : undefined,
|
||||
plugins: [workerPlugin, ...(intern ? [resolveFbStubsToFbPlugin] : [])],
|
||||
loader: {
|
||||
'.ttf': 'dataurl',
|
||||
},
|
||||
|
||||
@@ -30,7 +30,12 @@ export async function getPluginSourceFolders(): Promise<string[]> {
|
||||
const pluginFolders: string[] = [];
|
||||
const flipperConfigPath = path.join(homedir(), '.flipper', 'config.json');
|
||||
if (await fs.pathExists(flipperConfigPath)) {
|
||||
const config = await fs.readJson(flipperConfigPath);
|
||||
let config = {pluginPaths: []};
|
||||
try {
|
||||
config = await fs.readJson(flipperConfigPath);
|
||||
} catch (e) {
|
||||
console.error('Failed to read local flipper config: ', e);
|
||||
}
|
||||
if (config.pluginPaths) {
|
||||
pluginFolders.push(...config.pluginPaths);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ You also need to compile in the `litho-annotations` package, as Flipper reflects
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
debugImplementation 'com.facebook.flipper:flipper-litho-plugin:0.236.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper-litho-plugin:0.239.0'
|
||||
debugImplementation 'com.facebook.litho:litho-annotations:0.19.0'
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ To setup the <Link to={useBaseUrl("/docs/features/plugins/leak-canary")}>LeakCan
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
debugImplementation 'com.facebook.flipper:flipper-leakcanary2-plugin:0.236.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper-leakcanary2-plugin:0.239.0'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,6 +57,7 @@ test('it will merge equal rows', () => {
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test1",
|
||||
"pid": 0,
|
||||
"pidStr": "0",
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
@@ -67,6 +68,7 @@ test('it will merge equal rows', () => {
|
||||
"date": 2021-01-28T17:15:17.859Z,
|
||||
"message": "test2",
|
||||
"pid": 2,
|
||||
"pidStr": "2",
|
||||
"tag": "test",
|
||||
"tid": 3,
|
||||
"type": "warn",
|
||||
@@ -77,6 +79,7 @@ test('it will merge equal rows', () => {
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test3",
|
||||
"pid": 0,
|
||||
"pidStr": "0",
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
@@ -103,9 +106,12 @@ test('it supports deeplink and select nodes + navigating to bottom', async () =>
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([
|
||||
const current = instance.tableManagerRef.current;
|
||||
console.error('ref', current);
|
||||
expect(current?.getSelectedItems()).toEqual([
|
||||
{
|
||||
...entry2,
|
||||
pidStr: '2',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
@@ -116,6 +122,7 @@ test('it supports deeplink and select nodes + navigating to bottom', async () =>
|
||||
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([
|
||||
{
|
||||
...entry3,
|
||||
pidStr: '0',
|
||||
count: 1,
|
||||
},
|
||||
]);
|
||||
@@ -138,6 +145,7 @@ test('export / import plugin does work', async () => {
|
||||
"date": 2021-01-28T17:15:12.859Z,
|
||||
"message": "test1",
|
||||
"pid": 0,
|
||||
"pidStr": "0",
|
||||
"tag": "test",
|
||||
"tid": 1,
|
||||
"type": "error",
|
||||
@@ -148,6 +156,7 @@ test('export / import plugin does work', async () => {
|
||||
"date": 2021-01-28T17:15:17.859Z,
|
||||
"message": "test2",
|
||||
"pid": 2,
|
||||
"pidStr": "2",
|
||||
"tag": "test",
|
||||
"tid": 3,
|
||||
"type": "warn",
|
||||
|
||||
@@ -12,13 +12,16 @@ import {
|
||||
DeviceLogEntry,
|
||||
usePlugin,
|
||||
createDataSource,
|
||||
dataTablePowerSearchOperators,
|
||||
DataTableColumn,
|
||||
DataTable,
|
||||
theme,
|
||||
DataTableManager,
|
||||
createState,
|
||||
useValue,
|
||||
DataFormatter,
|
||||
DataTable,
|
||||
EnumLabels,
|
||||
SearchExpressionTerm,
|
||||
} from 'flipper-plugin';
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
@@ -32,28 +35,31 @@ import {baseRowStyle, logTypes} from './logTypes';
|
||||
|
||||
export type ExtendedLogEntry = DeviceLogEntry & {
|
||||
count: number;
|
||||
pidStr: string; //for the purposes of inferring (only supports string type)
|
||||
};
|
||||
|
||||
const logLevelEnumLabels = Object.entries(logTypes).reduce(
|
||||
(res, [key, {label}]) => {
|
||||
res[key] = label;
|
||||
return res;
|
||||
},
|
||||
{} as EnumLabels,
|
||||
);
|
||||
|
||||
function createColumnConfig(
|
||||
_os: 'iOS' | 'Android' | 'Metro',
|
||||
): DataTableColumn<ExtendedLogEntry>[] {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
title: '',
|
||||
title: 'Level',
|
||||
width: 30,
|
||||
filters: Object.entries(logTypes).map(([value, config]) => ({
|
||||
label: config.label,
|
||||
value,
|
||||
enabled: config.enabled,
|
||||
})),
|
||||
onRender(entry) {
|
||||
return entry.count > 1 ? (
|
||||
<Badge
|
||||
count={entry.count}
|
||||
size="small"
|
||||
style={{
|
||||
marginTop: 4,
|
||||
color: theme.white,
|
||||
background:
|
||||
(logTypes[entry.type]?.style as any)?.color ??
|
||||
@@ -64,17 +70,28 @@ function createColumnConfig(
|
||||
logTypes[entry.type]?.icon
|
||||
);
|
||||
},
|
||||
powerSearchConfig: {
|
||||
type: 'enum',
|
||||
inferEnumOptionsFromData: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
title: 'Time',
|
||||
width: 120,
|
||||
powerSearchConfig: {
|
||||
type: 'dateTime',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'pid',
|
||||
key: 'pidStr',
|
||||
title: 'PID',
|
||||
width: 60,
|
||||
visible: true,
|
||||
powerSearchConfig: {
|
||||
type: 'enum',
|
||||
inferEnumOptionsFromData: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'tid',
|
||||
@@ -86,6 +103,10 @@ function createColumnConfig(
|
||||
key: 'tag',
|
||||
title: 'Tag',
|
||||
width: 160,
|
||||
powerSearchConfig: {
|
||||
type: 'enum',
|
||||
inferEnumOptionsFromData: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'app',
|
||||
@@ -110,10 +131,25 @@ function getRowStyle(entry: DeviceLogEntry): CSSProperties | undefined {
|
||||
return (logTypes[entry.type]?.style as any) ?? baseRowStyle;
|
||||
}
|
||||
|
||||
const powerSearchInitialState: SearchExpressionTerm[] = [
|
||||
{
|
||||
field: {
|
||||
key: 'type',
|
||||
label: 'Level',
|
||||
},
|
||||
operator:
|
||||
dataTablePowerSearchOperators.enum_set_is_any_of(logLevelEnumLabels),
|
||||
searchValue: Object.entries(logTypes)
|
||||
.filter(([_, item]) => item.enabled)
|
||||
.map(([key]) => key),
|
||||
},
|
||||
];
|
||||
|
||||
export function devicePlugin(client: DevicePluginClient) {
|
||||
const rows = createDataSource<ExtendedLogEntry>([], {
|
||||
limit: 200000,
|
||||
persist: 'logs',
|
||||
indices: [['pidStr'], ['tag']], //there are for inferring enum types
|
||||
});
|
||||
const isPaused = createState(true);
|
||||
const tableManagerRef = createRef<
|
||||
@@ -122,6 +158,7 @@ export function devicePlugin(client: DevicePluginClient) {
|
||||
|
||||
client.onDeepLink((payload: unknown) => {
|
||||
if (typeof payload === 'string') {
|
||||
tableManagerRef.current?.setSearchExpression(powerSearchInitialState);
|
||||
// timeout as we want to await restoring any previous scroll positin first, then scroll to the
|
||||
setTimeout(() => {
|
||||
let hasMatch = false;
|
||||
@@ -168,11 +205,13 @@ export function devicePlugin(client: DevicePluginClient) {
|
||||
) {
|
||||
rows.update(lastIndex, {
|
||||
...previousRow,
|
||||
pidStr: previousRow.pid.toString(),
|
||||
count: previousRow.count + 1,
|
||||
});
|
||||
} else {
|
||||
rows.append({
|
||||
...entry,
|
||||
pidStr: entry.pid.toString(),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
@@ -248,6 +287,7 @@ export function Component() {
|
||||
) : undefined
|
||||
}
|
||||
tableManagerRef={plugin.tableManagerRef}
|
||||
powerSearchInitialState={powerSearchInitialState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ test('Reducer correctly combines initial response and followup chunk', () => {
|
||||
responseHeaders: [{key: 'Content-Type', value: 'text/plain'}],
|
||||
responseIsMock: false,
|
||||
responseLength: 5,
|
||||
status: 200,
|
||||
status: '200',
|
||||
url: 'http://test.com',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,7 +113,7 @@ test('Can handle custom headers', async () => {
|
||||
responseIsMock: false,
|
||||
responseLength: 0,
|
||||
'response_header_second-test-header': 'dolphins',
|
||||
status: 200,
|
||||
status: '200',
|
||||
url: 'http://www.fbflipper.com',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -233,7 +233,7 @@ test('binary data gets serialized correctly', async () => {
|
||||
],
|
||||
responseIsMock: false,
|
||||
responseLength: 24838,
|
||||
status: 200,
|
||||
status: '200',
|
||||
url: 'http://www.fbflipper.com',
|
||||
},
|
||||
],
|
||||
@@ -265,7 +265,7 @@ test('binary data gets serialized correctly', async () => {
|
||||
],
|
||||
responseIsMock: false,
|
||||
responseLength: 24838,
|
||||
status: 200,
|
||||
status: '200',
|
||||
url: 'http://www.fbflipper.com',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ The network plugin is shipped as a separate Maven artifact, as follows:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.236.0'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.239.0'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ import {
|
||||
usePlugin,
|
||||
useValue,
|
||||
createDataSource,
|
||||
DataTableLegacy as DataTable,
|
||||
DataTableColumnLegacy as DataTableColumn,
|
||||
DataTableManagerLegacy as DataTableManager,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
DataTableManager,
|
||||
theme,
|
||||
renderReactRoot,
|
||||
batch,
|
||||
dataTablePowerSearchOperators,
|
||||
} from 'flipper-plugin';
|
||||
import {
|
||||
Request,
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
getHeaderValue,
|
||||
getResponseLength,
|
||||
getRequestLength,
|
||||
formatStatus,
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
requestsToText,
|
||||
@@ -118,6 +118,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
||||
);
|
||||
const requests = createDataSource<Request, 'id'>([], {
|
||||
key: 'id',
|
||||
indices: [['method'], ['status']],
|
||||
});
|
||||
const selectedId = createState<string | undefined>(undefined);
|
||||
const tableManagerRef = createRef<undefined | DataTableManager<Request>>();
|
||||
@@ -136,11 +137,16 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
||||
return;
|
||||
} else if (payload.startsWith(searchTermDelim)) {
|
||||
tableManagerRef.current?.clearSelection();
|
||||
tableManagerRef.current?.setSearchValue(
|
||||
payload.slice(searchTermDelim.length),
|
||||
);
|
||||
tableManagerRef.current?.setSearchExpression([
|
||||
{
|
||||
field: {label: 'Row', key: 'entireRow', useWholeRow: true},
|
||||
operator:
|
||||
dataTablePowerSearchOperators.searializable_object_contains(),
|
||||
searchValue: payload.slice(searchTermDelim.length),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
tableManagerRef.current?.setSearchValue('');
|
||||
tableManagerRef.current?.setSearchExpression([]);
|
||||
tableManagerRef.current?.selectItemById(payload);
|
||||
}
|
||||
});
|
||||
@@ -537,6 +543,7 @@ function createRequestFromRequestInfo(
|
||||
domain,
|
||||
requestHeaders: data.headers,
|
||||
requestData: decodeBody(data.headers, data.data),
|
||||
status: '...',
|
||||
};
|
||||
customColumns
|
||||
.filter((c) => c.type === 'request')
|
||||
@@ -557,7 +564,7 @@ function updateRequestWithResponseInfo(
|
||||
const res = {
|
||||
...request,
|
||||
responseTime: new Date(response.timestamp),
|
||||
status: response.status,
|
||||
status: response.status.toString(),
|
||||
reason: response.reason,
|
||||
responseHeaders: response.headers,
|
||||
responseData: decodeBody(response.headers, response.data),
|
||||
@@ -659,12 +666,14 @@ const baseColumns: DataTableColumn<Request>[] = [
|
||||
key: 'requestTime',
|
||||
title: 'Request Time',
|
||||
width: 120,
|
||||
powerSearchConfig: {type: 'dateTime'},
|
||||
},
|
||||
{
|
||||
key: 'responseTime',
|
||||
title: 'Response Time',
|
||||
width: 120,
|
||||
visible: false,
|
||||
powerSearchConfig: {type: 'dateTime'},
|
||||
},
|
||||
{
|
||||
key: 'requestData',
|
||||
@@ -672,26 +681,36 @@ const baseColumns: DataTableColumn<Request>[] = [
|
||||
width: 120,
|
||||
visible: false,
|
||||
formatters: formatOperationName,
|
||||
powerSearchConfig: {type: 'object'},
|
||||
},
|
||||
{
|
||||
key: 'domain',
|
||||
powerSearchConfig: {type: 'string'},
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
title: 'Full URL',
|
||||
visible: false,
|
||||
powerSearchConfig: {type: 'string'},
|
||||
},
|
||||
{
|
||||
key: 'method',
|
||||
title: 'Method',
|
||||
width: 70,
|
||||
powerSearchConfig: {
|
||||
type: 'enum',
|
||||
inferEnumOptionsFromData: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: 'Status',
|
||||
width: 70,
|
||||
formatters: formatStatus,
|
||||
align: 'right',
|
||||
powerSearchConfig: {
|
||||
type: 'enum',
|
||||
inferEnumOptionsFromData: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'requestLength',
|
||||
@@ -699,6 +718,7 @@ const baseColumns: DataTableColumn<Request>[] = [
|
||||
width: 100,
|
||||
formatters: formatBytes,
|
||||
align: 'right',
|
||||
powerSearchConfig: {type: 'float'},
|
||||
},
|
||||
{
|
||||
key: 'responseLength',
|
||||
@@ -706,6 +726,7 @@ const baseColumns: DataTableColumn<Request>[] = [
|
||||
width: 100,
|
||||
formatters: formatBytes,
|
||||
align: 'right',
|
||||
powerSearchConfig: {type: 'float'},
|
||||
},
|
||||
{
|
||||
key: 'duration',
|
||||
@@ -713,6 +734,7 @@ const baseColumns: DataTableColumn<Request>[] = [
|
||||
width: 100,
|
||||
formatters: formatDuration,
|
||||
align: 'right',
|
||||
powerSearchConfig: {type: 'float'},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -727,7 +749,10 @@ const errorStyle = {
|
||||
function getRowStyle(row: Request) {
|
||||
return row.responseIsMock
|
||||
? mockingStyle
|
||||
: row.status && row.status >= 400 && row.status < 600
|
||||
: row.status &&
|
||||
row.status !== '...' &&
|
||||
parseInt(row.status, 10) >= 400 &&
|
||||
parseInt(row.status, 10) < 600
|
||||
? errorStyle
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
Atom,
|
||||
DataTableManagerLegacy as DataTableManager,
|
||||
getFlipperLib,
|
||||
} from 'flipper-plugin';
|
||||
import {Atom, DataTableManager, getFlipperLib} from 'flipper-plugin';
|
||||
import {createContext} from 'react';
|
||||
import {Header, Request} from '../types';
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface Request {
|
||||
requestData: string | Uint8Array | undefined;
|
||||
// response
|
||||
responseTime?: Date;
|
||||
status?: number;
|
||||
status: string;
|
||||
reason?: string;
|
||||
responseHeaders?: Array<Header>;
|
||||
responseData?: string | Uint8Array | undefined;
|
||||
|
||||
@@ -268,10 +268,6 @@ export function formatBytes(count: number | undefined): string {
|
||||
return count + ' B';
|
||||
}
|
||||
|
||||
export function formatStatus(status: number | undefined) {
|
||||
return status ? '' + status : '';
|
||||
}
|
||||
|
||||
export function formatOperationName(requestData: string): string {
|
||||
try {
|
||||
const parsedData = JSON.parse(requestData);
|
||||
|
||||
@@ -228,6 +228,7 @@ export type Inspectable =
|
||||
| InspectableSize
|
||||
| InspectableBounds
|
||||
| InspectableSpaceBox
|
||||
| InspectablePluginDeepLink
|
||||
| InspectableUnknown;
|
||||
|
||||
export type InspectableText = {
|
||||
@@ -285,6 +286,13 @@ export type InspectableObject = {
|
||||
fields: Record<MetadataId, Inspectable>;
|
||||
};
|
||||
|
||||
export type InspectablePluginDeepLink = {
|
||||
type: 'pluginDeeplink';
|
||||
label?: string;
|
||||
pluginId: string;
|
||||
deeplinkPayload: unknown;
|
||||
};
|
||||
|
||||
export type InspectableArray = {
|
||||
type: 'array';
|
||||
items: Inspectable[];
|
||||
|
||||
@@ -11,9 +11,10 @@ import {DeleteOutlined, PartitionOutlined} from '@ant-design/icons';
|
||||
import {
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
DataTableManager,
|
||||
DetailSidebar,
|
||||
Layout,
|
||||
DataTableManager,
|
||||
dataTablePowerSearchOperators,
|
||||
usePlugin,
|
||||
useValue,
|
||||
} from 'flipper-plugin';
|
||||
@@ -41,6 +42,7 @@ export function FrameworkEventsTable({
|
||||
const instance = usePlugin(plugin);
|
||||
|
||||
const focusedNode = useValue(instance.uiState.focusedNode);
|
||||
|
||||
const managerRef = useRef<DataTableManager<AugmentedFrameworkEvent> | null>(
|
||||
null,
|
||||
);
|
||||
@@ -51,13 +53,31 @@ export function FrameworkEventsTable({
|
||||
if (nodeId != null) {
|
||||
managerRef.current?.resetFilters();
|
||||
if (isTree) {
|
||||
managerRef.current?.addColumnFilter('treeId', nodeId as string, {
|
||||
exact: true,
|
||||
});
|
||||
managerRef.current?.setSearchExpression([
|
||||
{
|
||||
field: {
|
||||
key: 'treeId',
|
||||
label: 'TreeId',
|
||||
},
|
||||
operator: {
|
||||
...dataTablePowerSearchOperators.int_equals(),
|
||||
},
|
||||
searchValue: nodeId,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
managerRef.current?.addColumnFilter('nodeId', nodeId as string, {
|
||||
exact: true,
|
||||
});
|
||||
managerRef.current?.setSearchExpression([
|
||||
{
|
||||
field: {
|
||||
key: 'nodeId',
|
||||
label: 'NodeId',
|
||||
},
|
||||
operator: {
|
||||
...dataTablePowerSearchOperators.int_equals(),
|
||||
},
|
||||
searchValue: nodeId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}, [instance.uiActions, isTree, nodeId]);
|
||||
@@ -68,9 +88,9 @@ export function FrameworkEventsTable({
|
||||
const customColumns = [...customColumnKeys].map(
|
||||
(customKey: string) =>
|
||||
({
|
||||
key: customKey,
|
||||
key: `payload.${customKey}` as any,
|
||||
title: startCase(customKey),
|
||||
onRender: (row: AugmentedFrameworkEvent) => row.payload?.[customKey],
|
||||
powerSearchConfig: stringConfig,
|
||||
} as DataTableColumn<AugmentedFrameworkEvent>),
|
||||
);
|
||||
|
||||
@@ -135,42 +155,91 @@ export function FrameworkEventsTable({
|
||||
);
|
||||
}
|
||||
|
||||
const MonoSpace = (t: any) => (
|
||||
<span style={{fontFamily: 'monospace'}}>{t}</span>
|
||||
);
|
||||
|
||||
const stringConfig = [
|
||||
dataTablePowerSearchOperators.string_contains(),
|
||||
dataTablePowerSearchOperators.string_not_contains(),
|
||||
dataTablePowerSearchOperators.string_matches_exactly(),
|
||||
];
|
||||
const idConfig = [dataTablePowerSearchOperators.int_equals()];
|
||||
|
||||
const inferredEnum = [
|
||||
dataTablePowerSearchOperators.enum_set_is_any_of({}),
|
||||
dataTablePowerSearchOperators.enum_is({}),
|
||||
dataTablePowerSearchOperators.enum_set_is_none_of({}),
|
||||
dataTablePowerSearchOperators.enum_is_not({}),
|
||||
];
|
||||
|
||||
const staticColumns: DataTableColumn<AugmentedFrameworkEvent>[] = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
sortable: true,
|
||||
onRender: (row: FrameworkEvent) => formatTimestampMillis(row.timestamp),
|
||||
title: 'Timestamp',
|
||||
formatters: MonoSpace,
|
||||
|
||||
powerSearchConfig: [
|
||||
dataTablePowerSearchOperators.newer_than_absolute_date(),
|
||||
dataTablePowerSearchOperators.older_than_absolute_date(),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
title: 'Event type',
|
||||
onRender: (row: FrameworkEvent) => eventTypeToName(row.type),
|
||||
powerSearchConfig: {
|
||||
inferEnumOptionsFromData: true,
|
||||
operators: inferredEnum,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'duration',
|
||||
title: 'Duration',
|
||||
title: 'Duration (Nanos)',
|
||||
onRender: (row: FrameworkEvent) =>
|
||||
row.duration != null ? formatDuration(row.duration) : null,
|
||||
formatters: MonoSpace,
|
||||
|
||||
powerSearchConfig: [
|
||||
dataTablePowerSearchOperators.int_greater_or_equal(),
|
||||
dataTablePowerSearchOperators.int_greater_than(),
|
||||
dataTablePowerSearchOperators.int_equals(),
|
||||
dataTablePowerSearchOperators.int_less_or_equal(),
|
||||
dataTablePowerSearchOperators.int_less_than(),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'treeId',
|
||||
title: 'TreeId',
|
||||
powerSearchConfig: idConfig,
|
||||
|
||||
formatters: MonoSpace,
|
||||
},
|
||||
{
|
||||
key: 'rootComponentName',
|
||||
title: 'Root component name',
|
||||
powerSearchConfig: stringConfig,
|
||||
formatters: MonoSpace,
|
||||
},
|
||||
{
|
||||
key: 'nodeId',
|
||||
title: 'Component ID',
|
||||
powerSearchConfig: idConfig,
|
||||
formatters: MonoSpace,
|
||||
},
|
||||
{
|
||||
key: 'nodeName',
|
||||
title: 'Component name',
|
||||
powerSearchConfig: stringConfig,
|
||||
formatters: MonoSpace,
|
||||
},
|
||||
{
|
||||
key: 'thread',
|
||||
title: 'Thread',
|
||||
onRender: (row: FrameworkEvent) => startCase(row.thread),
|
||||
powerSearchConfig: stringConfig,
|
||||
formatters: MonoSpace,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Layout,
|
||||
styled,
|
||||
useLocalStorageState,
|
||||
usePlugin,
|
||||
} from 'flipper-plugin';
|
||||
import React, {useState} from 'react';
|
||||
import {
|
||||
@@ -33,6 +34,9 @@ import {any} from 'lodash/fp';
|
||||
import {InspectableColor} from '../../ClientTypes';
|
||||
import {transformAny} from '../../utils/dataTransform';
|
||||
import {SearchOutlined} from '@ant-design/icons';
|
||||
import {plugin} from '../../index';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import {Glyph} from 'flipper';
|
||||
|
||||
type ModalData = {
|
||||
data: unknown;
|
||||
@@ -315,6 +319,7 @@ function NamedAttribute({
|
||||
* disables hover and focsued states
|
||||
*/
|
||||
const readOnlyInput = css`
|
||||
overflow: hidden; //stop random scrollbars from showing up
|
||||
font-size: small;
|
||||
:hover {
|
||||
border-color: ${theme.disabledColor} !important;
|
||||
@@ -388,7 +393,7 @@ function StyledTextArea({
|
||||
return (
|
||||
<Input.TextArea
|
||||
autoSize
|
||||
className={!mutable ? readOnlyInput : ''}
|
||||
className={cx(!mutable && readOnlyInput)}
|
||||
bordered
|
||||
style={{color: color}}
|
||||
readOnly={!mutable}
|
||||
@@ -432,6 +437,7 @@ function AttributeValue({
|
||||
name: string;
|
||||
inspectable: Inspectable;
|
||||
}) {
|
||||
const instance = usePlugin(plugin);
|
||||
switch (inspectable.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
@@ -550,6 +556,39 @@ function AttributeValue({
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
case 'pluginDeeplink':
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
instance.client.selectPlugin(
|
||||
inspectable.pluginId,
|
||||
inspectable.deeplinkPayload,
|
||||
);
|
||||
}}
|
||||
style={{
|
||||
height: 26,
|
||||
boxSizing: 'border-box',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
type="ghost">
|
||||
<span
|
||||
style={{
|
||||
marginTop: 2,
|
||||
fontFamily: 'monospace',
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 'small',
|
||||
}}>
|
||||
{inspectable.label}
|
||||
</span>
|
||||
<Glyph
|
||||
style={{marginLeft: 8, marginBottom: 2}}
|
||||
size={12}
|
||||
name="share-external"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Id, ClientNode, MetadataId, Metadata} from '../../ClientTypes';
|
||||
import {Id, ClientNode, NodeMap, MetadataId, Metadata} from '../../ClientTypes';
|
||||
import {Color, OnSelectNode} from '../../DesktopTypes';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from 'flipper-plugin';
|
||||
import {plugin} from '../../index';
|
||||
import {head, last} from 'lodash';
|
||||
import {Badge, Typography} from 'antd';
|
||||
import {Badge, Tooltip, Typography} from 'antd';
|
||||
|
||||
import {useVirtualizer} from '@tanstack/react-virtual';
|
||||
import {ContextMenu} from './ContextMenu';
|
||||
@@ -60,7 +60,7 @@ export function Tree2({
|
||||
additionalHeightOffset,
|
||||
}: {
|
||||
additionalHeightOffset: number;
|
||||
nodes: Map<Id, ClientNode>;
|
||||
nodes: NodeMap;
|
||||
metadata: Map<MetadataId, Metadata>;
|
||||
rootId: Id;
|
||||
}) {
|
||||
@@ -125,12 +125,21 @@ export function Tree2({
|
||||
return;
|
||||
}
|
||||
prevSearchTerm.current = searchTerm;
|
||||
const matchingIndexes = findSearchMatchingIndexes(treeNodes, searchTerm);
|
||||
const matchingNodesIds = findMatchingNodes(nodes, searchTerm);
|
||||
|
||||
if (matchingIndexes.length > 0) {
|
||||
rowVirtualizer.scrollToIndex(matchingIndexes[0], {align: 'start'});
|
||||
matchingNodesIds.forEach((id) => {
|
||||
instance.uiActions.ensureAncestorsExpanded(id);
|
||||
});
|
||||
|
||||
if (matchingNodesIds.length > 0) {
|
||||
const firstTreeNode = treeNodes.find(searchPredicate(searchTerm));
|
||||
|
||||
const idx = firstTreeNode?.idx;
|
||||
if (idx != null) {
|
||||
rowVirtualizer.scrollToIndex(idx, {align: 'start'});
|
||||
}
|
||||
}
|
||||
}, [rowVirtualizer, searchTerm, treeNodes]);
|
||||
}, [instance.uiActions, nodes, rowVirtualizer, searchTerm, treeNodes]);
|
||||
|
||||
useKeyboardControls(
|
||||
treeNodes,
|
||||
@@ -488,7 +497,9 @@ function InlineAttributes({attributes}: {attributes: Record<string, string>}) {
|
||||
<>
|
||||
{Object.entries(attributes ?? {}).map(([key, value]) => (
|
||||
<TreeAttributeContainer key={key}>
|
||||
<span style={{color: theme.warningColor}}>{key}</span>
|
||||
<span style={{color: theme.warningColor}}>
|
||||
{highlightManager.render(key)}
|
||||
</span>
|
||||
<span>={highlightManager.render(value)}</span>
|
||||
</TreeAttributeContainer>
|
||||
))}
|
||||
@@ -577,33 +588,52 @@ function HighlightedText(props: {text: string}) {
|
||||
}
|
||||
|
||||
function nodeIcon(node: TreeNode) {
|
||||
const [icon, tooltip] = nodeData(node);
|
||||
|
||||
const iconComp =
|
||||
typeof icon === 'string' ? <NodeIconImage src={icon} /> : icon;
|
||||
|
||||
if (tooltip == null) {
|
||||
return iconComp;
|
||||
} else {
|
||||
return <Tooltip title={tooltip}>{iconComp}</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
function nodeData(node: TreeNode) {
|
||||
if (node.tags.includes('LithoMountable')) {
|
||||
return <NodeIconImage src="icons/litho-logo-blue.png" />;
|
||||
return ['icons/litho-logo-blue.png', 'Litho Mountable (Primitive)'];
|
||||
} else if (node.tags.includes('Litho')) {
|
||||
return <NodeIconImage src="icons/litho-logo.png" />;
|
||||
return ['icons/litho-logo.png', 'Litho Component'];
|
||||
} else if (node.tags.includes('CK')) {
|
||||
if (node.tags.includes('iOS')) {
|
||||
return <NodeIconImage src="icons/ck-mounted-logo.png" />;
|
||||
return ['icons/ck-mounted-logo.png', 'CK Mounted Component'];
|
||||
}
|
||||
return <NodeIconImage src="icons/ck-logo.png" />;
|
||||
return ['icons/ck-logo.png', 'CK Component'];
|
||||
} else if (node.tags.includes('BloksBoundTree')) {
|
||||
return <NodeIconImage src="facebook/bloks-logo-orange.png" />;
|
||||
return ['facebook/bloks-logo-orange.png', 'Bloks Bridged component'];
|
||||
} else if (node.tags.includes('BloksDerived')) {
|
||||
return <NodeIconImage src="facebook/bloks-logo-blue.png" />;
|
||||
return ['facebook/bloks-logo-blue.png', 'Bloks Derived (Server) component'];
|
||||
} else if (node.tags.includes('Warning')) {
|
||||
return (
|
||||
<WarningOutlined style={{...nodeiconStyle, color: theme.errorColor}} />
|
||||
);
|
||||
return [
|
||||
<WarningOutlined
|
||||
key="0"
|
||||
style={{...nodeiconStyle, color: theme.errorColor}}
|
||||
/>,
|
||||
null,
|
||||
];
|
||||
} else {
|
||||
return (
|
||||
return [
|
||||
<div
|
||||
key="0"
|
||||
style={{
|
||||
height: NodeIconSize,
|
||||
width: 0,
|
||||
marginRight: IconRightMargin,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
/>,
|
||||
null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,22 +649,24 @@ const NodeIconImage = styled.img({...nodeiconStyle});
|
||||
|
||||
const renderDepthOffset = 12;
|
||||
|
||||
//due to virtualisation the out of the box dom based scrolling doesnt work
|
||||
function findSearchMatchingIndexes(
|
||||
treeNodes: TreeNode[],
|
||||
searchTerm: string,
|
||||
): number[] {
|
||||
function findMatchingNodes(nodes: NodeMap, searchTerm: string): Id[] {
|
||||
if (!searchTerm) {
|
||||
return [];
|
||||
}
|
||||
return treeNodes
|
||||
.map((value, index) => [value, index] as [TreeNode, number])
|
||||
.filter(
|
||||
([value, _]) =>
|
||||
value.name.toLowerCase().includes(searchTerm) ||
|
||||
Object.values(value.inlineAttributes).find((inlineAttr) =>
|
||||
inlineAttr.toLocaleLowerCase().includes(searchTerm),
|
||||
),
|
||||
)
|
||||
.map(([_, index]) => index);
|
||||
return [...nodes.values()]
|
||||
.filter(searchPredicate(searchTerm))
|
||||
.map((node) => node.id);
|
||||
}
|
||||
|
||||
function searchPredicate(
|
||||
searchTerm: string,
|
||||
): (node: ClientNode) => string | true | undefined {
|
||||
return (node: ClientNode): string | true | undefined =>
|
||||
node.name.toLowerCase().includes(searchTerm) ||
|
||||
Object.keys(node.inlineAttributes).find((inlineAttr) =>
|
||||
inlineAttr.toLocaleLowerCase().includes(searchTerm),
|
||||
) ||
|
||||
Object.values(node.inlineAttributes).find((inlineAttr) =>
|
||||
inlineAttr.toLocaleLowerCase().includes(searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
||||
const snapshot = createState<SnapshotInfo | null>(null);
|
||||
const nodesAtom = createState<Map<Id, ClientNode>>(new Map());
|
||||
const frameworkEvents = createDataSource<AugmentedFrameworkEvent>([], {
|
||||
indices: [['nodeId']],
|
||||
indices: [
|
||||
['nodeId'],
|
||||
['type'], //for inferred values
|
||||
],
|
||||
limit: 10000,
|
||||
});
|
||||
const frameworkEventsCustomColumns = createState<Set<string>>(new Set());
|
||||
@@ -294,6 +297,7 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
||||
metadata,
|
||||
perfEvents,
|
||||
os: client.device.os,
|
||||
client,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
serverDir,
|
||||
staticDir,
|
||||
rootDir,
|
||||
sonarDir,
|
||||
} from './paths';
|
||||
import isFB from './isFB';
|
||||
import yargs from 'yargs';
|
||||
@@ -244,7 +245,6 @@ async function copyStaticResources(outDir: string, versionNumber: string) {
|
||||
'icon.png',
|
||||
'icon_grey.png',
|
||||
'icons.json',
|
||||
'index.web.dev.html',
|
||||
'index.web.html',
|
||||
'install_desktop.svg',
|
||||
'loading.html',
|
||||
@@ -651,8 +651,11 @@ async function installNodeBinary(outputPath: string, platform: BuildPlatform) {
|
||||
console.log(`✅ Node successfully downloaded and unpacked.`);
|
||||
}
|
||||
|
||||
console.log(`⚙️ Copying node binary from ${nodePath} to ${outputPath}`);
|
||||
await fs.copyFile(nodePath, outputPath);
|
||||
console.log(`⚙️ Moving node binary from ${nodePath} to ${outputPath}`);
|
||||
if (await fs.exists(outputPath)) {
|
||||
await fs.rm(outputPath);
|
||||
}
|
||||
await fs.move(nodePath, outputPath);
|
||||
} else {
|
||||
console.log(`⚙️ Downloading node version for ${platform} using pkg-fetch`);
|
||||
const nodePath = await pkgFetch({
|
||||
@@ -661,8 +664,11 @@ async function installNodeBinary(outputPath: string, platform: BuildPlatform) {
|
||||
nodeRange: SUPPORTED_NODE_PLATFORM,
|
||||
});
|
||||
|
||||
console.log(`⚙️ Copying node binary from ${nodePath} to ${outputPath}`);
|
||||
await fs.copyFile(nodePath, outputPath);
|
||||
console.log(`⚙️ Moving node binary from ${nodePath} to ${outputPath}`);
|
||||
if (await fs.exists(outputPath)) {
|
||||
await fs.rm(outputPath);
|
||||
}
|
||||
await fs.move(nodePath, outputPath);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -740,86 +746,138 @@ async function setUpWindowsBundle(outputDir: string) {
|
||||
|
||||
async function setUpMacBundle(
|
||||
outputDir: string,
|
||||
serverDir: string,
|
||||
platform: BuildPlatform,
|
||||
versionNumber: string,
|
||||
): Promise<{nodePath: string; resourcesPath: string}> {
|
||||
) {
|
||||
console.log(`⚙️ Creating Mac bundle in ${outputDir}`);
|
||||
|
||||
let appTemplate = path.join(staticDir, 'flipper-server-app-template');
|
||||
if (isFB) {
|
||||
appTemplate = path.join(
|
||||
staticDir,
|
||||
'facebook',
|
||||
'flipper-server-app-template',
|
||||
platform,
|
||||
);
|
||||
console.info('⚙️ Using internal template from: ' + appTemplate);
|
||||
}
|
||||
let serverOutputDir = '';
|
||||
let nodeBinaryFile = '';
|
||||
|
||||
await fs.copy(appTemplate, outputDir);
|
||||
/**
|
||||
* Use the most basic template for MacOS.
|
||||
* - Copy the contents of the template into the output directory.
|
||||
* - Replace the version placeholder value with the actual version.
|
||||
*/
|
||||
if (!isFB) {
|
||||
const template = path.join(staticDir, 'flipper-server-app-template');
|
||||
await fs.copy(template, outputDir);
|
||||
|
||||
function replacePropertyValue(
|
||||
obj: any,
|
||||
targetValue: string,
|
||||
replacementValue: string,
|
||||
): any {
|
||||
if (typeof obj === 'object' && !Array.isArray(obj) && obj !== null) {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
obj[key] = replacePropertyValue(
|
||||
obj[key],
|
||||
targetValue,
|
||||
replacementValue,
|
||||
);
|
||||
function replacePropertyValue(
|
||||
obj: any,
|
||||
targetValue: string,
|
||||
replacementValue: string,
|
||||
): any {
|
||||
if (typeof obj === 'object' && !Array.isArray(obj) && obj !== null) {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
obj[key] = replacePropertyValue(
|
||||
obj[key],
|
||||
targetValue,
|
||||
replacementValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (typeof obj === 'string' && obj === targetValue) {
|
||||
obj = replacementValue;
|
||||
}
|
||||
} else if (typeof obj === 'string' && obj === targetValue) {
|
||||
obj = replacementValue;
|
||||
return obj;
|
||||
}
|
||||
return obj;
|
||||
|
||||
console.log(`⚙️ Update plist with build information`);
|
||||
const plistPath = path.join(
|
||||
outputDir,
|
||||
'Flipper.app',
|
||||
'Contents',
|
||||
'Info.plist',
|
||||
);
|
||||
|
||||
/* eslint-disable node/no-sync*/
|
||||
const pListContents: Record<any, any> = plist.readFileSync(plistPath);
|
||||
replacePropertyValue(
|
||||
pListContents,
|
||||
'{flipper-server-version}',
|
||||
versionNumber,
|
||||
);
|
||||
plist.writeBinaryFileSync(plistPath, pListContents);
|
||||
/* eslint-enable node/no-sync*/
|
||||
|
||||
serverOutputDir = path.join(
|
||||
outputDir,
|
||||
'Flipper.app',
|
||||
'Contents',
|
||||
'Resources',
|
||||
'server',
|
||||
);
|
||||
|
||||
nodeBinaryFile = path.join(
|
||||
outputDir,
|
||||
'Flipper.app',
|
||||
'Contents',
|
||||
'MacOS',
|
||||
'flipper-runtime',
|
||||
);
|
||||
} else {
|
||||
serverOutputDir = path.join(
|
||||
sonarDir,
|
||||
'facebook',
|
||||
'flipper-server',
|
||||
'Resources',
|
||||
'server',
|
||||
);
|
||||
nodeBinaryFile = path.join(
|
||||
sonarDir,
|
||||
'facebook',
|
||||
'flipper-server',
|
||||
'Resources',
|
||||
'flipper-runtime',
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`⚙️ Writing plist`);
|
||||
const plistPath = path.join(
|
||||
outputDir,
|
||||
'Flipper.app',
|
||||
'Contents',
|
||||
'Info.plist',
|
||||
);
|
||||
|
||||
/* eslint-disable node/no-sync*/
|
||||
const pListContents: Record<any, any> = plist.readFileSync(plistPath);
|
||||
replacePropertyValue(
|
||||
pListContents,
|
||||
'{flipper-server-version}',
|
||||
versionNumber,
|
||||
);
|
||||
plist.writeBinaryFileSync(plistPath, pListContents);
|
||||
/* eslint-enable node/no-sync*/
|
||||
|
||||
const resourcesOutputDir = path.join(
|
||||
outputDir,
|
||||
'Flipper.app',
|
||||
'Contents',
|
||||
'Resources',
|
||||
'server',
|
||||
);
|
||||
|
||||
if (!(await fs.exists(resourcesOutputDir))) {
|
||||
await fs.mkdir(resourcesOutputDir);
|
||||
if (await fs.exists(serverOutputDir)) {
|
||||
await fs.rm(serverOutputDir, {recursive: true, force: true});
|
||||
}
|
||||
await fs.mkdirp(serverOutputDir);
|
||||
|
||||
console.log(`⚙️ Copying from ${serverDir} to ${serverOutputDir}`);
|
||||
|
||||
// Copy resources instead of moving. This is because we want to keep the original
|
||||
// files in the right location because they are used whilst bundling for
|
||||
// other platforms.
|
||||
await fs.copy(serverDir, serverOutputDir, {
|
||||
overwrite: true,
|
||||
dereference: true,
|
||||
});
|
||||
|
||||
console.log(`⚙️ Downloading compatible node version`);
|
||||
await installNodeBinary(nodeBinaryFile, platform);
|
||||
|
||||
if (isFB) {
|
||||
const {buildFlipperServer} = await import(
|
||||
// @ts-ignore only used inside Meta
|
||||
'./fb/build-flipper-server-macos'
|
||||
);
|
||||
|
||||
const outputPath = await buildFlipperServer(versionNumber, false);
|
||||
console.log(
|
||||
`⚙️ Successfully built platform: ${platform}, output: ${outputPath}`,
|
||||
);
|
||||
|
||||
const appPath = path.join(outputDir, 'Flipper.app');
|
||||
await fs.emptyDir(appPath);
|
||||
await fs.copy(outputPath, appPath);
|
||||
|
||||
// const appPath = path.join(outputDir, 'Flipper.app');
|
||||
// if (await fs.exists(appPath)) {
|
||||
// await fs.rm(appPath, {recursive: true, force: true});
|
||||
// }
|
||||
// await fs.move(outputPath, appPath);
|
||||
}
|
||||
const nodeOutputPath = path.join(
|
||||
outputDir,
|
||||
'Flipper.app',
|
||||
'Contents',
|
||||
'MacOS',
|
||||
'flipper-runtime',
|
||||
);
|
||||
return {resourcesPath: resourcesOutputDir, nodePath: nodeOutputPath};
|
||||
}
|
||||
|
||||
async function bundleServerReleaseForPlatform(
|
||||
dir: string,
|
||||
bundleDir: string,
|
||||
versionNumber: string,
|
||||
platform: BuildPlatform,
|
||||
) {
|
||||
@@ -830,39 +888,38 @@ async function bundleServerReleaseForPlatform(
|
||||
);
|
||||
await fs.mkdirp(outputDir);
|
||||
|
||||
let outputPaths = {
|
||||
nodePath: path.join(outputDir, 'flipper-runtime'),
|
||||
resourcesPath: outputDir,
|
||||
};
|
||||
|
||||
// On the mac, we need to set up a resource bundle which expects paths
|
||||
// to be in different places from Linux/Windows bundles.
|
||||
if (
|
||||
platform === BuildPlatform.MAC_X64 ||
|
||||
platform === BuildPlatform.MAC_AARCH64
|
||||
) {
|
||||
outputPaths = await setUpMacBundle(outputDir, platform, versionNumber);
|
||||
} else if (platform === BuildPlatform.LINUX) {
|
||||
await setUpLinuxBundle(outputDir);
|
||||
} else if (platform === BuildPlatform.WINDOWS) {
|
||||
await setUpWindowsBundle(outputDir);
|
||||
}
|
||||
await setUpMacBundle(outputDir, bundleDir, platform, versionNumber);
|
||||
if (argv.dmg) {
|
||||
await createMacDMG(platform, outputDir, distDir);
|
||||
}
|
||||
} else {
|
||||
const outputPaths = {
|
||||
nodePath: path.join(outputDir, 'flipper-runtime'),
|
||||
resourcesPath: outputDir,
|
||||
};
|
||||
|
||||
console.log(`⚙️ Copying from ${dir} to ${outputPaths.resourcesPath}`);
|
||||
await fs.copy(dir, outputPaths.resourcesPath, {
|
||||
overwrite: true,
|
||||
dereference: true,
|
||||
});
|
||||
if (platform === BuildPlatform.LINUX) {
|
||||
await setUpLinuxBundle(outputDir);
|
||||
} else if (platform === BuildPlatform.WINDOWS) {
|
||||
await setUpWindowsBundle(outputDir);
|
||||
}
|
||||
|
||||
console.log(`⚙️ Downloading compatible node version`);
|
||||
await installNodeBinary(outputPaths.nodePath, platform);
|
||||
console.log(
|
||||
`⚙️ Copying from ${bundleDir} to ${outputPaths.resourcesPath}`,
|
||||
);
|
||||
await fs.copy(bundleDir, outputPaths.resourcesPath, {
|
||||
overwrite: true,
|
||||
dereference: true,
|
||||
});
|
||||
|
||||
if (
|
||||
argv.dmg &&
|
||||
(platform === BuildPlatform.MAC_X64 ||
|
||||
platform === BuildPlatform.MAC_AARCH64)
|
||||
) {
|
||||
await createMacDMG(platform, outputDir, distDir);
|
||||
console.log(`⚙️ Downloading compatible node version`);
|
||||
await installNodeBinary(outputPaths.nodePath, platform);
|
||||
}
|
||||
|
||||
console.log(`✅ Wrote ${platform}-specific server version to ${outputDir}`);
|
||||
|
||||
@@ -210,6 +210,13 @@ async function buildDist(buildFolder: string) {
|
||||
const targetsRaw: Map<Platform, Map<Arch, string[]>>[] = [];
|
||||
const postBuildCallbacks: (() => void)[] = [];
|
||||
|
||||
const productName = process.env.FLIPPER_REACT_NATIVE_ONLY
|
||||
? 'Flipper-Electron'
|
||||
: 'Flipper';
|
||||
const appId = process.env.FLIPPER_REACT_NATIVE_ONLY
|
||||
? 'com.facebook.sonar-electron'
|
||||
: `com.facebook.sonar`;
|
||||
|
||||
if (argv.mac || argv['mac-dmg']) {
|
||||
targetsRaw.push(Platform.MAC.createTarget(['dir'], Arch.universal));
|
||||
// You can build mac apps on Linux but can't build dmgs, so we separate those.
|
||||
@@ -231,10 +238,14 @@ async function buildDist(buildFolder: string) {
|
||||
}
|
||||
}
|
||||
postBuildCallbacks.push(() =>
|
||||
spawn('zip', ['-qyr9', '../Flipper-mac.zip', 'Flipper.app'], {
|
||||
cwd: macPath,
|
||||
encoding: 'utf-8',
|
||||
}),
|
||||
spawn(
|
||||
'zip',
|
||||
['-qyr9', `../${productName}-mac.zip`, `${productName}.app`],
|
||||
{
|
||||
cwd: macPath,
|
||||
encoding: 'utf-8',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (argv.linux || argv['linux-deb'] || argv['linux-snap']) {
|
||||
@@ -273,8 +284,8 @@ async function buildDist(buildFolder: string) {
|
||||
await build({
|
||||
publish: 'never',
|
||||
config: {
|
||||
appId: `com.facebook.sonar`,
|
||||
productName: 'Flipper',
|
||||
appId,
|
||||
productName,
|
||||
directories: {
|
||||
buildResources: buildFolder,
|
||||
output: distDir,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import path from 'path';
|
||||
|
||||
export const rootDir = path.resolve(__dirname, '..');
|
||||
export const sonarDir = path.resolve(__dirname, '..', '..');
|
||||
export const appDir = path.join(rootDir, 'app');
|
||||
export const browserUiDir = path.join(rootDir, 'flipper-ui-browser');
|
||||
export const staticDir = path.join(rootDir, 'static');
|
||||
|
||||
@@ -49,6 +49,11 @@ const argv = yargs
|
||||
choices: ['stable', 'insiders'],
|
||||
default: 'stable',
|
||||
},
|
||||
open: {
|
||||
describe: 'Open Flipper in the default browser after starting',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
.version('DEV')
|
||||
.help()
|
||||
@@ -103,7 +108,9 @@ async function copyStaticResources() {
|
||||
async function restartServer() {
|
||||
try {
|
||||
await compileServerMain();
|
||||
await launchServer(true, ++startCount === 1); // only open on the first time
|
||||
// Only open the UI the first time it runs. Subsequent runs, likely triggered after
|
||||
// saving changes, should just reload the existing UI.
|
||||
await launchServer(true, argv.open && ++startCount === 1);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# 0.239.0 (16/11/2023)
|
||||
|
||||
* [D51346366](https://github.com/facebook/flipper/search?q=D51346366&type=Commits) - UIDebugger fix issue with scrollbars sometimes appearing in sidebar
|
||||
|
||||
|
||||
# 0.238.0 (14/11/2023)
|
||||
|
||||
* [D51199644](https://github.com/facebook/flipper/search?q=D51199644&type=Commits) - [Logs] Improve power search config to populate dropdown for level, PID & Tag
|
||||
* [D51199783](https://github.com/facebook/flipper/search?q=D51199783&type=Commits) - [Analytics] Improve power search config to populate dropdown for low cardinality columns
|
||||
|
||||
|
||||
# 0.237.0 (10/11/2023)
|
||||
|
||||
* [D51113095](https://github.com/facebook/flipper/search?q=D51113095&type=Commits) - UIdebugger added powersearch operators to Framework event table
|
||||
|
||||
|
||||
# 0.234.0 (1/11/2023)
|
||||
|
||||
* [D50595987](https://github.com/facebook/flipper/search?q=D50595987&type=Commits) - UIDebugger, new sidebar design
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="icon" href="icon.png">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<link id="flipper-theme-import" rel="stylesheet">
|
||||
|
||||
<title>Flipper</title>
|
||||
<script>
|
||||
window.flipperConfig = {
|
||||
theme: 'light',
|
||||
entryPoint: 'flipper-ui-browser/src/index-fast-refresh.bundle?platform=web&dev=true&minify=false',
|
||||
debug: true,
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.message {
|
||||
-webkit-app-region: drag;
|
||||
z-index: 999999;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 50px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: #525252;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.console {
|
||||
font-family: 'Fira Mono';
|
||||
width: 600px;
|
||||
height: 250px;
|
||||
box-sizing: border-box;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.console header {
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
background-color: #9254de;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.console .consolebody {
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 10px;
|
||||
height: calc(100% - 40px);
|
||||
overflow: scroll;
|
||||
background-color: #000;
|
||||
color: white;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
background-color: #9254de;
|
||||
color: white;
|
||||
font-family: system-ui;
|
||||
font-size: 15px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
background-color: #722ed1;
|
||||
}
|
||||
|
||||
input[type="submit"]:active {
|
||||
background-color: #722ed1;
|
||||
}
|
||||
|
||||
#troubleshoot {
|
||||
display: none;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="troubleshoot" class="message">
|
||||
</div>
|
||||
|
||||
<div id="root">
|
||||
<div id="loading" class="message">
|
||||
Connecting...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(async function () {
|
||||
// Line below needed to make Metro work. Alternatives could be considered.
|
||||
window.global = window;
|
||||
let connected = false;
|
||||
|
||||
// Listen to changes in the network state, reload when online.
|
||||
// This handles the case when the device is completely offline
|
||||
// i.e. no network connection.
|
||||
window.addEventListener('online', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
const root = document.getElementById('root');
|
||||
const troubleshootBox = document.getElementById('troubleshoot');
|
||||
|
||||
function showMessage(text, centered) {
|
||||
troubleshootBox.innerText = text;
|
||||
|
||||
root.style.display = 'none';
|
||||
troubleshootBox.style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideMessage() {
|
||||
root.style.display = 'block';
|
||||
troubleshootBox.style.display = 'none';
|
||||
}
|
||||
|
||||
window.flipperShowMessage = showMessage;
|
||||
window.flipperHideMessage = hideMessage;
|
||||
|
||||
window.GRAPH_SECRET = 'GRAPH_SECRET_REPLACE_ME';
|
||||
window.FLIPPER_APP_VERSION = 'FLIPPER_APP_VERSION_REPLACE_ME';
|
||||
window.FLIPPER_SESSION_ID = 'FLIPPER_SESSION_ID_REPLACE_ME';
|
||||
window.FLIPPER_UNIXNAME = 'FLIPPER_UNIXNAME_REPLACE_ME';
|
||||
|
||||
const params = new URL(location.href).searchParams;
|
||||
let token = params.get('token');
|
||||
if (!token) {
|
||||
const manifestResponse = await fetch('manifest.json');
|
||||
const manifest = await manifestResponse.json();
|
||||
token = manifest.token;
|
||||
}
|
||||
|
||||
const socket = new WebSocket(`ws://${location.host}?token=${token}`);
|
||||
window.devSocket = socket;
|
||||
|
||||
socket.addEventListener('message', ({ data: dataRaw }) => {
|
||||
const message = JSON.parse(dataRaw.toString())
|
||||
|
||||
if (typeof message.event === 'string') {
|
||||
switch (message.event) {
|
||||
case 'hasErrors': {
|
||||
console.warn('Error message received'. message.payload);
|
||||
break;
|
||||
}
|
||||
case 'plugins-source-updated': {
|
||||
window.postMessage({
|
||||
type: 'plugins-source-updated',
|
||||
data: message.payload
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.addEventListener('error', (e) => {
|
||||
if (!connected) {
|
||||
console.warn('Socket failed to connect. Is the server running? Have you provided a valid authentication token?');
|
||||
}
|
||||
else {
|
||||
console.warn('Socket failed with error.', e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
connected = true;
|
||||
})
|
||||
|
||||
// load correct theme (n.b. this doesn't handle system value specifically, will assume light in such cases)
|
||||
try {
|
||||
if (window.flipperConfig.theme === 'dark') {
|
||||
document.getElementById('flipper-theme-import').href = "themes/dark.css";
|
||||
} else {
|
||||
document.getElementById('flipper-theme-import').href = "themes/light.css";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize theme", e);
|
||||
document.getElementById('flipper-theme-import').href = "themes/light.css";
|
||||
}
|
||||
|
||||
function init() {
|
||||
const script = document.createElement('script');
|
||||
script.src = window.flipperConfig.entryPoint;
|
||||
script.onerror = (e) => {
|
||||
const retry = (retries) => {
|
||||
showMessage(`Failed to load entry point. Check Chrome Dev Tools console for more info. Retrying in: ${retries}`);
|
||||
retries -= 1;
|
||||
if (retries < 0) {
|
||||
window.location.reload();
|
||||
}
|
||||
else {
|
||||
setTimeout(() => retry(retries), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
retry(3);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -14,11 +14,7 @@
|
||||
|
||||
<title>Flipper</title>
|
||||
<script>
|
||||
window.flipperConfig = {
|
||||
theme: 'light',
|
||||
entryPoint: 'bundle.js',
|
||||
debug: false,
|
||||
}
|
||||
window.flipperConfig = FLIPPER_CONFIG_PLACEHOLDER;
|
||||
</script>
|
||||
<style>
|
||||
.message {
|
||||
@@ -32,6 +28,7 @@
|
||||
padding: 50px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
@@ -39,15 +36,22 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message p {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#troubleshoot {
|
||||
display: none;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="troubleshoot" class="message">
|
||||
<h1 id="tourbleshoot_title"></h1>
|
||||
<p id="tourbleshoot_details"></p>
|
||||
</div>
|
||||
|
||||
<div id="root">
|
||||
@@ -58,7 +62,7 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// FIXME: needed to make Metro work
|
||||
// Line below needed to make Metro work. Alternatives could be considered.
|
||||
window.global = window;
|
||||
|
||||
// Listen to changes in the network state, reload when online.
|
||||
@@ -70,9 +74,18 @@
|
||||
|
||||
const root = document.getElementById('root');
|
||||
const troubleshootBox = document.getElementById('troubleshoot');
|
||||
const troubleshootBoxTitle = document.getElementById('tourbleshoot_title');
|
||||
const troubleshootBoxDetails = document.getElementById('tourbleshoot_details');
|
||||
|
||||
function showMessage(text) {
|
||||
troubleshootBox.innerText = text;
|
||||
function showMessage({ title, detail }) {
|
||||
if (title) {
|
||||
troubleshootBoxTitle.innerText = title
|
||||
}
|
||||
if (detail) {
|
||||
const newMessage = document.createElement('p')
|
||||
newMessage.innerText = detail;
|
||||
troubleshootBoxDetails.appendChild(newMessage)
|
||||
}
|
||||
|
||||
root.style.display = 'none';
|
||||
troubleshootBox.style.display = 'flex';
|
||||
@@ -81,16 +94,13 @@
|
||||
function hideMessage() {
|
||||
root.style.display = 'block';
|
||||
troubleshootBox.style.display = 'none';
|
||||
troubleshootBoxTitle.innerHTML = ''
|
||||
troubleshootBoxDetails.innerHTML = ''
|
||||
}
|
||||
|
||||
window.flipperShowMessage = showMessage;
|
||||
window.flipperHideMessage = hideMessage;
|
||||
|
||||
window.GRAPH_SECRET = 'GRAPH_SECRET_REPLACE_ME';
|
||||
window.FLIPPER_APP_VERSION = 'FLIPPER_APP_VERSION_REPLACE_ME';
|
||||
window.FLIPPER_SESSION_ID = 'FLIPPER_SESSION_ID_REPLACE_ME';
|
||||
window.FLIPPER_UNIXNAME = 'FLIPPER_UNIXNAME_REPLACE_ME';
|
||||
|
||||
// load correct theme (n.b. this doesn't handle system value specifically, will assume light in such cases)
|
||||
try {
|
||||
if (window.flipperConfig.theme === 'dark') {
|
||||
@@ -117,12 +127,13 @@
|
||||
setTimeout(() => retry(retries), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
retry(3);
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -2961,10 +2961,15 @@
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@esbuild/linux-loong64@0.15.7":
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz#1ec4af4a16c554cbd402cc557ccdd874e3f7be53"
|
||||
integrity sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==
|
||||
"@esbuild/android-arm@0.15.18":
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80"
|
||||
integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==
|
||||
|
||||
"@esbuild/linux-loong64@0.15.18":
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239"
|
||||
integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==
|
||||
|
||||
"@eslint/eslintrc@^0.4.3":
|
||||
version "0.4.3"
|
||||
@@ -7503,132 +7508,133 @@ es6-error@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
||||
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
||||
|
||||
esbuild-android-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz#a521604d8c4c6befc7affedc897df8ccde189bea"
|
||||
integrity sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==
|
||||
esbuild-android-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5"
|
||||
integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==
|
||||
|
||||
esbuild-android-arm64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz#307b81f1088bf1e81dfe5f3d1d63a2d2a2e3e68e"
|
||||
integrity sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==
|
||||
esbuild-android-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04"
|
||||
integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==
|
||||
|
||||
esbuild-darwin-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz#270117b0c4ec6bcbc5cf3a297a7d11954f007e11"
|
||||
integrity sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==
|
||||
esbuild-darwin-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410"
|
||||
integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==
|
||||
|
||||
esbuild-darwin-arm64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz#97851eacd11dacb7719713602e3319e16202fc77"
|
||||
integrity sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==
|
||||
esbuild-darwin-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337"
|
||||
integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==
|
||||
|
||||
esbuild-freebsd-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz#1de15ffaf5ae916aa925800aa6d02579960dd8c4"
|
||||
integrity sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==
|
||||
esbuild-freebsd-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2"
|
||||
integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==
|
||||
|
||||
esbuild-freebsd-arm64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz#0f160dbf5c9a31a1d8dd87acbbcb1a04b7031594"
|
||||
integrity sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==
|
||||
esbuild-freebsd-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635"
|
||||
integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==
|
||||
|
||||
esbuild-linux-32@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz#422eb853370a5e40bdce8b39525380de11ccadec"
|
||||
integrity sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==
|
||||
esbuild-linux-32@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce"
|
||||
integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==
|
||||
|
||||
esbuild-linux-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz#f89c468453bb3194b14f19dc32e0b99612e81d2b"
|
||||
integrity sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==
|
||||
esbuild-linux-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz#532738075397b994467b514e524aeb520c191b6c"
|
||||
integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==
|
||||
|
||||
esbuild-linux-arm64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz#68a79d6eb5e032efb9168a0f340ccfd33d6350a1"
|
||||
integrity sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==
|
||||
esbuild-linux-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d"
|
||||
integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==
|
||||
|
||||
esbuild-linux-arm@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz#2b7c784d0b3339878013dfa82bf5eaf82c7ce7d3"
|
||||
integrity sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==
|
||||
esbuild-linux-arm@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc"
|
||||
integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==
|
||||
|
||||
esbuild-linux-mips64le@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz#bb8330a50b14aa84673816cb63cc6c8b9beb62cc"
|
||||
integrity sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==
|
||||
esbuild-linux-mips64le@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb"
|
||||
integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==
|
||||
|
||||
esbuild-linux-ppc64le@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz#52544e7fa992811eb996674090d0bc41f067a14b"
|
||||
integrity sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==
|
||||
esbuild-linux-ppc64le@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507"
|
||||
integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==
|
||||
|
||||
esbuild-linux-riscv64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz#a43ae60697992b957e454cbb622f7ee5297e8159"
|
||||
integrity sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==
|
||||
esbuild-linux-riscv64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6"
|
||||
integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==
|
||||
|
||||
esbuild-linux-s390x@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz#8c76a125dd10a84c166294d77416caaf5e1c7b64"
|
||||
integrity sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==
|
||||
esbuild-linux-s390x@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb"
|
||||
integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==
|
||||
|
||||
esbuild-netbsd-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz#19b2e75449d7d9c32b5d8a222bac2f1e0c3b08fd"
|
||||
integrity sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==
|
||||
esbuild-netbsd-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998"
|
||||
integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==
|
||||
|
||||
esbuild-openbsd-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz#1357b2bf72fd037d9150e751420a1fe4c8618ad7"
|
||||
integrity sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==
|
||||
esbuild-openbsd-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8"
|
||||
integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==
|
||||
|
||||
esbuild-sunos-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz#87ab2c604592a9c3c763e72969da0d72bcde91d2"
|
||||
integrity sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==
|
||||
esbuild-sunos-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971"
|
||||
integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==
|
||||
|
||||
esbuild-windows-32@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz#c81e688c0457665a8d463a669e5bf60870323e99"
|
||||
integrity sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==
|
||||
esbuild-windows-32@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3"
|
||||
integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==
|
||||
|
||||
esbuild-windows-64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz#2421d1ae34b0561a9d6767346b381961266c4eff"
|
||||
integrity sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==
|
||||
esbuild-windows-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0"
|
||||
integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==
|
||||
|
||||
esbuild-windows-arm64@0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz#7d5e9e060a7b454cb2f57f84a3f3c23c8f30b7d2"
|
||||
integrity sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==
|
||||
esbuild-windows-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7"
|
||||
integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==
|
||||
|
||||
esbuild@^0.15.7:
|
||||
version "0.15.7"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.7.tgz#8a1f1aff58671a3199dd24df95314122fc1ddee8"
|
||||
integrity sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==
|
||||
esbuild@^0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d"
|
||||
integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==
|
||||
optionalDependencies:
|
||||
"@esbuild/linux-loong64" "0.15.7"
|
||||
esbuild-android-64 "0.15.7"
|
||||
esbuild-android-arm64 "0.15.7"
|
||||
esbuild-darwin-64 "0.15.7"
|
||||
esbuild-darwin-arm64 "0.15.7"
|
||||
esbuild-freebsd-64 "0.15.7"
|
||||
esbuild-freebsd-arm64 "0.15.7"
|
||||
esbuild-linux-32 "0.15.7"
|
||||
esbuild-linux-64 "0.15.7"
|
||||
esbuild-linux-arm "0.15.7"
|
||||
esbuild-linux-arm64 "0.15.7"
|
||||
esbuild-linux-mips64le "0.15.7"
|
||||
esbuild-linux-ppc64le "0.15.7"
|
||||
esbuild-linux-riscv64 "0.15.7"
|
||||
esbuild-linux-s390x "0.15.7"
|
||||
esbuild-netbsd-64 "0.15.7"
|
||||
esbuild-openbsd-64 "0.15.7"
|
||||
esbuild-sunos-64 "0.15.7"
|
||||
esbuild-windows-32 "0.15.7"
|
||||
esbuild-windows-64 "0.15.7"
|
||||
esbuild-windows-arm64 "0.15.7"
|
||||
"@esbuild/android-arm" "0.15.18"
|
||||
"@esbuild/linux-loong64" "0.15.18"
|
||||
esbuild-android-64 "0.15.18"
|
||||
esbuild-android-arm64 "0.15.18"
|
||||
esbuild-darwin-64 "0.15.18"
|
||||
esbuild-darwin-arm64 "0.15.18"
|
||||
esbuild-freebsd-64 "0.15.18"
|
||||
esbuild-freebsd-arm64 "0.15.18"
|
||||
esbuild-linux-32 "0.15.18"
|
||||
esbuild-linux-64 "0.15.18"
|
||||
esbuild-linux-arm "0.15.18"
|
||||
esbuild-linux-arm64 "0.15.18"
|
||||
esbuild-linux-mips64le "0.15.18"
|
||||
esbuild-linux-ppc64le "0.15.18"
|
||||
esbuild-linux-riscv64 "0.15.18"
|
||||
esbuild-linux-s390x "0.15.18"
|
||||
esbuild-netbsd-64 "0.15.18"
|
||||
esbuild-openbsd-64 "0.15.18"
|
||||
esbuild-sunos-64 "0.15.18"
|
||||
esbuild-windows-32 "0.15.18"
|
||||
esbuild-windows-64 "0.15.18"
|
||||
esbuild-windows-arm64 "0.15.18"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
|
||||
Reference in New Issue
Block a user