diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a1ff8029..1a4df5d9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,38 +45,6 @@ jobs: draft: false prerelease: false - build-mac: - needs: - - release - runs-on: macos-latest - env: - desktop-directory: ./desktop - - steps: - - uses: actions/checkout@v3.5.3 - with: - ref: ${{ needs.release.outputs.tag }} - - uses: actions/setup-node@v3.6.0 - with: - node-version: '18.x' - - name: Install - uses: nick-fields/retry@v2.8.3 - with: - timeout_minutes: 10 - max_attempts: 3 - command: cd ${{env.desktop-directory}} && yarn - - name: Build - uses: nick-fields/retry@v2.8.3 - with: - timeout_minutes: 30 - max_attempts: 3 - command: cd ${{env.desktop-directory}} && yarn build --mac --mac-dmg - - name: Upload - uses: actions/upload-artifact@v3.1.2 - with: - name: 'Flipper-mac.dmg' - path: 'dist/Flipper-mac.dmg' - build-server-mac: needs: - release @@ -112,72 +80,6 @@ jobs: name: 'Flipper-server-mac-aarch64.dmg' path: 'dist/Flipper-server-mac-aarch64.dmg' - build-linux: - needs: - - release - runs-on: ubuntu-latest - env: - desktop-directory: ./desktop - - steps: - - uses: actions/checkout@v3.5.3 - with: - ref: ${{ needs.release.outputs.tag }} - - uses: actions/setup-node@v3.6.0 - with: - node-version: '18.x' - - name: Install - uses: nick-fields/retry@v2.8.3 - with: - timeout_minutes: 10 - max_attempts: 3 - command: cd ${{env.desktop-directory}} && yarn - - name: Build - uses: nick-fields/retry@v2.8.3 - with: - timeout_minutes: 30 - max_attempts: 3 - command: cd ${{env.desktop-directory}} && yarn build --linux - - name: Upload Linux - uses: actions/upload-artifact@v3.1.2 - with: - name: 'Flipper-linux.zip' - path: 'dist/Flipper-linux.zip' - - build-win: - needs: - - release - runs-on: windows-latest - env: - desktop-directory: ./desktop - - steps: - - uses: actions/checkout@v3.5.3 - with: - ref: ${{ needs.release.outputs.tag }} - - uses: actions/setup-node@v3.6.0 - with: - node-version: '18.x' - - name: Install - uses: nick-fields/retry@v2.8.3 - with: - timeout_minutes: 10 - max_attempts: 3 - shell: pwsh - command: cd ${{env.desktop-directory}}; yarn - - name: Build - uses: nick-fields/retry@v2.8.3 - with: - timeout_minutes: 30 - max_attempts: 3 - shell: pwsh - command: cd ${{env.desktop-directory}}; yarn build --win - - name: Upload Windows - uses: actions/upload-artifact@v3.1.2 - with: - name: 'Flipper-win.zip' - path: 'dist/Flipper-win.zip' - build-flipper-server: needs: - release @@ -210,9 +112,6 @@ jobs: publish: needs: - - build-win - - build-linux - - build-mac - build-server-mac - build-flipper-server - release @@ -222,12 +121,6 @@ jobs: - uses: actions/checkout@v3.5.3 with: ref: ${{ needs.release.outputs.tag }} - - name: Download Mac - if: ${{ needs.release.outputs.tag != '' }} - uses: actions/download-artifact@v1 - with: - name: 'Flipper-mac.dmg' - path: 'Flipper-mac.dmg' - name: Download Flipper Server x86-64 if: ${{ needs.release.outputs.tag != '' }} uses: actions/download-artifact@v1 @@ -240,18 +133,6 @@ jobs: with: name: 'Flipper-server-mac-aarch64.dmg' path: 'Flipper-server-mac-aarch64.dmg' - - name: Download Linux - if: ${{ needs.release.outputs.tag != '' }} - uses: actions/download-artifact@v1 - with: - name: 'Flipper-linux.zip' - path: 'Flipper-linux.zip' - - name: Download Windows - if: ${{ needs.release.outputs.tag != '' }} - uses: actions/download-artifact@v1 - with: - name: 'Flipper-win.zip' - path: 'Flipper-win.zip' - name: Download Flipper Server if: ${{ needs.release.outputs.tag != '' }} uses: actions/download-artifact@v1 @@ -265,7 +146,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: created_tag: ${{ needs.release.outputs.tag }} - args: Flipper-mac.dmg/Flipper-mac.dmg Flipper-linux.zip/Flipper-linux.zip Flipper-win.zip/Flipper-win.zip flipper-server.tgz/flipper-server.tgz Flipper-server-mac-x64.dmg/Flipper-server-mac-x64.dmg Flipper-server-mac-aarch64.dmg/Flipper-server-mac-aarch64.dmg + args: flipper-server.tgz/flipper-server.tgz Flipper-server-mac-x64.dmg/Flipper-server-mac-x64.dmg Flipper-server-mac-aarch64.dmg/Flipper-server-mac-aarch64.dmg - name: Set up npm token run: echo "//registry.yarnpkg.com/:_authToken=${{ secrets.FLIPPER_NPM_TOKEN }}" >> ~/.npmrc - name: Publish flipper-server on NPM diff --git a/.gitignore b/.gitignore index 9a301999a..d614cbca7 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ xplat/build # Mac OS X *.DS_Store +facebook/flipper-server/Resources/ # Automatically generated docs/extending/ui-components.mdx diff --git a/README.md b/README.md index 727eed423..e6eb68ffd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,24 @@

+--- +## Important Accouncement + +Flipper is moving away from its Electron distribution to an in-Browser experience. + +**How does this affect me?** + +Functionality hasn't changed. The UI remains unchanged. Flipper will run in your default browser instead of a standalone application. +If you build from source, Flipper will open in the browser instead of a standalone app. We also provide a MacOS app for the Flipper runtime which can be run and will also open Flipper in the browser. + +The last Electron release is [v0.239.0](https://github.com/facebook/flipper/releases/tag/v0.239.0). As such, future releases will not include Electron artifacts. + +### React Native support + +If you are debugging React Native applications, v0.239.0 will be the last release with support for it due to technical limitations for React Dev Tools and Hermes Debugger plugins. As such, please refer to that release when debugging React Native applications. + +--- +

Flipper (formerly Sonar) is a platform for debugging mobile apps on iOS and Android and JS apps in your browser or in Node.js. Visualize, inspect, and control your apps from a simple desktop interface. Use Flipper as is or extend it using the plugin API.

diff --git a/build.gradle b/build.gradle index 48fc54c05..a30924ed0 100644 --- a/build.gradle +++ b/build.gradle @@ -109,7 +109,7 @@ ext.deps = [ okhttp3 : 'com.squareup.okhttp3:okhttp:4.11.0', leakcanary : 'com.squareup.leakcanary:leakcanary-android:1.6.3', leakcanary2 : 'com.squareup.leakcanary:leakcanary-android:2.8.1', - protobuf : 'com.google.protobuf:protobuf-java:3.23.4', + protobuf : 'com.google.protobuf:protobuf-java:3.25.1', testCore : 'androidx.test:core:1.4.0', testRules : 'androidx.test:rules:1.5.0', // Plugin dependencies diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index 697d66c49..756a678ed 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -143,7 +143,7 @@ async function getFlipperServer( const {readyForIncomingConnections} = await startServer( { staticPath, - entry: 'index.web.dev.html', + entry: 'index.web.html', port, }, environmentInfo, diff --git a/desktop/doctor/src/fb-stubs/idbInstallationInstructions.tsx b/desktop/doctor/src/fb-stubs/idbInstallationInstructions.tsx deleted file mode 100644 index cb326a3a8..000000000 --- a/desktop/doctor/src/fb-stubs/idbInstallationInstructions.tsx +++ /dev/null @@ -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.`; diff --git a/desktop/doctor/src/fb-stubs/messages.tsx b/desktop/doctor/src/fb-stubs/messages.tsx new file mode 100644 index 000000000..77008f47d --- /dev/null +++ b/desktop/doctor/src/fb-stubs/messages.tsx @@ -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'); diff --git a/desktop/doctor/src/index.tsx b/desktop/doctor/src/index.tsx index 22fb87da6..f6b7b6376 100644 --- a/desktop/doctor/src/index.tsx +++ b/desktop/doctor/src/index.tsx @@ -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.', }; }, }, diff --git a/desktop/flipper-common/src/PluginDetails.tsx b/desktop/flipper-common/src/PluginDetails.tsx index c7a400e9c..ed8d88624 100644 --- a/desktop/flipper-common/src/PluginDetails.tsx +++ b/desktop/flipper-common/src/PluginDetails.tsx @@ -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 diff --git a/desktop/flipper-common/src/index.tsx b/desktop/flipper-common/src/index.tsx index 7925006b6..b89847fee 100644 --- a/desktop/flipper-common/src/index.tsx +++ b/desktop/flipper-common/src/index.tsx @@ -54,6 +54,7 @@ export { isConnectivityOrAuthError, isError, isAuthError, + FlipperServerDisconnectedError, getStringFromErrorLike, getErrorFromErrorLike, deserializeRemoteError, diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index cb285b123..64c04e6ef 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -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; + 'device-open-app': (serial: string, name: string) => Promise; 'device-forward-port': ( serial: string, local: string, @@ -371,6 +373,7 @@ export type FlipperServerCommands = { timeout?: number; internGraphUrl?: string; headers?: Record; + vpnMode?: 'vpn' | 'vpnless'; }, ) => Promise; 'intern-upload-scribe-logs': ( @@ -381,6 +384,7 @@ export type FlipperServerCommands = { 'is-logged-in': () => Promise; 'environment-info': () => Promise; 'move-pwa': () => Promise; + 'fetch-new-version': (version: string) => Promise; }; export type GraphResponse = { diff --git a/desktop/flipper-common/src/utils/errors.tsx b/desktop/flipper-common/src/utils/errors.tsx index 781c24113..76b1a1a6b 100644 --- a/desktop/flipper-common/src/utils/errors.tsx +++ b/desktop/flipper-common/src/utils/errors.tsx @@ -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; diff --git a/desktop/flipper-frontend-core/src/plugins.tsx b/desktop/flipper-frontend-core/src/plugins.tsx index 2375080b7..bd5573a2b 100644 --- a/desktop/flipper-frontend-core/src/plugins.tsx +++ b/desktop/flipper-frontend-core/src/plugins.tsx @@ -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; } }; diff --git a/desktop/flipper-plugin-core/src/data-source/DataSource.tsx b/desktop/flipper-plugin-core/src/data-source/DataSource.tsx index 75444ea62..cb7dc2041 100644 --- a/desktop/flipper-plugin-core/src/data-source/DataSource.tsx +++ b/desktop/flipper-plugin-core/src/data-source/DataSource.tsx @@ -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 = { entries: Entry[]; amount: number; }; +type SINewIndexValueEvent = { + type: 'siNewIndexValue'; + indexKey: string; + value: T; + firstOfKind: boolean; +}; +type ClearEvent = { + type: 'clear'; +}; type DataEvent = | AppendEvent | UpdateEvent | RemoveEvent - | ShiftEvent; + | ShiftEvent + | SINewIndexValueEvent + | ClearEvent; type Entry = { value: T; @@ -180,6 +192,8 @@ export class DataSource { [viewId: string]: DataSourceView; }; + private readonly outputEventEmitter = new EventEmitter(); + constructor( keyAttribute: keyof T | undefined, secondaryIndices: IndexDefinition[] = [], @@ -259,6 +273,10 @@ export class DataSource { }; } + 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 { this.shiftOffset = 0; this.idToIndex.clear(); this.rebuild(); + this.emitDataEvent({type: 'clear'}); } /** @@ -519,6 +538,16 @@ export class DataSource { } } + public addDataListener['type']>( + event: E, + cb: (data: Extract, {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 { 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 { } else { a.push(value); } + this.emitDataEvent({ + type: 'siNewIndexValue', + indexKey: indexValue, + value, + firstOfKind: !a, + }); } } @@ -627,11 +663,21 @@ export class DataSource { return this.getAllRecordsByIndex(indexQuery)[0]; } + public getAllIndexValues(index: IndexDefinition) { + 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, - ): any { + ): string { return JSON.stringify( Object.fromEntries(keys.map((k) => [k, String(record[k])])), ); @@ -989,6 +1035,10 @@ export class DataSourceView { } break; } + case 'clear': + case 'siNewIndexValue': { + break; + } default: throw new Error('unknown event type'); } diff --git a/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx b/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx index 20088bf10..0fb0a4705 100644 --- a/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx +++ b/desktop/flipper-plugin-core/src/data-source/__tests__/datasource-basics.node.tsx @@ -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, + }); }); diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index a42f69b26..695740797 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -121,6 +121,7 @@ test('Correct top level API exposed', () => { "ElementSearchResultSet", "ElementsInspectorElement", "ElementsInspectorProps", + "EnumLabels", "FieldConfig", "FileDescriptor", "FileEncoding", diff --git a/desktop/flipper-plugin/src/index.tsx b/desktop/flipper-plugin/src/index.tsx index 7b6e0ce6a..2627eddc4 100644 --- a/desktop/flipper-plugin/src/index.tsx +++ b/desktop/flipper-plugin/src/index.tsx @@ -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'; diff --git a/desktop/flipper-plugin/src/ui/MasterDetailWithPowerSearch.tsx b/desktop/flipper-plugin/src/ui/MasterDetailWithPowerSearch.tsx index d6b873431..d165bfe2e 100644 --- a/desktop/flipper-plugin/src/ui/MasterDetailWithPowerSearch.tsx +++ b/desktop/flipper-plugin/src/ui/MasterDetailWithPowerSearch.tsx @@ -213,7 +213,6 @@ export function MasterDetailWithPowerSearch({ )} diff --git a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchConfig.tsx b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchConfig.tsx index f2775d0b1..a9602b3e7 100644 --- a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchConfig.tsx +++ b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchConfig.tsx @@ -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 = { diff --git a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchContainer.tsx b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchContainer.tsx index 3d67666bb..ce1a1b45b 100644 --- a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchContainer.tsx +++ b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchContainer.tsx @@ -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 { diff --git a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchEnumSetTerm.tsx b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchEnumSetTerm.tsx index 3aad64df0..99a6c1fbc 100644 --- a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchEnumSetTerm.tsx +++ b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchEnumSetTerm.tsx @@ -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 = ({ @@ -22,6 +24,7 @@ export const PowerSearchEnumSetTerm: React.FC = ({ onChange, enumLabels, defaultValue, + allowFreeform, }) => { const options = React.useMemo(() => { return Object.entries(enumLabels).map(([key, label]) => ({ @@ -37,13 +40,14 @@ export const PowerSearchEnumSetTerm: React.FC = ({ return ( = ({ return ( ); }; diff --git a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTerm.tsx b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTerm.tsx index bef2230bf..3268ca107 100644 --- a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTerm.tsx +++ b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTerm.tsx @@ -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 = ({ }); }} enumLabels={searchTerm.operator.enumLabels} + allowFreeform={searchTerm.operator.allowFreeform} defaultValue={searchTerm.searchValue} /> ); @@ -131,6 +133,7 @@ export const PowerSearchTerm: React.FC = ({ }); }} enumLabels={searchTerm.operator.enumLabels} + allowFreeform={searchTerm.operator.allowFreeform} defaultValue={searchTerm.searchValue} /> ); @@ -166,7 +169,7 @@ export const PowerSearchTerm: React.FC = ({ } return ( - + diff --git a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTermFinder.tsx b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTermFinder.tsx index 208e80fbc..8c3e0ad15 100644 --- a/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTermFinder.tsx +++ b/desktop/flipper-plugin/src/ui/PowerSearch/PowerSearchTermFinder.tsx @@ -67,6 +67,7 @@ export const PowerSearchTermFinder = React.forwardRef< setSearchTermFinderValue(null); }}> { if (event.key === 'Enter') { diff --git a/desktop/flipper-plugin/src/ui/PowerSearch/index.tsx b/desktop/flipper-plugin/src/ui/PowerSearch/index.tsx index cf6b50c1f..052c8a58d 100644 --- a/desktop/flipper-plugin/src/ui/PowerSearch/index.tsx +++ b/desktop/flipper-plugin/src/ui/PowerSearch/index.tsx @@ -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 = ({ return ( - - - {searchExpression.map((searchTerm, i) => { - return ( - { - setSearchExpression((prevSearchExpression) => { - if (prevSearchExpression[i]) { - return [ - ...prevSearchExpression.slice(0, i), - ...prevSearchExpression.slice(i + 1), - ]; - } - return prevSearchExpression; - }); - }} - onFinalize={(finalSearchTerm) => { - setSearchExpression((prevSearchExpression) => { + + {searchExpression.map((searchTerm, i) => { + return ( + { + setSearchExpression((prevSearchExpression) => { + if (prevSearchExpression[i]) { return [ ...prevSearchExpression.slice(0, i), - finalSearchTerm, ...prevSearchExpression.slice(i + 1), ]; - }); - searchTermFinderRef.current?.focus(); - }} - /> - ); - })} - + } + return prevSearchExpression; + }); + }} + onFinalize={(finalSearchTerm) => { + setSearchExpression((prevSearchExpression) => { + return [ + ...prevSearchExpression.slice(0, i), + finalSearchTerm, + ...prevSearchExpression.slice(i + 1), + ]; + }); + searchTermFinderRef.current?.focus(); + }} + /> + ); + })} { 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; diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableDefaultPowerSearchOperators.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableDefaultPowerSearchOperators.tsx index 3641c88b6..1234ab969 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableDefaultPowerSearchOperators.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableDefaultPowerSearchOperators.tsx @@ -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) => ({ + enum_is: (enumLabels: EnumLabels, allowFreeform?: boolean) => ({ label: 'is', key: 'enum_is', valueType: 'ENUM', enumLabels, + allowFreeform, }), - enum_is_nullish_or: (enumLabels: Record) => ({ + 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) => ({ + 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) => ({ + 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) => ({ + 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) => ({ + 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 = {}; +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) => { diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearch.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearch.tsx index d7dd34108..0e01d276f 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearch.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearch.tsx @@ -67,6 +67,7 @@ import { FieldConfig, OperatorConfig, SearchExpressionTerm, + EnumLabels, } from '../PowerSearch'; import { dataTablePowerSearchOperatorProcessorConfig, @@ -104,6 +105,10 @@ type DataTableBaseProps = { * @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 = 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 = { //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 = { inversed?: boolean; sortable?: boolean; powerSearchConfig?: + | PowerSearchSimplifiedConfig | OperatorConfig[] | false - | {operators: OperatorConfig[]; useWholeRow?: boolean}; + | PowerSearchExtendedConfig; }; export interface TableRowRenderContext { @@ -263,6 +329,74 @@ export function DataTable( [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>({}); + 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 = + {}; + + 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( // 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( } return res; - }, [columns, props.enablePowerSearchWholeRowSearch]); + }, [ + columns, + props.enablePowerSearchWholeRowSearch, + inferredPowerSearchEnumLabels, + ]); const renderingConfig = useMemo>(() => { let startIndex = 0; @@ -461,6 +711,7 @@ export function DataTable( computeDataTableFilter( tableState.searchExpression, dataTablePowerSearchOperatorProcessorConfig, + props.treatUndefinedValuesAsMatchingFiltering, ), ); dataView.setFilterExpections( @@ -670,7 +921,7 @@ export function DataTable( {props.actionsTop ? {props.actionsTop} : null} {props.enableSearchbar && ( - + ( /> {contexMenu && ( - @@ -819,6 +1070,7 @@ DataTable.defaultProps = { enablePersistSettings: true, onRenderEmpty: undefined, enablePowerSearchWholeRowSearch: true, + treatUndefinedValuesAsMatchingFiltering: false, } as Partial>; /* eslint-disable react-hooks/rules-of-hooks */ diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearchManager.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearchManager.tsx index 7295309f9..e7a3335b4 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearchManager.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearchManager.tsx @@ -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 = } > | Action<'clearSelection', {}> + | Action<'setSearchExpressionFromSelection', {column: DataTableColumn}> | Action<'setFilterExceptions', {exceptions: string[] | undefined}> | Action<'appliedInitialScroll'> | Action<'toggleAutoScroll'> @@ -116,7 +120,7 @@ export type DataManagerState = { sorting: Sorting | 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, + 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( }); } - let searchExpression = config.initialSearchExpression; + let searchExpression = config.initialSearchExpression ?? []; if (prefs?.searchExpression?.length) { searchExpression = prefs.searchExpression; } @@ -506,25 +538,18 @@ export function getValueAtPath(obj: Record, 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; + } }); }; } diff --git a/desktop/flipper-plugin/src/ui/data-table/PowerSearchTableContextMenu.tsx b/desktop/flipper-plugin/src/ui/data-table/PowerSearchTableContextMenu.tsx index 066d2365e..f904b2775 100644 --- a/desktop/flipper-plugin/src/ui/data-table/PowerSearchTableContextMenu.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/PowerSearchTableContextMenu.tsx @@ -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( key={column.key ?? idx} onClick={() => { dispatch({ - type: 'setColumnFilterFromSelection', - column: column.key, + type: 'setSearchExpressionFromSelection', + column, }); }}> {friendlyColumnTitle(column)} diff --git a/desktop/flipper-server-client/src/FlipperServerClient.tsx b/desktop/flipper-server-client/src/FlipperServerClient.tsx index 698245198..2f0b7323b 100644 --- a/desktop/flipper-server-client/src/FlipperServerClient.tsx +++ b/desktop/flipper-server-client/src/FlipperServerClient.tsx @@ -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, + tokenProvider: () => string | null | undefined, onStateChange: (state: FlipperServerState) => void, ): Promise { - 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(); }); diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index c2407ff2e..76a756ed6 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -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 = 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) { diff --git a/desktop/flipper-server-core/src/app-connectivity/BrowserServerWebSocket.tsx b/desktop/flipper-server-core/src/app-connectivity/BrowserServerWebSocket.tsx index 72512b872..f1afa65c7 100644 --- a/desktop/flipper-server-core/src/app-connectivity/BrowserServerWebSocket.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/BrowserServerWebSocket.tsx @@ -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); diff --git a/desktop/flipper-server-core/src/app-connectivity/ServerRSocket.tsx b/desktop/flipper-server-core/src/app-connectivity/ServerRSocket.tsx index 84515b68d..375101781 100644 --- a/desktop/flipper-server-core/src/app-connectivity/ServerRSocket.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/ServerRSocket.tsx @@ -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; diff --git a/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx b/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx index 100ba2d48..58511fb81 100644 --- a/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx @@ -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; diff --git a/desktop/flipper-server-core/src/app-connectivity/ServerWebSocketBase.tsx b/desktop/flipper-server-core/src/app-connectivity/ServerWebSocketBase.tsx index b7ae2824e..84e1cbdb5 100644 --- a/desktop/flipper-server-core/src/app-connectivity/ServerWebSocketBase.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/ServerWebSocketBase.tsx @@ -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; diff --git a/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx b/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx index ba5d9702a..216f7b579 100644 --- a/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx @@ -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 => { 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 => { return generateAuthToken(); } - await exportTokenToManifest(token); - return token; }; diff --git a/desktop/flipper-server-core/src/devices/ServerDevice.tsx b/desktop/flipper-server-core/src/devices/ServerDevice.tsx index 5d65f8454..3366dde02 100644 --- a/desktop/flipper-server-core/src/devices/ServerDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ServerDevice.tsx @@ -82,4 +82,8 @@ export abstract class ServerDevice { async installApp(_appBundlePath: string): Promise { throw new Error('installApp not implemented'); } + + async openApp(_name: string): Promise { + throw new Error('openApp not implemented'); + } } diff --git a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx index 1a50a26a8..b6162d1fa 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx @@ -63,6 +63,7 @@ export interface IOSBridge { ipaPath: string, tempPath: string, ) => Promise; + openApp: (serial: string, name: string) => Promise; getInstalledApps: (serial: string) => Promise; ls: (serial: string, appBundleId: string, path: string) => Promise; pull: ( @@ -149,6 +150,11 @@ export class IDBBridge implements IOSBridge { await this._execIdb(`install ${ipaPath} --udid ${serial}`); } + async openApp(serial: string, name: string): Promise { + console.log(`Opening app via IDB ${name} ${serial}`); + await this._execIdb(`launch ${name} --udid ${serial} -f`); + } + async getActiveDevices(bootedOnly: boolean): Promise { return iosUtil .targets(this.idbPath, this.enablePhysicalDevices, bootedOnly) @@ -217,6 +223,10 @@ export class SimctlBridge implements IOSBridge { ); } + async openApp(): Promise { + throw new Error('openApp is not implemented for SimctlBridge'); + } + async installApp( serial: string, ipaPath: string, diff --git a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx index 3e88ad3b8..5ec90b9f4 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx @@ -140,6 +140,10 @@ export default class IOSDevice ); } + async openApp(name: string): Promise { + return this.iOSBridge.openApp(this.serial, name); + } + async readFlipperFolderForAllApps(): Promise { console.debug('IOSDevice.readFlipperFolderForAllApps', this.info.serial); const installedApps = await this.iOSBridge.getInstalledApps( diff --git a/desktop/flipper-server-core/src/fb-stubs/fetchNewVersion.tsx b/desktop/flipper-server-core/src/fb-stubs/fetchNewVersion.tsx new file mode 100644 index 000000000..5e3930797 --- /dev/null +++ b/desktop/flipper-server-core/src/fb-stubs/fetchNewVersion.tsx @@ -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 => {}; diff --git a/desktop/flipper-server-core/src/index.tsx b/desktop/flipper-server-core/src/index.tsx index 951b26a67..344ee7099 100644 --- a/desktop/flipper-server-core/src/index.tsx +++ b/desktop/flipper-server-core/src/index.tsx @@ -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'; diff --git a/desktop/flipper-server-core/src/server/attachSocketServer.tsx b/desktop/flipper-server-core/src/server/attachSocketServer.tsx index d89eb358a..aad29f673 100644 --- a/desktop/flipper-server-core/src/server/attachSocketServer.tsx +++ b/desktop/flipper-server-core/src/server/attachSocketServer.tsx @@ -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); }); }); diff --git a/desktop/flipper-server-core/src/server/startServer.tsx b/desktop/flipper-server-core/src/server/startServer.tsx index 5b7a4420b..ff3bc2340 100644 --- a/desktop/flipper-server-core/src/server/startServer.tsx +++ b/desktop/flipper-server-core/src/server/startServer.tsx @@ -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); } }); diff --git a/desktop/flipper-server-core/src/server/utilities.tsx b/desktop/flipper-server-core/src/server/utilities.tsx index 46764bbd5..b842a2c08 100644 --- a/desktop/flipper-server-core/src/server/utilities.tsx +++ b/desktop/flipper-server-core/src/server/utilities.tsx @@ -50,7 +50,9 @@ export async function checkServerRunning( port: number, ): Promise { 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 { 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( diff --git a/desktop/flipper-server-core/src/sessionId.tsx b/desktop/flipper-server-core/src/sessionId.tsx index 4df6f1020..8588035df 100644 --- a/desktop/flipper-server-core/src/sessionId.tsx +++ b/desktop/flipper-server-core/src/sessionId.tsx @@ -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()}`; diff --git a/desktop/flipper-server-core/src/tracker.tsx b/desktop/flipper-server-core/src/tracker.tsx index dd5ba2067..f39ab7324 100644 --- a/desktop/flipper-server-core/src/tracker.tsx +++ b/desktop/flipper-server-core/src/tracker.tsx @@ -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; diff --git a/desktop/flipper-server-core/src/utils/openUI.tsx b/desktop/flipper-server-core/src/utils/openUI.tsx new file mode 100644 index 000000000..7a4fcb511 --- /dev/null +++ b/desktop/flipper-server-core/src/utils/openUI.tsx @@ -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'); +} diff --git a/desktop/flipper-server-core/src/utils/processExit.tsx b/desktop/flipper-server-core/src/utils/processExit.tsx new file mode 100644 index 000000000..841f746d4 --- /dev/null +++ b/desktop/flipper-server-core/src/utils/processExit.tsx @@ -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)[] = []; +export const setProcessExitRoutine = ( + onBeforeExit: () => void | Promise, +) => { + onBeforeExitFns.push(onBeforeExit); +}; + +const resIsPromise = (res: void | Promise): res is Promise => + 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); + }); +}; diff --git a/desktop/flipper-server-core/src/utils/settings.tsx b/desktop/flipper-server-core/src/utils/settings.tsx index 5c4a9b3ba..bbc75869c 100644 --- a/desktop/flipper-server-core/src/utils/settings.tsx +++ b/desktop/flipper-server-core/src/utils/settings.tsx @@ -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 { 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 { 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 { - return {...getDefaultSettings(), ...userSettings}; +async function replaceDefaultSettings( + userSettings: Partial, +): Promise { + return {...(await getDefaultSettings()), ...userSettings}; } diff --git a/desktop/flipper-server/src/index.tsx b/desktop/flipper-server/src/index.tsx index ed49dadf6..c7c81ec42 100644 --- a/desktop/flipper-server/src/index.tsx +++ b/desktop/flipper-server/src/index.tsx @@ -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); + }); diff --git a/desktop/flipper-server/src/logger.tsx b/desktop/flipper-server/src/logger.tsx index 484e964a7..497e7d4c5 100644 --- a/desktop/flipper-server/src/logger.tsx +++ b/desktop/flipper-server/src/logger.tsx @@ -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((resolve) => { + logStreamToEnd?.end(resolve); + }); + }; + setProcessExitRoutine(finalizeLogger); } diff --git a/desktop/flipper-ui-browser/src/HMRClient.tsx b/desktop/flipper-ui-browser/src/HMRClient.tsx index ad76b730e..be6888346 100644 --- a/desktop/flipper-ui-browser/src/HMRClient.tsx +++ b/desktop/flipper-ui-browser/src/HMRClient.tsx @@ -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; } diff --git a/desktop/flipper-ui-browser/src/global.tsx b/desktop/flipper-ui-browser/src/global.tsx index edc96f165..04989972a 100644 --- a/desktop/flipper-ui-browser/src/global.tsx +++ b/desktop/flipper-ui-browser/src/global.tsx @@ -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; } } diff --git a/desktop/flipper-ui-browser/src/index.tsx b/desktop/flipper-ui-browser/src/index.tsx index d6c4b018e..6f9525b31 100644 --- a/desktop/flipper-ui-browser/src/index.tsx +++ b/desktop/flipper-ui-browser/src/index.tsx @@ -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() { diff --git a/desktop/flipper-ui-core/src/chrome/UpdateIndicator.tsx b/desktop/flipper-ui-core/src/chrome/UpdateIndicator.tsx index fdaaa3851..108dc4429 100644 --- a/desktop/flipper-ui-core/src/chrome/UpdateIndicator.tsx +++ b/desktop/flipper-ui-core/src/chrome/UpdateIndicator.tsx @@ -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 ? ( + ) : ( <> {' '} Run arc pull (optionally with --latest) in{' '} - ~/fbsource and restart Flipper to update to the latest - version. + ~/fbsource and{' '} + + . ) ) : ( diff --git a/desktop/flipper-ui-core/src/chrome/__tests__/flipper_messages.node.tsx b/desktop/flipper-ui-core/src/chrome/__tests__/flipper_messages.node.tsx index c29eb592c..d10ccab3b 100644 --- a/desktop/flipper-ui-core/src/chrome/__tests__/flipper_messages.node.tsx +++ b/desktop/flipper-ui-core/src/chrome/__tests__/flipper_messages.node.tsx @@ -121,82 +121,49 @@ test('It can render rows', async () => { (await renderer.findByText('unique-string')).parentElement?.parentElement, ).toMatchInlineSnapshot(`
- - +
00:00:00.000 - -
-
- - +
+
Android Phone - -
-
- - +
+
FB4A - -
-
- - +
+
unique-string - -
-
- - - -
-
- - - -
-
- - +
+
+
+
toClient:send - +
`); diff --git a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx index 49364b099..63597fca8 100644 --- a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx @@ -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, }; } diff --git a/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx b/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx index 014f9a941..94daf13f2 100644 --- a/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx +++ b/desktop/flipper-ui-core/src/sandy-chrome/Navbar.tsx @@ -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() { + {getRenderHostInstance().serverConfig.environmentInfo diff --git a/desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx b/desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx new file mode 100644 index 000000000..b15a8a777 --- /dev/null +++ b/desktop/flipper-ui-core/src/sandy-chrome/RatingButton.tsx @@ -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 ( + + {this.props.comment} + + ); + } +} + +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 ( + + + Dismiss + + + ); +} + +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, + 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 = 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; + switch (this.state.nextAction) { + case 'select-rating': + body = [ + {this.props.promptData.bodyText}, + + this.onSubmitRating(newRating)} /> + , + dismissRow(this.props.dismiss), + ]; + break; + case 'leave-comment': + const predefinedComments = Object.entries( + this.state.predefinedComments, + ).map((c: [string, unknown], idx: number) => ( + + this.setState({ + predefinedComments: { + ...this.state.predefinedComments, + [c[0]]: !c[1], + }, + }) + } + /> + )); + body = [ + {predefinedComments}, + + this.setState({comment: e.target.value})} + onKeyDown={(e) => + e.key == 'Enter' && this.onCommentSubmitted(this.state.comment) + } + autoFocus + /> + , + + this.onAllowUserSharingChanged(e.target.checked)} + /> + {'Tool owner can contact me '} + , + + + , + dismissRow(this.props.dismiss), + ]; + break; + case 'finished': + body = [ + + Thanks for the feedback! You can now help + + prioritize bugs and features for Flipper in Papercuts + + , + dismissRow(this.props.dismiss), + ]; + break; + default: { + console.error('Illegal state: nextAction: ' + this.state.nextAction); + return null; + } + } + return ( + + + {this.state.nextAction === 'finished' + ? this.props.promptData.postSubmitHeading + : this.props.promptData.preSubmitHeading} + + {body} + + ); + } +} + +export function SandyRatingButton() { + const [promptData, setPromptData] = + useState(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, + allowUserInfoSharing: boolean, + ) => { + UserFeedback.submitComment( + rating, + comment, + selectedPredefinedComments, + allowUserInfoSharing, + sessionId, + ); + }; + + if (!promptData) { + return null; + } + if (!promptData.shouldPopup || (hasTriggered && !isShown)) { + return null; + } + return ( + { + setIsShown(false); + }} + dismiss={onClick} + promptData={promptData} + /> + } + placement="right" + trigger="click"> + + + ); +} diff --git a/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx b/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx index 156ebfe33..6f015e3f6 100644 --- a/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx +++ b/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx @@ -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', }); diff --git a/desktop/flipper-ui-core/src/sandy-chrome/SetupDoctorScreen.tsx b/desktop/flipper-ui-core/src/sandy-chrome/SetupDoctorScreen.tsx index 552a7329e..569839f90 100644 --- a/desktop/flipper-ui-core/src/sandy-chrome/SetupDoctorScreen.tsx +++ b/desktop/flipper-ui-core/src/sandy-chrome/SetupDoctorScreen.tsx @@ -133,7 +133,11 @@ function CollapsableCategory(props: { key={check.key} header={check.label} extra={}> - {check.result.message} + {check.result.message?.split('\n').map((line, index) => ( + + {line} + + ))} {check.result.commands && ( {check.result.commands.map(({title, command}, i) => ( diff --git a/desktop/flipper-ui-core/src/sandy-chrome/appinspect/LaunchEmulator.tsx b/desktop/flipper-ui-core/src/sandy-chrome/appinspect/LaunchEmulator.tsx index 894c23f4d..185ed421f 100644 --- a/desktop/flipper-ui-core/src/sandy-chrome/appinspect/LaunchEmulator.tsx +++ b/desktop/flipper-ui-core/src/sandy-chrome/appinspect/LaunchEmulator.tsx @@ -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); } }; diff --git a/desktop/flipper-ui-core/src/utils/testUtils.tsx b/desktop/flipper-ui-core/src/utils/testUtils.tsx index 6dfc57add..4b89fb69a 100644 --- a/desktop/flipper-ui-core/src/utils/testUtils.tsx +++ b/desktop/flipper-ui-core/src/utils/testUtils.tsx @@ -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', diff --git a/desktop/package.json b/desktop/package.json index fe8453523..4b2887ae0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -171,7 +171,7 @@ "npm": "use yarn instead", "yarn": "^1.16" }, - "version": "0.236.0", + "version": "0.239.0", "workspaces": { "packages": [ "scripts", diff --git a/desktop/pkg-lib/package.json b/desktop/pkg-lib/package.json index 4ddd84909..31b20ea86 100644 --- a/desktop/pkg-lib/package.json +++ b/desktop/pkg-lib/package.json @@ -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", diff --git a/desktop/pkg-lib/src/runBuild.tsx b/desktop/pkg-lib/src/runBuild.tsx index ac07f17f4..020d5329a 100644 --- a/desktop/pkg-lib/src/runBuild.tsx +++ b/desktop/pkg-lib/src/runBuild.tsx @@ -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', }, diff --git a/desktop/plugin-lib/src/pluginPaths.tsx b/desktop/plugin-lib/src/pluginPaths.tsx index 382f14087..16d6dda80 100644 --- a/desktop/plugin-lib/src/pluginPaths.tsx +++ b/desktop/plugin-lib/src/pluginPaths.tsx @@ -30,7 +30,12 @@ export async function getPluginSourceFolders(): Promise { 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); } diff --git a/desktop/plugins/public/layout/docs/setup.mdx b/desktop/plugins/public/layout/docs/setup.mdx index 6e412487f..754d0f77b 100644 --- a/desktop/plugins/public/layout/docs/setup.mdx +++ b/desktop/plugins/public/layout/docs/setup.mdx @@ -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' // ... } diff --git a/desktop/plugins/public/leak_canary/docs/setup.mdx b/desktop/plugins/public/leak_canary/docs/setup.mdx index de5fa68a8..824c5bd3d 100644 --- a/desktop/plugins/public/leak_canary/docs/setup.mdx +++ b/desktop/plugins/public/leak_canary/docs/setup.mdx @@ -8,7 +8,7 @@ To setup the 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' } ``` diff --git a/desktop/plugins/public/logs/__tests__/logs.node.tsx b/desktop/plugins/public/logs/__tests__/logs.node.tsx index 96532406c..0588c2533 100644 --- a/desktop/plugins/public/logs/__tests__/logs.node.tsx +++ b/desktop/plugins/public/logs/__tests__/logs.node.tsx @@ -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", diff --git a/desktop/plugins/public/logs/index.tsx b/desktop/plugins/public/logs/index.tsx index bd7b62c98..e4ade415c 100644 --- a/desktop/plugins/public/logs/index.tsx +++ b/desktop/plugins/public/logs/index.tsx @@ -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[] { 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 ? ( item.enabled) + .map(([key]) => key), + }, +]; + export function devicePlugin(client: DevicePluginClient) { const rows = createDataSource([], { 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} /> ); } diff --git a/desktop/plugins/public/network/__tests__/chunks.node.tsx b/desktop/plugins/public/network/__tests__/chunks.node.tsx index 88f8e796e..bdc5bc024 100644 --- a/desktop/plugins/public/network/__tests__/chunks.node.tsx +++ b/desktop/plugins/public/network/__tests__/chunks.node.tsx @@ -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', }); }); diff --git a/desktop/plugins/public/network/__tests__/customheaders.node.tsx b/desktop/plugins/public/network/__tests__/customheaders.node.tsx index 63e860d44..0751e478e 100644 --- a/desktop/plugins/public/network/__tests__/customheaders.node.tsx +++ b/desktop/plugins/public/network/__tests__/customheaders.node.tsx @@ -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', }, ]); diff --git a/desktop/plugins/public/network/__tests__/encoding.node.tsx b/desktop/plugins/public/network/__tests__/encoding.node.tsx index 29297838b..2df48b125 100644 --- a/desktop/plugins/public/network/__tests__/encoding.node.tsx +++ b/desktop/plugins/public/network/__tests__/encoding.node.tsx @@ -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', }); }); diff --git a/desktop/plugins/public/network/docs/setup.mdx b/desktop/plugins/public/network/docs/setup.mdx index f48aa99c8..68e1262f8 100644 --- a/desktop/plugins/public/network/docs/setup.mdx +++ b/desktop/plugins/public/network/docs/setup.mdx @@ -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' } ``` diff --git a/desktop/plugins/public/network/index.tsx b/desktop/plugins/public/network/index.tsx index 5a991a007..1b364a8c2 100644 --- a/desktop/plugins/public/network/index.tsx +++ b/desktop/plugins/public/network/index.tsx @@ -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) { ); const requests = createDataSource([], { key: 'id', + indices: [['method'], ['status']], }); const selectedId = createState(undefined); const tableManagerRef = createRef>(); @@ -136,11 +137,16 @@ export function plugin(client: PluginClient) { 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[] = [ 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[] = [ 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[] = [ width: 100, formatters: formatBytes, align: 'right', + powerSearchConfig: {type: 'float'}, }, { key: 'responseLength', @@ -706,6 +726,7 @@ const baseColumns: DataTableColumn[] = [ width: 100, formatters: formatBytes, align: 'right', + powerSearchConfig: {type: 'float'}, }, { key: 'duration', @@ -713,6 +734,7 @@ const baseColumns: DataTableColumn[] = [ 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; } diff --git a/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx b/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx index d058e0201..9322d0bc8 100644 --- a/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx +++ b/desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx @@ -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'; diff --git a/desktop/plugins/public/network/types.tsx b/desktop/plugins/public/network/types.tsx index 383d71eb2..1eef07226 100644 --- a/desktop/plugins/public/network/types.tsx +++ b/desktop/plugins/public/network/types.tsx @@ -23,7 +23,7 @@ export interface Request { requestData: string | Uint8Array | undefined; // response responseTime?: Date; - status?: number; + status: string; reason?: string; responseHeaders?: Array
; responseData?: string | Uint8Array | undefined; diff --git a/desktop/plugins/public/network/utils.tsx b/desktop/plugins/public/network/utils.tsx index a6a62c681..6f65d775d 100644 --- a/desktop/plugins/public/network/utils.tsx +++ b/desktop/plugins/public/network/utils.tsx @@ -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); diff --git a/desktop/plugins/public/ui-debugger/ClientTypes.tsx b/desktop/plugins/public/ui-debugger/ClientTypes.tsx index d58e71514..85a11890b 100644 --- a/desktop/plugins/public/ui-debugger/ClientTypes.tsx +++ b/desktop/plugins/public/ui-debugger/ClientTypes.tsx @@ -228,6 +228,7 @@ export type Inspectable = | InspectableSize | InspectableBounds | InspectableSpaceBox + | InspectablePluginDeepLink | InspectableUnknown; export type InspectableText = { @@ -285,6 +286,13 @@ export type InspectableObject = { fields: Record; }; +export type InspectablePluginDeepLink = { + type: 'pluginDeeplink'; + label?: string; + pluginId: string; + deeplinkPayload: unknown; +}; + export type InspectableArray = { type: 'array'; items: Inspectable[]; diff --git a/desktop/plugins/public/ui-debugger/components/FrameworkEventsTable.tsx b/desktop/plugins/public/ui-debugger/components/FrameworkEventsTable.tsx index 01466dd58..3eb3cf0e6 100644 --- a/desktop/plugins/public/ui-debugger/components/FrameworkEventsTable.tsx +++ b/desktop/plugins/public/ui-debugger/components/FrameworkEventsTable.tsx @@ -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 | 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), ); @@ -135,42 +155,91 @@ export function FrameworkEventsTable({ ); } +const MonoSpace = (t: any) => ( + {t} +); + +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[] = [ { 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, }, ]; diff --git a/desktop/plugins/public/ui-debugger/components/sidebarV2/AttributesInspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebarV2/AttributesInspector.tsx index 9e6e2cf0d..2169bf2ba 100644 --- a/desktop/plugins/public/ui-debugger/components/sidebarV2/AttributesInspector.tsx +++ b/desktop/plugins/public/ui-debugger/components/sidebarV2/AttributesInspector.tsx @@ -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 ( ); + case 'pluginDeeplink': + return ( + + ); } return null; } diff --git a/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx b/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx index 52d9b42b9..88a548a5f 100644 --- a/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx +++ b/desktop/plugins/public/ui-debugger/components/tree/Tree.tsx @@ -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; + nodes: NodeMap; metadata: Map; 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}) { <> {Object.entries(attributes ?? {}).map(([key, value]) => ( - {key} + + {highlightManager.render(key)} + ={highlightManager.render(value)} ))} @@ -577,33 +588,52 @@ function HighlightedText(props: {text: string}) { } function nodeIcon(node: TreeNode) { + const [icon, tooltip] = nodeData(node); + + const iconComp = + typeof icon === 'string' ? : icon; + + if (tooltip == null) { + return iconComp; + } else { + return {iconComp}; + } +} + +function nodeData(node: TreeNode) { if (node.tags.includes('LithoMountable')) { - return ; + return ['icons/litho-logo-blue.png', 'Litho Mountable (Primitive)']; } else if (node.tags.includes('Litho')) { - return ; + return ['icons/litho-logo.png', 'Litho Component']; } else if (node.tags.includes('CK')) { if (node.tags.includes('iOS')) { - return ; + return ['icons/ck-mounted-logo.png', 'CK Mounted Component']; } - return ; + return ['icons/ck-logo.png', 'CK Component']; } else if (node.tags.includes('BloksBoundTree')) { - return ; + return ['facebook/bloks-logo-orange.png', 'Bloks Bridged component']; } else if (node.tags.includes('BloksDerived')) { - return ; + return ['facebook/bloks-logo-blue.png', 'Bloks Derived (Server) component']; } else if (node.tags.includes('Warning')) { - return ( - - ); + return [ + , + null, + ]; } else { - return ( + return [
- ); + />, + 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), + ); } diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 170952a40..10dcc97f6 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -51,7 +51,10 @@ export function plugin(client: PluginClient) { const snapshot = createState(null); const nodesAtom = createState>(new Map()); const frameworkEvents = createDataSource([], { - indices: [['nodeId']], + indices: [ + ['nodeId'], + ['type'], //for inferred values + ], limit: 10000, }); const frameworkEventsCustomColumns = createState>(new Set()); @@ -294,6 +297,7 @@ export function plugin(client: PluginClient) { metadata, perfEvents, os: client.device.os, + client, }; } diff --git a/desktop/scripts/build-flipper-server-release.tsx b/desktop/scripts/build-flipper-server-release.tsx index 3dddd977c..d7100e652 100644 --- a/desktop/scripts/build-flipper-server-release.tsx +++ b/desktop/scripts/build-flipper-server-release.tsx @@ -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 = 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 = 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}`); diff --git a/desktop/scripts/build-release.tsx b/desktop/scripts/build-release.tsx index c17cd2d22..e1fa3b25c 100755 --- a/desktop/scripts/build-release.tsx +++ b/desktop/scripts/build-release.tsx @@ -210,6 +210,13 @@ async function buildDist(buildFolder: string) { const targetsRaw: Map>[] = []; 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, diff --git a/desktop/scripts/paths.tsx b/desktop/scripts/paths.tsx index daa12d36a..87e5fe5d4 100644 --- a/desktop/scripts/paths.tsx +++ b/desktop/scripts/paths.tsx @@ -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'); diff --git a/desktop/scripts/start-flipper-server-dev.tsx b/desktop/scripts/start-flipper-server-dev.tsx index af87be91b..75056c22d 100644 --- a/desktop/scripts/start-flipper-server-dev.tsx +++ b/desktop/scripts/start-flipper-server-dev.tsx @@ -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( diff --git a/desktop/static/CHANGELOG.md b/desktop/static/CHANGELOG.md index 93ffb478a..92e452e07 100644 --- a/desktop/static/CHANGELOG.md +++ b/desktop/static/CHANGELOG.md @@ -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 diff --git a/desktop/static/index.web.dev.html b/desktop/static/index.web.dev.html deleted file mode 100644 index 0c3606a0a..000000000 --- a/desktop/static/index.web.dev.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - - - - - - - Flipper - - - - - -
-
- -
-
- Connecting... -
-
- - - - - diff --git a/desktop/static/index.web.html b/desktop/static/index.web.html index 80c2bdc17..1955e4d15 100644 --- a/desktop/static/index.web.html +++ b/desktop/static/index.web.html @@ -14,11 +14,7 @@ Flipper
+

+

@@ -58,7 +62,7 @@ diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 643bb5211..022081525 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -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" diff --git a/docs/extending/power-search.mdx b/docs/extending/power-search.mdx index 5b424a5f0..5fed042f0 100644 --- a/docs/extending/power-search.mdx +++ b/docs/extending/power-search.mdx @@ -8,6 +8,99 @@ By default, your [table](../tutorial/js-table.mdx) has a power search bar. It al For instance, for string values it can check if a string contains a substring or even matches some other string exactly. At the same time, for dates Flipper can filter out records after or before a certain date. Since Flipper does not have a way of identifying the column type in advance, it always assumes that every column is a string. If you want you can tell Flipper how to handle a column and what power search operators should be available. -You can see a live example of how you can provide the power search config [here](https://fburl.com/code/rrxj86e5). +## Simplified config + +Power search provides a list of default predicates for every column data type. You can specify the column data type like this: + +```tsx +import {DataTableColumn} from 'flipper-plugin' + +type MyRow = { + timestamp: number; + eventType: string; +} + +const columns: DataTableColumn[] = [ + { + key: 'timestamp', + title: 'Timestamp', + sortable: true, + powerSearchConfig: {type: 'dateTime'}, + }, + { + key: 'eventType', + title: 'Event', + powerSearchConfig: {type: 'enum'} + }, +] +``` + +[Complete list of possible "types"](https://github.com/facebook/flipper/blob/main/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearch.tsx#L148). + +## Advanced config + +If the default list of predicates is not tailored enouhg for your use-case, you can provide a list of predicates explicitly. + +```tsx +import {DataTableColumn, dataTablePowerSearchOperators} from 'flipper-plugin' + +type MyRow = { + timestamp: number; + eventType: string; +} + +const EVENT_TYPE_ENUM_LABELS = { + yodaValue: 'Yoda Label', + lukeValue: 'Luke Label' +} + +const columns: DataTableColumn[] = [ + { + key: 'timestamp', + title: 'Timestamp', + sortable: true, + powerSearchConfig: [ + dataTablePowerSearchOperators.same_as_absolute_date_no_time(), + ] + }, + { + key: 'eventType', + title: 'Event', + powerSearchConfig: { + // You can also provide power search config as an object + operators: [ + dataTablePowerSearchOperators.enum_is(EVENT_TYPE_ENUM_LABELS), + dataTablePowerSearchOperators.enum_is_not(EVENT_TYPE_ENUM_LABELS), + ], + // It could have exra options + // See https://github.com/facebook/flipper/blob/main/desktop/flipper-plugin/src/ui/data-table/DataTableWithPowerSearch.tsx#L157 + } + }, +] +``` + +## Using legacy search + +While we would encourage using the new power search, some plugins might decide to stick to the legacy experience. To do that you have to use different imports from 'flipper-plugin': `MasterDetailLegacy` instead of `MasterDetail`, `DataTableLegacy` instead of `DataTable`, `DataTableColumnLegacy` instead of `DataTable`, `DataTableManagerLegacy` instead of `DataTableManager`. + +```tsx +import {MasterDetailLegacy, DataTableColumnLegacy} from 'flipper-plugin'; + +const columns: DataTableColumnLegacy[] = [ + // colun definition +] + +export const Component = () => { + return +} +``` + +## Examples + +You can see a live examplse of how you can provide the power search config here: + +0. [Logs](https://github.com/facebook/flipper/blob/main/desktop/plugins/public/logs/index.tsx#L49) +0. [Network](https://github.com/facebook/flipper/blob/main/desktop/plugins/public/network/index.tsx#L664) +0. [Intern-only](https://fburl.com/code/liiu1wns). You can find the complete list of supported operators [here](https://github.com/facebook/flipper/blob/main/desktop/flipper-plugin/src/ui/data-table/DataTableDefaultPowerSearchOperators.tsx). diff --git a/docs/getting-started/android-native.mdx b/docs/getting-started/android-native.mdx index 8d5c5bc09..f18c458ec 100644 --- a/docs/getting-started/android-native.mdx +++ b/docs/getting-started/android-native.mdx @@ -24,10 +24,10 @@ repositories { } dependencies { - debugImplementation 'com.facebook.flipper:flipper:0.236.0' + debugImplementation 'com.facebook.flipper:flipper:0.239.0' debugImplementation 'com.facebook.soloader:soloader:0.10.5' - releaseImplementation 'com.facebook.flipper:flipper-noop:0.236.0' + releaseImplementation 'com.facebook.flipper:flipper-noop:0.239.0' } ``` @@ -124,10 +124,10 @@ repositories { } dependencies { - debugImplementation 'com.facebook.flipper:flipper:0.236.1-SNAPSHOT' + debugImplementation 'com.facebook.flipper:flipper:0.239.1-SNAPSHOT' debugImplementation 'com.facebook.soloader:soloader:0.10.5' - releaseImplementation 'com.facebook.flipper:flipper-noop:0.236.1-SNAPSHOT' + releaseImplementation 'com.facebook.flipper:flipper-noop:0.239.1-SNAPSHOT' } ``` diff --git a/docs/getting-started/react-native-ios.mdx b/docs/getting-started/react-native-ios.mdx index 55ae4a470..837a25712 100644 --- a/docs/getting-started/react-native-ios.mdx +++ b/docs/getting-started/react-native-ios.mdx @@ -51,7 +51,7 @@ Add all of the code below to your `ios/Podfile`: platform :ios, '9.0' def flipper_pods() - flipperkit_version = '0.236.0' # should match the version of your Flipper client app + flipperkit_version = '0.239.0' # should match the version of your Flipper client app pod 'FlipperKit', '~>' + flipperkit_version, :configuration => 'Debug' pod 'FlipperKit/FlipperKitLayoutPlugin', '~>' + flipperkit_version, :configuration => 'Debug' pod 'FlipperKit/SKIOSNetworkPlugin', '~>' + flipperkit_version, :configuration => 'Debug' diff --git a/docs/getting-started/react-native.mdx b/docs/getting-started/react-native.mdx index 3ae9808fb..2d6fdd86b 100644 --- a/docs/getting-started/react-native.mdx +++ b/docs/getting-started/react-native.mdx @@ -34,7 +34,7 @@ Latest version of Flipper requires react-native 0.69+! If you use react-native < Android: -1. Bump the `FLIPPER_VERSION` variable in `android/gradle.properties`, for example: `FLIPPER_VERSION=0.236.0`. +1. Bump the `FLIPPER_VERSION` variable in `android/gradle.properties`, for example: `FLIPPER_VERSION=0.239.0`. 2. Run `./gradlew clean` in the `android` directory. iOS: @@ -44,7 +44,7 @@ react-native version => 0.69.0 2. Run `pod install --repo-update` in the `ios` directory. react-native version < 0.69.0 -1. Call `use_flipper` with a specific version in `ios/Podfile`, for example: `use_flipper!({ 'Flipper' => '0.236.0' })`. +1. Call `use_flipper` with a specific version in `ios/Podfile`, for example: `use_flipper!({ 'Flipper' => '0.239.0' })`. 2. Run `pod install --repo-update` in the `ios` directory. ## Manual Setup diff --git a/gradle.properties b/gradle.properties index 534de3e27..807a0bd4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. # POM publishing constants -VERSION_NAME=0.236.1-SNAPSHOT +VERSION_NAME=0.239.1-SNAPSHOT GROUP=com.facebook.flipper SONATYPE_STAGING_PROFILE=comfacebook POM_URL=https://github.com/facebook/flipper diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm index dfc18b098..a0eae87a2 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm @@ -200,7 +200,7 @@ static std::vector& attributeGenerators() { forNode:(SKComponentLayoutWrapper*)node { SKHighlightOverlay* overlay = [SKHighlightOverlay sharedInstance]; if (highlighted) { - CKComponentViewContext viewContext = node.component.viewContext; + RCComponentViewContext viewContext = node.component.viewContext; [overlay mountInView:viewContext.view withFrame:viewContext.frame]; } else { [overlay unmount]; diff --git a/js/js-flipper/package.json b/js/js-flipper/package.json index fb8effe64..0c2184ec7 100644 --- a/js/js-flipper/package.json +++ b/js/js-flipper/package.json @@ -1,7 +1,7 @@ { "name": "js-flipper", "title": "JS Flipper Bindings for Web-Socket based clients", - "version": "0.236.0", + "version": "0.239.0", "main": "lib/index.js", "browser": { "os": false diff --git a/packer/Cargo.lock b/packer/Cargo.lock index 6749dd28a..85f38cc73 100644 --- a/packer/Cargo.lock +++ b/packer/Cargo.lock @@ -186,9 +186,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "digest" @@ -661,18 +661,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", diff --git a/react-native/react-native-flipper/package.json b/react-native/react-native-flipper/package.json index b423a3895..b072b23ab 100644 --- a/react-native/react-native-flipper/package.json +++ b/react-native/react-native-flipper/package.json @@ -1,7 +1,7 @@ { "name": "react-native-flipper", "title": "React Native Flipper Bindings", - "version": "0.236.0", + "version": "0.239.0", "description": "Flipper bindings for React Native", "main": "index.js", "types": "index.d.ts", diff --git a/website/static/img/flipper-launcher-release-skycastle.png b/website/static/img/flipper-launcher-release-skycastle.png new file mode 100644 index 000000000..ca00ff1d3 Binary files /dev/null and b/website/static/img/flipper-launcher-release-skycastle.png differ