Compare commits

...

113 Commits

Author SHA1 Message Date
0c4d607788 Merge branch 'main' of github.com:facebook/flipper into universalBuild 2023-11-29 09:19:25 +01:00
Lorenzo Blasa
57584a38fa Electron distribution announcement
Summary:
Update our README file with an announcement of our distribution change. I will pair the announcement with an update to our GitHub workflow as to not produce any more Electron builds for future releases.

Changelog: Flipper Electron distribution change announcement.

Reviewed By: aigoncharov

Differential Revision: D51616454

fbshipit-source-id: 5ff513b3d99c8100ed8241d1bdafebf1d6dcfa10
2023-11-28 09:03:54 -08:00
Andrey Goncharov
24fa44448e Use SESSION_ID
Reviewed By: lblasa

Differential Revision: D51585227

fbshipit-source-id: e5574a27b77311b699630a30f72a2d67187ed544
2023-11-28 06:51:03 -08:00
dependabot[bot]
272b96f1d4 Bump data-encoding from 2.4.0 to 2.5.0 in /packer (#5310)
Summary:
Bumps [data-encoding](https://github.com/ia0/data-encoding) from 2.4.0 to 2.5.0.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/ia0/data-encoding/commits">compare view</a></li>
</ul>
</details>
<br />

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=data-encoding&package-manager=cargo&previous-version=2.4.0&new-version=2.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

 ---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `dependabot rebase` will rebase this PR
- `dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `dependabot merge` will merge this PR after your CI passes on it
- `dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `dependabot cancel merge` will cancel a previously requested merge and block automerging
- `dependabot reopen` will reopen this PR if it is closed
- `dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

</details>

Pull Request resolved: https://github.com/facebook/flipper/pull/5310

Reviewed By: mweststrate

Differential Revision: D51588872

Pulled By: passy

fbshipit-source-id: bbce09141bf6adf384fad0ae2cc6d91f5b36af6e
2023-11-28 05:34:00 -08:00
Richard Zito
f24d7dbf6a CKComponentViewContext -> RCComponentViewContext
Summary: This lives in RenderCore, it should have an RC prefix.

Differential Revision: D51586116

fbshipit-source-id: 102c71f68d6505d34b5a4aac2bd97a49dcaf8408
2023-11-28 04:46:33 -08:00
Anton Kastritskiy
ba6133e38a Ask to restart flipper to pick up ios SDK
Summary: Running a check is run it only executes the `run` function. The `run` function for `ios.sdk` is a pure function that detects if ios SDK available from `EnvironmentInfo`. This info is picked up when flipper first started. Even if you installed ios SDK flipper won't pick it up while running. Restarting flipper will pick up the SDK and make it available. We should be clear about it.

Reviewed By: mweststrate

Differential Revision: D51614258

fbshipit-source-id: 8b2a8b5b94ec0038acf9f383e55e166c52b2ccab
2023-11-28 04:39:59 -08:00
dependabot[bot]
1a4c602fd8 Bump serde from 1.0.188 to 1.0.193 in /packer (#5311)
Summary:
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.188 to 1.0.193.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/serde-rs/serde/releases">serde's releases</a>.</em></p>
<blockquote>
<h2>v1.0.193</h2>
<ul>
<li>Fix field names used for the deserialization of <code>RangeFrom</code> and <code>RangeTo</code> (<a href="https://redirect.github.com/serde-rs/serde/issues/2653">https://github.com/facebook/flipper/issues/2653</a>, <a href="https://redirect.github.com/serde-rs/serde/issues/2654">https://github.com/facebook/flipper/issues/2654</a>, <a href="https://redirect.github.com/serde-rs/serde/issues/2655">https://github.com/facebook/flipper/issues/2655</a>, thanks <a href="https://github.com/emilbonnek"><code>@​emilbonnek</code></a>)</li>
</ul>
<h2>v1.0.192</h2>
<ul>
<li>Allow internal tag field in untagged variant (<a href="https://redirect.github.com/serde-rs/serde/issues/2646">https://github.com/facebook/flipper/issues/2646</a>, thanks <a href="https://github.com/robsdedude"><code>@​robsdedude</code></a>)</li>
</ul>
<h2>v1.0.191</h2>
<ul>
<li>Documentation improvements</li>
</ul>
<h2>v1.0.190</h2>
<ul>
<li>Preserve NaN sign when deserializing f32 from f64 or vice versa (<a href="https://redirect.github.com/serde-rs/serde/issues/2637">https://github.com/facebook/flipper/issues/2637</a>)</li>
</ul>
<h2>v1.0.189</h2>
<ul>
<li>Fix &quot;cannot infer type&quot; error when internally tagged enum contains untagged variant (<a href="https://redirect.github.com/serde-rs/serde/issues/2613">https://github.com/facebook/flipper/issues/2613</a>, thanks <a href="https://github.com/ahl"><code>@​ahl</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="44613c7d01"><code>44613c7</code></a> Release 1.0.193</li>
<li><a href="c706281df3"><code>c706281</code></a> Merge pull request <a href="https://redirect.github.com/serde-rs/serde/issues/2655">https://github.com/facebook/flipper/issues/2655</a> from dtolnay/rangestartend</li>
<li><a href="65d75b8fe3"><code>65d75b8</code></a> Add RangeFrom and RangeTo tests</li>
<li><a href="332b0cba40"><code>332b0cb</code></a> Merge pull request <a href="https://redirect.github.com/serde-rs/serde/issues/2654">https://github.com/facebook/flipper/issues/2654</a> from dtolnay/rangestartend</li>
<li><a href="8c4af41296"><code>8c4af41</code></a> Fix more RangeFrom / RangeEnd mixups</li>
<li><a href="24a78f071b"><code>24a78f0</code></a> Merge pull request <a href="https://redirect.github.com/serde-rs/serde/issues/2653">https://github.com/facebook/flipper/issues/2653</a> from emilbonnek/fix/range-to-from-de-mixup</li>
<li><a href="c91c33436d"><code>c91c334</code></a> Fix Range{From,To} deserialize mixup</li>
<li><a href="2083f43a28"><code>2083f43</code></a> Update ui test suite to nightly-2023-11-19</li>
<li><a href="4676abdc9e"><code>4676abd</code></a> Release 1.0.192</li>
<li><a href="35700eb23e"><code>35700eb</code></a> Merge pull request <a href="https://redirect.github.com/serde-rs/serde/issues/2646">https://github.com/facebook/flipper/issues/2646</a> from robsdedude/fix/2643/allow-tag-field-in-untagged</li>
<li>Additional commits viewable in <a href="https://github.com/serde-rs/serde/compare/v1.0.188...v1.0.193">compare view</a></li>
</ul>
</details>
<br />

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.188&new-version=1.0.193)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

 ---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `dependabot rebase` will rebase this PR
- `dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `dependabot merge` will merge this PR after your CI passes on it
- `dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `dependabot cancel merge` will cancel a previously requested merge and block automerging
- `dependabot reopen` will reopen this PR if it is closed
- `dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

</details>

Pull Request resolved: https://github.com/facebook/flipper/pull/5311

Reviewed By: mweststrate

Differential Revision: D51588875

Pulled By: passy

fbshipit-source-id: 14d7861ca16790b353f4a9d3bfd09750366080ea
2023-11-28 03:47:27 -08:00
Anton Kastritskiy
3b1763bd7d explicit instructions for idb
Summary: Add explicit instructions how to install idb internally

Reviewed By: LukeDefeo

Differential Revision: D51600754

fbshipit-source-id: c40d5d07c4cb570a7c51699ce0e4b8a67c506147
2023-11-28 03:40:23 -08:00
Anton Kastritskiy
4e10bb1b43 Back out "remove shortcuts"
Summary:
Original commit changeset: fbb59b416f92

Original Phabricator Diff: D51159922

Reviewed By: lblasa

Differential Revision: D51586735

fbshipit-source-id: 3317b590ec72e0040a67be41e74ddb5a72722598
2023-11-27 15:33:57 -08:00
Anton Kastritskiy
d8f507dba0 Back out "remove all mentions of metro from flipper-ui-core"
Summary:
Original commit changeset: edc33c2b989e

Original Phabricator Diff: D51200129

Reviewed By: lblasa

Differential Revision: D51586736

fbshipit-source-id: e81355f7a0f0533ebc47f4cfc6c40af4865dbad0
2023-11-27 15:33:57 -08:00
Anton Kastritskiy
244573abe3 Back out "remove metro button"
Summary:
Original commit changeset: ebd844a57b0b

Original Phabricator Diff: D51199560

Reviewed By: lblasa

Differential Revision: D51586734

fbshipit-source-id: dfb37d4a6f6bd03da96ab17cd4af8e43de2b0f07
2023-11-27 15:33:57 -08:00
Pascal Hartig
a9c5dd746a Store plugin ID
Summary:
We need the build ID from GraphQL to fetch the download VPN off-VPN.

Changelog: Plugin Marketplace now stores a unique ID per build

Reviewed By: LukeDefeo

Differential Revision: D51525419

fbshipit-source-id: d6740b3c4724d15cc996155b6e7581b0f16a9da6
2023-11-27 09:04:07 -08:00
Anton Kastritskiy
ce5527513e only simulators in "Virtual device"
Summary: Currently if you plug in a physical iOS device via USB, flipper will show it to you in "Virtual device menu". See "ak iphone mini" in the test plan. This diff ensures that we only show virtual device in this menu.

Reviewed By: lblasa

Differential Revision: D51588117

fbshipit-source-id: e2853a6b34ed3fe6d821a9c504b9ffd19e4074fa
2023-11-27 08:42:12 -08:00
Lorenzo Blasa
2318bffd07 Built artifact is already universal, no architecture is needed
Summary: ^

Reviewed By: antonk52

Differential Revision: D51568857

fbshipit-source-id: 124d77e4dd175a13f491f8242b3a28a898ff5670
2023-11-27 06:23:06 -08:00
Lorenzo Blasa
5e26d863f1 Packaging changes
Summary:
This change has a few changes in the way our MacOS app was built and packaged.

- Instead of placing the Node binary inside the MacOS folder, place it inside the Resources folder. This is more in compliance with how an app is bundled. Also, with the added benefit of making it a resource which makes it eligible for code signing.
- Both, Node binary and server bundle are placed in the right location before building the MacOS app. By doing this, we ensure the app is not modified after the built process which messes up with code signing, if in place.

Reviewed By: antonk52

Differential Revision: D51568778

fbshipit-source-id: 0b1b0ad9947550ddf0f6d4b04e5aff41f7edcdee
2023-11-27 04:55:05 -08:00
Lorenzo Blasa
d22d362c31 Codesign capabilities
Summary:
Add codesign capabilities to Flipper Server Cocoa app.

Also, push the version to the build step instead of overriding once built. Otherwise, the Info.plist will be marked as 'being tampered with'.

Note: we are still not using the signing, but the capability is there.

Reviewed By: antonk52

Differential Revision: D51547008

fbshipit-source-id: 33abcd2fce33a7daf2ae8941b54989dba82fc0e3
2023-11-27 04:55:05 -08:00
Andrey Goncharov
72a92e1380 Shutdown Flipper when new version is downloaded
Reviewed By: passy

Differential Revision: D51527986

fbshipit-source-id: d4cf1ec82070821afff5c9cdee5a85cb1421a7ef
2023-11-23 04:08:07 -08:00
Andrey Goncharov
685a0e53d7 Add command to fetch new Flipper version
Summary: It is a copy-paste of what we have in jest-e2e

Reviewed By: passy

Differential Revision: D51527040

fbshipit-source-id: 36b62edee006580023e9ce2cd309dddfd0dbce84
2023-11-23 04:08:07 -08:00
Anton Kastritskiy
4477f3b550 Update esbuild
Summary:
Update esbuild to the latest version that is backward compatible to the current version.

Specifically I was looking for a version that adds support for the typescript's `satisfies` feature [0.15.3](https://github.com/evanw/esbuild/releases/tag/v0.15.13)

Reviewed By: passy

Differential Revision: D51537364

fbshipit-source-id: 6fb8f01ee7b4d19db048d03277be6b6642124002
2023-11-23 02:56:07 -08:00
dependabot[bot]
b908ab50ac Bump com.google.protobuf:protobuf-java from 3.23.4 to 3.25.1 (#5300)
Summary:
Bumps [com.google.protobuf:protobuf-java](https://github.com/protocolbuffers/protobuf) from 3.23.4 to 3.25.1.
<details>
<summary>Commits</summary>
<ul>
<li><a href="7f94235e55"><code>7f94235</code></a> Updating version.json and repo version numbers to: 25.1</li>
<li><a href="e4b00c75ec"><code>e4b00c7</code></a> Add support for extensions in CRuby, JRuby, and FFI Ruby (<a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14703">#14703</a>) (<a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14756">#14756</a>)</li>
<li><a href="2495d4f96b"><code>2495d4f</code></a> Add support for options in CRuby, JRuby and FFI (<a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14594">#14594</a>) (<a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14739">#14739</a>)</li>
<li><a href="a29f47df0f"><code>a29f47d</code></a> Bump mac PHP version to 8.2 to fix non-hermetic breakages. (<a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14741">#14741</a>)</li>
<li><a href="f36432a6d1"><code>f36432a</code></a> Merge pull request <a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14674">#14674</a> from anandolee/25.x</li>
<li><a href="74f5cf4106"><code>74f5cf4</code></a> Raise warnings for python syntax usages</li>
<li><a href="edb1afd049"><code>edb1afd</code></a> Move python/BUILD to python/BUILD.bazel (<a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14658">#14658</a>)</li>
<li><a href="666689e0f7"><code>666689e</code></a> Merge pull request <a href="https://redirect.github.com/protocolbuffers/protobuf/issues/14620">#14620</a> from protocolbuffers/win2019-25.x</li>
<li><a href="1577c305cf"><code>1577c30</code></a> Error on staleness failure</li>
<li><a href="1155c804ef"><code>1155c80</code></a> Update <code>cc_file_list_aspect</code> to handle targets with missing <code>hdrs/textual_hdrs</code></li>
<li>Additional commits viewable in <a href="https://github.com/protocolbuffers/protobuf/compare/v3.23.4...v3.25.1">compare view</a></li>
</ul>
</details>
<br />

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.protobuf:protobuf-java&package-manager=gradle&previous-version=3.23.4&new-version=3.25.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

 ---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `dependabot rebase` will rebase this PR
- `dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `dependabot merge` will merge this PR after your CI passes on it
- `dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `dependabot cancel merge` will cancel a previously requested merge and block automerging
- `dependabot reopen` will reopen this PR if it is closed
- `dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

</details>

Pull Request resolved: https://github.com/facebook/flipper/pull/5300

Reviewed By: antonk52

Differential Revision: D51525558

Pulled By: passy

fbshipit-source-id: df9f980c75af74e622cc3a899c5a53684ffd8b90
2023-11-22 07:05:56 -08:00
Andrey Goncharov
5693ac7205 Disconnect all mobile clients when all UI clients leave
Summary: Context https://fb.workplace.com/groups/flippersupport/permalink/1730762380737746/

Reviewed By: lblasa

Differential Revision: D51510348

fbshipit-source-id: afafcdd6b89bf1038fec65a7c3e8c2dd9cfd0768
2023-11-22 02:59:27 -08:00
Andrey Goncharov
1c706b52bc Add an empty line for a term finder only when it is absolutely necessary
Reviewed By: mweststrate

Differential Revision: D51507094

fbshipit-source-id: cb5e18ab702f60ebbf2e5573180fd21299371139
2023-11-21 12:48:27 -08:00
Andrey Goncharov
88d8567310 Align buttons to the top
Reviewed By: mweststrate

Differential Revision: D51506570

fbshipit-source-id: 5fc52b214eb61f5da0c3417016e87a79866d4506
2023-11-21 12:48:27 -08:00
Andrey Goncharov
0425dfd4e5 Add vertical spacing in multiline mode
Reviewed By: mweststrate

Differential Revision: D51505906

fbshipit-source-id: 6cbf94173d404077de1f8d7c764d91799e25404c
2023-11-21 12:48:27 -08:00
Andrey Goncharov
202e6b6148 Fix autocomplete for Logs plugin
Summary: It was a typo before

Reviewed By: passy

Differential Revision: D51502529

fbshipit-source-id: c4ed789b382301ba9e183e5dd7a491592efd22dd
2023-11-21 11:40:23 -08:00
Andrey Goncharov
294f39eceb Allow freeform entries for enum terms
Summary: Solves https://fb.workplace.com/groups/flippersupport/posts/1726178417862809/?comment_id=1726429847837666&reply_comment_id=1730206487460002

Reviewed By: passy

Differential Revision: D51497924

fbshipit-source-id: d0737b2b82f29ff8ae654e7cad2ef1daa8244756
2023-11-21 11:40:23 -08:00
Pascal Hartig
864e296f35 Recover from corrupted config.json
Summary: This has caused a few tasks to be raised. We do replace broken configs with a fresh template elsewhere, but we shouldn't throw in this place since we have a fallback.

Reviewed By: antonk52

Differential Revision: D51393314

fbshipit-source-id: 8c04946c5b0e74f5f0d42c9492aa381aa608fd35
2023-11-21 10:30:06 -08:00
Andrey Goncharov
877253191d Support multi-line wrapping for long queries / small screens
Reviewed By: antonk52

Differential Revision: D51497340

fbshipit-source-id: 23bcc9a69c3e59e17ee9505c954c955f2a1c9e5f
2023-11-21 07:56:19 -08:00
Andrey Goncharov
7e2508f045 Fix server dummy timeout value
Summary: It has to fit in 32 bit int

Reviewed By: lblasa

Differential Revision: D51478071

fbshipit-source-id: c4512e946c3e47bc235920b133d9f6c739c5ac76
2023-11-21 04:13:12 -08:00
Andrey Goncharov
fb37f27ff5 Suport device-open-app command
Summary: It is an escape hatch for testing iOS Sample app if `navigate('/')` cannot be made to work easily (D51394323)

Reviewed By: lblasa

Differential Revision: D51466697

fbshipit-source-id: fd6c2f37504807b4b634b85cb10e837523dadba8
2023-11-20 07:14:09 -08:00
Anton Kastritskiy
387d546e77 inconsistent right padding
Summary: There is too much padding on the right wether right sidebar open or not, this fixes it

Reviewed By: lblasa

Differential Revision: D51428892

fbshipit-source-id: 6fa9b0c06ddf32c32d162a7fd2bd765e8996c00a
2023-11-17 10:06:47 -08:00
Lorenzo Blasa
ec9bc8a29f Avoid template copy
Summary:
As mentioned on the previous diff, we no longer use a pre-built template when creating a release build.

This change moves the built binary directly into place instead of updating the existing template after which it was moved to its final destination.

Reviewed By: antonk52

Differential Revision: D51424944

fbshipit-source-id: 284fb53bb5f00f92b3ca9db3b28cfd1e4dacfa19
2023-11-17 05:51:47 -08:00
Lorenzo Blasa
e3038a3393 MacOS app to be built on each release build
Summary:
So far we have used a 'template' approach, in which the release build script will copy and use to create the final deliverable.

The template itself was updated locally by running:
    cd ~/fbsource/xplat/sonar/facebook/flipper-server
    yarn start

This would:
- Build the MacOS app for all supported architectures: x64 and arm64
- Update the template found on the static directory

After the update, we would just commit the changes to the repo.

## About this change
Instead, build the MacOS app when the release script is used. This is leaves way less margin for error as we have removed all the manual steps that had to be done as listed above.

Reviewed By: antonk52

Differential Revision: D51397661

fbshipit-source-id: 2234c9996fa98f32db244764acf3e35dc9a388c9
2023-11-17 05:51:47 -08:00
Andrey Goncharov
b9fa86e77f Fix filter from selection
Reviewed By: LukeDefeo

Differential Revision: D51425090

fbshipit-source-id: 53dda8f65d2e8d17468903c20e88d45038e65be1
2023-11-17 05:31:25 -08:00
Luke De Feo
bf67b19c4a Allow searching on inline attribute keys
Summary: useful for bloks debugging

Reviewed By: lblasa

Differential Revision: D51349211

fbshipit-source-id: a11eca19bdecf989ceb6a95e9a3cb504020c7467
2023-11-17 04:18:41 -08:00
Luke De Feo
288e8e2d48 Make search expand relevant nodes
Summary:
Previously we were searching the tree nodes which did not include all nodes since if a not was not expanded it isnt in the tree node list, so now we expand first then find the first tree node that matches

changelog: UIDebugger search now expands matching elements

Reviewed By: lblasa

Differential Revision: D51349213

fbshipit-source-id: 4eef436781ed6ec23187d1aec7633ee4b959d3fa
2023-11-17 04:18:41 -08:00
Luke De Feo
dd9279bf7a Bloks debugger state deeplink from UIDebugger
Summary:
This adds a deeplink into bloks debugger from uidebugger for state events

changelog: UIDebugger - show bloks state & deeplink to bloks debugger

Reviewed By: lblasa

Differential Revision: D51349212

fbshipit-source-id: 6f7ca826228ce11a01fe5eb197f6ce092d2757a9
2023-11-17 04:18:41 -08:00
Andrey Goncharov
e225d9e1c3 Bring back RatingButton
Summary: Removed previously during redesign by mistake

Reviewed By: antonk52

Differential Revision: D51347837

fbshipit-source-id: d634ad4c8983271a3936f458cabb63f006a4bb0a
2023-11-17 03:34:58 -08:00
generatedunixname89002005306973
ace3626938 Flipper Snapshot Bump: v0.239.1-SNAPSHOT
Summary: Releasing snapshot version 0.239.1-SNAPSHOT

Reviewed By: aigoncharov

Differential Revision: D51393963

fbshipit-source-id: 96a5ffd64465c0391694707fb8d5d14992807ea1
2023-11-16 05:36:45 -08:00
generatedunixname89002005306973
7fa24636ca Flipper Release: v0.239.0
Summary: Releasing version 0.239.0

Reviewed By: aigoncharov

Differential Revision: D51393961

fbshipit-source-id: ce286c17042b59b227396bbd2c22f0d0257edcd2
2023-11-16 05:36:45 -08:00
Andrey Goncharov
d515342526 Add FlipperServerDisconnectedError to prevent excessive error logging
Reviewed By: passy

Differential Revision: D51393196

fbshipit-source-id: f49857b397a3fb629ad44f89a4c59b12ba2f67c4
2023-11-16 04:18:41 -08:00
Andrey Goncharov
7c972a982a Fix plugin backward compatibility
Reviewed By: lblasa

Differential Revision: D51391318

fbshipit-source-id: f4cd42077bbd358bda30860659e845fa0ee8e845
2023-11-16 03:14:11 -08:00
Andrey Goncharov
edc46dc2b1 Fix resetFilters not resetting power search
Reviewed By: antonk52

Differential Revision: D51351225

fbshipit-source-id: 4f0406b047ed1cc44d7de54976db2a422b07b7c0
2023-11-15 07:30:47 -08:00
Luke De Feo
39d84e3bfc Fix occasional scrollbars
Summary:
Sometimes after a while scroll bars woudl start to appear in the text areas like so
{F1150744703}

Its hard to repro but seemed to be after flipper was open for a long period. Playing with chrome dev tools setting overflow: hidden makes the problem go away

changelog: UIDebugger fix issue with scrollbars sometimes appearing in sidebar

Reviewed By: antonk52

Differential Revision: D51346366

fbshipit-source-id: 2d9218ac582164c9726a92c3c0c99567382f8929
2023-11-15 05:09:56 -08:00
Andrey Goncharov
bfc4e959bc Export power search enabled components and types by default
Summary: Provides power search typings to all users by default

Reviewed By: LukeDefeo

Differential Revision: D51306600

fbshipit-source-id: c1f0d318d8b6953dd02af16d2c51abbf0e6e9590
2023-11-15 05:00:21 -08:00
Andrey Goncharov
b34718ac32 Extend docs
Reviewed By: antonk52

Differential Revision: D51305961

fbshipit-source-id: a4ca7365def6ebf43b84668f63887cc81f82fb36
2023-11-15 05:00:21 -08:00
Andrey Goncharov
f01568bf59 Prevent duplicate browser connection reporting
Reviewed By: lblasa

Differential Revision: D51347107

fbshipit-source-id: 14f4507835794d76b17f9a6891f22dbc0cc1a8f7
2023-11-15 04:22:11 -08:00
Andrey Goncharov
ed7a7f7bd0 Fix processExit
Summary: setTimeout should terminate process if graceful termination takes too long. Before, we scheduled the timer only *after* the graceful termination completes which does not make any sense

Reviewed By: lblasa

Differential Revision: D51346673

fbshipit-source-id: b5adadbcf474a8c66839e1fc70bcc6482c47635e
2023-11-15 03:46:46 -08:00
Lorenzo Blasa
6ccae92918 PWA for development
Summary: This was not possible in the past because the entry points for dev and production were different. This is no longer the case.

Reviewed By: antonk52

Differential Revision: D51346250

fbshipit-source-id: fc482a9d90352ea4e897092e7670920f148fa274
2023-11-15 03:45:25 -08:00
Lorenzo Blasa
6b54bd3173 Remove no longer needed index.web.dev.html
Summary: This is a duplicate, is not needed, causes confusion.

Reviewed By: aigoncharov

Differential Revision: D51307091

fbshipit-source-id: 4d55d727ea5f20100ecd15ad6e23aa0c01722524
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
f1b35ca592 Make HTML entry points identical
Summary: Make both prod and dev HTML entry points identical.

Reviewed By: aigoncharov

Differential Revision: D51307116

fbshipit-source-id: 5aea8e455d623aba260e3e37a2c549ebc67dd3b0
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
65d2ce7ed5 Move dev socket
Summary: Not entirely sure what the use of this socket is, but it can definitely be defined inside flipper-ui-browser instead.

Reviewed By: aigoncharov

Differential Revision: D51307090

fbshipit-source-id: 36eb336536e8672fb0b2bcf12dad31c7fbc00a39
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
af3f11521b Remove probably duplicated logs
Summary: These logs only available on debug but we already have these logs coming from the actual used socket, so remove.

Reviewed By: aigoncharov

Differential Revision: D51307089

fbshipit-source-id: 32e3eada42fa54b429df0bfcdd936d24cebaf0cb
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
067693f3c8 Standard message between prod and dev
Summary: These two texts were different, they should be the same for consistency.

Reviewed By: aigoncharov

Differential Revision: D51307086

fbshipit-source-id: a71fb7e6cf072df73e7f9fb386245f266984900b
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
11ec4c3107 Remove unused css
Summary: This CSS was unused, so remove.

Reviewed By: aigoncharov

Differential Revision: D51307093

fbshipit-source-id: a978d76fca7cfb07c96180c4ece0b1bdf1087894
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
7de92cb34a Config set from server
Summary: Instead of having duplicate configs defined on our HTML. Move the config definition to the server.

Reviewed By: aigoncharov

Differential Revision: D51307092

fbshipit-source-id: 68f4afc918cf191b3a15b3981429c5a05d5df8df
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
9166939214 Rename constants
Summary: After the move, let's rename the constants to make them match our code standards.

Reviewed By: aigoncharov

Differential Revision: D51307087

fbshipit-source-id: 4e44e956fd88abd3e8359fe94fa4e31d17f61a55
2023-11-14 10:53:09 -08:00
Lorenzo Blasa
49abb4dd41 Move constants to flipperConfig
Summary: Recently we have added a few constants to be used by our main entry point. This change moves them to a central place: flipperConfig.

Reviewed By: aigoncharov

Differential Revision: D51307088

fbshipit-source-id: 09f0ef0e69e2067ce5c8501367629eeec7523858
2023-11-14 10:53:09 -08:00
Luke De Feo
0889a0e02d Tooltip for node type icon
Reviewed By: aigoncharov

Differential Revision: D51307310

fbshipit-source-id: 9a522928102de6a28c0307568451acaed969edc5
2023-11-14 08:40:48 -08:00
Anton Kastritskiy
6c4c579f27 Increase max width for resizable sidebar
Reviewed By: LukeDefeo

Differential Revision: D51305977

fbshipit-source-id: 03fbdba01d01aaecdd182538aed64ae396818f4d
2023-11-14 08:14:37 -08:00
generatedunixname89002005306973
b633766199 Flipper Snapshot Bump: v0.238.1-SNAPSHOT
Summary: Releasing snapshot version 0.238.1-SNAPSHOT

Reviewed By: aigoncharov

Differential Revision: D51298284

fbshipit-source-id: 51f30bd24af2bb2a40b3e4d87cd7acb7e9bbe4ce
2023-11-14 06:28:51 -08:00
generatedunixname89002005306973
ed80151768 Flipper Release: v0.238.0
Summary: Releasing version 0.238.0

Reviewed By: aigoncharov

Differential Revision: D51298282

fbshipit-source-id: ed64ff1a72194f6f4de0a87a8e05a2de83e29cc1
2023-11-14 06:28:51 -08:00
Andrey Goncharov
9910807826 Fix power search for slogs
Reviewed By: antonk52

Differential Revision: D51232391

fbshipit-source-id: 6501f60ee4168c62d1d4efefcfcc698d4954e7ac
2023-11-14 06:28:11 -08:00
Anton Kastritskiy
f6445fea43 Check if Android Studio is installed
Summary:
Since Android Studio is technically not required, I made this check optional.
Though, these steps will gurantee that android env will work for an engineer who opened flipper for the first time

Reviewed By: lblasa

Differential Revision: D51267272

fbshipit-source-id: f30e58f322ea080b00a27ae86b871df2b39e1bb9
2023-11-14 04:29:59 -08:00
Anton Kastritskiy
d88cf41a24 Always write ANDROID_HOME and ANDROID_SDK_ROOT
Summary:
When flipper starts it writes to both env vars the value from settings. This is needed for subprocesses to have access to them.

The problem with the check for both env vars is
- In prod environment applications does not have access to them
- In dev, when developers launch flipper from terminal (`yarn run flipper-server`) it picks up these env vars if they are set which can cause different behaviour in dev and prod

To make flipper work more deterministic, for users and us. I removed this check and now we always write to these env vars what the user has provided in flipper settings or the default.

Reviewed By: lblasa

Differential Revision: D51266495

fbshipit-source-id: cf3adfd4ba83a733a30b5b0b29c270b32ff3a61a
2023-11-14 02:54:59 -08:00
Lorenzo Blasa
bc77dcf326 Simplify shutdown logic
Summary:
The previous logic aimed to reuse an existing server during bootstrap if the launched version was higher than the running one. This is no longer required or wanted.

## Risk assessment: LOW
### Rationale
It is extremely rare to launch Flipper whilst already having another instance running. This can happen during development, but it is extremely rare in production.

Launcher (singleton) launches Server (singleton).
Launcher can be executed multiple times and this will not create newer server instances.

If anything, if we are unable to kill any other instance, whatever that may be, continue. This is to cover the cases where a shutdown may have been acknowledged but the process is still shutting down.

Reviewed By: antonk52

Differential Revision: D51232901

fbshipit-source-id: 8b8b85f4bac68f5670b1878e827871f78dc68999
2023-11-13 12:51:45 -08:00
Anton Kastritskiy
1199e1f667 Update settings location to reflect new nav bar
Reviewed By: elboman

Differential Revision: D51257953

fbshipit-source-id: f19d2a1343276ee066b422e85b71e4d0a8e7bdb9
2023-11-13 08:26:13 -08:00
Anton Kastritskiy
cb485613e4 Internal Xcode install instruction
Reviewed By: elboman

Differential Revision: D51257660

fbshipit-source-id: 6cf213135f1070def3e62e936d6147b15749b7de
2023-11-13 08:26:13 -08:00
Luke De Feo
9dea899701 use new api for enum
Summary: Also fixed time column

Reviewed By: aigoncharov

Differential Revision: D51255567

fbshipit-source-id: 70a708a0e502eeaf1e51b8ace41dc588b9c0543d
2023-11-13 06:02:32 -08:00
Luke De Feo
437e67cd7f Remove uneeded operators from simplified config
Summary:
The single value == / contains are covered by the setlike oper /ators.

Less operators is better as less overwhelming. Plus with the group operators you can add futher options or'ed togther which is a complain from the user.

Reviewed By: aigoncharov

Differential Revision: D51255253

fbshipit-source-id: 72f909319fd3d8034ebe4725a5a5254ecfeb074b
2023-11-13 06:02:32 -08:00
Luke De Feo
4ada8b9322 Summary:
Make as manything as inferred enum as possible

changelog: [Logs] Improve power search config to populate dropdown for level, PID & Tag

Reviewed By: aigoncharov

Differential Revision: D51199644

fbshipit-source-id: 383b61abca5d91a8e318bbfb1aac7d3852074167
2023-11-13 04:43:05 -08:00
Andrey Goncharov
a400eb2872 Finalize log stream before exiting process
Reviewed By: antonk52

Differential Revision: D51229230

fbshipit-source-id: 0e7f657a170eb8602ade9abf1db1976c5b51dc3f
2023-11-11 08:21:12 -08:00
Anton Kastritskiy
0cbd640e5c expand on ios doctor suggestions
Summary:
this should help to address support posts like this one

https://fb.workplace.com/groups/flippersupport/posts/1724631861350798

Reviewed By: potomak

Differential Revision: D51203549

fbshipit-source-id: 097faeb6c9d506bc1fa2ac4602cba95e99a4e2cb
2023-11-10 09:54:08 -08:00
Anton Kastritskiy
45157c3675 Change order of ios checks
Summary:
Current order is confusion, the new order better reflects order at which users gets started with iOS

1. Install Xcode
2. Point Xcode-select at most recent xcode version
3. Install SDK
4. Accept Xcode SDK license
5. check idb health

Reviewed By: lblasa

Differential Revision: D51202202

fbshipit-source-id: da39acf7ef325889c2288b3941cef0ca803356cd
2023-11-10 09:54:08 -08:00
Anton Kastritskiy
b7a4741e40 remove all mentions of metro from flipper-ui-core
Summary: All functionality was about selecting plugins

Reviewed By: lblasa

Differential Revision: D51200129

fbshipit-source-id: edc33c2b989eabec2ca4a7e285f92c50950977ed
2023-11-10 09:34:52 -08:00
Anton Kastritskiy
5269800738 remove metro button
Reviewed By: lblasa

Differential Revision: D51199560

fbshipit-source-id: ebd844a57b0b30859b186361136cc8a2f897d9f2
2023-11-10 09:34:52 -08:00
Andrey Goncharov
9ca6f01c40 Use simplified enum config for network plugin
Reviewed By: LukeDefeo

Differential Revision: D51203032

fbshipit-source-id: fc2a9a1dcd55ca05381037743622b5aa9b45f24f
2023-11-10 09:08:28 -08:00
Andrey Goncharov
cd9db40e4f Support occasionally stringified null and undefined in enums
Reviewed By: LukeDefeo

Differential Revision: D51202685

fbshipit-source-id: 74d0a3d7ed956f3fafc393f180b30cd2fcc55384
2023-11-10 09:08:28 -08:00
Andrey Goncharov
2d28ca2c37 Support enum label inferral for simplified config
Reviewed By: LukeDefeo

Differential Revision: D51199436

fbshipit-source-id: c611f0d5a21d7500447342d3e2a5e12c55e9b67d
2023-11-10 09:08:28 -08:00
Anton Kastritskiy
2d253b1387 fix displayed error in virtual device picker
Summary: Error thrown from server is serialiased that is a string, not instance of Error

Reviewed By: LukeDefeo

Differential Revision: D51201870

fbshipit-source-id: 833818789a50a21d3d27e0388635e0e2b7470c9d
2023-11-10 08:03:34 -08:00
Anton Kastritskiy
e5f6ad0ca6 default adb path
Summary:
As I was setting up a new mac I had to change this setting in flipper settings.

From a few android people that I've spoken to no one could answer me if the old one is still used.

Ideally we should infer this during the first time flipper starts

- `/opt/android_sdk` current default
- `$HOME/Library/Android/sdk` adb from Android Studio
- `$HOME/fbsource/third-party/toolchains/android-sdk` adb that is always available in fbsource

Reviewed By: lblasa

Differential Revision: D51120929

fbshipit-source-id: edb2a58b9c9f37465ea2fc5493975dd427d5523b
2023-11-10 08:03:34 -08:00
generatedunixname89002005306973
b08e6feb44 Flipper Snapshot Bump: v0.237.1-SNAPSHOT
Summary: Releasing snapshot version 0.237.1-SNAPSHOT

Reviewed By: aigoncharov

Differential Revision: D51199541

fbshipit-source-id: 97420b530bee02d2721af752a13dcd8020ada369
2023-11-10 06:50:40 -08:00
generatedunixname89002005306973
91efcce5c5 Flipper Release: v0.237.0
Summary: Releasing version 0.237.0

Reviewed By: aigoncharov

Differential Revision: D51199543

fbshipit-source-id: 4579641a9c113cd14127db69a180e76358b68de9
2023-11-10 06:50:40 -08:00
Andrey Goncharov
a1070b8cea Add regex support
Reviewed By: LukeDefeo

Differential Revision: D51198464

fbshipit-source-id: 445cc47f90c2730f3b0728e5bf667330274d103d
2023-11-10 04:34:09 -08:00
Anton Kastritskiy
d023bcc42e remove shortcuts
Reviewed By: aigoncharov

Differential Revision: D51159922

fbshipit-source-id: fbb59b416f92b9156c74a12247da8d0df07f1a4e
2023-11-10 04:33:02 -08:00
Andrey Goncharov
04b4bf7bdf Setup prefetcher during startup
Reviewed By: passy

Differential Revision: D51198236

fbshipit-source-id: 00781afda72258f2aee64a9d0481117979c49031
2023-11-10 04:11:35 -08:00
Andrey Goncharov
4b3f572205 Preserve previous error messages
Reviewed By: passy

Differential Revision: D51197113

fbshipit-source-id: 237c6f1f894cb4d758150ff2bddf14c104d3b381
2023-11-10 03:39:32 -08:00
Andrey Goncharov
8348d617d0 Fix token replacements
Reviewed By: lblasa

Differential Revision: D51196650

fbshipit-source-id: 184c104b32a1d619163c799ae70419e6aad23e98
2023-11-10 03:35:14 -08:00
Andrey Goncharov
6e19c4155c Track session length
Differential Revision: D51172955

fbshipit-source-id: d4f93564a94e232066347c945fa4798033dc0da1
2023-11-09 14:34:52 -08:00
Andrey Goncharov
8ef29c8160 Embed auth token into HTML
Summary:
Auth token used be injected in the manifest file. Instead, have the server injected into the main HTML page.

The main driver to this change are:
- Simplify
- There are instances in which for some reason reading/writing the token from the manifest fails. This will address that problem.

Reviewed By: lblasa

Differential Revision: D51160521

fbshipit-source-id: 4626fd8f56bc8b61182a53a5d9cf5acad1e723bc
2023-11-09 14:05:43 -08:00
Lorenzo Blasa
69378c4b09 Add logs for early exit when writing token to manifest
Summary: ^

Reviewed By: aigoncharov

Differential Revision: D51154908

fbshipit-source-id: 0c04ee50d07db1478ec5a77faa6d6157b7c9960b
2023-11-09 09:08:33 -08:00
Andrey Goncharov
d54bd7c3ba Refactor browser connection performance tracking
Reviewed By: lblasa

Differential Revision: D51158256

fbshipit-source-id: 17e020dd3c26ac73bf2cf0ceb4c664638c6778e9
2023-11-09 08:25:28 -08:00
Andrey Goncharov
54217f2c79 Use simplified power search config
Reviewed By: lblasa

Differential Revision: D51116200

fbshipit-source-id: a73036020649c06cb5afeb78d9c219a77dac7d4a
2023-11-09 05:08:16 -08:00
Andrey Goncharov
b5cb7fcce2 Support simplified power search config
Summary: It is quite cumbersome to list all of the operators. Much simpler to use a predefined set of power search operators we set up for each specific filed type

Reviewed By: lblasa

Differential Revision: D51116029

fbshipit-source-id: 5dd0b7f4176097109666107dfc3cab996379b818
2023-11-09 05:08:16 -08:00
Andrey Goncharov
284dee0460 Extend default list of operators for unconfigured columns
Summary:
It does not make too much sense to keep the unknown value conversion behind a flag. It is relatively cheap. Let's do it for all strings.
Also, let's extend the list of default string operators

Reviewed By: lblasa

Differential Revision: D51115573

fbshipit-source-id: a62c08a90d8ddf6f23f59412c3fd981e19225e47
2023-11-09 05:08:16 -08:00
Andrey Goncharov
51e149765e Add more logging
Reviewed By: antonk52

Differential Revision: D51154065

fbshipit-source-id: 7532d1e141562adacb9170b112185bd6f730cc5a
2023-11-09 03:57:59 -08:00
Pascal Hartig
f856cedf81 Update release docs
Summary: Document the new release flow that does no longer involve local builds.

Reviewed By: antonk52

Differential Revision: D51115563

fbshipit-source-id: 0c518e51dba64b2325047d6b1e485216e48d9777
2023-11-09 02:21:11 -08:00
Lorenzo Blasa
3993e7461d Shutdown after 5 hours
Summary:
If after 5 hours there are no connected clients, shutdown the server.

This is to prevent cases whereas the long-lived instance makes users use stale versions of Flipper if Flipper stays indefinitely running in the background.

Reviewed By: passy

Differential Revision: D51111926

fbshipit-source-id: 4c38e392cf8a6a4fb840bffdea92c0b0314aefb9
2023-11-08 10:39:18 -08:00
Lorenzo Blasa
640fb86edc Do not shutdown when there's no connected clients
Summary:
^

Also, track client closes with their code and/or error.

Reviewed By: antonk52

Differential Revision: D51110574

fbshipit-source-id: 2416e36256b000664b7677fcf2c03b045d318ed2
2023-11-08 10:39:18 -08:00
Luke De Feo
4d0a5ff42b Change how custom columsn work
Summary:
Previously the render function was selecting the data from the paylaod object, this mean what was on screen and waht powersearch saw was diffferent.

Now we supply a dotted key path and remove this from render, power search operator also uses this dotted key path so the search works

changelog: UIdebugger added powersearch operators to Framework event table

Reviewed By: aigoncharov

Differential Revision: D51113095

fbshipit-source-id: 3c951c2a8a7a0a35e0aa79a194b979b699f83008
2023-11-08 10:30:55 -08:00
Luke De Feo
03c2828630 Power search config for framework event table
Summary: Basic config, added inferred enum support for the event type.

Reviewed By: aigoncharov

Differential Revision: D51113094

fbshipit-source-id: 13acd83e7f7a5d4ee6b62641b13616cc49377e0a
2023-11-08 10:30:55 -08:00
Luke De Feo
e461229075 Enum dropdown width matches content rather than parent input
Reviewed By: aigoncharov

Differential Revision: D51112879

fbshipit-source-id: e3647df81ce7bbed91a606e68d44a503c367c948
2023-11-08 10:30:55 -08:00
Lorenzo Blasa
92d1454140 Open flag when starting server in debug mode
Summary:
It was hard-coded to always open the first time.

This allows to run the server on debug mode without opening UI.

Reviewed By: antonk52

Differential Revision: D51115746

fbshipit-source-id: 9467f0fbff45987247a2bb3bf5eb1aa578de1913
2023-11-08 09:20:13 -08:00
Lorenzo Blasa
9b9eb00b63 Endpoint to Open UI
Summary: Expose an endpoint to open Flipper UI. This will be used by the Flipper Server Cocoa app to open the UI.

Reviewed By: antonk52

Differential Revision: D51115327

fbshipit-source-id: 1ab1c32d93945cf8d75b145905983738331a6468
2023-11-08 09:20:13 -08:00
Lorenzo Blasa
137e75ad46 Refactor Open
Summary: Extract our launch UI logic into flipper-server-core.

Reviewed By: passy

Differential Revision: D51115241

fbshipit-source-id: 185e381eab6b480d86a5e1201f45c070104d0cea
2023-11-08 09:20:13 -08:00
Lorenzo Blasa
9164e04e29 Better appId and productName
Summary:
As to not conflict with non-electron installations, this will ensure the product name and app id matches the installation.

By doing this, both Flipper installations can co-exist, which I think is desirable.

Reviewed By: antonk52

Differential Revision: D51078955

fbshipit-source-id: fabaa6eb2a45ac542297b0456a09e938a2ec2e0b
2023-11-08 02:51:34 -08:00
Andrey Goncharov
3dc9cc5d3d Skip unknown values during filtering by default
Reviewed By: LukeDefeo

Differential Revision: D51078471

fbshipit-source-id: b3a005f31eebd77e9ff77349640133032f4b0164
2023-11-08 02:13:47 -08:00
Andrey Goncharov
4bb0f59ab8 Migrate to power search
Reviewed By: LukeDefeo

Differential Revision: D51027189

fbshipit-source-id: 4fb3699a278db280237e5182d41d3c746e44a2bb
2023-11-08 02:08:25 -08:00
Andrey Goncharov
a8f5fecc2b Infer enum labels
Reviewed By: LukeDefeo

Differential Revision: D51067952

fbshipit-source-id: ed39d3ab037a2169120187bf20bf4a023488c025
2023-11-08 02:08:25 -08:00
Andrey Goncharov
701ae01501 Support tracking secondary indecies in DataSource
Reviewed By: LukeDefeo

Differential Revision: D51026559

fbshipit-source-id: 1f8f40ceedf70dfdc8978e0d6e447a1a58f8f82a
2023-11-08 02:08:25 -08:00
Andrey Goncharov
da5856138d Add event emitter to datasource
Reviewed By: LukeDefeo

Differential Revision: D51026560

fbshipit-source-id: 8348c6765633d7eecf0d1c80bc5bbd5af8130298
2023-11-08 02:08:25 -08:00
Aria Fallah
3536ffe737 Add worker plugin to plugin esbuild
Summary:
## Context
https://fb.workplace.com/groups/flippersupport/posts/1722856878194963

## Changes
* Add a worker plugin that takes modules suffixed with `?worker`, bundles them, treats them as web workers, and returns a function as a default export that instanitates a new worker

Reviewed By: antonk52

Differential Revision: D51059224

fbshipit-source-id: cef89486f7a2d5b8ce38354df4a5749271a6c41d
2023-11-07 11:09:10 -08:00
Anton Kastritskiy
39b1b37172 Off vpn error banner
Summary:
Some features of bloks do not work off vpn.

Until we figure out how to make them off vpn

Reviewed By: LukeDefeo

Differential Revision: D51076469

fbshipit-source-id: c83d96e89d33d245845312b39928a7460a235217
2023-11-07 10:57:06 -08:00
103 changed files with 2466 additions and 1154 deletions

View File

@@ -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
View File

@@ -50,6 +50,7 @@ xplat/build
# Mac OS X
*.DS_Store
facebook/flipper-server/Resources/
# Automatically generated
docs/extending/ui-components.mdx

View File

@@ -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>

View File

@@ -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

View File

@@ -143,7 +143,7 @@ async function getFlipperServer(
const {readyForIncomingConnections} = await startServer(
{
staticPath,
entry: 'index.web.dev.html',
entry: 'index.web.html',
port,
},
environmentInfo,

View File

@@ -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.`;

View 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');

View File

@@ -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,
message,
hasProblem: true,
...getIdbInstallationInstructions(settings.idbPath),
};
}
return {
hasProblem: false,
message:
'Flipper is configured to use your IDB installation.',
};
},
},

View File

@@ -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

View File

@@ -54,6 +54,7 @@ export {
isConnectivityOrAuthError,
isError,
isAuthError,
FlipperServerDisconnectedError,
getStringFromErrorLike,
getErrorFromErrorLike,
deserializeRemoteError,

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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;
}
};

View File

@@ -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');
}

View File

@@ -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,
});
});

View File

@@ -121,6 +121,7 @@ test('Correct top level API exposed', () => {
"ElementSearchResultSet",
"ElementsInspectorElement",
"ElementsInspectorProps",
"EnumLabels",
"FieldConfig",
"FileDescriptor",
"FileEncoding",

View File

@@ -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';

View File

@@ -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>
)}

View File

@@ -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 = {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -67,6 +67,7 @@ export const PowerSearchTermFinder = React.forwardRef<
setSearchTermFinderValue(null);
}}>
<Input
size="small"
bordered={false}
onKeyUp={(event) => {
if (event.key === 'Enter') {

View File

@@ -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,11 +128,9 @@ export const PowerSearch: React.FC<PowerSearchProps> = ({
return (
<PowerSearchContainer>
<Space size={[theme.space.tiny, 0]}>
<SearchOutlined
style={{
marginLeft: theme.space.tiny,
marginRight: theme.space.tiny,
margin: theme.space.tiny,
color: theme.textColorSecondary,
}}
/>
@@ -159,7 +163,6 @@ export const PowerSearch: React.FC<PowerSearchProps> = ({
/>
);
})}
</Space>
<PowerSearchTermFinder
ref={searchTermFinderRef}
options={options}

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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 */

View File

@@ -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;
}
});
};
}

View File

@@ -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)}

View File

@@ -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();
});

View File

@@ -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;
}
// 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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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');
}
}

View File

@@ -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,

View File

@@ -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(

View 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> => {};

View File

@@ -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';

View File

@@ -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);
});
});

View File

@@ -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);
}
});

View File

@@ -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(

View File

@@ -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()}`;

View File

@@ -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;

View 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');
}

View 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);
});
};

View File

@@ -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';
}
function getWindowsSdkPath() {
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 replaceDefaultSettings(userSettings: Partial<Settings>): Settings {
return {...getDefaultSettings(), ...userSettings};
async function replaceDefaultSettings(
userSettings: Partial<Settings>,
): Promise<Settings> {
return {...(await getDefaultSettings()), ...userSettings};
}

View File

@@ -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;
}
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) => {
// 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);
process.exit(1);
});
reportBrowserConnection(false);
return processExit(1);
})
.finally(() => {
clearTimeout(runtimeTimeout);
});

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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() {

View File

@@ -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,16 +91,28 @@ export default function UpdateIndicator() {
isProduction()
) {
reportPlatformFailures(
checkForUpdate(version).then((res) => {
checkForUpdate(version)
.then((res) => {
if (res.kind === 'error') {
console.warn('Version check failure: ', res);
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>
.
</>
)
) : (

View File

@@ -120,6 +120,9 @@ test('It can render rows', async () => {
expect(
(await renderer.findByText('unique-string')).parentElement?.parentElement,
).toMatchInlineSnapshot(`
<div
style="position: absolute; top: 0px; left: 0px; width: 100%; height: 24px; transform: translateY(24px);"
>
<div
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
>
@@ -127,76 +130,40 @@ test('It can render rows', async () => {
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
width="14%"
>
<span>
<span
style="background-color: rgb(255, 245, 102);"
/>
00:00:00.000
</span>
</div>
<div
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
width="14%"
>
<span>
<span
style="background-color: rgb(255, 245, 102);"
/>
Android Phone
</span>
</div>
<div
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
width="14%"
>
<span>
<span
style="background-color: rgb(255, 245, 102);"
/>
FB4A
</span>
</div>
<div
class="css-12luweq-TableBodyColumnContainer e1luu51r0"
width="14%"
>
<span>
<span
style="background-color: rgb(255, 245, 102);"
/>
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%"
/>
<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);"
/>
toClient:send
</span>
</div>
</div>
</div>
`);

View File

@@ -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,
};
}

View File

@@ -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

View 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>
);
}

View File

@@ -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',
});

View File

@@ -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) => (

View File

@@ -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);
}
};

View File

@@ -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',

View File

@@ -171,7 +171,7 @@
"npm": "use yarn instead",
"yarn": "^1.16"
},
"version": "0.236.0",
"version": "0.239.0",
"workspaces": {
"packages": [
"scripts",

View File

@@ -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",

View File

@@ -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',
},

View File

@@ -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);
}

View File

@@ -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'
// ...
}

View File

@@ -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'
}
```

View File

@@ -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",

View File

@@ -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}
/>
);
}

View File

@@ -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',
});
});

View File

@@ -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',
},
]);

View File

@@ -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',
});
});

View File

@@ -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'
}
```

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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);

View File

@@ -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[];

View File

@@ -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,
},
];

View File

@@ -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;
}

View File

@@ -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),
);
}

View File

@@ -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,
};
}

View File

@@ -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,23 +746,23 @@ 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,
@@ -779,7 +785,7 @@ async function setUpMacBundle(
return obj;
}
console.log(`⚙️ Writing plist`);
console.log(`⚙️ Update plist with build information`);
const plistPath = path.join(
outputDir,
'Flipper.app',
@@ -797,7 +803,7 @@ async function setUpMacBundle(
plist.writeBinaryFileSync(plistPath, pListContents);
/* eslint-enable node/no-sync*/
const resourcesOutputDir = path.join(
serverOutputDir = path.join(
outputDir,
'Flipper.app',
'Contents',
@@ -805,21 +811,73 @@ async function setUpMacBundle(
'server',
);
if (!(await fs.exists(resourcesOutputDir))) {
await fs.mkdir(resourcesOutputDir);
}
const nodeOutputPath = path.join(
nodeBinaryFile = path.join(
outputDir,
'Flipper.app',
'Contents',
'MacOS',
'flipper-runtime',
);
return {resourcesPath: resourcesOutputDir, nodePath: nodeOutputPath};
} else {
serverOutputDir = path.join(
sonarDir,
'facebook',
'flipper-server',
'Resources',
'server',
);
nodeBinaryFile = path.join(
sonarDir,
'facebook',
'flipper-server',
'Resources',
'flipper-runtime',
);
}
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);
}
}
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 setUpMacBundle(outputDir, bundleDir, platform, versionNumber);
if (argv.dmg) {
await createMacDMG(platform, outputDir, distDir);
}
} else {
const outputPaths = {
nodePath: path.join(outputDir, 'flipper-runtime'),
resourcesPath: outputDir,
};
if (platform === BuildPlatform.LINUX) {
await setUpLinuxBundle(outputDir);
} else if (platform === BuildPlatform.WINDOWS) {
await setUpWindowsBundle(outputDir);
}
console.log(`⚙️ Copying from ${dir} to ${outputPaths.resourcesPath}`);
await fs.copy(dir, outputPaths.resourcesPath, {
console.log(
`⚙️ Copying from ${bundleDir} to ${outputPaths.resourcesPath}`,
);
await fs.copy(bundleDir, outputPaths.resourcesPath, {
overwrite: true,
dereference: true,
});
console.log(`⚙️ Downloading compatible node version`);
await installNodeBinary(outputPaths.nodePath, platform);
if (
argv.dmg &&
(platform === BuildPlatform.MAC_X64 ||
platform === BuildPlatform.MAC_AARCH64)
) {
await createMacDMG(platform, outputDir, distDir);
}
console.log(`✅ Wrote ${platform}-specific server version to ${outputDir}`);

View File

@@ -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'], {
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,

View File

@@ -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');

View File

@@ -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(

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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).

View File

@@ -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'
}
```

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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];

View File

@@ -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