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