feat: initial implementation (#1)

Alex Potsides committed May 23, 2025 at 16:23 UTC f831cd4e7eff8f10db7c44a3b111dc46ff4892a7
226 files changed +8982 -1
.cspell.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "import": [
3 + "./node_modules/aegir/cspell.json"
4 + ],
5 + "dictionaries": ["project"],
6 + "dictionaryDefinitions": [{
7 + "name": "project",
8 + "path": "./.github/dictionary.txt",
9 + "addWords": true
10 + }]
11 +}
.github/dependabot.yml new
+30
@@ -0,0 +1,30 @@
1 +version: 2
2 +updates:
3 +- package-ecosystem: npm
4 + directories:
5 + - "/"
6 + schedule:
7 + interval: daily
8 + time: "10:00"
9 + open-pull-requests-limit: 20
10 + commit-message:
11 + prefix: "deps"
12 + prefix-development: "chore"
13 + groups:
14 + interplanetary-deps: # group all deps that should be updated when Helia deps need updated
15 + patterns:
16 + - "*helia*"
17 + - "*libp2p*"
18 + - "*multiformats*"
19 + - "*blockstore*"
20 + - "*datastore*"
21 + kubo-deps: # group kubo, kubo-rpc-client, and ipfsd-ctl updates
22 + patterns:
23 + - "*kubo*"
24 + - "ipfsd-ctl"
25 +- package-ecosystem: "github-actions"
26 + directory: "/"
27 + schedule:
28 + interval: "weekly"
29 + commit-message:
30 + prefix: chore
.github/dictionary.txt new
+1
@@ -0,0 +1 @@
1 +dataurl
.github/workflows/add-to-project.yml new
+29
@@ -0,0 +1,29 @@
1 +name: Add to Ecosystem WG Project
2 +
3 +on:
4 + issues:
5 + types:
6 + - opened
7 + pull_request_target:
8 + types:
9 + - opened
10 +
11 +permissions: {}
12 +
13 +jobs:
14 + add-to-project:
15 + runs-on: ubuntu-latest
16 + steps:
17 + - name: Generate GitHub App token
18 + uses: electron/github-app-auth-action@384fd19694fe7b6dcc9a684746c6976ad78228ae # v1.1.1
19 + id: generate-token
20 + with:
21 + creds: ${{ secrets.ECOSYSTEM_ISSUE_TRIAGE_GH_APP_CREDS }}
22 + org: electron
23 + - name: Add to Project
24 + uses: dsanders11/project-actions/add-item@3a81985616963f32fae17d1d1b406c631f3201a1 # v1.1.0
25 + with:
26 + field: Opened
27 + field-value: ${{ github.event.pull_request.created_at || github.event.issue.created_at }}
28 + project-number: 89
29 + token: ${{ steps.generate-token.outputs.token }}
.github/workflows/js-test-and-release.yml new
+28
@@ -0,0 +1,28 @@
1 +name: test & maybe release
2 +
3 +on:
4 + push:
5 + branches:
6 + - main
7 + pull_request:
8 + workflow_dispatch:
9 +
10 +permissions:
11 + contents: write
12 + id-token: write
13 + packages: write
14 + pull-requests: write
15 +
16 +concurrency:
17 + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }}
18 + cancel-in-progress: true
19 +
20 +jobs:
21 + js-test-and-release:
22 + uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v1.0
23 + secrets:
24 + DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
25 + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
26 + NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
27 + UCI_GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN }}
28 + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
.github/workflows/semantic-pull-request.yml new
+12
@@ -0,0 +1,12 @@
1 +name: Semantic PR
2 +
3 +on:
4 + pull_request_target:
5 + types:
6 + - opened
7 + - edited
8 + - synchronize
9 +
10 +jobs:
11 + main:
12 + uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3
.github/workflows/stale.yml new
+13
@@ -0,0 +1,13 @@
1 +name: Close and mark stale issue
2 +
3 +on:
4 + schedule:
5 + - cron: '0 0 * * *'
6 +
7 +permissions:
8 + issues: write
9 + pull-requests: write
10 +
11 +jobs:
12 + stale:
13 + uses: pl-strflt/.github/.github/workflows/reusable-stale-issue.yml@v0.3
.gitignore
-1
@@ -7,6 +7,5 @@ node_modules
7 package-lock.json
8 yarn.lock
9 .vscode
10 -interop/*-image.json
10 .tmp-compiled-docs
11 tsconfig-doc-check.aegir.json
CODE_OF_CONDUCT.md new
+3
@@ -0,0 +1,3 @@
1 +# Contributor Code of Conduct
2 +
3 +The `js-libp2p` project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
LICENSE-APACHE new
+201
@@ -0,0 +1,201 @@
1 +Apache License
2 + Version 2.0, January 2004
3 + http://www.apache.org/licenses/
4 +
5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 +
7 + 1. Definitions.
8 +
9 + "License" shall mean the terms and conditions for use, reproduction,
10 + and distribution as defined by Sections 1 through 9 of this document.
11 +
12 + "Licensor" shall mean the copyright owner or entity authorized by
13 + the copyright owner that is granting the License.
14 +
15 + "Legal Entity" shall mean the union of the acting entity and all
16 + other entities that control, are controlled by, or are under common
17 + control with that entity. For the purposes of this definition,
18 + "control" means (i) the power, direct or indirect, to cause the
19 + direction or management of such entity, whether by contract or
20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 + outstanding shares, or (iii) beneficial ownership of such entity.
22 +
23 + "You" (or "Your") shall mean an individual or Legal Entity
24 + exercising permissions granted by this License.
25 +
26 + "Source" form shall mean the preferred form for making modifications,
27 + including but not limited to software source code, documentation
28 + source, and configuration files.
29 +
30 + "Object" form shall mean any form resulting from mechanical
31 + transformation or translation of a Source form, including but
32 + not limited to compiled object code, generated documentation,
33 + and conversions to other media types.
34 +
35 + "Work" shall mean the work of authorship, whether in Source or
36 + Object form, made available under the License, as indicated by a
37 + copyright notice that is included in or attached to the work
38 + (an example is provided in the Appendix below).
39 +
40 + "Derivative Works" shall mean any work, whether in Source or Object
41 + form, that is based on (or derived from) the Work and for which the
42 + editorial revisions, annotations, elaborations, or other modifications
43 + represent, as a whole, an original work of authorship. For the purposes
44 + of this License, Derivative Works shall not include works that remain
45 + separable from, or merely link (or bind by name) to the interfaces of,
46 + the Work and Derivative Works thereof.
47 +
48 + "Contribution" shall mean any work of authorship, including
49 + the original version of the Work and any modifications or additions
50 + to that Work or Derivative Works thereof, that is intentionally
51 + submitted to Licensor for inclusion in the Work by the copyright owner
52 + or by an individual or Legal Entity authorized to submit on behalf of
53 + the copyright owner. For the purposes of this definition, "submitted"
54 + means any form of electronic, verbal, or written communication sent
55 + to the Licensor or its representatives, including but not limited to
56 + communication on electronic mailing lists, source code control systems,
57 + and issue tracking systems that are managed by, or on behalf of, the
58 + Licensor for the purpose of discussing and improving the Work, but
59 + excluding communication that is conspicuously marked or otherwise
60 + designated in writing by the copyright owner as "Not a Contribution."
61 +
62 + "Contributor" shall mean Licensor and any individual or Legal Entity
63 + on behalf of whom a Contribution has been received by Licensor and
64 + subsequently incorporated within the Work.
65 +
66 + 2. Grant of Copyright License. Subject to the terms and conditions of
67 + this License, each Contributor hereby grants to You a perpetual,
68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 + copyright license to reproduce, prepare Derivative Works of,
70 + publicly display, publicly perform, sublicense, and distribute the
71 + Work and such Derivative Works in Source or Object form.
72 +
73 + 3. Grant of Patent License. Subject to the terms and conditions of
74 + this License, each Contributor hereby grants to You a perpetual,
75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 + (except as stated in this section) patent license to make, have made,
77 + use, offer to sell, sell, import, and otherwise transfer the Work,
78 + where such license applies only to those patent claims licensable
79 + by such Contributor that are necessarily infringed by their
80 + Contribution(s) alone or by combination of their Contribution(s)
81 + with the Work to which such Contribution(s) was submitted. If You
82 + institute patent litigation against any entity (including a
83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
84 + or a Contribution incorporated within the Work constitutes direct
85 + or contributory patent infringement, then any patent licenses
86 + granted to You under this License for that Work shall terminate
87 + as of the date such litigation is filed.
88 +
89 + 4. Redistribution. You may reproduce and distribute copies of the
90 + Work or Derivative Works thereof in any medium, with or without
91 + modifications, and in Source or Object form, provided that You
92 + meet the following conditions:
93 +
94 + (a) You must give any other recipients of the Work or
95 + Derivative Works a copy of this License; and
96 +
97 + (b) You must cause any modified files to carry prominent notices
98 + stating that You changed the files; and
99 +
100 + (c) You must retain, in the Source form of any Derivative Works
101 + that You distribute, all copyright, patent, trademark, and
102 + attribution notices from the Source form of the Work,
103 + excluding those notices that do not pertain to any part of
104 + the Derivative Works; and
105 +
106 + (d) If the Work includes a "NOTICE" text file as part of its
107 + distribution, then any Derivative Works that You distribute must
108 + include a readable copy of the attribution notices contained
109 + within such NOTICE file, excluding those notices that do not
110 + pertain to any part of the Derivative Works, in at least one
111 + of the following places: within a NOTICE text file distributed
112 + as part of the Derivative Works; within the Source form or
113 + documentation, if provided along with the Derivative Works; or,
114 + within a display generated by the Derivative Works, if and
115 + wherever such third-party notices normally appear. The contents
116 + of the NOTICE file are for informational purposes only and
117 + do not modify the License. You may add Your own attribution
118 + notices within Derivative Works that You distribute, alongside
119 + or as an addendum to the NOTICE text from the Work, provided
120 + that such additional attribution notices cannot be construed
121 + as modifying the License.
122 +
123 + You may add Your own copyright statement to Your modifications and
124 + may provide additional or different license terms and conditions
125 + for use, reproduction, or distribution of Your modifications, or
126 + for any such Derivative Works as a whole, provided Your use,
127 + reproduction, and distribution of the Work otherwise complies with
128 + the conditions stated in this License.
129 +
130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
131 + any Contribution intentionally submitted for inclusion in the Work
132 + by You to the Licensor shall be under the terms and conditions of
133 + this License, without any additional terms or conditions.
134 + Notwithstanding the above, nothing herein shall supersede or modify
135 + the terms of any separate license agreement you may have executed
136 + with Licensor regarding such Contributions.
137 +
138 + 6. Trademarks. This License does not grant permission to use the trade
139 + names, trademarks, service marks, or product names of the Licensor,
140 + except as required for reasonable and customary use in describing the
141 + origin of the Work and reproducing the content of the NOTICE file.
142 +
143 + 7. Disclaimer of Warranty. Unless required by applicable law or
144 + agreed to in writing, Licensor provides the Work (and each
145 + Contributor provides its Contributions) on an "AS IS" BASIS,
146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 + implied, including, without limitation, any warranties or conditions
148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 + PARTICULAR PURPOSE. You are solely responsible for determining the
150 + appropriateness of using or redistributing the Work and assume any
151 + risks associated with Your exercise of permissions under this License.
152 +
153 + 8. Limitation of Liability. In no event and under no legal theory,
154 + whether in tort (including negligence), contract, or otherwise,
155 + unless required by applicable law (such as deliberate and grossly
156 + negligent acts) or agreed to in writing, shall any Contributor be
157 + liable to You for damages, including any direct, indirect, special,
158 + incidental, or consequential damages of any character arising as a
159 + result of this License or out of the use or inability to use the
160 + Work (including but not limited to damages for loss of goodwill,
161 + work stoppage, computer failure or malfunction, or any and all
162 + other commercial damages or losses), even if such Contributor
163 + has been advised of the possibility of such damages.
164 +
165 + 9. Accepting Warranty or Additional Liability. While redistributing
166 + the Work or Derivative Works thereof, You may choose to offer,
167 + and charge a fee for, acceptance of support, warranty, indemnity,
168 + or other liability obligations and/or rights consistent with this
169 + License. However, in accepting such obligations, You may act only
170 + on Your own behalf and on Your sole responsibility, not on behalf
171 + of any other Contributor, and only if You agree to indemnify,
172 + defend, and hold each Contributor harmless for any liability
173 + incurred by, or claims asserted against, such Contributor by reason
174 + of your accepting any such warranty or additional liability.
175 +
176 + END OF TERMS AND CONDITIONS
177 +
178 + APPENDIX: How to apply the Apache License to your work.
179 +
180 + To apply the Apache License to your work, attach the following
181 + boilerplate notice, with the fields enclosed by brackets "[]"
182 + replaced with your own identifying information. (Don't include
183 + the brackets!) The text should be enclosed in the appropriate
184 + comment syntax for the file format. We also recommend that a
185 + file or class name and description of purpose be included on the
186 + same "printed page" as the copyright notice for easier
187 + identification within third-party archives.
188 +
189 + Copyright [yyyy] [name of copyright owner]
190 +
191 + Licensed under the Apache License, Version 2.0 (the "License");
192 + you may not use this file except in compliance with the License.
193 + You may obtain a copy of the License at
194 +
195 + http://www.apache.org/licenses/LICENSE-2.0
196 +
197 + Unless required by applicable law or agreed to in writing, software
198 + distributed under the License is distributed on an "AS IS" BASIS,
199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 + See the License for the specific language governing permissions and
201 + limitations under the License.
LICENSE-MIT new
+19
@@ -0,0 +1,19 @@
1 +The MIT License (MIT)
2 +
3 +Permission is hereby granted, free of charge, to any person obtaining a copy
4 +of this software and associated documentation files (the "Software"), to deal
5 +in the Software without restriction, including without limitation the rights
6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 +copies of the Software, and to permit persons to whom the Software is
8 +furnished to do so, subject to the following conditions:
9 +
10 +The above copyright notice and this permission notice shall be included in
11 +all copies or substantial portions of the Software.
12 +
13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 +THE SOFTWARE.
README.md new
+28
@@ -0,0 +1,28 @@
1 +# js-libp2p-inspector-monorepo
2 +
3 +[![codecov](https://img.shields.io/codecov/c/github/ipshipyard/js-libp2p-inspector.svg?style=flat-square)](https://codecov.io/gh/ipshipyard/js-libp2p-inspector)
4 +[![CI](https://img.shields.io/github/actions/workflow/status/ipshipyard/js-libp2p-inspector/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipshipyard/js-libp2p-inspector/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
5 +
6 +> Inspect a running libp2p node
7 +
8 +# Packages
9 +
10 +- [`packages/libp2p-devtools`](https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-devtools) Browser DevTools plugin to inspect a libp2p node running on the current page
11 +- [`packages/libp2p-inspector`](https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-inspector) Inspect a running libp2p node
12 +- [`packages/libp2p-inspector-metrics`](https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-inspector-metrics) Collect libp2p metrics and send them to browser DevTools
13 +- [`packages/libp2p-inspector-ui`](https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-inspector-ui) The user interface for inspecting a libp2p node
14 +
15 +# API Docs
16 +
17 +- <https://ipshipyard.github.io/js-libp2p-inspector>
18 +
19 +# License
20 +
21 +Licensed under either of
22 +
23 +- Apache 2.0, ([LICENSE-APACHE](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
24 +- MIT ([LICENSE-MIT](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
25 +
26 +# Contribution
27 +
28 +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
package.json new
+47
@@ -0,0 +1,47 @@
1 +{
2 + "name": "js-libp2p-inspector-monorepo",
3 + "version": "1.0.0",
4 + "description": "Inspect a running libp2p node",
5 + "license": "Apache-2.0 OR MIT",
6 + "homepage": "https://github.com/ipshipyard/js-libp2p-inspector#readme",
7 + "repository": {
8 + "type": "git",
9 + "url": "git+https://github.com/ipshipyard/js-libp2p-inspector.git"
10 + },
11 + "bugs": {
12 + "url": "https://github.com/ipshipyard/js-libp2p-inspector/issues"
13 + },
14 + "private": true,
15 + "scripts": {
16 + "reset": "aegir run clean && aegir clean interop/node_modules packages/*/node_modules node_modules package-lock.json packages/*/package-lock.json interop/*/package-lock.json",
17 + "test": "aegir run test",
18 + "test:node": "aegir run test:node",
19 + "test:chrome": "aegir run test:chrome",
20 + "test:chrome-webworker": "aegir run test:chrome-webworker",
21 + "test:firefox": "aegir run test:firefox",
22 + "test:firefox-webworker": "aegir run test:firefox-webworker",
23 + "test:electron-main": "aegir run test:electron-main",
24 + "test:webkit": "aegir run test:webkit",
25 + "test:cli": "aegir run test:cli",
26 + "test:interop": "aegir run test:interop",
27 + "coverage": "aegir run coverage",
28 + "build": "aegir run build",
29 + "generate": "aegir run generate",
30 + "clean": "aegir run clean",
31 + "lint": "aegir run lint",
32 + "dep-check": "aegir run dep-check",
33 + "doc-check": "aegir run doc-check",
34 + "spell-check": "aegir spell-check",
35 + "release": "run-s build docs:no-publish npm:release docs",
36 + "npm:release": "aegir run release --concurrency 1",
37 + "docs": "aegir docs",
38 + "docs:no-publish": "aegir docs --publish false"
39 + },
40 + "devDependencies": {
41 + "aegir": "^47.0.12",
42 + "npm-run-all": "^4.1.5"
43 + },
44 + "workspaces": [
45 + "packages/*"
46 + ]
47 +}
packages/libp2p-devtools/.aegir.js new
+12
@@ -0,0 +1,12 @@
1 +
2 +/** @type {import('aegir/types').PartialOptions} */
3 +export default {
4 + build: {
5 + bundlesizeMax: '18kB'
6 + },
7 + dependencyCheck: {
8 + ignore: [
9 + 'esbuild'
10 + ]
11 + }
12 +}
packages/libp2p-devtools/.gitignore new
+3
@@ -0,0 +1,3 @@
1 +dist
2 +node_modules
3 +.vscode
packages/libp2p-devtools/CODE_OF_CONDUCT.md new
+3
@@ -0,0 +1,3 @@
1 +# Contributor Code of Conduct
2 +
3 +This project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
packages/libp2p-devtools/LICENSE-APACHE new
+201
@@ -0,0 +1,201 @@
1 +Apache License
2 + Version 2.0, January 2004
3 + http://www.apache.org/licenses/
4 +
5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 +
7 + 1. Definitions.
8 +
9 + "License" shall mean the terms and conditions for use, reproduction,
10 + and distribution as defined by Sections 1 through 9 of this document.
11 +
12 + "Licensor" shall mean the copyright owner or entity authorized by
13 + the copyright owner that is granting the License.
14 +
15 + "Legal Entity" shall mean the union of the acting entity and all
16 + other entities that control, are controlled by, or are under common
17 + control with that entity. For the purposes of this definition,
18 + "control" means (i) the power, direct or indirect, to cause the
19 + direction or management of such entity, whether by contract or
20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 + outstanding shares, or (iii) beneficial ownership of such entity.
22 +
23 + "You" (or "Your") shall mean an individual or Legal Entity
24 + exercising permissions granted by this License.
25 +
26 + "Source" form shall mean the preferred form for making modifications,
27 + including but not limited to software source code, documentation
28 + source, and configuration files.
29 +
30 + "Object" form shall mean any form resulting from mechanical
31 + transformation or translation of a Source form, including but
32 + not limited to compiled object code, generated documentation,
33 + and conversions to other media types.
34 +
35 + "Work" shall mean the work of authorship, whether in Source or
36 + Object form, made available under the License, as indicated by a
37 + copyright notice that is included in or attached to the work
38 + (an example is provided in the Appendix below).
39 +
40 + "Derivative Works" shall mean any work, whether in Source or Object
41 + form, that is based on (or derived from) the Work and for which the
42 + editorial revisions, annotations, elaborations, or other modifications
43 + represent, as a whole, an original work of authorship. For the purposes
44 + of this License, Derivative Works shall not include works that remain
45 + separable from, or merely link (or bind by name) to the interfaces of,
46 + the Work and Derivative Works thereof.
47 +
48 + "Contribution" shall mean any work of authorship, including
49 + the original version of the Work and any modifications or additions
50 + to that Work or Derivative Works thereof, that is intentionally
51 + submitted to Licensor for inclusion in the Work by the copyright owner
52 + or by an individual or Legal Entity authorized to submit on behalf of
53 + the copyright owner. For the purposes of this definition, "submitted"
54 + means any form of electronic, verbal, or written communication sent
55 + to the Licensor or its representatives, including but not limited to
56 + communication on electronic mailing lists, source code control systems,
57 + and issue tracking systems that are managed by, or on behalf of, the
58 + Licensor for the purpose of discussing and improving the Work, but
59 + excluding communication that is conspicuously marked or otherwise
60 + designated in writing by the copyright owner as "Not a Contribution."
61 +
62 + "Contributor" shall mean Licensor and any individual or Legal Entity
63 + on behalf of whom a Contribution has been received by Licensor and
64 + subsequently incorporated within the Work.
65 +
66 + 2. Grant of Copyright License. Subject to the terms and conditions of
67 + this License, each Contributor hereby grants to You a perpetual,
68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 + copyright license to reproduce, prepare Derivative Works of,
70 + publicly display, publicly perform, sublicense, and distribute the
71 + Work and such Derivative Works in Source or Object form.
72 +
73 + 3. Grant of Patent License. Subject to the terms and conditions of
74 + this License, each Contributor hereby grants to You a perpetual,
75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 + (except as stated in this section) patent license to make, have made,
77 + use, offer to sell, sell, import, and otherwise transfer the Work,
78 + where such license applies only to those patent claims licensable
79 + by such Contributor that are necessarily infringed by their
80 + Contribution(s) alone or by combination of their Contribution(s)
81 + with the Work to which such Contribution(s) was submitted. If You
82 + institute patent litigation against any entity (including a
83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
84 + or a Contribution incorporated within the Work constitutes direct
85 + or contributory patent infringement, then any patent licenses
86 + granted to You under this License for that Work shall terminate
87 + as of the date such litigation is filed.
88 +
89 + 4. Redistribution. You may reproduce and distribute copies of the
90 + Work or Derivative Works thereof in any medium, with or without
91 + modifications, and in Source or Object form, provided that You
92 + meet the following conditions:
93 +
94 + (a) You must give any other recipients of the Work or
95 + Derivative Works a copy of this License; and
96 +
97 + (b) You must cause any modified files to carry prominent notices
98 + stating that You changed the files; and
99 +
100 + (c) You must retain, in the Source form of any Derivative Works
101 + that You distribute, all copyright, patent, trademark, and
102 + attribution notices from the Source form of the Work,
103 + excluding those notices that do not pertain to any part of
104 + the Derivative Works; and
105 +
106 + (d) If the Work includes a "NOTICE" text file as part of its
107 + distribution, then any Derivative Works that You distribute must
108 + include a readable copy of the attribution notices contained
109 + within such NOTICE file, excluding those notices that do not
110 + pertain to any part of the Derivative Works, in at least one
111 + of the following places: within a NOTICE text file distributed
112 + as part of the Derivative Works; within the Source form or
113 + documentation, if provided along with the Derivative Works; or,
114 + within a display generated by the Derivative Works, if and
115 + wherever such third-party notices normally appear. The contents
116 + of the NOTICE file are for informational purposes only and
117 + do not modify the License. You may add Your own attribution
118 + notices within Derivative Works that You distribute, alongside
119 + or as an addendum to the NOTICE text from the Work, provided
120 + that such additional attribution notices cannot be construed
121 + as modifying the License.
122 +
123 + You may add Your own copyright statement to Your modifications and
124 + may provide additional or different license terms and conditions
125 + for use, reproduction, or distribution of Your modifications, or
126 + for any such Derivative Works as a whole, provided Your use,
127 + reproduction, and distribution of the Work otherwise complies with
128 + the conditions stated in this License.
129 +
130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
131 + any Contribution intentionally submitted for inclusion in the Work
132 + by You to the Licensor shall be under the terms and conditions of
133 + this License, without any additional terms or conditions.
134 + Notwithstanding the above, nothing herein shall supersede or modify
135 + the terms of any separate license agreement you may have executed
136 + with Licensor regarding such Contributions.
137 +
138 + 6. Trademarks. This License does not grant permission to use the trade
139 + names, trademarks, service marks, or product names of the Licensor,
140 + except as required for reasonable and customary use in describing the
141 + origin of the Work and reproducing the content of the NOTICE file.
142 +
143 + 7. Disclaimer of Warranty. Unless required by applicable law or
144 + agreed to in writing, Licensor provides the Work (and each
145 + Contributor provides its Contributions) on an "AS IS" BASIS,
146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 + implied, including, without limitation, any warranties or conditions
148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 + PARTICULAR PURPOSE. You are solely responsible for determining the
150 + appropriateness of using or redistributing the Work and assume any
151 + risks associated with Your exercise of permissions under this License.
152 +
153 + 8. Limitation of Liability. In no event and under no legal theory,
154 + whether in tort (including negligence), contract, or otherwise,
155 + unless required by applicable law (such as deliberate and grossly
156 + negligent acts) or agreed to in writing, shall any Contributor be
157 + liable to You for damages, including any direct, indirect, special,
158 + incidental, or consequential damages of any character arising as a
159 + result of this License or out of the use or inability to use the
160 + Work (including but not limited to damages for loss of goodwill,
161 + work stoppage, computer failure or malfunction, or any and all
162 + other commercial damages or losses), even if such Contributor
163 + has been advised of the possibility of such damages.
164 +
165 + 9. Accepting Warranty or Additional Liability. While redistributing
166 + the Work or Derivative Works thereof, You may choose to offer,
167 + and charge a fee for, acceptance of support, warranty, indemnity,
168 + or other liability obligations and/or rights consistent with this
169 + License. However, in accepting such obligations, You may act only
170 + on Your own behalf and on Your sole responsibility, not on behalf
171 + of any other Contributor, and only if You agree to indemnify,
172 + defend, and hold each Contributor harmless for any liability
173 + incurred by, or claims asserted against, such Contributor by reason
174 + of your accepting any such warranty or additional liability.
175 +
176 + END OF TERMS AND CONDITIONS
177 +
178 + APPENDIX: How to apply the Apache License to your work.
179 +
180 + To apply the Apache License to your work, attach the following
181 + boilerplate notice, with the fields enclosed by brackets "[]"
182 + replaced with your own identifying information. (Don't include
183 + the brackets!) The text should be enclosed in the appropriate
184 + comment syntax for the file format. We also recommend that a
185 + file or class name and description of purpose be included on the
186 + same "printed page" as the copyright notice for easier
187 + identification within third-party archives.
188 +
189 + Copyright [yyyy] [name of copyright owner]
190 +
191 + Licensed under the Apache License, Version 2.0 (the "License");
192 + you may not use this file except in compliance with the License.
193 + You may obtain a copy of the License at
194 +
195 + http://www.apache.org/licenses/LICENSE-2.0
196 +
197 + Unless required by applicable law or agreed to in writing, software
198 + distributed under the License is distributed on an "AS IS" BASIS,
199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 + See the License for the specific language governing permissions and
201 + limitations under the License.
packages/libp2p-devtools/LICENSE-MIT new
+19
@@ -0,0 +1,19 @@
1 +The MIT License (MIT)
2 +
3 +Permission is hereby granted, free of charge, to any person obtaining a copy
4 +of this software and associated documentation files (the "Software"), to deal
5 +in the Software without restriction, including without limitation the rights
6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 +copies of the Software, and to permit persons to whom the Software is
8 +furnished to do so, subject to the following conditions:
9 +
10 +The above copyright notice and this permission notice shall be included in
11 +all copies or substantial portions of the Software.
12 +
13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 +THE SOFTWARE.
packages/libp2p-devtools/README.md new
+119
@@ -0,0 +1,119 @@
1 +# @ipshipyard/libp2p-devtools
2 +
3 +[![codecov](https://img.shields.io/codecov/c/github/ipshipyard/js-libp2p-inspector.svg?style=flat-square)](https://codecov.io/gh/ipshipyard/js-libp2p-inspector)
4 +[![CI](https://img.shields.io/github/actions/workflow/status/ipshipyard/js-libp2p-inspector/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipshipyard/js-libp2p-inspector/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
5 +
6 +> Browser DevTools plugin to inspect a libp2p node running on the current page
7 +
8 +# About
9 +
10 +<!--
11 +
12 +!IMPORTANT!
13 +
14 +Everything in this README between "# About" and "# Install" is automatically
15 +generated and will be overwritten the next time the doc generator is run.
16 +
17 +To make changes to this section, please update the @packageDocumentation section
18 +of src/index.js or src/index.ts
19 +
20 +To experiment with formatting, please run "npm run docs" from the root of this
21 +repo and examine the changes made.
22 +
23 +-->
24 +
25 +A [DevTools](https://developer.chrome.com/docs/devtools) plugin that adds a "libp2p" tab to your developer tools that contacts a libp2p node running on the current page.
26 +
27 +Works with [@ipshipyard/libp2p-inspector-metrics](https://www.npmjs.com/package/@ipshipyard/libp2p-inspector-metrics) which supplies metrics and allows us to interact with the running node.
28 +
29 +<img width="840" alt="image" src="https://github.com/ipfs-shipyard/js-libp2p-devtools/assets/665810/f8f6a7c8-377f-41d6-948f-95d8469f58b8">
30 +
31 +## Installation instructions
32 +
33 +### 1. Browser installation
34 +
35 +Until this plugin is published on the relevant browser plugin stores, please run this locally.
36 +
37 +1. Clone this repo
38 +2. Run `npm i && npm run build`
39 +3. Install as an unpacked/temporary add on for your browser:
40 +
41 +- Chrome: [How to load an unpacked extension](https://knowledge.workspace.google.com/kb/load-unpacked-extensions-000005962)
42 +- Firefox: [How to install temporary add-ons](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing)
43 +
44 +### 2. App installation
45 +
46 +Configure `@ipshipyard/libp2p-inspector-metrics` as your metrics implementation:
47 +
48 +```ts
49 +import { createLibp2p } from 'libp2p'
50 +import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
51 +
52 +const node = await createLibp2p({
53 + metrics: inspectorMetrics(),
54 + //... other options her
55 +})
56 +```
57 +
58 +### 3. Permissions
59 +
60 +With the move to Manifest v3, users must now [opt in](https://blog.mozilla.org/addons/2022/11/17/unified-extensions-button-and-how-to-handle-permissions-in-manifest-v3/) to running content scripts on a page.
61 +
62 +#### Chrome
63 +
64 +Please click the extensions button, then grant the extension permission to run:
65 +
66 +<img src="public/img/grant-permissions-light-chrome.png" width="700"/>
67 +
68 +#### Firefox
69 +
70 +Please click the extensions button, then grant the extension permission to run:
71 +
72 +<img src="public/img/grant-permissions-light-firefox.png" width="700"/>
73 +
74 +### 4. Go!
75 +
76 +Browse to your webapp and open the DevTools, you should see a "libp2p" tab towards the right hand side of the toolbar. Click it to see stats about the running node.
77 +
78 +## What's next?
79 +
80 +1. Tests
81 +
82 +- There aren't a lot of tests here yet
83 +
84 +2. Better UI
85 +
86 +- It's quite rough
87 +
88 +3. Graphs
89 +
90 +- We don't do much with the collected metrics yet. It would be nice to use Chart.js or similar to show some useful graphs
91 +- Bonus points for letting the user define their own graphs a la Grafana/Prometheus
92 +- More bonus points for being able to export/import graph configs
93 +
94 +4. Dynamic panels
95 +
96 +- We should be able to inspect the libp2p node's configured services (or protocols?) and, for example, only show a DHT tab if the DHT is configured
97 +
98 +5. Light theme support
99 +
100 +- There are partial overrides for light theme font/background/border colors but we need better detection of when it's enabled
101 +
102 +6. ??? more features here
103 +
104 +# Install
105 +
106 +```console
107 +$ npm i @ipshipyard/libp2p-devtools
108 +```
109 +
110 +# License
111 +
112 +Licensed under either of
113 +
114 +- Apache 2.0, ([LICENSE-APACHE](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-devtools/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
115 +- MIT ([LICENSE-MIT](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-devtools/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
116 +
117 +# Contribution
118 +
119 +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
packages/libp2p-devtools/devtools.html new
+8
@@ -0,0 +1,8 @@
1 +<!doctype html>
2 +<html>
3 + <head>
4 + <script type="text/javascript" src="devtools.js"></script>
5 + </head>
6 + <body>
7 + </body>
8 +</html>
packages/libp2p-devtools/devtools.js new
+3
@@ -0,0 +1,3 @@
1 +/* global chrome */
2 +
3 +chrome.devtools.panels.create('libp2p', '/public/img/libp2p-monochrome.svg', '/public/panel.html')
packages/libp2p-devtools/esbuild.js new
+40
@@ -0,0 +1,40 @@
1 +import * as esbuild from 'esbuild'
2 +
3 +await esbuild.build({
4 + entryPoints: [
5 + 'src/app.tsx'
6 + ],
7 + bundle: true,
8 + outfile: 'dist/app.js',
9 + format: 'esm',
10 + loader: {
11 + '.png': 'file',
12 + '.svg': 'dataurl'
13 + }
14 +})
15 +
16 +await esbuild.build({
17 + entryPoints: [
18 + 'src/service-worker.ts'
19 + ],
20 + bundle: true,
21 + outfile: 'dist/service-worker.js',
22 + format: 'esm',
23 + loader: {
24 + '.png': 'file',
25 + '.svg': 'dataurl'
26 + }
27 +})
28 +
29 +await esbuild.build({
30 + entryPoints: [
31 + 'src/content-script.ts'
32 + ],
33 + bundle: true,
34 + outfile: 'dist/content-script.js',
35 + format: 'esm',
36 + loader: {
37 + '.png': 'file',
38 + '.svg': 'dataurl'
39 + }
40 +})
packages/libp2p-devtools/manifest.json new
+29
@@ -0,0 +1,29 @@
1 +{
2 + "manifest_version": 3,
3 + "name": "libp2p DevTools",
4 + "version": "0.0.1",
5 + "description": "Inspect a libp2p node running on the current page",
6 + "author": "IP Shipyard",
7 + "icons": {
8 + "16": "/public/img/libp2p-16.png",
9 + "48": "/public/img/libp2p-48.png",
10 + "128": "/public/img/libp2p-128.png"
11 + },
12 + "permissions": [
13 + "clipboardWrite",
14 + "activeTab",
15 + "scripting"
16 + ],
17 + "devtools_page": "devtools.html",
18 + // https://stackoverflow.com/questions/75043889/manifest-v3-background-scripts-service-worker-on-firefox
19 + "background": {
20 + // for chrome
21 + "service_worker": "/dist/service-worker.js",
22 + // for firefox
23 + "scripts": ["/dist/service-worker.js"]
24 + },
25 + "content_scripts": [{
26 + "matches": ["<all_urls>"],
27 + "js": ["/dist/content-script.js"]
28 + }]
29 +}
packages/libp2p-devtools/package.json new
+161
@@ -0,0 +1,161 @@
1 +{
2 + "name": "@ipshipyard/libp2p-devtools",
3 + "version": "0.0.0",
4 + "description": "Browser DevTools plugin to inspect a libp2p node running on the current page",
5 + "author": "info@ipshipyard.com",
6 + "license": "Apache-2.0 OR MIT",
7 + "homepage": "https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-devtools#readme",
8 + "repository": {
9 + "type": "git",
10 + "url": "git+https://github.com/ipshipyard/js-libp2p-inspector.git"
11 + },
12 + "bugs": {
13 + "url": "https://github.com/ipshipyard/js-libp2p-inspector/issues"
14 + },
15 + "publishConfig": {
16 + "access": "public",
17 + "provenance": true
18 + },
19 + "type": "module",
20 + "types": "./dist/src/index.d.ts",
21 + "files": [
22 + "dist",
23 + "public",
24 + "src",
25 + "devtools.html",
26 + "devtools.js",
27 + "manifest.json",
28 + "!dist/test",
29 + "!**/*.tsbuildinfo"
30 + ],
31 + "exports": {
32 + ".": {
33 + "types": "./dist/src/index.d.ts",
34 + "import": "./dist/src/index.js"
35 + }
36 + },
37 + "release": {
38 + "branches": [
39 + "main"
40 + ],
41 + "plugins": [
42 + [
43 + "@semantic-release/commit-analyzer",
44 + {
45 + "preset": "conventionalcommits",
46 + "releaseRules": [
47 + {
48 + "breaking": true,
49 + "release": "major"
50 + },
51 + {
52 + "revert": true,
53 + "release": "patch"
54 + },
55 + {
56 + "type": "feat",
57 + "release": "minor"
58 + },
59 + {
60 + "type": "fix",
61 + "release": "patch"
62 + },
63 + {
64 + "type": "docs",
65 + "release": "patch"
66 + },
67 + {
68 + "type": "test",
69 + "release": "patch"
70 + },
71 + {
72 + "type": "deps",
73 + "release": "patch"
74 + },
75 + {
76 + "scope": "no-release",
77 + "release": false
78 + }
79 + ]
80 + }
81 + ],
82 + [
83 + "@semantic-release/release-notes-generator",
84 + {
85 + "preset": "conventionalcommits",
86 + "presetConfig": {
87 + "types": [
88 + {
89 + "type": "feat",
90 + "section": "Features"
91 + },
92 + {
93 + "type": "fix",
94 + "section": "Bug Fixes"
95 + },
96 + {
97 + "type": "chore",
98 + "section": "Trivial Changes"
99 + },
100 + {
101 + "type": "docs",
102 + "section": "Documentation"
103 + },
104 + {
105 + "type": "deps",
106 + "section": "Dependencies"
107 + },
108 + {
109 + "type": "test",
110 + "section": "Tests"
111 + }
112 + ]
113 + }
114 + }
115 + ],
116 + "@semantic-release/changelog",
117 + "@semantic-release/npm",
118 + "@semantic-release/github",
119 + [
120 + "@semantic-release/git",
121 + {
122 + "assets": [
123 + "CHANGELOG.md",
124 + "package.json"
125 + ]
126 + }
127 + ]
128 + ]
129 + },
130 + "scripts": {
131 + "clean": "aegir clean",
132 + "lint": "aegir lint",
133 + "build": "tsc && node esbuild.js",
134 + "test": "echo \"Error: no test specified\" && exit 1",
135 + "dep-check": "aegir dep-check",
136 + "start:firefox": "web-ext run --source-dir . --verbose --devtools",
137 + "release": "aegir release"
138 + },
139 + "dependencies": {
140 + "@ipshipyard/libp2p-inspector-metrics": "~0.0.0",
141 + "@ipshipyard/libp2p-inspector-ui": "~0.0.0",
142 + "@libp2p/interface": "^2.10.2",
143 + "it-pipe": "^3.0.1",
144 + "it-pushable": "^3.2.3",
145 + "it-rpc": "^1.1.0",
146 + "it-stream-types": "^2.0.2",
147 + "multiformats": "^13.3.6",
148 + "react": "^19.1.0",
149 + "react-dom": "^19.1.0",
150 + "react-syntax-highlighter": "^15.6.1",
151 + "uint8arrays": "^5.1.0"
152 + },
153 + "devDependencies": {
154 + "@types/chrome": "^0.0.323",
155 + "@types/react": "^19.1.5",
156 + "@types/react-dom": "^19.1.5",
157 + "@types/react-syntax-highlighter": "^15.5.13",
158 + "aegir": "^47.0.13",
159 + "web-ext": "^8.7.0"
160 + }
161 +}
packages/libp2p-devtools/public/img/grant-permissions-dark-chrome.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/grant-permissions-dark-chrome.png differ
packages/libp2p-devtools/public/img/grant-permissions-dark-firefox.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/grant-permissions-dark-firefox.png differ
packages/libp2p-devtools/public/img/grant-permissions-light-chrome.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/grant-permissions-light-chrome.png differ
packages/libp2p-devtools/public/img/grant-permissions-light-firefox.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/grant-permissions-light-firefox.png differ
packages/libp2p-devtools/public/img/helia.svg new
+17
@@ -0,0 +1,17 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="807" height="930" viewBox="0, 0, 807, 930">
4 + <g id="Layer_1">
5 + <g>
6 + <path d="M807,697.368 L807,232.63 L403.466,0.261 L0,232.63 L0,697.368 L403.466,929.739 L806.966,697.368 L807,697.368 z" fill="#073A53"/>
7 + <path d="M250.599,748.783 L217.869,790.127 L232.201,748.783 C232.201,748.783 250.582,749.266 250.582,748.783 L250.617,748.783 L250.599,748.783 z" fill="#F7DF1E"/>
8 + <path d="M190.272,659.205 C180.177,664.287 171.098,676.604 168.756,687.594 C166.413,698.585 162.744,719.98 147.24,725.097 C147.24,725.097 156.594,729.559 167.808,721.91 C179.023,714.279 187.894,698.189 185.862,684.718 C183.071,666.371 190.926,658.912 190.272,659.239 L190.272,659.205 z" fill="#F7DF1E"/>
9 + <path d="M470.049,583.201 C470.049,583.201 475.648,592.038 498.714,591.832 C521.764,591.591 526.69,582.34 516.372,570.488 C506.018,558.636 493.546,555.001 477.405,559.291 C461.246,563.614 445.536,575.88 455.028,594.381 C464.52,612.883 505.002,669.11 522.005,685.045 C522.005,685.045 537.112,661.134 539.731,642.116 C542.711,620.91 543.969,596.345 531.462,600.221 C518.99,604.063 481.488,614.743 470.049,583.184 L470.049,583.218 L470.049,583.201 z" fill="#F7DF1E"/>
10 + <path d="M583.107,545.509 C583.107,545.509 593.616,570.505 600.713,582.047 C607.81,593.623 611.91,597.034 632.531,604.304 C653.151,611.556 733.444,639.963 743.281,641.996 C743.281,641.996 662.712,583.856 654.96,577.982 C647.19,572.073 635.011,571.556 625.209,572.314 C609.878,573.434 595.907,565.286 583.073,545.544 L583.107,545.544 L583.107,545.509 z" fill="#F7DF1E"/>
11 + <path d="M613.822,636.845 C613.822,636.845 608.723,654.399 617.578,671.78 C626.467,689.162 636.303,691.402 646.105,692.745 C655.907,694.089 672.341,699.05 676.872,712.608 C676.872,712.608 682.35,695.019 667.07,682.805 C651.79,670.626 631.721,670.73 623.366,660.394 C613.667,648.387 614.684,642.685 613.84,636.81 L613.84,636.845 L613.822,636.845 z" fill="#F7DF1E"/>
12 + <path d="M560.144,690.213 L603.745,783.237 L565.881,740.928 C565.881,740.928 566.088,712.935 557.233,701.376 L560.11,690.213 L560.144,690.213 z" fill="#F7DF1E"/>
13 + <path d="M515.889,772.901 L510.756,810.799 L498.663,790.575 L515.889,772.901 z" fill="#F7DF1E"/>
14 + <path d="M721.954,525.957 C700.404,524.424 699.077,501.323 676.562,496.379 C654.047,491.435 632.496,499.721 608.827,492.365 L604.675,485.922 L608.706,476.758 L781.834,449.833 L608.672,422.89 C608.362,422.167 608.276,421.96 607.983,421.219 C613.65,413.226 623.16,403.304 634.667,404.992 C653.513,407.748 669.051,414.604 681.162,404.82 C693.272,395.035 694.323,377.584 716.614,383.269 C740.938,389.453 755.787,386.646 763.384,365.526 C763.384,365.526 744.986,374.208 727.897,365.887 C710.809,357.567 698.853,351.986 683.16,360.496 C667.466,369.005 659.232,367.386 652.703,365.216 C646.226,363.062 626.536,355.155 595.735,369.229 L731.015,260.288 L568.276,323.269 C568.362,313.432 570.498,299.289 581.23,294.087 C598.37,285.766 615.063,282.7 619.576,267.799 C624.09,252.898 615.149,237.894 636.769,230.038 C660.369,221.477 671.067,210.796 665.468,189.056 C665.468,189.056 655.149,206.576 636.338,209.315 C617.526,212.054 604.503,214.155 596.32,230.021 C588.138,245.887 580.403,249.177 573.805,251.055 C567.294,252.898 546.897,257.377 529.447,285.629 C529.102,285.456 529.24,285.525 528.895,285.353 L592.289,121.545 L482.16,258.324 C482.022,258.221 482.074,258.256 481.936,258.152 C497.733,228.936 491.445,209.022 489.791,202.441 C488.12,195.775 487.121,187.437 496.785,172.433 C506.449,157.428 501.781,145.094 494.787,127.419 C487.793,109.745 497.819,92.053 497.819,92.053 C476.182,98.031 472.272,112.639 476.595,137.359 C480.575,160.029 463.107,159.754 452.427,171.089 C441.763,182.424 447.431,198.428 448.757,217.429 C449.601,229.47 438.128,238.445 429.601,243.389 L402.745,70.727 L375.751,244.268 C375.182,244.233 375.406,244.25 374.838,244.199 C373.787,211.072 358.352,197.032 353.632,192.192 C348.843,187.265 343.778,180.564 344.605,162.734 C345.432,144.904 335.182,136.584 320.247,124.835 C305.311,113.087 305.105,92.742 305.105,92.742 C289.411,108.797 293.356,123.388 309.532,142.579 C324.364,160.167 309.118,168.729 305.604,183.888 C302.073,199.048 315.027,210.038 325.725,225.801 C332.357,235.568 327.327,248.712 322.538,257.291 L213.218,121.528 L276.612,285.336 C276.267,285.508 276.405,285.439 276.06,285.611 C258.61,257.36 238.214,252.898 231.702,251.038 C225.087,249.16 217.369,245.87 209.187,230.004 C201.004,214.138 187.981,212.037 169.169,209.298 C150.358,206.559 140.039,189.039 140.039,189.039 C134.44,210.779 145.138,221.46 168.739,230.021 C190.358,237.876 181.417,252.881 185.931,267.782 C190.444,282.683 207.137,285.749 224.277,294.07 C235.009,299.272 237.146,313.415 237.232,323.252 L74.492,260.271 L209.772,369.212 C178.971,355.138 159.281,363.045 152.804,365.198 C146.275,367.369 138.041,368.988 122.347,360.478 C106.654,351.968 94.716,357.55 77.61,365.87 C60.521,374.191 42.123,365.508 42.123,365.508 C49.72,386.628 64.569,389.436 88.893,383.252 C111.185,377.584 112.235,395.018 124.346,404.802 C136.456,414.587 151.977,407.731 170.84,404.975 C182.348,403.286 191.857,413.209 197.524,421.202 C197.214,421.926 197.128,422.132 196.835,422.873 L23.673,449.816 L196.801,476.741 L183.777,484.751 C178.799,487.817 175.009,492.486 173.045,497.981 L170.702,504.544 C168.187,511.59 162.951,517.378 156.112,520.376 C146.654,524.51 137.317,524.458 126.275,522.322 C108.256,518.808 93.493,524.338 81.469,532.452 C73.045,538.136 66.068,554.088 59.608,559.807 C53.148,565.527 37.885,560.927 37.885,560.927 C47.842,576.603 76.714,577.482 83.743,568.903 C96.456,553.365 116.421,550.161 133.286,555.57 C148.756,560.548 168.411,552.09 179.419,541.943 C189.858,532.365 189.014,520.169 203.691,511.521 C216.508,503.959 232.563,504.131 232.563,504.131 C232.012,511.556 228.997,524.889 219.54,534.657 C208.188,546.371 74.423,657.327 74.423,657.327 L220.487,587.37 C222.4,632.211 231.271,681.376 239.385,700.842 C242.882,709.197 247.688,717.018 253.528,724.925 C269.945,747.147 290.996,765.476 314.734,779.585 L314.906,779.481 C340.126,794.503 374.579,812.367 397.939,814.21 C414.459,810.834 417.009,800.928 417.009,786.027 C417.009,771.126 410.307,766.837 403.313,766.441 C396.302,766.027 391.617,761.462 387.207,755.157 C382.779,748.852 395.269,748.215 412.461,747.939 C429.67,747.629 433.133,739.36 437.043,729.266 C440.919,719.188 425.656,718.223 394.89,721.875 C368.877,724.942 360.678,720.721 358.576,717.173 C373.063,703.977 390.066,692.022 398.938,694.313 C413.839,698.258 416.716,689.817 413.081,687.267 C409.842,685.01 394.442,669.817 387.758,665.183 C387.052,664.666 379.902,660.066 380.178,659.257 C384.795,646.061 393.029,650.867 407.276,653.882 C421.522,656.879 434.407,640.428 431.513,634.864 C427.224,626.612 423.934,631.246 416.044,615.415 C407.913,581.961 415.803,526.543 450.687,516.396 C481.178,507.507 514.477,517.309 522.332,519.945 C523.107,520.221 523.865,519.532 523.727,518.756 L515.028,470.091 L508.327,439.428 C508.534,438.515 509.378,437.86 510.325,437.998 C546.398,442.942 570.567,487.817 582.401,504.682 C594.236,521.547 616.648,539.618 635.132,544.648 C653.633,549.73 661.144,541.444 673.651,541.788 C686.157,542.133 692.962,553.916 713.117,554.019 C733.238,554.123 740.628,544.648 752.583,544.476 C761.558,544.338 770.395,549.248 770.395,549.248 C766.037,537.585 743.47,527.508 721.902,525.974 L721.954,525.957 z M265.242,611.642 C269.256,622.667 250.979,629.541 243.364,620.721 C236.543,612.762 238.351,584.734 276.405,589.007 L276.439,589.007 C276.439,589.007 260.074,597.465 265.242,611.642 z M329.291,608.662 C291.806,613.296 282.073,588.696 282.073,588.696 L282.073,588.662 C307.034,597.361 326.19,599.963 328.757,581.995 C331.323,564.028 310.462,554.347 293.253,551.453 C276.732,548.696 255.767,542.564 255.767,534.071 C255.767,525.578 267.361,512.779 310.169,512.779 C352.977,512.779 364.243,536.999 366.965,548.3 C369.704,559.635 366.759,604.028 329.273,608.662 L329.291,608.662 z" fill="#F7DF1E"/>
15 + </g>
16 + </g>
17 +</svg>
packages/libp2p-devtools/public/img/icon-copy.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="512" height="512" viewBox="0, 0, 512, 512">
4 + <g id="Background">
5 + <rect x="0" y="0" width="512" height="512" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M240,0 L364.1,0 C376.8,0 389,5.1 398,14.1 L465.9,82 C474.9,91 480,103.2 480,115.9 L480,336 C480,362.5 458.5,384 432,384 L240,384 C213.5,384 192,362.5 192,336 L192,48 C192,21.5 213.5,0 240,0 z M80,128 L160,128 L160,192 L96,192 L96,448 L288,448 L288,416 L352,416 L352,464 C352,490.5 330.5,512 304,512 L80,512 C53.5,512 32,490.5 32,464 L32,176 C32,149.5 53.5,128 80,128 z" fill="#868686"/>
10 + </g>
11 +</svg>
packages/libp2p-devtools/public/img/icon-delete.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="512" height="512" viewBox="0, 0, 512, 512">
4 + <g id="Background">
5 + <rect x="0" y="0" width="512" height="512" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M217.198,3.484 C200.98,3.484 185.065,6.667 173.546,18.035 C162.027,29.403 158.389,45.545 158.389,62.294 L81.391,62.294 C70.706,62.294 61.99,71.009 61.99,81.694 L42.589,81.694 L42.589,120.496 L469.411,120.496 L469.411,81.694 L450.01,81.694 C450.01,71.009 441.294,62.294 430.609,62.294 L353.611,62.294 C353.611,45.545 349.973,29.403 338.454,18.035 C326.935,6.667 311.02,3.484 294.802,3.484 z M217.198,43.499 L294.802,43.499 C305.412,43.499 308.746,46 309.959,47.137 C311.171,48.273 313.597,51.456 313.597,62.294 L198.403,62.294 C198.403,51.456 200.829,48.273 202.041,47.137 C203.254,46 206.588,43.499 217.198,43.499 z M81.391,139.897 L81.391,450.313 C81.391,482.37 107.537,508.516 139.594,508.516 L372.406,508.516 C404.463,508.516 430.609,482.37 430.609,450.313 L430.609,139.897 z M158.995,198.1 L197.797,198.1 L197.797,430.912 L158.995,430.912 z M236.599,198.1 L275.401,198.1 L275.401,430.912 L236.599,430.912 z M314.203,198.1 L353.005,198.1 L353.005,430.912 L314.203,430.912 z" fill="#868686"/>
10 + </g>
11 +</svg>
packages/libp2p-devtools/public/img/icon-disclosure-triangle-closed.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="32" height="32" viewBox="0, 0, 32, 32">
4 + <g id="Background">
5 + <rect x="0" y="0" width="32" height="32" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <path d="M8.252,31.493 L8.252,0.501 L23.748,15.997 L8.252,31.493 z" fill="#6C6C6C"/>
9 + </g>
10 +</svg>
packages/libp2p-devtools/public/img/icon-disclosure-triangle-open.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="32" height="32" viewBox="0, 0, 32, 32">
4 + <g id="Background">
5 + <rect x="0" y="0" width="32" height="32" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <path d="M0.504,8.249 L31.496,8.249 L16,23.745 L0.504,8.249 z" fill="#6C6C6C"/>
9 + </g>
10 +</svg>
packages/libp2p-devtools/public/img/icon-spinner.svg new
+8
@@ -0,0 +1,8 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
4 + <path fill="#868686" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity="0.25"/>
5 + <path fill="#868686" d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z">
6 + <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
7 + </path>
8 +</svg>
packages/libp2p-devtools/public/img/image.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/image.png differ
packages/libp2p-devtools/public/img/libp2p-128.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/libp2p-128.png differ
packages/libp2p-devtools/public/img/libp2p-16.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/libp2p-16.png differ
packages/libp2p-devtools/public/img/libp2p-48.png
Binary files /dev/null and b/packages/libp2p-devtools/public/img/libp2p-48.png differ
packages/libp2p-devtools/public/img/libp2p-monochrome.svg new
+38
@@ -0,0 +1,38 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="159" height="159" viewBox="0, 0, 159, 159">
4 + <g id="Background">
5 + <rect x="0" y="0" width="159" height="159" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <g>
9 + <path d="M148.689,92.902 L125.815,105.916 L125.62,106.013 L125.62,132.397 L148.689,119.286 L148.689,92.902 z" fill="#6C6C6C"/>
10 + <path d="M125.62,106.013 L102.747,119.028 L102.552,119.125 L102.552,145.509 L125.62,132.397 L125.62,106.013 z" fill="#969797"/>
11 + <path d="M33.38,132.075 L33.38,105.691 L10.311,92.451 L10.311,118.803 L33.38,132.075 z" fill="#C4C4C4"/>
12 + <path d="M56.448,145.348 L56.448,118.964 L33.38,105.691 L33.38,132.075 L56.448,145.348 z" fill="#C4C4C4"/>
13 + <path d="M79.516,158.62 L79.516,132.236 L56.448,118.964 L56.448,145.348 L79.516,158.62 z" fill="#9C9C9C"/>
14 + <path d="M102.552,119.124 L79.712,132.139 L79.516,132.236 L79.516,158.62 L102.552,145.509 L102.552,119.124 z" fill="#969797"/>
15 + <path d="M148.689,66.582 L125.615,79.666 L125.588,106.045 L125.62,106.045 L148.655,92.966 L148.689,66.582 z" fill="#7D7D7D"/>
16 + <path d="M125.62,79.661 L102.747,92.676 L102.552,92.773 L102.552,119.157 L125.62,106.046 L125.62,79.661 L125.62,79.694 L125.62,79.661 z" fill="#7D7D7D"/>
17 + <path d="M33.38,105.691 L33.38,79.339 L10.311,66.034 L10.311,92.418 L33.38,105.691 z" fill="#C4C4C4"/>
18 + <path d="M56.415,118.963 L56.448,92.612 L33.38,79.307 L33.38,105.691 L56.415,118.963 z" fill="#ACACAC"/>
19 + <path d="M79.483,132.268 L79.483,105.884 L56.448,92.612 L56.415,118.963 L56.448,118.996 L79.483,132.268 z" fill="#696969"/>
20 + <path d="M102.552,92.773 L79.679,105.755 L79.484,105.884 L79.484,132.268 L102.552,119.157 L102.552,92.773 z" fill="#636363"/>
21 + <path d="M79.484,26.764 L79.755,26.614 L102.552,13.653 L79.484,0.38 L56.448,13.492 L79.484,26.764 z" fill="#B6B6B6"/>
22 + <path d="M102.552,40.037 L102.747,39.908 L125.62,26.925 L102.552,13.653 L79.484,26.764 L102.552,40.037 z" fill="#A6A6A6"/>
23 + <path d="M125.62,53.31 L125.815,53.181 L148.655,40.198 L125.62,26.925 L102.552,40.037 L125.62,53.31 z" fill="#8E8E8E"/>
24 + <path d="M148.689,40.198 L125.815,53.181 L125.62,53.309 L125.62,79.661 L148.689,66.582 L148.689,66.55 L148.689,40.198 z" fill="#7D7D7D"/>
25 + <path d="M56.448,39.875 L56.676,39.746 L79.483,26.764 L56.448,13.491 L33.38,26.603 L56.448,39.875 z" fill="#C2C2C2"/>
26 + <path d="M79.484,53.148 L102.552,40.037 L79.484,26.764 L56.448,39.876 L79.484,53.148 z" fill="#C2C2C2"/>
27 + <path d="M102.552,66.421 L125.62,53.309 L102.552,39.94 L102.421,39.94 L102.552,39.94 L79.418,53.18 L102.552,66.421 z" fill="#777777"/>
28 + <path d="M125.62,53.277 L102.747,66.292 L102.552,66.421 L102.548,92.787 L125.649,79.646 L125.62,53.277 z" fill="#515151"/>
29 + <path d="M33.38,52.987 L33.575,52.858 L56.448,39.876 L33.38,26.603 L10.311,39.715 L33.38,52.987 z" fill="#D0D0D0"/>
30 + <path d="M33.38,79.339 L33.38,52.987 L10.311,39.715 L10.311,66.099 L33.38,79.339 z" fill="#C4C4C4"/>
31 + <path d="M56.48,66.296 L79.516,53.153 L56.48,39.912 L33.412,53.024 L56.48,66.296 z" fill="#BBBBBB"/>
32 + <path d="M56.448,92.612 L56.448,66.26 L33.38,52.987 L33.38,79.339 L33.38,79.371 L56.448,92.612 z" fill="#ACACAC"/>
33 + <path d="M79.516,79.5 L79.712,79.403 L102.559,66.421 L79.516,53.148 L56.448,66.26 L79.516,79.5 z" fill="#BBBBBB"/>
34 + <path d="M79.48,105.883 L79.516,79.5 L56.448,66.227 L56.448,92.579 L56.448,92.612 L79.48,105.883 z" fill="#ACACAC"/>
35 + <path d="M102.552,66.421 L79.712,79.403 L79.516,79.5 L79.516,79.532 L79.477,105.9 L102.552,92.773 L102.552,66.421 z" fill="#B6B6B6"/>
36 + </g>
37 + </g>
38 +</svg>
packages/libp2p-devtools/public/img/libp2p.svg new
+38
@@ -0,0 +1,38 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="159" height="159" viewBox="0, 0, 159, 159">
4 + <g id="Background">
5 + <rect x="0" y="0" width="159" height="159" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <g>
9 + <path d="M148.689,92.902 L125.815,105.916 L125.62,106.013 L125.62,132.397 L148.689,119.286 L148.689,92.902 z" fill="#A33768"/>
10 + <path d="M125.62,106.013 L102.747,119.028 L102.552,119.125 L102.552,145.509 L125.62,132.397 L125.62,106.013 z" fill="#3DA1BD"/>
11 + <path d="M33.38,132.075 L33.38,105.691 L10.311,92.451 L10.311,118.803 L33.38,132.075 z" fill="#81D2E0"/>
12 + <path d="M56.448,145.348 L56.448,118.964 L33.38,105.691 L33.38,132.075 L56.448,145.348 z" fill="#81D2E0"/>
13 + <path d="M79.516,158.62 L79.516,132.236 L56.448,118.964 L56.448,145.348 L79.516,158.62 z" fill="#36A4CB"/>
14 + <path d="M102.552,119.124 L79.712,132.139 L79.516,132.236 L79.516,158.62 L102.552,145.509 L102.552,119.124 z" fill="#3DA1BD"/>
15 + <path d="M148.689,66.582 L125.615,79.666 L125.588,106.045 L125.62,106.045 L148.655,92.966 L148.689,66.582 z" fill="#994FA0"/>
16 + <path d="M125.62,79.661 L102.747,92.676 L102.552,92.773 L102.552,119.157 L125.62,106.046 L125.62,79.661 L125.62,79.694 L125.62,79.661 z" fill="#994FA0"/>
17 + <path d="M33.38,105.691 L33.38,79.339 L10.311,66.034 L10.311,92.418 L33.38,105.691 z" fill="#FFC41A"/>
18 + <path d="M56.415,118.963 L56.448,92.612 L33.38,79.307 L33.38,105.691 L56.415,118.963 z" fill="#F39B35"/>
19 + <path d="M79.483,132.268 L79.483,105.884 L56.448,92.612 L56.415,118.963 L56.448,118.996 L79.483,132.268 z" fill="#38727B"/>
20 + <path d="M102.552,92.773 L79.679,105.755 L79.484,105.884 L79.484,132.268 L102.552,119.157 L102.552,92.773 z" fill="#1C6B7D"/>
21 + <path d="M79.484,26.764 L79.755,26.614 L102.552,13.653 L79.484,0.38 L56.448,13.492 L79.484,26.764 z" fill="#C4CD46"/>
22 + <path d="M102.552,40.037 L102.747,39.908 L125.62,26.925 L102.552,13.653 L79.484,26.764 L102.552,40.037 z" fill="#F064A4"/>
23 + <path d="M125.62,53.31 L125.815,53.181 L148.655,40.198 L125.62,26.925 L102.552,40.037 L125.62,53.31 z" fill="#AA6AAB"/>
24 + <path d="M148.689,40.198 L125.815,53.181 L125.62,53.309 L125.62,79.661 L148.689,66.582 L148.689,66.55 L148.689,40.198 z" fill="#994FA0"/>
25 + <path d="M56.448,39.875 L56.676,39.746 L79.483,26.764 L56.448,13.491 L33.38,26.603 L56.448,39.875 z" fill="#CDDD40"/>
26 + <path d="M79.484,53.148 L102.552,40.037 L79.484,26.764 L56.448,39.876 L79.484,53.148 z" fill="#CDDD40"/>
27 + <path d="M102.552,66.421 L125.62,53.309 L102.552,39.94 L102.421,39.94 L102.552,39.94 L79.418,53.18 L102.552,66.421 z" fill="#8F499E"/>
28 + <path d="M125.62,53.277 L102.747,66.292 L102.552,66.421 L102.548,92.787 L125.649,79.646 L125.62,53.277 z" fill="#5F356B"/>
29 + <path d="M33.38,52.987 L33.575,52.858 L56.448,39.876 L33.38,26.603 L10.311,39.715 L33.38,52.987 z" fill="#FFD75A"/>
30 + <path d="M33.38,79.339 L33.38,52.987 L10.311,39.715 L10.311,66.099 L33.38,79.339 z" fill="#FFC41A"/>
31 + <path d="M56.48,66.296 L79.516,53.153 L56.48,39.912 L33.412,53.024 L56.48,66.296 z" fill="#F8B730"/>
32 + <path d="M56.448,92.612 L56.448,66.26 L33.38,52.987 L33.38,79.339 L33.38,79.371 L56.448,92.612 z" fill="#F39B35"/>
33 + <path d="M79.516,79.5 L79.712,79.403 L102.559,66.421 L79.516,53.148 L56.448,66.26 L79.516,79.5 z" fill="#F8B730"/>
34 + <path d="M79.48,105.883 L79.516,79.5 L56.448,66.227 L56.448,92.579 L56.448,92.612 L79.48,105.883 z" fill="#F39B35"/>
35 + <path d="M102.552,66.421 L79.712,79.403 L79.516,79.5 L79.516,79.532 L79.477,105.9 L102.552,92.773 L102.552,66.421 z" fill="#FAAA36"/>
36 + </g>
37 + </g>
38 +</svg>
packages/libp2p-devtools/public/img/multiaddr-certified.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M498,47.75 C541.354,47.75 576.5,82.896 576.5,126.25 L576.5,244.75 L636.5,244.75 C658.591,244.75 676.5,262.659 676.5,284.75 L676.5,673.75 C676.5,695.841 658.591,713.75 636.5,713.75 L163.5,713.75 C141.409,713.75 123.5,695.841 123.5,673.75 L123.5,284.75 C123.5,262.659 141.409,244.75 163.5,244.75 L224.5,244.75 L224.5,126.25 C224.5,82.896 259.646,47.75 303,47.75 L498,47.75 z M400,337.25 C358.855,337.25 325.5,370.605 325.5,411.75 C325.734,440.771 341.322,464.576 366.5,478.251 L366.5,616.25 L433.5,616.25 L433.5,477.978 C459.406,465.824 473.191,439.454 474.5,411.75 C474.5,370.605 441.145,337.25 400,337.25 z M498,124.75 L303,124.75 C302.172,124.75 301.5,125.422 301.5,126.25 L301.5,244.75 L499.5,244.75 L499.5,126.25 C499.5,125.422 498.828,124.75 498,124.75 z" fill="#A49100"/>
10 + </g>
11 +</svg>
packages/libp2p-devtools/public/img/multiaddr-uncertified.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M498,47.75 C541.354,47.75 576.5,82.896 576.5,126.25 L576.5,244.75 L636.5,244.75 C658.591,244.75 676.5,262.659 676.5,284.75 L676.5,673.75 C676.5,695.841 658.591,713.75 636.5,713.75 L163.5,713.75 C141.409,713.75 123.5,695.841 123.5,673.75 L123.5,284.75 C123.5,262.659 141.409,244.75 163.5,244.75 L224.5,244.75 L224.5,126.25 C224.5,82.896 259.646,47.75 303,47.75 L498,47.75 z M400,337.25 C358.855,337.25 325.5,370.605 325.5,411.75 C325.734,440.771 341.322,464.576 366.5,478.251 L366.5,616.25 L433.5,616.25 L433.5,477.978 C459.406,465.824 473.191,439.454 474.5,411.75 C474.5,370.605 441.145,337.25 400,337.25 z M498,124.75 L303,124.75 C302.172,124.75 301.5,125.422 301.5,126.25 L301.5,244.75 L499.5,244.75 L499.5,126.25 C499.5,125.422 498.828,124.75 498,124.75 z" fill="#868686"/>
10 + </g>
11 +</svg>
packages/libp2p-devtools/public/img/transport-circuit-relay.svg new
+1
@@ -0,0 +1 @@
1 +<svg width="2500" height="1475" viewBox="0 0 256 151" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M208.615 119.908H96.683c-10.537 0-19.11-8.572-19.11-19.107 0-10.539 8.572-19.111 19.11-19.111h61.129c17.84 0 32.353-14.513 32.353-32.353 0-17.84-14.512-32.354-32.353-32.354H47.223C44.17 7.143 34.995 0 24.152 0 10.811 0 0 10.81 0 24.152c0 13.341 10.812 24.152 24.153 24.152 11.239 0 20.687-7.677 23.381-18.076h110.279c10.535 0 19.107 8.573 19.107 19.11 0 10.536-8.572 19.109-19.107 19.109h-61.13c-17.839 0-32.353 14.513-32.353 32.354 0 17.84 14.513 32.352 32.354 32.352h111.93c2.878 10.121 12.189 17.53 23.234 17.53 13.341 0 24.152-10.81 24.152-24.151 0-13.342-10.81-24.152-24.152-24.152-11.044 0-20.354 7.408-23.233 17.528z" fill="#F26B00"/></svg>
\ No newline at end of file
packages/libp2p-devtools/public/img/transport-quic.svg new
+3
@@ -0,0 +1,3 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
2 +<path d="M23.056 30.85L52.33 47.76c-.024.008-.136.104-.255.176L34.512 58.1c-1.732.998-3.465.894-5.173-.08L2.722 42.602C.894 41.557 0 39.984 0 37.876v-12.07c0-2.475 1.692-4.43 4.127-4.8 1.062-.152 2.06.072 2.986.615l4.263 2.483c.295.176.52.208.822 0l1.764-1.03-7.09-4.07c1.1-.703 2.355-.974 3.696-.623.52.152 1.022.36 1.485.63l4.143 2.395a.57.57 0 0 0 .679 0l1.836-1.07-7.17-4.087 3.105-1.804 14.697-8.478c1.764-1.022 3.537-1.022 5.31 0l26.8 15.496c1.676.966 2.54 2.467 2.54 4.4v12.167c0 1.924-.806 3.433-2.475 4.383-1.637.934-3.29.806-4.926-.144L30.29 27.06c-.255-.152-.44-.152-.68 0L27.8 28.12l29.235 16.893a4.1 4.1 0 0 1-3.018.711 6.38 6.38 0 0 1-2.14-.742l-26.21-15.144c-.32-.184-.55-.216-.862 0-.535.35-1.1.647-1.74 1.014zm18.002 12.86l-.543-.335-5.804-3.37c-.447-.27-.806-.303-1.23.024-.36.263-.742.48-1.142.663-.184.08-.47.112-.63.024l-14.673-8.47-.27-.224.295-.192 14.617-8.47c.287-.16.487-.104.742.048l14.92 8.63-2.028 1.182.088.136 6.6 3.816c.112.064.335.032.455-.04l1.892-1.102c.144-.104.232-.255.255-.43l.008-6.57c.008-.64-.184-.974-.742-1.3L32.74 15.507c-.55-.32-.926-.32-1.453 0l-8.19 4.742-13.308 7.68c-.295.176-.43.36-.415.726v6.77a.66.66 0 0 0 .367.655l21.882 12.66c.247.16.56.16.8-.008l2.427-1.405z" fill="#26465b"/><path d="M41.058 43.712l-8.662 5.014a.67.67 0 0 1-.79.008L9.724 36.072c-.27-.152-.367-.335-.367-.655v-6.77c0-.36.12-.55.415-.726l13.308-7.68 8.19-4.742c.527-.32.918-.32 1.453 0l21.14 12.222c.56.32.766.663.742 1.3l-.008 6.57c-.024.176-.112.327-.255.43l-1.892 1.102c-.12.072-.343.104-.455.04l-6.6-3.816c-.032-.024-.04-.048-.088-.136l2.02-1.174-14.905-8.638c-.255-.152-.447-.216-.742-.048l-14.617 8.47-.295.192c.08.08.176.152.27.224l14.673 8.47c.16.088.447.064.63-.024.407-.176.782-.4 1.142-.663.43-.327.774-.287 1.23-.024l5.804 3.37.55.343z" fill="#38c2e4"/>
3 +</svg>
\ No newline at end of file
packages/libp2p-devtools/public/img/transport-tcp.svg new
+18
@@ -0,0 +1,18 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M601.376,550.452 L700.688,550.452 L700.688,311.998 L588.798,200.108 L518.572,270.334 L601.376,353.138 L601.376,550.452 L601.376,550.452 z M700.95,600.239 L556.305,600.239 L354.537,600.239 L271.733,517.435 L306.846,482.322 L375.237,550.714 L515.952,550.714 L377.334,411.834 L412.709,376.459 L551.327,515.077 L551.327,374.363 L483.197,306.233 L518.048,271.382 L345.889,98.437 L176.089,98.437 L176.089,98.437 L0,98.437 L99.05,197.488 L99.05,197.75 L99.574,197.75 L304.487,197.75 L377.072,270.334 L270.947,376.459 L198.362,303.875 L198.362,247.537 L99.05,247.537 L99.05,345.015 L270.947,516.911 L200.983,586.875 L312.873,698.765 L482.673,698.765 L800,698.765 L800,698.765 L700.95,600.239 L700.95,600.239 z" fill="#868686" display="none"/>
10 + <path d="M734.164,22.5 C761.412,22.5 783.5,44.588 783.5,71.836 L783.5,734.164 C783.5,761.412 761.412,783.5 734.165,783.5 L71.836,783.5 C44.588,783.5 22.5,761.412 22.5,734.164 L22.5,71.836 C22.5,44.588 44.588,22.5 71.836,22.5 L734.164,22.5 z M674.802,140.905 L131.198,140.905 L131.198,565.249 L266.406,565.249 L266.406,665.095 L539.594,665.095 L539.594,565.249 L674.802,565.249 L674.802,140.905 z" fill="#868686"/>
11 + <g>
12 + <path d="M201.922,208.856 L271.259,208.856 L271.259,333.663 L201.922,333.663 L201.922,208.856 z" fill="#868686"/>
13 + <path d="M312.862,208.856 L382.199,208.856 L382.199,333.663 L312.862,333.663 L312.862,208.856 z" fill="#868686"/>
14 + <path d="M423.801,208.856 L493.138,208.856 L493.138,333.663 L423.801,333.663 L423.801,208.856 z" fill="#868686"/>
15 + <path d="M534.741,208.856 L604.078,208.856 L604.078,333.663 L534.741,333.663 L534.741,208.856 z" fill="#868686"/>
16 + </g>
17 + </g>
18 +</svg>
packages/libp2p-devtools/public/img/transport-unknown.svg new
+4
@@ -0,0 +1,4 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 +</svg>
packages/libp2p-devtools/public/img/transport-webrtc.svg new
+28
@@ -0,0 +1,28 @@
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3 +<svg width="800px" height="800px" viewBox="0 -3.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
4 + <g>
5 + <path d="M142.076578,191.086817 C142.076578,159.280656 116.294759,133.494615 84.4885969,133.494615 C52.6782136,133.494615 26.896394,159.280656 26.896394,191.086817 C26.896394,222.892979 52.6782136,248.67902 84.4885969,248.67902 C116.294759,248.67902 142.076578,222.892979 142.076578,191.086817" fill="#FF6600" transform="translate(84.486486, 191.086817) scale(1, -1) translate(-84.486486, -191.086817) ">
6 + </path>
7 + <path d="M255.979703,110.454356 C255.979703,78.652416 230.197884,52.862153 198.391722,52.862153 C166.581339,52.862153 140.799519,78.652416 140.799519,110.454356 C140.799519,142.260518 166.581339,168.050781 198.391722,168.050781 C230.197884,168.050781 255.979703,142.260518 255.979703,110.454356" fill="#FFCC00" transform="translate(198.389611, 110.456467) scale(1, -1) translate(-198.389611, -110.456467) ">
8 + </path>
9 + <path d="M115.200498,109.176452 C115.200498,77.3745125 89.4186786,51.5842495 57.6082953,51.5842495 C25.8063553,51.5842495 0.0203140271,77.3745125 0.0203140271,109.176452 C0.0203140271,140.982614 25.8063553,166.772877 57.6082953,166.772877 C89.4186786,166.772877 115.200498,140.982614 115.200498,109.176452" fill="#0089CC" transform="translate(57.610406, 109.178563) scale(1, -1) translate(-57.610406, -109.178563) ">
10 + </path>
11 + <path d="M230.385749,191.086817 C230.385749,159.280656 204.603929,133.494615 172.789324,133.494615 C140.987384,133.494615 115.201343,159.280656 115.201343,191.086817 C115.201343,222.892979 140.987384,248.67902 172.789324,248.67902 C204.603929,248.67902 230.385749,222.892979 230.385749,191.086817" fill="#009939" transform="translate(172.793546, 191.086817) scale(1, -1) translate(-172.793546, -191.086817) ">
12 + </path>
13 + <path d="M185.592001,57.9843213 C185.592001,26.1781597 159.805959,0.392118349 127.999798,0.392118349 C96.1936359,0.392118349 70.4075946,26.1781597 70.4075946,57.9843213 C70.4075946,89.790483 96.1936359,115.576524 127.999798,115.576524 C159.805959,115.576524 185.592001,89.790483 185.592001,57.9843213" fill="#BF0000" transform="translate(127.999798, 57.984321) scale(1, -1) translate(-127.999798, -57.984321) ">
14 + </path>
15 + <path d="M140.798675,57.9788331 C140.798675,56.76721 140.904217,55.580917 140.980207,54.3861807 C166.525612,60.2796505 185.590734,83.1189569 185.590734,110.454356 C185.590734,111.665979 185.485192,112.856494 185.409202,114.05123 C159.863796,108.153539 140.798675,85.3142322 140.798675,57.9788331" fill="#FC0007" transform="translate(163.194704, 84.218705) scale(1, -1) translate(-163.194704, -84.218705) ">
16 + </path>
17 + <path d="M148.39686,162.570614 C158.322038,145.219495 176.973434,133.495881 198.394255,133.495881 C207.124696,133.495881 215.369643,135.496959 222.787141,138.975626 C212.866185,156.326744 194.214789,168.050358 172.789746,168.050358 C164.059305,168.050358 155.814358,166.049281 148.39686,162.570614" fill="#1CD306" transform="translate(185.592001, 150.773120) scale(1, -1) translate(-185.592001, -150.773120) ">
18 + </path>
19 + <path d="M115.200498,191.086817 C115.200498,177.015947 120.258075,164.139813 128.642338,154.138646 C137.018157,164.139813 142.075734,177.015947 142.075734,191.086817 C142.075734,205.157688 137.018157,218.033822 128.642338,228.034989 C120.258075,218.033822 115.200498,205.157688 115.200498,191.086817" fill="#0F7504" transform="translate(128.638116, 191.086817) scale(1, -1) translate(-128.638116, -191.086817) ">
20 + </path>
21 + <path d="M34.806984,138.212768 C41.8023132,135.190043 49.5026635,133.497148 57.6082953,133.497148 C78.818032,133.497148 97.2963396,144.992791 107.293286,162.061056 C100.297956,165.083782 92.5933844,166.772455 84.4919743,166.772455 C63.2822376,166.772455 44.7997083,155.276811 34.806984,138.212768" fill="#0C5E87" transform="translate(71.050135, 150.134801) scale(1, -1) translate(-71.050135, -150.134801) ">
22 + </path>
23 + <path d="M70.6545631,114.036032 C70.5194692,112.431792 70.4054838,110.819109 70.4054838,109.176875 C70.4054838,81.862584 89.4410536,59.044386 114.956907,53.1255861 C115.087779,54.7298257 115.201765,56.3425087 115.201765,57.9805218 C115.201765,85.2948125 96.1704167,108.121454 70.6545631,114.036032" fill="#6B0001" transform="translate(92.803624, 83.580809) scale(1, -1) translate(-92.803624, -83.580809) ">
24 + </path>
25 + <path d="M76.0304545,111.503866 L67.0213825,111.503866 C59.0677312,111.503866 52.6001125,117.950377 52.6001125,125.88292 L52.6001125,207.428953 C52.6001125,215.361496 59.0677312,221.812228 67.0213825,221.812228 L179.989405,221.812228 C187.943056,221.812228 194.406453,215.361496 194.406453,207.428953 L194.406453,125.88292 C194.406453,117.950377 187.943056,111.503866 179.989405,111.503866 L141.50454,111.503866 L64.2899534,73.6522544 L76.0304545,111.503866 L76.0304545,111.503866 Z" fill="#FFFFFF" transform="translate(123.503283, 147.732241) scale(1, -1) translate(-123.503283, -147.732241) ">
26 + </path>
27 + </g>
28 +</svg>
\ No newline at end of file
packages/libp2p-devtools/public/img/transport-websocket.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <path d="M601.376,550.452 L700.688,550.452 L700.688,311.998 L588.798,200.108 L518.572,270.334 L601.376,353.138 L601.376,550.452 L601.376,550.452 z M700.95,600.239 L556.305,600.239 L354.537,600.239 L271.733,517.435 L306.846,482.322 L375.237,550.714 L515.952,550.714 L377.334,411.834 L412.709,376.459 L551.327,515.077 L551.327,374.363 L483.197,306.233 L518.048,271.382 L345.889,98.437 L176.089,98.437 L176.089,98.437 L0,98.437 L99.05,197.488 L99.05,197.75 L99.574,197.75 L304.487,197.75 L377.072,270.334 L270.947,376.459 L198.362,303.875 L198.362,247.537 L99.05,247.537 L99.05,345.015 L270.947,516.911 L200.983,586.875 L312.873,698.765 L482.673,698.765 L800,698.765 L800,698.765 L700.95,600.239 L700.95,600.239 z" fill="#868686"/>
9 + </g>
10 +</svg>
packages/libp2p-devtools/public/img/transport-webtransport.svg new
+13
@@ -0,0 +1,13 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="154" height="154" viewBox="0, 0, 154, 154">
4 + <g id="Background">
5 + <rect x="0" y="0" width="154" height="154" fill="#1A5E9A"/>
6 + </g>
7 + <g id="Layer_1">
8 + <g>
9 + <path d="M137.665,46 L137.665,35.814 L96.703,35.814 L86.473,35.814 L86.162,35.814 L71.59,85.331 L57.027,35.814 L46.475,35.814 L51.3,51.988 L41.451,85.331 L26.884,35.814 L16.335,35.814 L40.446,117.175 L41.451,117.175 L56.519,66.756 L71.59,117.175 L72.593,117.175 L93.237,47.512 L94.208,45.86 z" fill="#FFFFFF"/>
10 + <path d="M110.094,42.668 L119.718,42.668 L119.718,118.186 L110.094,118.186 L110.094,42.668 z" fill="#FFFFFF"/>
11 + </g>
12 + </g>
13 +</svg>
packages/libp2p-devtools/public/panel.html new
+11
@@ -0,0 +1,11 @@
1 +<!DOCTYPE html>
2 +<html>
3 + <head>
4 + <meta name="viewport" content="width=device-width, initial-scale=1">
5 + <link rel="stylesheet" href="/dist/app.css"/>
6 + </head>
7 + <body class="sans-serif f5">
8 + <div id="app"></div>
9 + <script type="text/javascript" src="/dist/app.js"></script>
10 + </body>
11 +</html>
packages/libp2p-devtools/src/app.css new
+70
@@ -0,0 +1,70 @@
1 +@import url('reset.css');
2 +
3 +:root {
4 + --background-color: #282828;
5 + --text-color: #E3E3E3;
6 + --active-text-color: #a8c7fa;
7 + --dark-text-color: #8F8F8F;
8 +
9 + --warning-text-color: #F2B8B5;
10 + --dark-warning-color: #734444;
11 + --dark-warning-color-hover: #854e4e;
12 + --dark-warning-color-active: #714242;
13 +
14 + --active-text-color: #a8c7fa;
15 + --primary-action-text-color: #062E6F;
16 + --primary-action-color: #A8C7FA;
17 + --primary-action-color-hover: #A0BDED;
18 + --primary-action-color-active: #839FCC;
19 +
20 + --action-border-color: #047DB7;
21 +
22 + --success-text-color: #6DD58C;
23 + --dark-success-color: #2c5639;
24 +}
25 +
26 +.light {
27 + --background-color: #FFF;
28 + --text-color: #1f1f1f;
29 + --active-text-color: #a8c7fa;
30 + --dark-text-color: #8F8F8F;
31 +
32 + --warning-text-color: #8a0000;
33 + --dark-warning-color: #4a0000;
34 +}
35 +
36 +body {
37 + font-family: Arial, Tahoma, sans-serif;
38 + font-size: 14px;
39 +}
40 +
41 +h1, h2, h3, h4, h5, h6 {
42 + font-size: 16px;
43 +}
44 +
45 +h1, h2, h3, h4, h5, h6, p, li, small, form {
46 + display: block;
47 + margin-bottom: 12px;
48 +}
49 +li {
50 + margin-left: 12px;
51 +}
52 +code {
53 + font-family: 'Courier New', Courier, monospace, Tahoma, sans-serif;
54 +}
55 +
56 +.dark {
57 + background-color: var(--background-color);
58 + color: var(--text-color);
59 +}
60 +.dark small {
61 + color: var(--dark-text-color);
62 +}
63 +
64 +.light {
65 + background-color: var(--background-color);
66 + color: #1f1f1f;
67 +}
68 +.light small {
69 + color: var(--dark-text-color);
70 +}
packages/libp2p-devtools/src/app.tsx new
+342
@@ -0,0 +1,342 @@
1 +import '@ipshipyard/libp2p-inspector-ui/index.css'
2 +import { LIBP2P_INSPECTOR_METRICS_KEY, valueCodecs } from '@ipshipyard/libp2p-inspector-metrics'
3 +import { Inspector, FloatingPanel, FatalErrorPanel } from '@ipshipyard/libp2p-inspector-ui'
4 +import { TypedEventEmitter } from '@libp2p/interface'
5 +import { pipe } from 'it-pipe'
6 +import { pushable } from 'it-pushable'
7 +import { rpc } from 'it-rpc'
8 +import { base64 } from 'multiformats/bases/base64'
9 +import { Component } from 'react'
10 +import { createRoot } from 'react-dom/client'
11 +import SyntaxHighlighter from 'react-syntax-highlighter'
12 +import { dark } from 'react-syntax-highlighter/dist/esm/styles/hljs'
13 +import { DetectingPanel } from './panels/detecting.js'
14 +import { GrantPermissions } from './panels/grant-permissions.js'
15 +import { evalOnPage } from './utils/eval-on-page.js'
16 +import { getPlatform } from './utils/get-platform.js'
17 +import { sendMessage, events } from './utils/send-message.js'
18 +import type { ClientMessage, InspectorRPCEvents, MetricsRPC, Peer, RPCMessage, SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
19 +import type { TypedEventTarget, Message } from '@libp2p/interface'
20 +import type { RPC } from 'it-rpc'
21 +import type { Duplex } from 'it-stream-types'
22 +import type { ReactElement } from 'react'
23 +
24 +const platform = getPlatform()
25 +
26 +/**
27 + * Sent by the DevTools service worker to the DevTools panel when the inspected
28 + * page has finished (re)loading
29 + */
30 +export interface PageLoadedMessage {
31 + source: typeof SOURCE_CLIENT
32 + type: 'page-loaded'
33 + tabId: number
34 +}
35 +
36 +/**
37 + * Sent by the DevTools service worker to the DevTools panel when it has failed
38 + * to send a message to the inspected page as there is no receiving end present.
39 + *
40 + * This normally means the content script has not been loaded due to the user
41 + * not having granted permission for the script to run.
42 + */
43 +export interface PermissionsErrorMessage {
44 + source: typeof SOURCE_CLIENT
45 + type: 'permissions-error'
46 + tabId: number
47 +}
48 +
49 +/**
50 + * This event is intercepted by the service worker which injects a content
51 + * script into the current page which copies the passed value to the clipboard.
52 + */
53 +export interface CopyToClipboardMessage {
54 + source: typeof SOURCE_CLIENT
55 + type: 'copy-to-clipboard'
56 + tabId: number
57 + value: string
58 +}
59 +
60 +/**
61 + * Messages that are sent from the service worker to the devtools panel
62 + */
63 +export type WorkerMessage = PageLoadedMessage | PermissionsErrorMessage
64 +
65 +/**
66 + * Messages that are sent from the devtools panel to the service worker
67 + */
68 +export type DevToolsMessage = CopyToClipboardMessage | ClientMessage & { tabId: number }
69 +
70 +interface OfflineAppState {
71 + status: 'init' | 'missing' | 'permissions'
72 +}
73 +
74 +interface ErrorAppState {
75 + status: 'error'
76 + error: Error
77 +}
78 +
79 +interface OnlineAppState {
80 + status: 'online'
81 + self: Peer
82 + peers: Peer[]
83 + debug: string
84 + capabilities: Record<string, string[]>
85 + pubsub: Record<string, Message[]>
86 +}
87 +
88 +type AppState = OfflineAppState | ErrorAppState | OnlineAppState
89 +
90 +const MissingPanel = (): ReactElement => {
91 + return (
92 + <>
93 + <FloatingPanel>
94 + <h2>Missing</h2>
95 + <p>@ipshipyard/libp2p-inspector-metrics was not found on the on the current page, or there may not be a libp2p node running.</p>
96 + <p>Please ensure you have configured your libp2p node correctly:</p>
97 + <SyntaxHighlighter language='javascript' style={dark}>
98 + {`import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
99 +import { createLibp2p } from 'libp2p'
100 +
101 +const node = await createLibp2p({
102 + metrics: inspectorMetrics({ /* ... */ })
103 + // ...other config here
104 +})`}
105 + </SyntaxHighlighter>
106 + </FloatingPanel>
107 + </>
108 + )
109 +}
110 +
111 +const GrantPermissionsPanel = (): ReactElement => {
112 + return (
113 + <>
114 + <FloatingPanel>
115 + <h2>Permissions</h2>
116 + <p>No data has been received from the libp2p node running on the page.</p>
117 + <p>You may need to grant this extension access to the current page.</p>
118 + {
119 + platform === 'unknown'
120 + ? (
121 + <p>Please see your browser documentation for how to do this.</p>
122 + )
123 + : <GrantPermissions />
124 + }
125 + </FloatingPanel>
126 + </>
127 + )
128 +}
129 +
130 +export interface AppProps {
131 + messages: Duplex<AsyncGenerator<Uint8Array>>
132 +}
133 +
134 +class App extends Component<AppProps> {
135 + state: AppState
136 + nodeConnected: PromiseWithResolvers<boolean>
137 + private readonly rpc: RPC
138 + private readonly metrics: MetricsRPC
139 + private readonly events: TypedEventTarget<InspectorRPCEvents>
140 +
141 + constructor (props: AppProps) {
142 + super(props)
143 +
144 + this.state = {
145 + status: 'init'
146 + }
147 +
148 + this.rpc = rpc({
149 + valueCodecs
150 + })
151 +
152 + // remote metrics instance
153 + this.metrics = this.rpc.createClient<MetricsRPC>('metrics')
154 +
155 + // create event emitter to receive events from inspector-metrics
156 + this.events = new TypedEventEmitter<InspectorRPCEvents>()
157 + this.events.addEventListener('metrics', (evt) => {
158 + // handle incoming metrics
159 +
160 + })
161 + this.events.addEventListener('self', (evt) => {
162 + this.setState(s => ({
163 + ...s,
164 + self: evt.detail
165 + }))
166 + })
167 + this.events.addEventListener('peers', (evt) => {
168 + this.setState(s => ({
169 + ...s,
170 + peers: evt.detail
171 + }))
172 + })
173 + this.events.addEventListener('pubsub:message', (evt) => {
174 + this.setState(s => ({
175 + ...s,
176 + pubsub: {
177 + // @ts-expect-error fixme
178 + ...(s.pubsub ?? {}),
179 + [evt.detail.topic ?? '']: [
180 + // @ts-expect-error fixme
181 + ...(s.pubsub[evt.detail.topic] ?? []),
182 + evt.detail
183 + ]
184 + }
185 + }))
186 + })
187 +
188 + this.rpc.createTarget('inspector', this.events)
189 +
190 + // send RPC messages
191 + Promise.resolve()
192 + .then(async () => {
193 + await pipe(
194 + props.messages,
195 + this.rpc,
196 + props.messages
197 + )
198 + })
199 + .catch(err => {
200 + // eslint-disable-next-line no-console
201 + console.error('error while reading RPC messages', err)
202 + })
203 +
204 + this.nodeConnected = Promise.withResolvers()
205 +
206 + // the inspected page was reloaded while the dev tools panel is open
207 + events.addEventListener('page-loaded', () => {
208 + this.nodeConnected.reject(new Error('Page reloaded'))
209 + this.nodeConnected = Promise.withResolvers()
210 + this.setState({
211 + status: 'init'
212 + })
213 + this.init()
214 + })
215 +
216 + this.init()
217 + }
218 +
219 + init (): void {
220 + Promise.resolve().then(async () => {
221 + const metricsPresent = await evalOnPage<boolean>(`globalThis?.${LIBP2P_INSPECTOR_METRICS_KEY} === true`)
222 +
223 + if (!metricsPresent) {
224 + this.setState({
225 + status: 'missing'
226 + })
227 + return
228 + }
229 +
230 + const signal = AbortSignal.timeout(2_000)
231 +
232 + try {
233 + const { self, peers, debug, capabilities } = await this.metrics.init({
234 + signal
235 + })
236 +
237 + this.setState({
238 + status: 'online',
239 + self,
240 + peers,
241 + debug,
242 + capabilities,
243 + pubsub: {}
244 + })
245 + } catch (err: any) {
246 + if (signal.aborted) {
247 + this.setState({
248 + status: 'permissions'
249 + })
250 + return
251 + }
252 +
253 + this.setState({
254 + status: 'error',
255 + error: err
256 + })
257 + }
258 + })
259 + .catch(err => {
260 + this.setState({
261 + status: 'error',
262 + error: err
263 + })
264 + })
265 + }
266 +
267 + copyToClipboard = (value: string): void => {
268 + sendMessage<CopyToClipboardMessage>({
269 + type: 'copy-to-clipboard',
270 + value
271 + })
272 + }
273 +
274 + render (): ReactElement {
275 + if (this.state.status === 'init') {
276 + return (
277 + <DetectingPanel />
278 + )
279 + }
280 +
281 + if (this.state.status === 'missing') {
282 + return (
283 + <MissingPanel />
284 + )
285 + }
286 +
287 + if (this.state.status === 'error') {
288 + return (
289 + <FatalErrorPanel error={this.state.error} />
290 + )
291 + }
292 +
293 + if (this.state.status === 'permissions') {
294 + return (
295 + <GrantPermissionsPanel />
296 + )
297 + }
298 +
299 + if (this.state.status === 'online') {
300 + return (
301 + <Inspector {...this.state} metrics={this.metrics} copyToClipboard={this.copyToClipboard} />
302 + )
303 + }
304 +
305 + return (
306 + <p>{this.state.status}</p>
307 + )
308 + }
309 +}
310 +
311 +const body = document.getElementsByTagName('body')[0]
312 +
313 +if (body != null) {
314 + body.className = `${body.className} ${platform}`
315 +}
316 +
317 +const app = document.getElementById('app')
318 +
319 +if (app != null) {
320 + const source = pushable<Uint8Array>()
321 +
322 + // receive RPC messages
323 + events.addEventListener('libp2p-rpc', (event) => {
324 + source.push(base64.decode(event.detail.message))
325 + })
326 +
327 + const messages: Duplex<AsyncGenerator<Uint8Array>> = {
328 + source,
329 + async sink (source) {
330 + for await (const buf of source) {
331 + // send RPC messages
332 + sendMessage<RPCMessage>({
333 + type: 'libp2p-rpc',
334 + message: base64.encode(buf)
335 + })
336 + }
337 + }
338 + }
339 +
340 + const root = createRoot(app)
341 + root.render(<App messages={messages} />)
342 +}
packages/libp2p-devtools/src/constants.ts new
+2
@@ -0,0 +1,2 @@
1 +export const SOURCE_SERVICE_WORKER = '@ipshipyard/libp2p-inspector-metrics:worker'
2 +export const SOURCE_CONTENT_SCRIPT = '@ipshipyard/libp2p-inspector-metrics:content'
packages/libp2p-devtools/src/content-script.ts new
+69
@@ -0,0 +1,69 @@
1 +/**
2 + * This script is injected into the webpage being monitored
3 + *
4 + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts
5 + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts
6 + */
7 +
8 +import { SOURCE_METRICS, SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
9 +import { SOURCE_SERVICE_WORKER } from './constants.js'
10 +import { getBrowserInstance } from './utils/get-browser.js'
11 +import type { DevToolsMessage } from './app.tsx'
12 +
13 +const browser = getBrowserInstance()
14 +
15 +let port: chrome.runtime.Port | undefined
16 +
17 +/**
18 + * Receive events broadcast by `@ipshipyard/libp2p-inspector-metrics` and forward them on to
19 + * the service worker, which forwards them on to the dev tools panel
20 + */
21 +window.addEventListener('message', (event) => {
22 + // Only accept messages from same frame
23 + if (event.source !== window) {
24 + return
25 + }
26 +
27 + const message = event.data
28 +
29 + if (message?.source !== SOURCE_METRICS) {
30 + // ignore messages from other sources
31 + return
32 + }
33 +
34 + // send message to worker
35 + port?.postMessage(message)
36 +})
37 +
38 +/**
39 + * Receive events broadcast by the service worker and forward them on to
40 + * `@ipshipyard/libp2p-inspector-metrics`.
41 + */
42 +browser.runtime.onConnect.addListener((p) => {
43 + // only accept incoming connections from the service worker
44 + if (p.name !== SOURCE_SERVICE_WORKER) {
45 + return
46 + }
47 +
48 + port = p
49 + port.onMessage.addListener((message: DevToolsMessage) => {
50 + if (message.source === SOURCE_CLIENT) {
51 + // intercept copy-to-clipboard
52 + if (message.type === 'copy-to-clipboard') {
53 + navigator.clipboard.writeText(message.value)
54 + .catch(err => {
55 + // eslint-disable-next-line no-console
56 + console.error('could not write to clipboard', err)
57 + })
58 +
59 + return
60 + }
61 +
62 + window.postMessage(message, '*')
63 + }
64 + })
65 +
66 + port.onDisconnect.addListener(() => {
67 + port = undefined
68 + })
69 +})
packages/libp2p-devtools/src/index.ts new
+72
@@ -0,0 +1,72 @@
1 +/**
2 + * @packageDocumentation
3 + *
4 + * A [DevTools](https://developer.chrome.com/docs/devtools) plugin that adds a "libp2p" tab to your developer tools that contacts a libp2p node running on the current page.
5 + *
6 + * Works with [@ipshipyard/libp2p-inspector-metrics](https://www.npmjs.com/package/@ipshipyard/libp2p-inspector-metrics) which supplies metrics and allows us to interact with the running node.
7 + *
8 + * <img width="840" alt="image" src="https://github.com/ipfs-shipyard/js-libp2p-devtools/assets/665810/f8f6a7c8-377f-41d6-948f-95d8469f58b8">
9 + *
10 + * ## Installation instructions
11 + *
12 + * ### 1. Browser installation
13 + *
14 + * Until this plugin is published on the relevant browser plugin stores, please run this locally.
15 + *
16 + * 1. Clone this repo
17 + * 2. Run `npm i && npm run build`
18 + * 3. Install as an unpacked/temporary add on for your browser:
19 + * - Chrome: [How to load an unpacked extension](https://knowledge.workspace.google.com/kb/load-unpacked-extensions-000005962)
20 + * - Firefox: [How to install temporary add-ons](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing)
21 + *
22 + * ### 2. App installation
23 + *
24 + * Configure `@ipshipyard/libp2p-inspector-metrics` as your metrics implementation:
25 + *
26 + * ```ts
27 + * import { createLibp2p } from 'libp2p'
28 + * import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
29 + *
30 + * const node = await createLibp2p({
31 + * metrics: inspectorMetrics(),
32 + * //... other options her
33 + * })
34 + * ```
35 + *
36 + * ### 3. Permissions
37 + *
38 + * With the move to Manifest v3, users must now [opt in](https://blog.mozilla.org/addons/2022/11/17/unified-extensions-button-and-how-to-handle-permissions-in-manifest-v3/) to running content scripts on a page.
39 + *
40 + * #### Chrome
41 + *
42 + * Please click the extensions button, then grant the extension permission to run:
43 + *
44 + * <img src="public/img/grant-permissions-light-chrome.png" width="700"/>
45 + *
46 + * #### Firefox
47 + *
48 + * Please click the extensions button, then grant the extension permission to run:
49 + *
50 + * <img src="public/img/grant-permissions-light-firefox.png" width="700"/>
51 + *
52 + * ### 4. Go!
53 + *
54 + * Browse to your webapp and open the DevTools, you should see a "libp2p" tab towards the right hand side of the toolbar. Click it to see stats about the running node.
55 + *
56 + * ## What's next?
57 + *
58 + * 1. Tests
59 + * - There aren't a lot of tests here yet
60 + * 2. Better UI
61 + * - It's quite rough
62 + * 3. Graphs
63 + * - We don't do much with the collected metrics yet. It would be nice to use Chart.js or similar to show some useful graphs
64 + * - Bonus points for letting the user define their own graphs a la Grafana/Prometheus
65 + * - More bonus points for being able to export/import graph configs
66 + * 4. Dynamic panels
67 + * - We should be able to inspect the libp2p node's configured services (or protocols?) and, for example, only show a DHT tab if the DHT is configured
68 + * 5. Light theme support
69 + * - There are partial overrides for light theme font/background/border colors but we need better detection of when it's enabled
70 + * 6. ??? more features here
71 + */
72 +export {}
packages/libp2p-devtools/src/panels/detecting.tsx new
+13
@@ -0,0 +1,13 @@
1 +import { FloatingPanel } from '@ipshipyard/libp2p-inspector-ui'
2 +import type { JSX } from 'react'
3 +
4 +export const DetectingPanel = (): JSX.Element => {
5 + return (
6 + <>
7 + <FloatingPanel>
8 + <h2>Please wait</h2>
9 + <p>Connecting to libp2p...</p>
10 + </FloatingPanel>
11 + </>
12 + )
13 +}
packages/libp2p-devtools/src/panels/grant-permissions.css new
+23
@@ -0,0 +1,23 @@
1 +.GrantPermissions {
2 + display: block;
3 + height: 375px;
4 + width: 100%;
5 + background-size: contain;
6 + background-repeat: no-repeat;
7 +}
8 +
9 +.chrome.dark .GrantPermissions {
10 + background-image: url(../../public/img/grant-permissions-dark-chrome.png);
11 +}
12 +
13 +.chrome.light .GrantPermissions {
14 + background-image: url(../../public/img/grant-permissions-dark-chrome.png);
15 +}
16 +
17 +.firefox.dark .GrantPermissions {
18 + background-image: url(../../public/img/grant-permissions-dark-firefox.png);
19 +}
20 +
21 +.firefox.light .GrantPermissions {
22 + background-image: url(../../public/img/grant-permissions-dark-firefox.png);
23 +}
packages/libp2p-devtools/src/panels/grant-permissions.tsx new
+8
@@ -0,0 +1,8 @@
1 +import type { JSX } from 'react'
2 +import './grant-permissions.css'
3 +
4 +export function GrantPermissions (): JSX.Element {
5 + return (
6 + <div className='GrantPermissions' />
7 + )
8 +}
packages/libp2p-devtools/src/reset.css new
+48
@@ -0,0 +1,48 @@
1 +/* http://meyerweb.com/eric/tools/css/reset/
2 + v2.0 | 20110126
3 + License: none (public domain)
4 +*/
5 +
6 +html, body, div, span, applet, object, iframe,
7 +h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 +a, abbr, acronym, address, big, cite, code,
9 +del, dfn, em, img, ins, kbd, q, s, samp,
10 +small, strike, strong, sub, sup, tt, var,
11 +b, u, i, center,
12 +dl, dt, dd, ol, ul, li,
13 +fieldset, form, label, legend,
14 +table, caption, tbody, tfoot, thead, tr, th, td,
15 +article, aside, canvas, details, embed,
16 +figure, figcaption, footer, header, hgroup,
17 +menu, nav, output, ruby, section, summary,
18 +time, mark, audio, video {
19 + margin: 0;
20 + padding: 0;
21 + border: 0;
22 + font-size: 100%;
23 + font: inherit;
24 + vertical-align: baseline;
25 +}
26 +/* HTML5 display-role reset for older browsers */
27 +article, aside, details, figcaption, figure,
28 +footer, header, hgroup, menu, nav, section {
29 + display: block;
30 +}
31 +body {
32 + line-height: 1;
33 +}
34 +ol, ul {
35 + list-style: none;
36 +}
37 +blockquote, q {
38 + quotes: none;
39 +}
40 +blockquote:before, blockquote:after,
41 +q:before, q:after {
42 + content: '';
43 + content: none;
44 +}
45 +table {
46 + border-collapse: collapse;
47 + border-spacing: 0;
48 +}
packages/libp2p-devtools/src/service-worker.ts new
+136
@@ -0,0 +1,136 @@
1 +import { SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
2 +import { SOURCE_SERVICE_WORKER } from './constants.js'
3 +import { getBrowserInstance } from './utils/get-browser.js'
4 +import type { DevToolsMessage, PageLoadedMessage, WorkerMessage } from './app.tsx'
5 +
6 +const browser = getBrowserInstance()
7 +
8 +/**
9 + * Holds connections between tab id -> devTools
10 + */
11 +const toDevTools: Record<number, chrome.runtime.Port> = {}
12 +
13 +/**
14 + * Holds connections between tab id -> browser
15 + */
16 +const toContentScript: Record<number, chrome.runtime.Port> = {}
17 +
18 +/**
19 + * Listen for incoming connections from the dev tools panel, when a message is
20 + * received, open a port to the content script and forward the message.
21 + *
22 + * Listen for incoming messages on the forwarding port and relay them back to
23 + * the dev tools panel.
24 + */
25 +browser.runtime.onConnect.addListener(port => {
26 + // only accept incoming connections from the dev tools panel
27 + if (port.name !== SOURCE_CLIENT) {
28 + return
29 + }
30 +
31 + let tabId: number | undefined
32 +
33 + const onContentScriptMessage = (message: WorkerMessage): void => {
34 + port.postMessage(message)
35 + }
36 +
37 + const onDevToolsPanelMessage = (message: DevToolsMessage): void => {
38 + if (tabId == null) {
39 + tabId = message.tabId
40 + }
41 +
42 + if (toDevTools[tabId] !== port) {
43 + toDevTools[tabId]?.onMessage.removeListener(onDevToolsPanelMessage)
44 + toDevTools[tabId] = port
45 + }
46 +
47 + if (toContentScript[tabId] == null) {
48 + toContentScript[tabId] = browser.tabs.connect(message.tabId, {
49 + name: SOURCE_SERVICE_WORKER
50 + })
51 +
52 + toContentScript[tabId].onMessage.addListener(onContentScriptMessage)
53 +
54 + toContentScript[tabId].onDisconnect.addListener((port) => {
55 + port.onMessage.removeListener(onContentScriptMessage)
56 +
57 + delete toContentScript[message.tabId]
58 + })
59 + }
60 +
61 + // intercept copy-to-clipboard message
62 + if (message.type === 'copy-to-clipboard') {
63 + copyToClipboard(tabId, message.value); return
64 + }
65 +
66 + // forward message to content script
67 + toContentScript[tabId].postMessage(message)
68 + }
69 +
70 + // Listen to messages sent from the DevTools page
71 + port.onMessage.addListener(onDevToolsPanelMessage)
72 +
73 + // if the dev tools panel disconnects, clean up references - we will reconnect
74 + // if the dev tools panel is reloaded
75 + port.onDisconnect.addListener(() => {
76 + // do not process messages on a closed port
77 + port.onMessage.removeListener(onDevToolsPanelMessage)
78 +
79 + // clean up ports
80 + if (tabId != null) {
81 + // stop processing content script messages
82 + toContentScript[tabId]?.onMessage.removeListener(onContentScriptMessage)
83 +
84 + delete toContentScript[tabId]
85 +
86 + delete toDevTools[tabId]
87 + }
88 + })
89 +})
90 +
91 +browser.tabs?.onUpdated.addListener((tabId, changeInfo): void => {
92 + if (changeInfo.status !== 'complete') {
93 + return
94 + }
95 +
96 + const port = toDevTools[tabId]
97 +
98 + if (port != null) {
99 + const message: PageLoadedMessage = {
100 + source: SOURCE_CLIENT,
101 + type: 'page-loaded',
102 + tabId
103 + }
104 +
105 + port.postMessage(message)
106 + }
107 +})
108 +
109 +function copyToClipboard (tabId: number, text: string): void {
110 + function contentCopy (text: string): void {
111 + const input = document.createElement('textarea')
112 +
113 + // position absolutely to stop the page from jumping when we call .focus()
114 + input.style.position = 'absolute'
115 + input.style.top = `${window.scrollY}px`
116 + input.style.left = `${window.scrollX}px`
117 +
118 + document.body.appendChild(input)
119 + input.value = text
120 + input.focus()
121 + input.select()
122 + document.execCommand('copy')
123 + input.remove()
124 + }
125 +
126 + // execute the content script
127 + chrome.scripting.executeScript({
128 + target: { tabId },
129 + func: contentCopy,
130 + args: [text]
131 + })
132 + .catch(err => {
133 + // eslint-disable-next-line no-console
134 + console.error('error executing copy script', err)
135 + })
136 +}
packages/libp2p-devtools/src/svg.d.ts new
+5
@@ -0,0 +1,5 @@
1 +// allows importing .svg files
2 +declare module '*.svg' {
3 + const content: string
4 + export default content
5 +}
packages/libp2p-devtools/src/utils/delay.ts new
+7
@@ -0,0 +1,7 @@
1 +export async function delay (ms: number): Promise<void> {
2 + await new Promise<void>(resolve => {
3 + setTimeout(() => {
4 + resolve()
5 + }, ms)
6 + })
7 +}
packages/libp2p-devtools/src/utils/eval-on-page.ts new
+25
@@ -0,0 +1,25 @@
1 +import { getBrowserInstance } from './get-browser.js'
2 +
3 +const browser = getBrowserInstance()
4 +
5 +export async function evalOnPage <R, T = R> (src: string, transform: (arg: R) => T = (o: any) => o): Promise<T> {
6 + const deferred = Promise.withResolvers<T>()
7 +
8 + // test to make sure @ipshipyard/libp2p-inspector-metrics is present on the page
9 + browser.devtools.inspectedWindow.eval<R>(src, (result, isException) => {
10 + if (isException != null) {
11 + // eslint-disable-next-line no-console
12 + console.error('could not eval', src, isException)
13 + deferred.reject(new Error('Exception'))
14 + return
15 + }
16 +
17 + try {
18 + deferred.resolve(transform(result))
19 + } catch (err) {
20 + deferred.reject(err)
21 + }
22 + })
23 +
24 + return deferred.promise
25 +}
packages/libp2p-devtools/src/utils/get-agent.ts new
+10
@@ -0,0 +1,10 @@
1 +import { base64 } from 'multiformats/bases/base64'
2 +import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
3 +
4 +export function getAgent (metadata: Record<string, string>): string {
5 + if (metadata.AgentVersion != null) {
6 + return uint8ArrayToString(base64.decode(metadata.AgentVersion))
7 + }
8 +
9 + return ''
10 +}
packages/libp2p-devtools/src/utils/get-browser.ts new
+8
@@ -0,0 +1,8 @@
1 +export function getBrowserInstance (): typeof chrome {
2 + if (globalThis.chrome != null) {
3 + return globalThis.chrome
4 + }
5 +
6 + // Get extension api Chrome or Firefox
7 + return (globalThis as any).browser
8 +}
packages/libp2p-devtools/src/utils/get-bytes.ts new
+14
@@ -0,0 +1,14 @@
1 +import { bases } from 'multiformats/basics'
2 +
3 +// @ts-expect-error - Not easy to combine these types.
4 +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
5 +
6 +export function getBytes (str: string): Uint8Array {
7 + str = str?.trim()
8 +
9 + if (str == null || str === '') {
10 + throw new Error('Please enter a key')
11 + }
12 +
13 + return multibaseDecoder.decode(str)
14 +}
packages/libp2p-devtools/src/utils/get-platform.ts new
+16
@@ -0,0 +1,16 @@
1 +export type Platform = 'chrome' | 'firefox' | 'unknown'
2 +
3 +export function getPlatform (): Platform {
4 + const userAgent = navigator.userAgent
5 +
6 + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
7 + if (userAgent.includes('Firefox/')) {
8 + return 'firefox'
9 + }
10 +
11 + if (userAgent.includes('Chrome/') || userAgent.includes('Chromium/')) {
12 + return 'chrome'
13 + }
14 +
15 + return 'unknown'
16 +}
packages/libp2p-devtools/src/utils/get-theme.ts new
+9
@@ -0,0 +1,9 @@
1 +import { getBrowserInstance } from './get-browser.js'
2 +
3 +const browser = getBrowserInstance()
4 +
5 +export type BrowserTheme = 'light' | 'dark'
6 +
7 +export function getBrowserTheme (): BrowserTheme {
8 + return browser.devtools.panels.themeName === 'dark' ? 'dark' : 'light'
9 +}
packages/libp2p-devtools/src/utils/send-message.ts new
+48
@@ -0,0 +1,48 @@
1 +import { SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
2 +import { TypedEventEmitter } from '@libp2p/interface'
3 +import { getBrowserInstance } from './get-browser.js'
4 +import type { DevToolsMessage, WorkerMessage } from '../app.tsx'
5 +import type { RPCMessage } from '@ipshipyard/libp2p-inspector-metrics'
6 +import type { TypedEventTarget } from '@libp2p/interface'
7 +
8 +const browser = getBrowserInstance()
9 +let port: chrome.runtime.Port | undefined
10 +
11 +// listen for incoming connections from the service worker script
12 +
13 +export interface DevToolEvents {
14 + 'page-loaded': CustomEvent
15 + 'libp2p-rpc': CustomEvent<RPCMessage>
16 +}
17 +
18 +export const events: TypedEventTarget<DevToolEvents> = new TypedEventEmitter()
19 +
20 +export function sendMessage <Message extends Omit<DevToolsMessage, 'source' | 'tabId'>> (message: Omit<Message, 'source' | 'tabId'>): void {
21 + if (port == null) {
22 + port = browser.runtime.connect({
23 + name: SOURCE_CLIENT
24 + })
25 +
26 + port.onMessage.addListener((message: RPCMessage | WorkerMessage) => {
27 + if (message.type === 'page-loaded') {
28 + events.safeDispatchEvent('page-loaded', {})
29 + }
30 +
31 + if (message.type === 'libp2p-rpc') {
32 + events.safeDispatchEvent<RPCMessage>('libp2p-rpc', {
33 + detail: message
34 + })
35 + }
36 + })
37 +
38 + port.onDisconnect.addListener(() => {
39 + port = undefined
40 + })
41 + }
42 +
43 + port.postMessage({
44 + ...message,
45 + source: SOURCE_CLIENT,
46 + tabId: browser.devtools.inspectedWindow.tabId
47 + })
48 +}
packages/libp2p-devtools/tsconfig.json new
+20
@@ -0,0 +1,20 @@
1 +{
2 + "extends": "aegir/src/config/tsconfig.aegir.json",
3 + "compilerOptions": {
4 + "outDir": "dist",
5 + "jsx": "react-jsx",
6 + "emitDeclarationOnly": true
7 + },
8 + "include": [
9 + "src",
10 + "test"
11 + ],
12 + "references": [
13 + {
14 + "path": "../libp2p-inspector-metrics"
15 + },
16 + {
17 + "path": "../libp2p-inspector-ui"
18 + }
19 + ]
20 +}
packages/libp2p-devtools/typedoc.json new
+5
@@ -0,0 +1,5 @@
1 +{
2 + "entryPoints": [
3 + "./src/index.ts"
4 + ]
5 +}
packages/libp2p-inspector-metrics/CODE_OF_CONDUCT.md new
+3
@@ -0,0 +1,3 @@
1 +# Contributor Code of Conduct
2 +
3 +This project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
packages/libp2p-inspector-metrics/LICENSE-APACHE new
+201
@@ -0,0 +1,201 @@
1 +Apache License
2 + Version 2.0, January 2004
3 + http://www.apache.org/licenses/
4 +
5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 +
7 + 1. Definitions.
8 +
9 + "License" shall mean the terms and conditions for use, reproduction,
10 + and distribution as defined by Sections 1 through 9 of this document.
11 +
12 + "Licensor" shall mean the copyright owner or entity authorized by
13 + the copyright owner that is granting the License.
14 +
15 + "Legal Entity" shall mean the union of the acting entity and all
16 + other entities that control, are controlled by, or are under common
17 + control with that entity. For the purposes of this definition,
18 + "control" means (i) the power, direct or indirect, to cause the
19 + direction or management of such entity, whether by contract or
20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 + outstanding shares, or (iii) beneficial ownership of such entity.
22 +
23 + "You" (or "Your") shall mean an individual or Legal Entity
24 + exercising permissions granted by this License.
25 +
26 + "Source" form shall mean the preferred form for making modifications,
27 + including but not limited to software source code, documentation
28 + source, and configuration files.
29 +
30 + "Object" form shall mean any form resulting from mechanical
31 + transformation or translation of a Source form, including but
32 + not limited to compiled object code, generated documentation,
33 + and conversions to other media types.
34 +
35 + "Work" shall mean the work of authorship, whether in Source or
36 + Object form, made available under the License, as indicated by a
37 + copyright notice that is included in or attached to the work
38 + (an example is provided in the Appendix below).
39 +
40 + "Derivative Works" shall mean any work, whether in Source or Object
41 + form, that is based on (or derived from) the Work and for which the
42 + editorial revisions, annotations, elaborations, or other modifications
43 + represent, as a whole, an original work of authorship. For the purposes
44 + of this License, Derivative Works shall not include works that remain
45 + separable from, or merely link (or bind by name) to the interfaces of,
46 + the Work and Derivative Works thereof.
47 +
48 + "Contribution" shall mean any work of authorship, including
49 + the original version of the Work and any modifications or additions
50 + to that Work or Derivative Works thereof, that is intentionally
51 + submitted to Licensor for inclusion in the Work by the copyright owner
52 + or by an individual or Legal Entity authorized to submit on behalf of
53 + the copyright owner. For the purposes of this definition, "submitted"
54 + means any form of electronic, verbal, or written communication sent
55 + to the Licensor or its representatives, including but not limited to
56 + communication on electronic mailing lists, source code control systems,
57 + and issue tracking systems that are managed by, or on behalf of, the
58 + Licensor for the purpose of discussing and improving the Work, but
59 + excluding communication that is conspicuously marked or otherwise
60 + designated in writing by the copyright owner as "Not a Contribution."
61 +
62 + "Contributor" shall mean Licensor and any individual or Legal Entity
63 + on behalf of whom a Contribution has been received by Licensor and
64 + subsequently incorporated within the Work.
65 +
66 + 2. Grant of Copyright License. Subject to the terms and conditions of
67 + this License, each Contributor hereby grants to You a perpetual,
68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 + copyright license to reproduce, prepare Derivative Works of,
70 + publicly display, publicly perform, sublicense, and distribute the
71 + Work and such Derivative Works in Source or Object form.
72 +
73 + 3. Grant of Patent License. Subject to the terms and conditions of
74 + this License, each Contributor hereby grants to You a perpetual,
75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 + (except as stated in this section) patent license to make, have made,
77 + use, offer to sell, sell, import, and otherwise transfer the Work,
78 + where such license applies only to those patent claims licensable
79 + by such Contributor that are necessarily infringed by their
80 + Contribution(s) alone or by combination of their Contribution(s)
81 + with the Work to which such Contribution(s) was submitted. If You
82 + institute patent litigation against any entity (including a
83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
84 + or a Contribution incorporated within the Work constitutes direct
85 + or contributory patent infringement, then any patent licenses
86 + granted to You under this License for that Work shall terminate
87 + as of the date such litigation is filed.
88 +
89 + 4. Redistribution. You may reproduce and distribute copies of the
90 + Work or Derivative Works thereof in any medium, with or without
91 + modifications, and in Source or Object form, provided that You
92 + meet the following conditions:
93 +
94 + (a) You must give any other recipients of the Work or
95 + Derivative Works a copy of this License; and
96 +
97 + (b) You must cause any modified files to carry prominent notices
98 + stating that You changed the files; and
99 +
100 + (c) You must retain, in the Source form of any Derivative Works
101 + that You distribute, all copyright, patent, trademark, and
102 + attribution notices from the Source form of the Work,
103 + excluding those notices that do not pertain to any part of
104 + the Derivative Works; and
105 +
106 + (d) If the Work includes a "NOTICE" text file as part of its
107 + distribution, then any Derivative Works that You distribute must
108 + include a readable copy of the attribution notices contained
109 + within such NOTICE file, excluding those notices that do not
110 + pertain to any part of the Derivative Works, in at least one
111 + of the following places: within a NOTICE text file distributed
112 + as part of the Derivative Works; within the Source form or
113 + documentation, if provided along with the Derivative Works; or,
114 + within a display generated by the Derivative Works, if and
115 + wherever such third-party notices normally appear. The contents
116 + of the NOTICE file are for informational purposes only and
117 + do not modify the License. You may add Your own attribution
118 + notices within Derivative Works that You distribute, alongside
119 + or as an addendum to the NOTICE text from the Work, provided
120 + that such additional attribution notices cannot be construed
121 + as modifying the License.
122 +
123 + You may add Your own copyright statement to Your modifications and
124 + may provide additional or different license terms and conditions
125 + for use, reproduction, or distribution of Your modifications, or
126 + for any such Derivative Works as a whole, provided Your use,
127 + reproduction, and distribution of the Work otherwise complies with
128 + the conditions stated in this License.
129 +
130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
131 + any Contribution intentionally submitted for inclusion in the Work
132 + by You to the Licensor shall be under the terms and conditions of
133 + this License, without any additional terms or conditions.
134 + Notwithstanding the above, nothing herein shall supersede or modify
135 + the terms of any separate license agreement you may have executed
136 + with Licensor regarding such Contributions.
137 +
138 + 6. Trademarks. This License does not grant permission to use the trade
139 + names, trademarks, service marks, or product names of the Licensor,
140 + except as required for reasonable and customary use in describing the
141 + origin of the Work and reproducing the content of the NOTICE file.
142 +
143 + 7. Disclaimer of Warranty. Unless required by applicable law or
144 + agreed to in writing, Licensor provides the Work (and each
145 + Contributor provides its Contributions) on an "AS IS" BASIS,
146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 + implied, including, without limitation, any warranties or conditions
148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 + PARTICULAR PURPOSE. You are solely responsible for determining the
150 + appropriateness of using or redistributing the Work and assume any
151 + risks associated with Your exercise of permissions under this License.
152 +
153 + 8. Limitation of Liability. In no event and under no legal theory,
154 + whether in tort (including negligence), contract, or otherwise,
155 + unless required by applicable law (such as deliberate and grossly
156 + negligent acts) or agreed to in writing, shall any Contributor be
157 + liable to You for damages, including any direct, indirect, special,
158 + incidental, or consequential damages of any character arising as a
159 + result of this License or out of the use or inability to use the
160 + Work (including but not limited to damages for loss of goodwill,
161 + work stoppage, computer failure or malfunction, or any and all
162 + other commercial damages or losses), even if such Contributor
163 + has been advised of the possibility of such damages.
164 +
165 + 9. Accepting Warranty or Additional Liability. While redistributing
166 + the Work or Derivative Works thereof, You may choose to offer,
167 + and charge a fee for, acceptance of support, warranty, indemnity,
168 + or other liability obligations and/or rights consistent with this
169 + License. However, in accepting such obligations, You may act only
170 + on Your own behalf and on Your sole responsibility, not on behalf
171 + of any other Contributor, and only if You agree to indemnify,
172 + defend, and hold each Contributor harmless for any liability
173 + incurred by, or claims asserted against, such Contributor by reason
174 + of your accepting any such warranty or additional liability.
175 +
176 + END OF TERMS AND CONDITIONS
177 +
178 + APPENDIX: How to apply the Apache License to your work.
179 +
180 + To apply the Apache License to your work, attach the following
181 + boilerplate notice, with the fields enclosed by brackets "[]"
182 + replaced with your own identifying information. (Don't include
183 + the brackets!) The text should be enclosed in the appropriate
184 + comment syntax for the file format. We also recommend that a
185 + file or class name and description of purpose be included on the
186 + same "printed page" as the copyright notice for easier
187 + identification within third-party archives.
188 +
189 + Copyright [yyyy] [name of copyright owner]
190 +
191 + Licensed under the Apache License, Version 2.0 (the "License");
192 + you may not use this file except in compliance with the License.
193 + You may obtain a copy of the License at
194 +
195 + http://www.apache.org/licenses/LICENSE-2.0
196 +
197 + Unless required by applicable law or agreed to in writing, software
198 + distributed under the License is distributed on an "AS IS" BASIS,
199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 + See the License for the specific language governing permissions and
201 + limitations under the License.
packages/libp2p-inspector-metrics/LICENSE-MIT new
+19
@@ -0,0 +1,19 @@
1 +The MIT License (MIT)
2 +
3 +Permission is hereby granted, free of charge, to any person obtaining a copy
4 +of this software and associated documentation files (the "Software"), to deal
5 +in the Software without restriction, including without limitation the rights
6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 +copies of the Software, and to permit persons to whom the Software is
8 +furnished to do so, subject to the following conditions:
9 +
10 +The above copyright notice and this permission notice shall be included in
11 +all copies or substantial portions of the Software.
12 +
13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 +THE SOFTWARE.
packages/libp2p-inspector-metrics/README.md new
+64
@@ -0,0 +1,64 @@
1 +# @ipshipyard/libp2p-inspector-metrics
2 +
3 +[![codecov](https://img.shields.io/codecov/c/github/ipshipyard/js-libp2p-inspector.svg?style=flat-square)](https://codecov.io/gh/ipshipyard/js-libp2p-inspector)
4 +[![CI](https://img.shields.io/github/actions/workflow/status/ipshipyard/js-libp2p-inspector/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipshipyard/js-libp2p-inspector/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
5 +
6 +> Collect libp2p metrics and send them to browser DevTools
7 +
8 +# About
9 +
10 +<!--
11 +
12 +!IMPORTANT!
13 +
14 +Everything in this README between "# About" and "# Install" is automatically
15 +generated and will be overwritten the next time the doc generator is run.
16 +
17 +To make changes to this section, please update the @packageDocumentation section
18 +of src/index.js or src/index.ts
19 +
20 +To experiment with formatting, please run "npm run docs" from the root of this
21 +repo and examine the changes made.
22 +
23 +-->
24 +
25 +Configure your browser-based libp2p node with DevTools metrics:
26 +
27 +```typescript
28 +import { createLibp2p } from 'libp2p'
29 +import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
30 +
31 +const node = await createLibp2p({
32 + metrics: inspectorMetrics()
33 +})
34 +```
35 +
36 +Then use the [DevTools plugin](https://github.com/ipfs-shipyard/js-libp2p-devtools)
37 +for Chrome or Firefox to inspect the state of your running node.
38 +
39 +# Install
40 +
41 +```console
42 +$ npm i @ipshipyard/libp2p-inspector-metrics
43 +```
44 +
45 +## Browser `<script>` tag
46 +
47 +Loading this module through a script tag will make its exports available as `IpshipyardLibp2pInspectorMetrics` in the global namespace.
48 +
49 +```html
50 +<script src="https://unpkg.com/@ipshipyard/libp2p-inspector-metrics/dist/index.min.js"></script>
51 +```
52 +
53 +> Collect libp2p metrics and send them to an inspector
54 +
55 +# License
56 +
57 +Licensed under either of
58 +
59 +- Apache 2.0, ([LICENSE-APACHE](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-inspector-metrics/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
60 +- MIT ([LICENSE-MIT](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-inspector-metrics/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
61 +
62 +# Contribution
63 +
64 +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
packages/libp2p-inspector-metrics/package.json new
+168
@@ -0,0 +1,168 @@
1 +{
2 + "name": "@ipshipyard/libp2p-inspector-metrics",
3 + "version": "0.0.0",
4 + "description": "Collect libp2p metrics and send them to browser DevTools",
5 + "author": "info@ipshipyard.com",
6 + "license": "Apache-2.0 OR MIT",
7 + "homepage": "https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-inspector-metrics#readme",
8 + "repository": {
9 + "type": "git",
10 + "url": "git+https://github.com/ipshipyard/js-libp2p-inspector.git"
11 + },
12 + "bugs": {
13 + "url": "https://github.com/ipshipyard/js-libp2p-inspector/issues"
14 + },
15 + "publishConfig": {
16 + "access": "public",
17 + "provenance": true
18 + },
19 + "type": "module",
20 + "types": "./dist/src/index.d.ts",
21 + "files": [
22 + "src",
23 + "dist",
24 + "!dist/test",
25 + "!**/*.tsbuildinfo"
26 + ],
27 + "exports": {
28 + ".": {
29 + "types": "./dist/src/index.d.ts",
30 + "import": "./dist/src/index.js"
31 + }
32 + },
33 + "release": {
34 + "branches": [
35 + "main"
36 + ],
37 + "plugins": [
38 + [
39 + "@semantic-release/commit-analyzer",
40 + {
41 + "preset": "conventionalcommits",
42 + "releaseRules": [
43 + {
44 + "breaking": true,
45 + "release": "major"
46 + },
47 + {
48 + "revert": true,
49 + "release": "patch"
50 + },
51 + {
52 + "type": "feat",
53 + "release": "minor"
54 + },
55 + {
56 + "type": "fix",
57 + "release": "patch"
58 + },
59 + {
60 + "type": "docs",
61 + "release": "patch"
62 + },
63 + {
64 + "type": "test",
65 + "release": "patch"
66 + },
67 + {
68 + "type": "deps",
69 + "release": "patch"
70 + },
71 + {
72 + "scope": "no-release",
73 + "release": false
74 + }
75 + ]
76 + }
77 + ],
78 + [
79 + "@semantic-release/release-notes-generator",
80 + {
81 + "preset": "conventionalcommits",
82 + "presetConfig": {
83 + "types": [
84 + {
85 + "type": "feat",
86 + "section": "Features"
87 + },
88 + {
89 + "type": "fix",
90 + "section": "Bug Fixes"
91 + },
92 + {
93 + "type": "chore",
94 + "section": "Trivial Changes"
95 + },
96 + {
97 + "type": "docs",
98 + "section": "Documentation"
99 + },
100 + {
101 + "type": "deps",
102 + "section": "Dependencies"
103 + },
104 + {
105 + "type": "test",
106 + "section": "Tests"
107 + }
108 + ]
109 + }
110 + }
111 + ],
112 + "@semantic-release/changelog",
113 + "@semantic-release/npm",
114 + "@semantic-release/github",
115 + [
116 + "@semantic-release/git",
117 + {
118 + "assets": [
119 + "CHANGELOG.md",
120 + "package.json"
121 + ]
122 + }
123 + ]
124 + ]
125 + },
126 + "scripts": {
127 + "clean": "aegir clean",
128 + "lint": "aegir lint",
129 + "dep-check": "aegir dep-check",
130 + "doc-check": "aegir doc-check",
131 + "build": "aegir build",
132 + "test": "aegir test -t browser",
133 + "test:chrome": "aegir test -t browser --cov",
134 + "test:firefox": "aegir test -t browser --browser firefox",
135 + "release": "aegir release"
136 + },
137 + "dependencies": {
138 + "@libp2p/identify": "^3.0.32",
139 + "@libp2p/interface": "^2.10.2",
140 + "@libp2p/interface-internal": "^2.3.14",
141 + "@libp2p/logger": "^5.1.18",
142 + "@libp2p/peer-id": "^5.1.5",
143 + "@libp2p/ping": "^2.0.32",
144 + "@libp2p/simple-metrics": "^1.3.12",
145 + "@libp2p/utils": "^6.6.5",
146 + "@multiformats/multiaddr": "^12.4.0",
147 + "cborg": "^4.2.11",
148 + "dns-packet": "^5.6.1",
149 + "it-length-prefixed": "^10.0.1",
150 + "it-pipe": "^3.0.1",
151 + "it-pushable": "^3.2.3",
152 + "it-rpc": "^1.1.0",
153 + "multicast-dns": "^7.2.5",
154 + "multiformats": "^13.3.6",
155 + "progress-events": "^1.0.1",
156 + "stream-to-it": "^1.0.1",
157 + "uint8arrays": "^5.1.0"
158 + },
159 + "devDependencies": {
160 + "@libp2p/crypto": "^5.1.4",
161 + "aegir": "^47.0.13",
162 + "sinon-ts": "^2.0.0"
163 + },
164 + "browser": {
165 + "./src/messages/messages.js": "./src/messages/messages.browser.js"
166 + },
167 + "sideEffects": false
168 +}
packages/libp2p-inspector-metrics/src/index.ts new
+406
@@ -0,0 +1,406 @@
1 +/**
2 + * @packageDocumentation
3 + *
4 + * Configure your browser-based libp2p node with DevTools metrics:
5 + *
6 + * ```typescript
7 + * import { createLibp2p } from 'libp2p'
8 + * import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
9 + *
10 + * const node = await createLibp2p({
11 + * metrics: inspectorMetrics()
12 + * })
13 + * ```
14 + *
15 + * Then use the [DevTools plugin](https://github.com/ipfs-shipyard/js-libp2p-devtools)
16 + * for Chrome or Firefox to inspect the state of your running node.
17 + */
18 +
19 +import { isPubSub, serviceCapabilities, start, stop } from '@libp2p/interface'
20 +import { simpleMetrics } from '@libp2p/simple-metrics'
21 +import { pipe } from 'it-pipe'
22 +import { pushable } from 'it-pushable'
23 +import { rpc } from 'it-rpc'
24 +import { base64 } from 'multiformats/bases/base64'
25 +import manifest from '../package.json' with { type: 'json' }
26 +import { messages } from './messages/index.js'
27 +import { valueCodecs } from './rpc/index.js'
28 +import { metricsRpc } from './rpc/rpc.js'
29 +import { debounce } from './utils/debounce.js'
30 +import { findCapability } from './utils/find-capability.js'
31 +import { getPeers } from './utils/get-peers.js'
32 +import { getSelf } from './utils/get-self.js'
33 +import type { Messages } from './messages/index.js'
34 +import type { InspectorRPC } from './rpc/index.js'
35 +import type { ComponentLogger, Connection, Libp2pEvents, Logger, Metrics, MultiaddrConnection, PeerId, PeerStore, Stream, ContentRouting, PeerRouting, TypedEventTarget, Startable, Message, SubscriptionChangeData, NodeInfo } from '@libp2p/interface'
36 +import type { TransportManager, Registrar, ConnectionManager, AddressManager } from '@libp2p/interface-internal'
37 +import type { Pushable } from 'it-pushable'
38 +import type { RPC } from 'it-rpc'
39 +
40 +export * from './rpc/index.js'
41 +
42 +export const SOURCE_CLIENT = '@ipshipyard/libp2p-inspector-metrics:client'
43 +export const SOURCE_METRICS = '@ipshipyard/libp2p-inspector-metrics:metrics'
44 +export const LIBP2P_INSPECTOR_METRICS_KEY = '________ipshipyard_libp2p_inspector_metrics'
45 +
46 +// let inspector know we are here
47 +Object.defineProperty(globalThis, LIBP2P_INSPECTOR_METRICS_KEY, {
48 + value: true,
49 + enumerable: false,
50 + writable: false
51 +})
52 +
53 +// don't wait for inspector RPC forever
54 +const RPC_TIMEOUT = 10_000
55 +
56 +/**
57 + * Sent by the client to discover basic node info & versions
58 + */
59 +export interface IdentifyMessage {
60 + source: typeof SOURCE_CLIENT
61 + type: 'libp2p-identify'
62 +}
63 +
64 +/**
65 + * Sent to the client to let it know basic node info & versions
66 + */
67 +export interface IdentifyResponse {
68 + source: typeof SOURCE_METRICS
69 + type: 'libp2p-identify',
70 + name: string
71 + version: string
72 + userAgent: string
73 + inspector: string
74 +}
75 +
76 +/**
77 + * Invoke a method on the libp2p object
78 + */
79 +export interface RPCMessage {
80 + source: typeof SOURCE_CLIENT | typeof SOURCE_METRICS
81 + type: 'libp2p-rpc'
82 +
83 + /**
84 + * The RPC message encoded as a multibase string
85 + */
86 + message: string
87 +}
88 +
89 +/**
90 + * Messages that are sent from the inspector to the client
91 + */
92 +export type InspectorMessage = IdentifyResponse | RPCMessage
93 +
94 +/**
95 + * Messages that are sent from the client to the inspector
96 + */
97 +export type ClientMessage = IdentifyMessage | RPCMessage
98 +
99 +export interface Address {
100 + /**
101 + * The multiaddr this address represents
102 + */
103 + multiaddr: string
104 +
105 + /**
106 + * If `true`, this multiaddr came from a signed peer record
107 + */
108 + isCertified?: boolean
109 +
110 + /**
111 + * If `true`, the current node has an active connection to this peer via this
112 + * address
113 + */
114 + isConnected?: boolean
115 +}
116 +
117 +export interface InspectorMetricsInit {
118 + /**
119 + * How often to pass metrics to the DevTools panel
120 + */
121 + intervalMs?: number
122 +
123 + /**
124 + * When used under Node.js this is the mDNS service tag that is advertised
125 + *
126 + * @default '_libp2p_inspector_metrics._tcp.local'
127 + */
128 + serviceTag?: string
129 +
130 + /**
131 + * How to accept/publish RPC messages.
132 + *
133 + * `window` - uses the [window.postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
134 + * API so communicate with a browser plugin (only available in browsers).
135 + *
136 + * `mdns` - start a TCP socket and advertise it to local connections via the
137 + * `_libp2p_inspector_metrics._tcp.local` service tag (not available in
138 + * browsers)
139 + *
140 + * `libp2p` - accept incoming libp2p streams with the `/libp2p/devtools/1.0.0`
141 + * protocol
142 + *
143 + * Defaults to `window` in browsers and `mdns` in Node.js
144 + *
145 + * @default 'window|mdns'
146 + */
147 + rpc?: 'mdns' | 'window' | 'libp2p'
148 +}
149 +
150 +export interface InspectorMetricsComponents {
151 + logger: ComponentLogger
152 + events: TypedEventTarget<Libp2pEvents>
153 + peerId: PeerId
154 + transportManager: TransportManager
155 + registrar: Registrar
156 + connectionManager: ConnectionManager
157 + peerStore: PeerStore
158 + nodeInfo: NodeInfo
159 + contentRouting: ContentRouting
160 + peerRouting: PeerRouting
161 + addressManager: AddressManager
162 +}
163 +
164 +class InspectorMetrics implements Metrics, Startable {
165 + private readonly log: Logger
166 + private readonly components: InspectorMetricsComponents
167 + private readonly simpleMetrics: Metrics
168 + private readonly intervalMs?: number
169 + private readonly rpcQueue: Pushable<Uint8Array>
170 + private readonly rpc: RPC
171 + private readonly inspector: InspectorRPC
172 + private readonly messages: Messages
173 +
174 + constructor (components: InspectorMetricsComponents, init: InspectorMetricsInit = {}) {
175 + this.log = components.logger.forComponent('libp2p:devtools-metrics')
176 + this.intervalMs = init?.intervalMs
177 + this.components = components
178 +
179 + // create RPC endpoint
180 + this.rpcQueue = pushable()
181 + this.rpc = rpc({
182 + valueCodecs
183 + })
184 + this.inspector = this.rpc.createClient('inspector', {
185 + timeout: RPC_TIMEOUT
186 + })
187 + this.messages = messages(components, init)
188 +
189 + // collect information on current peers and sent it to the dev tools panel
190 + this.onPeersUpdate = debounce(this.onPeersUpdate.bind(this), 1000)
191 + this.onSelfUpdate = debounce(this.onSelfUpdate.bind(this), 1000)
192 + this.onIncomingMessage = this.onIncomingMessage.bind(this)
193 +
194 + // relay pubsub messages to dev tools panel
195 + this.onPubSubMessage = this.onPubSubMessage.bind(this)
196 + this.onPubSubSubscriptionChange = this.onPubSubSubscriptionChange.bind(this)
197 +
198 + // collect metrics
199 + this.simpleMetrics = simpleMetrics({
200 + intervalMs: this.intervalMs,
201 + onMetrics: (metrics) => {
202 + this.inspector.safeDispatchEvent('metrics', {
203 + detail: metrics
204 + }).catch(err => {
205 + this.log.error('error sending metrics', err)
206 + })
207 + }
208 + })({})
209 + }
210 +
211 + readonly [Symbol.toStringTag] = '@ipshipyard/libp2p-inspector-metrics'
212 +
213 + readonly [serviceCapabilities]: string[] = [
214 + '@libp2p/metrics'
215 + ]
216 +
217 + trackMultiaddrConnection (maConn: MultiaddrConnection): void {
218 + this.simpleMetrics.trackMultiaddrConnection(maConn)
219 + }
220 +
221 + trackProtocolStream (stream: Stream, connection: Connection): void {
222 + this.simpleMetrics.trackProtocolStream(stream, connection)
223 + }
224 +
225 + registerMetric (name: any, options: any): any {
226 + return this.simpleMetrics.registerMetric(name, options)
227 + }
228 +
229 + registerMetricGroup (name: any, options: any): any {
230 + return this.simpleMetrics.registerMetricGroup(name, options)
231 + }
232 +
233 + registerCounter (name: any, options: any): any {
234 + return this.simpleMetrics.registerCounter(name, options)
235 + }
236 +
237 + registerCounterGroup (name: any, options: any): any {
238 + return this.simpleMetrics.registerCounterGroup(name, options)
239 + }
240 +
241 + registerHistogram (name: any, options: any): any {
242 + return this.simpleMetrics.registerHistogram(name, options)
243 + }
244 +
245 + registerHistogramGroup (name: any, options: any): any {
246 + return this.simpleMetrics.registerHistogramGroup(name, options)
247 + }
248 +
249 + registerSummary (name: any, options: any): any {
250 + return this.simpleMetrics.registerSummary(name, options)
251 + }
252 +
253 + registerSummaryGroup (name: any, options: any): any {
254 + return this.simpleMetrics.registerSummaryGroup(name, options)
255 + }
256 +
257 + createTrace (): any {
258 + return this.simpleMetrics.createTrace()
259 + }
260 +
261 + traceFunction <T extends (...args: any[]) => any> (name: string, fn: T, options?: any): T {
262 + return this.simpleMetrics.traceFunction(name, fn, options)
263 + }
264 +
265 + async start (): Promise<void> {
266 + // send peer updates
267 + this.components.events.addEventListener('peer:connect', this.onPeersUpdate)
268 + this.components.events.addEventListener('peer:disconnect', this.onPeersUpdate)
269 + this.components.events.addEventListener('peer:identify', this.onPeersUpdate)
270 + this.components.events.addEventListener('peer:update', this.onPeersUpdate)
271 +
272 + // send node status updates
273 + this.components.events.addEventListener('self:peer:update', this.onSelfUpdate)
274 +
275 + // process incoming messages from devtools
276 + this.messages.addEventListener('message', this.onIncomingMessage)
277 +
278 + // create rpc target
279 + this.rpc.createTarget('metrics', metricsRpc(this.components))
280 +
281 + // start metrics
282 + await start(this.simpleMetrics, this.messages)
283 +
284 + // send RPC messages
285 + Promise.resolve()
286 + .then(async () => {
287 + await pipe(
288 + this.rpcQueue,
289 + this.rpc,
290 + async source => {
291 + for await (const buf of source) {
292 + this.messages.postMessage({
293 + source: SOURCE_METRICS,
294 + type: 'libp2p-rpc',
295 + message: base64.encode(buf)
296 + })
297 + }
298 + }
299 + )
300 + })
301 + .catch(err => {
302 + this.log.error('error while reading RPC messages', err)
303 + })
304 +
305 + const pubsub = findCapability('@libp2p/pubsub', this.components)
306 +
307 + if (isPubSub(pubsub)) {
308 + pubsub.addEventListener('message', this.onPubSubMessage)
309 + pubsub.addEventListener('subscription-change', this.onPubSubSubscriptionChange)
310 + }
311 + }
312 +
313 + async stop (): Promise<void> {
314 + this.messages.removeEventListener('message', this.onIncomingMessage)
315 + this.components.events.removeEventListener('self:peer:update', this.onSelfUpdate)
316 + this.components.events.removeEventListener('peer:connect', this.onPeersUpdate)
317 + this.components.events.removeEventListener('peer:disconnect', this.onPeersUpdate)
318 + this.components.events.removeEventListener('peer:identify', this.onPeersUpdate)
319 + this.components.events.removeEventListener('peer:update', this.onPeersUpdate)
320 + await stop(this.simpleMetrics, this.messages)
321 + }
322 +
323 + private onIncomingMessage (event: MessageEvent<ClientMessage>): void {
324 + // Only accept messages from same frame
325 + // @ts-expect-error types are wonky
326 + if (event.source !== this.messages) {
327 + return
328 + }
329 +
330 + const message = event.data
331 +
332 + // Only accept messages of correct format (our messages)
333 + if (message?.source !== SOURCE_CLIENT) {
334 + return
335 + }
336 +
337 + // respond to identify message without invoking RPC since it will be used by
338 + // the client to understand what version of RPC we support
339 +
340 + if (message.type === 'libp2p-identify') {
341 + // @ts-expect-error wat
342 + this.messages.postMessage({
343 + source: SOURCE_METRICS,
344 + type: 'libp2p-identify',
345 + name: this.components.nodeInfo.name,
346 + version: this.components.nodeInfo.version,
347 + userAgent: this.components.nodeInfo.userAgent,
348 + inspector: manifest.version
349 + })
350 +
351 + return
352 + }
353 +
354 + if (message.type === 'libp2p-rpc') {
355 + this.rpcQueue.push(base64.decode(message.message))
356 + }
357 + }
358 +
359 + private onPubSubMessage (event: CustomEvent<Message>): void {
360 + this.inspector.safeDispatchEvent('pubsub:message', {
361 + detail: event.detail
362 + })
363 + .catch(err => {
364 + this.log.error('error relaying pubsub message', err)
365 + })
366 + }
367 +
368 + private onPubSubSubscriptionChange (event: CustomEvent<SubscriptionChangeData>): void {
369 + this.inspector.safeDispatchEvent('pubsub:subscription-change', {
370 + detail: event.detail
371 + })
372 + .catch(err => {
373 + this.log.error('error relaying pubsub subscription change', err)
374 + })
375 + }
376 +
377 + private onSelfUpdate (): void {
378 + Promise.resolve()
379 + .then(async () => {
380 + await this.inspector.safeDispatchEvent('self', {
381 + detail: await getSelf(this.components)
382 + })
383 + })
384 + .catch(err => {
385 + this.log.error('error sending peers message', err)
386 + })
387 + }
388 +
389 + private onPeersUpdate (): void {
390 + Promise.resolve()
391 + .then(async () => {
392 + await this.inspector.safeDispatchEvent('peers', {
393 + detail: await getPeers(this.components, this.log)
394 + })
395 + })
396 + .catch(err => {
397 + this.log.error('error sending peers message', err)
398 + })
399 + }
400 +}
401 +
402 +export function inspectorMetrics (init?: Partial<InspectorMetricsInit>): (components: InspectorMetricsComponents) => Metrics {
403 + return (components) => {
404 + return new InspectorMetrics(components, init)
405 + }
406 +}
packages/libp2p-inspector-metrics/src/messages/index.ts new
+24
@@ -0,0 +1,24 @@
1 +import type { InspectorMessage, ClientMessage } from '../index.js'
2 +import type { ComponentLogger, PeerId, TypedEventTarget } from '@libp2p/interface'
3 +import type { ListenOptions } from 'node:net'
4 +
5 +export interface ChannelMessages {
6 + message: MessageEvent<ClientMessage>
7 +}
8 +
9 +export interface Messages extends TypedEventTarget<ChannelMessages> {
10 + postMessage (message: InspectorMessage): void
11 + addEventListener(type: 'message', listener: (evt: MessageEvent<ClientMessage>) => void): void
12 +}
13 +
14 +export interface MessagesComponents {
15 + peerId: PeerId
16 + logger: ComponentLogger
17 +}
18 +
19 +export interface MessagesInit {
20 + serviceTag?: string
21 + listenOptions?: ListenOptions | string | number
22 +}
23 +
24 +export { messages } from './messages.js'
packages/libp2p-inspector-metrics/src/messages/message-event.ts new
+13
@@ -0,0 +1,13 @@
1 +import type { Messages } from './index.ts'
2 +
3 +export class MessageEvent<T> extends Event {
4 + data: T
5 + source: Messages
6 +
7 + constructor (source: Messages, data: T, eventInitDict?: EventInit) {
8 + super('message', eventInitDict)
9 +
10 + this.source = source
11 + this.data = data
12 + }
13 +}
packages/libp2p-inspector-metrics/src/messages/messages.browser.ts new
+3
@@ -0,0 +1,3 @@
1 +export function messages (): MessageEventSource {
2 + return window
3 +}
packages/libp2p-inspector-metrics/src/messages/messages.ts new
+211
@@ -0,0 +1,211 @@
1 +import { createServer } from 'node:net'
2 +import { start, stop, TypedEventEmitter } from '@libp2p/interface'
3 +import { getThinWaistAddresses } from '@libp2p/utils/get-thin-waist-addresses'
4 +import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
5 +import { multiaddr } from '@multiformats/multiaddr'
6 +import { encode, decode } from 'it-length-prefixed'
7 +import { pushable } from 'it-pushable'
8 +import multicastDNS from 'multicast-dns'
9 +import { duplex } from 'stream-to-it'
10 +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
11 +import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
12 +import { MessageEvent } from './message-event.ts'
13 +import type { InspectorMessage } from '../index.js'
14 +import type { ChannelMessages, Messages, MessagesComponents, MessagesInit } from './index.js'
15 +import type { Logger } from '@libp2p/interface'
16 +import type { Multiaddr } from '@multiformats/multiaddr'
17 +import type { Answer, TxtAnswer } from 'dns-packet'
18 +import type { Pushable } from 'it-pushable'
19 +import type { MulticastDNS } from 'multicast-dns'
20 +import type { Server, Socket, ListenOptions, AddressInfo } from 'node:net'
21 +
22 +interface MDNSPortInit extends MessagesInit {
23 + messages: TCPPortMessages
24 +}
25 +
26 +class MDNSPortAdvertisement {
27 + private readonly components: MessagesComponents
28 + private readonly log: Logger
29 + private readonly serviceTag: string
30 + private mdns?: MulticastDNS
31 + private messages: TCPPortMessages
32 +
33 + constructor (components: MessagesComponents, init: MDNSPortInit) {
34 + this.components = components
35 + this.log = components.logger.forComponent('libp2p:inspector-metrics:messages')
36 + this.serviceTag = init.serviceTag ?? '_libp2p_inspector_metrics._tcp.local'
37 + this.messages = init.messages
38 +
39 + this._onMdnsQuery = this._onMdnsQuery.bind(this)
40 + this._onMdnsWarning = this._onMdnsWarning.bind(this)
41 + this._onMdnsError = this._onMdnsError.bind(this)
42 + }
43 +
44 + async start (): Promise<void> {
45 + this.mdns = multicastDNS()
46 + this.mdns.on('query', this._onMdnsQuery)
47 + this.mdns.on('warning', this._onMdnsWarning)
48 + this.mdns.on('error', this._onMdnsError)
49 + }
50 +
51 + async stop (): Promise<void> {
52 + this.mdns?.destroy()
53 + this.mdns = undefined
54 + }
55 +
56 + _onMdnsQuery (event: multicastDNS.QueryPacket): void {
57 + const address = this.messages.server?.address()
58 +
59 + if (this.mdns == null || address == null) {
60 + return
61 + }
62 +
63 + if (event.questions[0]?.name !== this.serviceTag) {
64 + return
65 + }
66 +
67 + const answers: Answer[] = [{
68 + name: this.serviceTag,
69 + type: 'PTR',
70 + class: 'IN',
71 + ttl: 120,
72 + data: `${this.components.peerId}.${this.serviceTag}`
73 + }, ...toMultiaddrs(address)
74 + // mDNS requires link-local addresses only
75 + // https://github.com/libp2p/specs/blob/master/discovery/mdns.md#issues
76 + .filter(isLinkLocal)
77 + .map((ma): TxtAnswer => {
78 + return {
79 + name: `${this.components.peerId}.${this.serviceTag}`,
80 + type: 'TXT',
81 + class: 'IN',
82 + ttl: 120,
83 + data: ma.toString()
84 + }
85 + })
86 + ]
87 +
88 + this.mdns.respond(answers)
89 + }
90 +
91 + _onMdnsWarning (err: Error): void {
92 + this.log.error('mdns warning', err)
93 + }
94 +
95 + _onMdnsError (err: Error): void {
96 + this.log.error('mdns error', err)
97 + }
98 +}
99 +
100 +function toMultiaddrs (addr: string | AddressInfo): Multiaddr[] {
101 + if (typeof addr === 'string') {
102 + // TODO: wrap with encodeURIComponent https://github.com/multiformats/multiaddr/pull/174
103 + return [multiaddr(`/unix/${addr}`)]
104 + }
105 +
106 + const { family, address, port } = addr
107 +
108 + return getThinWaistAddresses(multiaddr(`/ip${family === 'IPv6' ? 6 : 4}/${address}/tcp/${port}`))
109 +}
110 +
111 +function isLinkLocal (ma: Multiaddr): boolean {
112 + // match private ip4/ip6 & loopback addresses
113 + if (isPrivate(ma)) {
114 + return true
115 + }
116 +
117 + return false
118 +}
119 +
120 +interface Client {
121 + pushable: Pushable<Uint8Array>
122 + socket: Socket
123 +}
124 +
125 +class TCPPortMessages extends TypedEventEmitter<ChannelMessages> {
126 + private readonly log: Logger
127 + public server?: Server
128 + private clients: Client[]
129 + private readonly listenOptions: ListenOptions | string | number
130 + private advertisement: MDNSPortAdvertisement
131 +
132 + constructor (components: MessagesComponents, init: MessagesInit) {
133 + super()
134 +
135 + this.log = components.logger.forComponent('libp2p:inspector-metrics:messages')
136 + this.clients = []
137 + this.listenOptions = init.listenOptions ?? 0
138 +
139 + this.advertisement = new MDNSPortAdvertisement(components, {
140 + ...init,
141 + messages: this
142 + })
143 +
144 + this.onSocket = this.onSocket.bind(this)
145 + }
146 +
147 + async start (): Promise<void> {
148 + this.server = createServer(this.onSocket)
149 +
150 + await new Promise<void>((resolve, reject) => {
151 + this.server?.listen(this.listenOptions, () => {
152 + resolve()
153 + })
154 + this.server?.on('error', (err) => {
155 + reject(err)
156 + })
157 + })
158 +
159 + await start(this.advertisement)
160 + }
161 +
162 + async stop (): Promise<void> {
163 + await stop(this.advertisement)
164 + this.server?.close()
165 + this.clients.forEach(({ socket }) => {
166 + socket.destroy()
167 + })
168 + }
169 +
170 + onSocket (socket: Socket): void {
171 + const client = {
172 + socket,
173 + pushable: pushable()
174 + }
175 +
176 + this.clients.push(client)
177 +
178 + const duplexSocket = duplex(socket)
179 +
180 + Promise.all([
181 + duplexSocket.sink(encode(client.pushable)),
182 + (async () => {
183 + for await (const buf of decode(duplexSocket.source)) {
184 + try {
185 + const data = JSON.parse(uint8ArrayToString(buf.subarray()))
186 +
187 + this.dispatchEvent(new MessageEvent(this, data))
188 + } catch (err) {
189 + // eslint-disable-next-line no-console
190 + console.error('could not parse message', uint8ArrayToString(buf.subarray()), err)
191 + }
192 + }
193 + })()
194 + ])
195 + .catch(err => {
196 + // eslint-disable-next-line no-console
197 + console.error('socket error during messages decode', err)
198 + })
199 + }
200 +
201 + postMessage (message: InspectorMessage): void {
202 + const buf = uint8ArrayFromString(JSON.stringify(message))
203 + this.clients.forEach(({ pushable }) => {
204 + pushable.push(buf)
205 + })
206 + }
207 +}
208 +
209 +export function messages (components: MessagesComponents, init: MessagesInit = {}): Messages {
210 + return new TCPPortMessages(components, init)
211 +}
packages/libp2p-inspector-metrics/src/rpc/codecs/cid.ts new
+9
@@ -0,0 +1,9 @@
1 +import { CID } from 'multiformats/cid'
2 +import type { ValueCodec } from 'it-rpc'
3 +
4 +export const cidCodec: ValueCodec<CID> = {
5 + type: 4096,
6 + canEncode: (val) => val.code != null && val.version != null && val.multihash != null && val['/'] != null,
7 + encode: (val) => val.bytes,
8 + decode: (buf) => CID.decode(buf)
9 +}
packages/libp2p-inspector-metrics/src/rpc/codecs/custom-progress-event.ts new
+17
@@ -0,0 +1,17 @@
1 +import { encode, decode } from 'cborg'
2 +import { CustomProgressEvent } from 'progress-events'
3 +import type { ValueCodec } from 'it-rpc'
4 +
5 +export const customProgressEventCodec: ValueCodec<CustomProgressEvent> = {
6 + type: 4099,
7 + canEncode: (val) => val instanceof CustomProgressEvent,
8 + encode: (val, codec, context, invocation) => encode({
9 + type: val.type,
10 + detail: codec.toValue(val.detail, context, invocation)
11 + }),
12 + decode: (val, codec, pushable, invocation) => {
13 + const { type, detail } = decode(val)
14 +
15 + return new CustomProgressEvent(type, codec.fromValue(detail, pushable, invocation))
16 + }
17 +}
packages/libp2p-inspector-metrics/src/rpc/codecs/multiaddr.ts new
+10
@@ -0,0 +1,10 @@
1 +import { isMultiaddr, multiaddr } from '@multiformats/multiaddr'
2 +import type { Multiaddr } from '@multiformats/multiaddr'
3 +import type { ValueCodec } from 'it-rpc'
4 +
5 +export const multiaddrCodec: ValueCodec<Multiaddr> = {
6 + type: 4097,
7 + canEncode: (val) => isMultiaddr(val),
8 + encode: (val) => val.bytes,
9 + decode: (buf) => multiaddr(buf)
10 +}
packages/libp2p-inspector-metrics/src/rpc/codecs/peer-id.ts new
+12
@@ -0,0 +1,12 @@
1 +import { isPeerId } from '@libp2p/interface'
2 +import { peerIdFromMultihash } from '@libp2p/peer-id'
3 +import * as Digest from 'multiformats/hashes/digest'
4 +import type { PeerId } from '@libp2p/interface'
5 +import type { ValueCodec } from 'it-rpc'
6 +
7 +export const peerIdCodec: ValueCodec<PeerId> = {
8 + type: 4098,
9 + canEncode: (val) => isPeerId(val),
10 + encode: (val) => val.toMultihash().bytes,
11 + decode: (buf) => peerIdFromMultihash(Digest.decode(buf))
12 +}
packages/libp2p-inspector-metrics/src/rpc/index.ts new
+168
@@ -0,0 +1,168 @@
1 +import { cidCodec } from './codecs/cid.js'
2 +import { customProgressEventCodec } from './codecs/custom-progress-event.js'
3 +import { multiaddrCodec } from './codecs/multiaddr.js'
4 +import { peerIdCodec } from './codecs/peer-id.js'
5 +import type { ContentRouting, PeerId, PeerRouting, AbortOptions, PubSubRPCMessage, SubscriptionChangeData, IdentifyResult, Direction, ConnectionTimeline, ConnectionStatus, ConnectionLimits } from '@libp2p/interface'
6 +import type { OpenConnectionOptions } from '@libp2p/interface-internal'
7 +import type { Multiaddr } from '@multiformats/multiaddr'
8 +import type { ValueCodec } from 'it-rpc'
9 +
10 +export const valueCodecs: Array<ValueCodec<any>> = [
11 + cidCodec,
12 + multiaddrCodec,
13 + peerIdCodec,
14 + customProgressEventCodec
15 +]
16 +
17 +export interface PeerAddress {
18 + multiaddr: Multiaddr
19 + isConnected?: boolean
20 + isCertified?: boolean
21 +}
22 +
23 +export interface Peer {
24 + /**
25 + * The identifier of the remote peer
26 + */
27 + id: PeerId
28 +
29 + /**
30 + * The list of addresses the peer has that we know about
31 + */
32 + addresses: PeerAddress[]
33 +
34 + /**
35 + * Any peer store tags the peer has
36 + */
37 + tags: Record<string, number>
38 +
39 + /**
40 + * Any peer store metadata the peer has
41 + */
42 + metadata: Record<string, string>
43 +
44 + /**
45 + * The protocols the peer supports, if known
46 + */
47 + protocols: string[]
48 +}
49 +
50 +export interface Connection {
51 + id: string
52 + remoteAddr: Multiaddr
53 + remotePeer: PeerId
54 + tags: string[]
55 + direction: Direction
56 + timeline: ConnectionTimeline
57 + multiplexer?: string
58 + encryption?: string
59 + status: ConnectionStatus
60 + limits?: ConnectionLimits
61 + rtt?: number
62 +}
63 +
64 +/**
65 + * RPC operations exposed by the metrics
66 + */
67 +export interface MetricsRPC {
68 + /**
69 + * Called by DevTools on initial connect
70 + */
71 + init(options?: AbortOptions): Promise<{ self: Peer, peers: Peer[], debug: string, capabilities: Record<string, string[]> }>
72 +
73 + /**
74 + * Update the currently active debugging namespaces
75 + */
76 + setDebug(namespace?: string): Promise<void>
77 +
78 + /**
79 + * Open a connection to the passed peer or multiaddr
80 + */
81 + openConnection(peerIdOrMultiaddr: string, options?: OpenConnectionOptions): Promise<Connection>
82 +
83 + /**
84 + * Close connections open to the specified peer
85 + */
86 + closeConnection(peerId: PeerId, options?: AbortOptions): Promise<void>
87 +
88 + /**
89 + * Make content routing queries
90 + */
91 + contentRouting: ContentRouting
92 +
93 + /**
94 + * Make peer routing queries
95 + */
96 + peerRouting: PeerRouting
97 +
98 + /**
99 + * PubSub operations
100 + */
101 + pubsub: {
102 + /**
103 + * Subscribe to a PubSub topic
104 + */
105 + subscribe(component: string, topic: string): Promise<void>
106 +
107 + /**
108 + * Unsubscribe from a PubSub topic
109 + */
110 + unsubscribe(component: string, topic: string): Promise<void>
111 +
112 + /**
113 + * Get the list of subscriptions for the current node
114 + */
115 + getTopics (component: string): Promise<string[]>
116 +
117 + /**
118 + * Get the list of peers we know about who subscribe to the topic
119 + */
120 + getSubscribers (component: string, topic: string): Promise<PeerId[]>
121 +
122 + /**
123 + * Publish a message to a given topic
124 + */
125 + publish (component: string, topic: string, message: Uint8Array): Promise<void>
126 + },
127 +
128 + /**
129 + * Ping a remote node and return the RTT
130 + */
131 + ping(component: string, peerIdOrMultiaddr: string, options?: AbortOptions): Promise<number>
132 +
133 + identify(component: string, peerIdOrMultiaddr: string, options?: AbortOptions): Promise<IdentifyResult>
134 +}
135 +
136 +export interface InspectorRPCEvents {
137 + /**
138 + * Node metrics have been updated
139 + */
140 + metrics: CustomEvent<Record<string, any>>
141 +
142 + /**
143 + * The node's status has changed - new addresses and/or protocols, etc
144 + */
145 + self: CustomEvent<Peer>
146 +
147 + /**
148 + * The node's connected peers have changed
149 + */
150 + peers: CustomEvent<Peer[]>
151 +
152 + /**
153 + * A pubsub message was received
154 + */
155 + 'pubsub:message': CustomEvent<PubSubRPCMessage>
156 +
157 + /**
158 + * The subscriptions of a peer have changed
159 + */
160 + 'pubsub:subscription-change': CustomEvent<SubscriptionChangeData>
161 +}
162 +
163 +/**
164 + * RPC operations exposed by the DevTools
165 + */
166 +export interface InspectorRPC {
167 + safeDispatchEvent<Detail>(type: keyof InspectorRPCEvents, detail?: CustomEventInit<Detail>, options?: AbortOptions): Promise<void>
168 +}
packages/libp2p-inspector-metrics/src/rpc/rpc.ts new
+115
@@ -0,0 +1,115 @@
1 +import { enable, disable } from '@libp2p/logger'
2 +import { peerIdFromString } from '@libp2p/peer-id'
3 +import { multiaddr } from '@multiformats/multiaddr'
4 +import { gatherCapabilities } from '../utils/gather-capabilities.js'
5 +import { getComponent } from '../utils/get-implementation.ts'
6 +import { getPeers } from '../utils/get-peers.js'
7 +import { getPubSub } from '../utils/get-pubsub.js'
8 +import { getSelf } from '../utils/get-self.js'
9 +import type { MetricsRPC } from './index.js'
10 +import type { InspectorMetricsComponents } from '../index.js'
11 +import type { Identify } from '@libp2p/identify'
12 +import type { PeerId } from '@libp2p/interface'
13 +import type { Ping } from '@libp2p/ping'
14 +import type { Multiaddr } from '@multiformats/multiaddr'
15 +
16 +function toPeerIdOrMultiaddr (peerIdOrMultiaddr: string): PeerId | Multiaddr {
17 + let peer: PeerId | Multiaddr
18 +
19 + try {
20 + peer = peerIdFromString(peerIdOrMultiaddr)
21 + } catch {
22 + peer = multiaddr(peerIdOrMultiaddr)
23 + }
24 +
25 + return peer
26 +}
27 +
28 +export function metricsRpc (components: InspectorMetricsComponents): MetricsRPC {
29 + const log = components.logger.forComponent('libp2p:devtools-metrics:metrics-rpc')
30 + let debug = globalThis.localStorage?.getItem('debug') ?? globalThis.process?.env?.DEBUG ?? ''
31 +
32 + return {
33 + init: async () => {
34 + return {
35 + self: await getSelf(components),
36 + peers: await getPeers(components, log),
37 + debug,
38 + capabilities: gatherCapabilities(components)
39 + }
40 + },
41 + setDebug: async (namespace?) => {
42 + if (namespace?.length != null && namespace?.length > 0) {
43 + enable(namespace)
44 + globalThis.localStorage?.setItem('debug', namespace)
45 + } else {
46 + disable()
47 + globalThis.localStorage?.removeItem('debug')
48 + }
49 +
50 + debug = namespace ?? ''
51 + },
52 + openConnection: async (peerIdOrMultiaddr, options?) => {
53 + const peer = toPeerIdOrMultiaddr(peerIdOrMultiaddr)
54 + const conn = await components.connectionManager.openConnection(peer, options)
55 +
56 + return {
57 + id: conn.id,
58 + remoteAddr: conn.remoteAddr,
59 + remotePeer: conn.remotePeer,
60 + tags: conn.tags,
61 + direction: conn.direction,
62 + timeline: conn.timeline,
63 + multiplexer: conn.multiplexer,
64 + encryption: conn.encryption,
65 + status: conn.status,
66 + limits: conn.limits,
67 + rtt: conn.rtt
68 + }
69 + },
70 + closeConnection: async (peerId, options?) => {
71 + await Promise.all(
72 + components.connectionManager.getConnections(peerId)
73 + .map(async connection => {
74 + try {
75 + await connection.close(options)
76 + } catch (err: any) {
77 + connection.abort(err)
78 + }
79 + })
80 + )
81 + },
82 + contentRouting: components.contentRouting,
83 + peerRouting: components.peerRouting,
84 + pubsub: {
85 + async getTopics (component) {
86 + return getPubSub(component, components).getTopics()
87 + },
88 + async subscribe (component, topic) {
89 + getPubSub(component, components).subscribe(topic)
90 + },
91 + async unsubscribe (component, topic) {
92 + getPubSub(component, components).unsubscribe(topic)
93 + },
94 + async publish (component, topic, message) {
95 + await getPubSub(component, components).publish(topic, message)
96 + },
97 + async getSubscribers (component: string, topic: string) {
98 + return getPubSub(component, components).getSubscribers(topic)
99 + }
100 + },
101 + ping: async (component, peerIdOrMultiaddr, options) => {
102 + const ping = getComponent<Ping>(component, components, '@libp2p/ping')
103 + const peer = toPeerIdOrMultiaddr(peerIdOrMultiaddr)
104 +
105 + return ping.ping(peer, options)
106 + },
107 + identify: async (component, peerIdOrMultiaddr, options) => {
108 + const identify = getComponent<Identify>(component, components, '@libp2p/identify')
109 + const peer = toPeerIdOrMultiaddr(peerIdOrMultiaddr)
110 + const connection = await components.connectionManager.openConnection(peer, options)
111 +
112 + return identify.identify(connection, options)
113 + }
114 + }
115 +}
packages/libp2p-inspector-metrics/src/utils/debounce.ts new
+23
@@ -0,0 +1,23 @@
1 +export function debounce (callback: () => void, wait: number = 100): () => void {
2 + let timeout: ReturnType<typeof setTimeout>
3 + let start: number | undefined
4 +
5 + return (): void => {
6 + if (start == null) {
7 + start = Date.now()
8 + }
9 +
10 + if (timeout != null && Date.now() - start > wait) {
11 + clearTimeout(timeout)
12 + start = undefined
13 + callback()
14 + return
15 + }
16 +
17 + clearTimeout(timeout)
18 + timeout = setTimeout(() => {
19 + start = undefined
20 + callback()
21 + }, wait)
22 + }
23 +}
packages/libp2p-inspector-metrics/src/utils/find-capability.ts new
+9
@@ -0,0 +1,9 @@
1 +import { gatherCapabilities } from './gather-capabilities.js'
2 +
3 +export function findCapability (capability: string, components: any): any | undefined {
4 + for (const [name, capabilities] of Object.entries(gatherCapabilities(components))) {
5 + if (capabilities.includes(capability)) {
6 + return components[name]
7 + }
8 + }
9 +}
packages/libp2p-inspector-metrics/src/utils/gather-capabilities.ts new
+14
@@ -0,0 +1,14 @@
1 +import { serviceCapabilities } from '@libp2p/interface'
2 +
3 +export function gatherCapabilities (components: any): Record<string, string[]> {
4 + const capabilities: Record<string, string[]> = {}
5 + const services: Record<string, any> = components.components ?? components
6 +
7 + Object.entries(services).forEach(([name, component]) => {
8 + if (component?.[serviceCapabilities] != null && Array.isArray(component[serviceCapabilities])) {
9 + capabilities[name] = component[serviceCapabilities]
10 + }
11 + })
12 +
13 + return capabilities
14 +}
packages/libp2p-inspector-metrics/src/utils/get-implementation.ts new
+11
@@ -0,0 +1,11 @@
1 +import { InvalidParametersError, serviceCapabilities } from '@libp2p/interface'
2 +
3 +export function getComponent <T> (component: string, components: any, capability: string): T {
4 + const output = components[component]
5 +
6 + if (output[serviceCapabilities]?.includes(capability) === true) {
7 + return output
8 + }
9 +
10 + throw new InvalidParametersError(`Component ${component} did not have capabilitiy ${capability}`)
11 +}
packages/libp2p-inspector-metrics/src/utils/get-peers.ts new
+53
@@ -0,0 +1,53 @@
1 +import { base64 } from 'multiformats/bases/base64'
2 +import { toObject } from './to-object.js'
3 +import type { Peer } from '../rpc/index.js'
4 +import type { Logger, PeerStore } from '@libp2p/interface'
5 +import type { ConnectionManager } from '@libp2p/interface-internal'
6 +
7 +export interface Components {
8 + connectionManager: ConnectionManager
9 + peerStore: PeerStore
10 +}
11 +
12 +export async function getPeers (components: Components, log: Logger): Promise<Peer[]> {
13 + const peers: Peer[] = []
14 + const connections = components.connectionManager.getConnectionsMap()
15 + const connectedAddresses = [...connections.values()].flatMap(conn => conn).map(conn => conn.remoteAddr.toString())
16 +
17 + for (const [peerId, conns] of connections.entries()) {
18 + try {
19 + const peer = await components.peerStore.get(peerId)
20 +
21 + peers.push({
22 + id: peerId,
23 + addresses: peer.addresses.map(({ isCertified, multiaddr }) => {
24 + return {
25 + multiaddr,
26 + isCertified,
27 + isConnected: connectedAddresses.includes(multiaddr.toString())
28 + }
29 + }),
30 + protocols: [...peer.protocols],
31 + tags: toObject(peer.tags, (t) => t.value),
32 + metadata: toObject(peer.metadata, (buf) => base64.encode(buf))
33 + })
34 + } catch (err) {
35 + log.error('could not load peer data from peer store', err)
36 +
37 + peers.push({
38 + id: peerId,
39 + addresses: conns.map(conn => {
40 + return {
41 + multiaddr: conn.remoteAddr,
42 + isConnected: connectedAddresses.includes(conn.remoteAddr.toString())
43 + }
44 + }),
45 + protocols: [],
46 + tags: {},
47 + metadata: {}
48 + })
49 + }
50 + }
51 +
52 + return peers
53 +}
packages/libp2p-inspector-metrics/src/utils/get-pubsub.ts new
+12
@@ -0,0 +1,12 @@
1 +import { InvalidParametersError, isPubSub } from '@libp2p/interface'
2 +import type { PubSub } from '@libp2p/interface'
3 +
4 +export function getPubSub (component: string, components: any): PubSub {
5 + const pubsub = components[component]
6 +
7 + if (!isPubSub(pubsub)) {
8 + throw new InvalidParametersError(`Component ${component} did not implement the PubSub interface`)
9 + }
10 +
11 + return pubsub
12 +}
packages/libp2p-inspector-metrics/src/utils/get-self.ts new
+21
@@ -0,0 +1,21 @@
1 +import { base64 } from 'multiformats/bases/base64'
2 +import { toObject } from './to-object.js'
3 +import type { Peer } from '../rpc/index.js'
4 +import type { PeerId, PeerStore } from '@libp2p/interface'
5 +
6 +export interface Components {
7 + peerId: PeerId
8 + peerStore: PeerStore
9 +}
10 +
11 +export async function getSelf (components: Components): Promise<Peer> {
12 + const peer = await components.peerStore.get(components.peerId)
13 +
14 + return {
15 + id: peer.id,
16 + addresses: peer.addresses,
17 + protocols: [...peer.protocols],
18 + tags: toObject(peer.tags, (t) => t.value),
19 + metadata: toObject(peer.metadata, (buf) => base64.encode(buf))
20 + }
21 +}
packages/libp2p-inspector-metrics/src/utils/to-object.ts new
+9
@@ -0,0 +1,9 @@
1 +export function toObject <T, R> (map: Map<string, T>, transform: (value: T) => R): Record<string, R> {
2 + const output: Record<string, any> = {}
3 +
4 + for (const [key, value] of map.entries()) {
5 + output[key] = transform(value)
6 + }
7 +
8 + return output
9 +}
packages/libp2p-inspector-metrics/test/index.spec.ts new
+100
@@ -0,0 +1,100 @@
1 +import { generateKeyPair } from '@libp2p/crypto/keys'
2 +import { TypedEventEmitter, start, stop } from '@libp2p/interface'
3 +import { defaultLogger } from '@libp2p/logger'
4 +import { peerIdFromPrivateKey } from '@libp2p/peer-id'
5 +import { expect } from 'aegir/chai'
6 +import { stubInterface } from 'sinon-ts'
7 +import { LIBP2P_INSPECTOR_METRICS_KEY, inspectorMetrics } from '../src/index.js'
8 +import type { ComponentLogger, ContentRouting, Libp2pEvents, Metrics, NodeInfo, PeerId, PeerRouting, PeerStore } from '@libp2p/interface'
9 +import type { AddressManager, ConnectionManager, Registrar, TransportManager } from '@libp2p/interface-internal'
10 +import type { StubbedInstance } from 'sinon-ts'
11 +
12 +interface StubbedComponents {
13 + logger: ComponentLogger
14 + events: TypedEventEmitter<Libp2pEvents>
15 + peerId: PeerId
16 + transportManager: StubbedInstance<TransportManager>
17 + registrar: StubbedInstance<Registrar>
18 + connectionManager: StubbedInstance<ConnectionManager>
19 + peerStore: StubbedInstance<PeerStore>
20 + contentRouting: StubbedInstance<ContentRouting>
21 + peerRouting: StubbedInstance<PeerRouting>
22 + addressManager: StubbedInstance<AddressManager>
23 + nodeInfo: NodeInfo
24 +}
25 +
26 +describe('inspector-metrics', () => {
27 + let components: StubbedComponents
28 + let metrics: Metrics
29 +
30 + beforeEach(async () => {
31 + components = {
32 + logger: defaultLogger(),
33 + events: new TypedEventEmitter(),
34 + peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')),
35 + transportManager: stubInterface(),
36 + registrar: stubInterface(),
37 + connectionManager: stubInterface(),
38 + peerStore: stubInterface(),
39 + contentRouting: stubInterface(),
40 + peerRouting: stubInterface(),
41 + addressManager: stubInterface(),
42 + nodeInfo: {
43 + name: 'test',
44 + userAgent: 'test',
45 + version: 'test'
46 + }
47 + }
48 +
49 + metrics = inspectorMetrics({
50 + intervalMs: 10
51 + })(components)
52 +
53 + await start(metrics)
54 + })
55 +
56 + afterEach(async () => {
57 + await stop(metrics)
58 + })
59 + /*
60 + it('should broadcast metrics', async () => {
61 + const event = await raceEvent<MessageEvent<ApplicationMessage>>(window, 'message', AbortSignal.timeout(1000), {
62 + filter: (evt) => {
63 + return evt.data.source === SOURCE_METRICS && evt.data.type === 'metrics'
64 + }
65 + })
66 +
67 + expect(event).to.have.nested.property('data.metrics')
68 + })
69 +
70 + it('should identify node', async () => {
71 + components.transportManager.getListeners.returns([])
72 + components.registrar.getProtocols.returns([])
73 + components.peerStore.get.withArgs(components.peerId).resolves({
74 + id: components.peerId,
75 + addresses: [],
76 + metadata: new Map(),
77 + protocols: [],
78 + tags: new Map()
79 + })
80 +
81 + // devtools asks the node to reveal itself
82 + window.postMessage({
83 + source: SOURCE_DEVTOOLS,
84 + type: 'identify'
85 + }, '*')
86 +
87 + const event = await raceEvent<MessageEvent<ApplicationMessage>>(window, 'message', AbortSignal.timeout(1000), {
88 + filter: (evt) => {
89 + return evt.data.source === SOURCE_METRICS && evt.data.type === 'self'
90 + }
91 + })
92 +
93 + expect(event).to.have.nested.property('data.peer.id')
94 + })
95 +*/
96 +
97 + it('should signal presence of metrics', () => {
98 + expect(globalThis).to.have.property(LIBP2P_INSPECTOR_METRICS_KEY).that.is.true()
99 + })
100 +})
packages/libp2p-inspector-metrics/tsconfig.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "extends": "aegir/src/config/tsconfig.aegir.json",
3 + "compilerOptions": {
4 + "outDir": "dist"
5 + },
6 + "include": [
7 + "package.json",
8 + "src",
9 + "test"
10 + ]
11 +}
packages/libp2p-inspector-metrics/typedoc.json new
+5
@@ -0,0 +1,5 @@
1 +{
2 + "entryPoints": [
3 + "./src/index.ts"
4 + ]
5 +}
packages/libp2p-inspector-ui/.aegir.js new
+12
@@ -0,0 +1,12 @@
1 +
2 +/** @type {import('aegir/types').PartialOptions} */
3 +export default {
4 + build: {
5 + bundlesizeMax: '18kB'
6 + },
7 + dependencyCheck: {
8 + ignore: [
9 + 'fast-glob'
10 + ]
11 + }
12 +}
packages/libp2p-inspector-ui/.gitignore new
+3
@@ -0,0 +1,3 @@
1 +dist
2 +node_modules
3 +.vscode
packages/libp2p-inspector-ui/CODE_OF_CONDUCT.md new
+3
@@ -0,0 +1,3 @@
1 +# Contributor Code of Conduct
2 +
3 +This project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
packages/libp2p-inspector-ui/LICENSE-APACHE new
+201
@@ -0,0 +1,201 @@
1 +Apache License
2 + Version 2.0, January 2004
3 + http://www.apache.org/licenses/
4 +
5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 +
7 + 1. Definitions.
8 +
9 + "License" shall mean the terms and conditions for use, reproduction,
10 + and distribution as defined by Sections 1 through 9 of this document.
11 +
12 + "Licensor" shall mean the copyright owner or entity authorized by
13 + the copyright owner that is granting the License.
14 +
15 + "Legal Entity" shall mean the union of the acting entity and all
16 + other entities that control, are controlled by, or are under common
17 + control with that entity. For the purposes of this definition,
18 + "control" means (i) the power, direct or indirect, to cause the
19 + direction or management of such entity, whether by contract or
20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 + outstanding shares, or (iii) beneficial ownership of such entity.
22 +
23 + "You" (or "Your") shall mean an individual or Legal Entity
24 + exercising permissions granted by this License.
25 +
26 + "Source" form shall mean the preferred form for making modifications,
27 + including but not limited to software source code, documentation
28 + source, and configuration files.
29 +
30 + "Object" form shall mean any form resulting from mechanical
31 + transformation or translation of a Source form, including but
32 + not limited to compiled object code, generated documentation,
33 + and conversions to other media types.
34 +
35 + "Work" shall mean the work of authorship, whether in Source or
36 + Object form, made available under the License, as indicated by a
37 + copyright notice that is included in or attached to the work
38 + (an example is provided in the Appendix below).
39 +
40 + "Derivative Works" shall mean any work, whether in Source or Object
41 + form, that is based on (or derived from) the Work and for which the
42 + editorial revisions, annotations, elaborations, or other modifications
43 + represent, as a whole, an original work of authorship. For the purposes
44 + of this License, Derivative Works shall not include works that remain
45 + separable from, or merely link (or bind by name) to the interfaces of,
46 + the Work and Derivative Works thereof.
47 +
48 + "Contribution" shall mean any work of authorship, including
49 + the original version of the Work and any modifications or additions
50 + to that Work or Derivative Works thereof, that is intentionally
51 + submitted to Licensor for inclusion in the Work by the copyright owner
52 + or by an individual or Legal Entity authorized to submit on behalf of
53 + the copyright owner. For the purposes of this definition, "submitted"
54 + means any form of electronic, verbal, or written communication sent
55 + to the Licensor or its representatives, including but not limited to
56 + communication on electronic mailing lists, source code control systems,
57 + and issue tracking systems that are managed by, or on behalf of, the
58 + Licensor for the purpose of discussing and improving the Work, but
59 + excluding communication that is conspicuously marked or otherwise
60 + designated in writing by the copyright owner as "Not a Contribution."
61 +
62 + "Contributor" shall mean Licensor and any individual or Legal Entity
63 + on behalf of whom a Contribution has been received by Licensor and
64 + subsequently incorporated within the Work.
65 +
66 + 2. Grant of Copyright License. Subject to the terms and conditions of
67 + this License, each Contributor hereby grants to You a perpetual,
68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 + copyright license to reproduce, prepare Derivative Works of,
70 + publicly display, publicly perform, sublicense, and distribute the
71 + Work and such Derivative Works in Source or Object form.
72 +
73 + 3. Grant of Patent License. Subject to the terms and conditions of
74 + this License, each Contributor hereby grants to You a perpetual,
75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 + (except as stated in this section) patent license to make, have made,
77 + use, offer to sell, sell, import, and otherwise transfer the Work,
78 + where such license applies only to those patent claims licensable
79 + by such Contributor that are necessarily infringed by their
80 + Contribution(s) alone or by combination of their Contribution(s)
81 + with the Work to which such Contribution(s) was submitted. If You
82 + institute patent litigation against any entity (including a
83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
84 + or a Contribution incorporated within the Work constitutes direct
85 + or contributory patent infringement, then any patent licenses
86 + granted to You under this License for that Work shall terminate
87 + as of the date such litigation is filed.
88 +
89 + 4. Redistribution. You may reproduce and distribute copies of the
90 + Work or Derivative Works thereof in any medium, with or without
91 + modifications, and in Source or Object form, provided that You
92 + meet the following conditions:
93 +
94 + (a) You must give any other recipients of the Work or
95 + Derivative Works a copy of this License; and
96 +
97 + (b) You must cause any modified files to carry prominent notices
98 + stating that You changed the files; and
99 +
100 + (c) You must retain, in the Source form of any Derivative Works
101 + that You distribute, all copyright, patent, trademark, and
102 + attribution notices from the Source form of the Work,
103 + excluding those notices that do not pertain to any part of
104 + the Derivative Works; and
105 +
106 + (d) If the Work includes a "NOTICE" text file as part of its
107 + distribution, then any Derivative Works that You distribute must
108 + include a readable copy of the attribution notices contained
109 + within such NOTICE file, excluding those notices that do not
110 + pertain to any part of the Derivative Works, in at least one
111 + of the following places: within a NOTICE text file distributed
112 + as part of the Derivative Works; within the Source form or
113 + documentation, if provided along with the Derivative Works; or,
114 + within a display generated by the Derivative Works, if and
115 + wherever such third-party notices normally appear. The contents
116 + of the NOTICE file are for informational purposes only and
117 + do not modify the License. You may add Your own attribution
118 + notices within Derivative Works that You distribute, alongside
119 + or as an addendum to the NOTICE text from the Work, provided
120 + that such additional attribution notices cannot be construed
121 + as modifying the License.
122 +
123 + You may add Your own copyright statement to Your modifications and
124 + may provide additional or different license terms and conditions
125 + for use, reproduction, or distribution of Your modifications, or
126 + for any such Derivative Works as a whole, provided Your use,
127 + reproduction, and distribution of the Work otherwise complies with
128 + the conditions stated in this License.
129 +
130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
131 + any Contribution intentionally submitted for inclusion in the Work
132 + by You to the Licensor shall be under the terms and conditions of
133 + this License, without any additional terms or conditions.
134 + Notwithstanding the above, nothing herein shall supersede or modify
135 + the terms of any separate license agreement you may have executed
136 + with Licensor regarding such Contributions.
137 +
138 + 6. Trademarks. This License does not grant permission to use the trade
139 + names, trademarks, service marks, or product names of the Licensor,
140 + except as required for reasonable and customary use in describing the
141 + origin of the Work and reproducing the content of the NOTICE file.
142 +
143 + 7. Disclaimer of Warranty. Unless required by applicable law or
144 + agreed to in writing, Licensor provides the Work (and each
145 + Contributor provides its Contributions) on an "AS IS" BASIS,
146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 + implied, including, without limitation, any warranties or conditions
148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 + PARTICULAR PURPOSE. You are solely responsible for determining the
150 + appropriateness of using or redistributing the Work and assume any
151 + risks associated with Your exercise of permissions under this License.
152 +
153 + 8. Limitation of Liability. In no event and under no legal theory,
154 + whether in tort (including negligence), contract, or otherwise,
155 + unless required by applicable law (such as deliberate and grossly
156 + negligent acts) or agreed to in writing, shall any Contributor be
157 + liable to You for damages, including any direct, indirect, special,
158 + incidental, or consequential damages of any character arising as a
159 + result of this License or out of the use or inability to use the
160 + Work (including but not limited to damages for loss of goodwill,
161 + work stoppage, computer failure or malfunction, or any and all
162 + other commercial damages or losses), even if such Contributor
163 + has been advised of the possibility of such damages.
164 +
165 + 9. Accepting Warranty or Additional Liability. While redistributing
166 + the Work or Derivative Works thereof, You may choose to offer,
167 + and charge a fee for, acceptance of support, warranty, indemnity,
168 + or other liability obligations and/or rights consistent with this
169 + License. However, in accepting such obligations, You may act only
170 + on Your own behalf and on Your sole responsibility, not on behalf
171 + of any other Contributor, and only if You agree to indemnify,
172 + defend, and hold each Contributor harmless for any liability
173 + incurred by, or claims asserted against, such Contributor by reason
174 + of your accepting any such warranty or additional liability.
175 +
176 + END OF TERMS AND CONDITIONS
177 +
178 + APPENDIX: How to apply the Apache License to your work.
179 +
180 + To apply the Apache License to your work, attach the following
181 + boilerplate notice, with the fields enclosed by brackets "[]"
182 + replaced with your own identifying information. (Don't include
183 + the brackets!) The text should be enclosed in the appropriate
184 + comment syntax for the file format. We also recommend that a
185 + file or class name and description of purpose be included on the
186 + same "printed page" as the copyright notice for easier
187 + identification within third-party archives.
188 +
189 + Copyright [yyyy] [name of copyright owner]
190 +
191 + Licensed under the Apache License, Version 2.0 (the "License");
192 + you may not use this file except in compliance with the License.
193 + You may obtain a copy of the License at
194 +
195 + http://www.apache.org/licenses/LICENSE-2.0
196 +
197 + Unless required by applicable law or agreed to in writing, software
198 + distributed under the License is distributed on an "AS IS" BASIS,
199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 + See the License for the specific language governing permissions and
201 + limitations under the License.
packages/libp2p-inspector-ui/LICENSE-MIT new
+19
@@ -0,0 +1,19 @@
1 +The MIT License (MIT)
2 +
3 +Permission is hereby granted, free of charge, to any person obtaining a copy
4 +of this software and associated documentation files (the "Software"), to deal
5 +in the Software without restriction, including without limitation the rights
6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 +copies of the Software, and to permit persons to whom the Software is
8 +furnished to do so, subject to the following conditions:
9 +
10 +The above copyright notice and this permission notice shall be included in
11 +all copies or substantial portions of the Software.
12 +
13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 +THE SOFTWARE.
packages/libp2p-inspector-ui/README.md new
+32
@@ -0,0 +1,32 @@
1 +# js-libp2p inspector UI
2 +
3 +> The UI for the libp2p inspector
4 +
5 +A React app for inspecting a running libp2p node.
6 +
7 +Can be deployed as a Chrome DevTools extension, and Electron app or run in a
8 +browser.
9 +
10 +# @ipshipyard/libp2p-inspector-ui
11 +
12 +[![codecov](https://img.shields.io/codecov/c/github/ipshipyard/js-libp2p-inspector.svg?style=flat-square)](https://codecov.io/gh/ipshipyard/js-libp2p-inspector)
13 +[![CI](https://img.shields.io/github/actions/workflow/status/ipshipyard/js-libp2p-inspector/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipshipyard/js-libp2p-inspector/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
14 +
15 +> The user interface for inspecting a libp2p node
16 +
17 +# Install
18 +
19 +```console
20 +$ npm i @ipshipyard/libp2p-inspector-ui
21 +```
22 +
23 +# License
24 +
25 +Licensed under either of
26 +
27 +- Apache 2.0, ([LICENSE-APACHE](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-inspector-ui/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
28 +- MIT ([LICENSE-MIT](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-inspector-ui/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
29 +
30 +# Contribution
31 +
32 +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
packages/libp2p-inspector-ui/copy-assets.js new
+16
@@ -0,0 +1,16 @@
1 +import fs from 'node:fs/promises'
2 +import path from 'node:path'
3 +import fg from 'fast-glob'
4 +
5 +// copy assets to the dist folder so imports from relative paths still work
6 +for await (const asset of fg.stream([
7 + 'public/**/*',
8 + 'src/**/*.css'
9 +], {})) {
10 + const dest = path.join('dist', asset)
11 + const dir = path.join('dist', path.dirname(asset))
12 + await fs.mkdir(dir, {
13 + recursive: true
14 + })
15 + await fs.cp(asset, dest)
16 +}
packages/libp2p-inspector-ui/package.json new
+175
@@ -0,0 +1,175 @@
1 +{
2 + "name": "@ipshipyard/libp2p-inspector-ui",
3 + "version": "0.0.0",
4 + "description": "The user interface for inspecting a libp2p node",
5 + "author": "info@ipshipyard.com",
6 + "license": "Apache-2.0 OR MIT",
7 + "homepage": "https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-inspector-ui#readme",
8 + "repository": {
9 + "type": "git",
10 + "url": "git+https://github.com/ipshipyard/js-libp2p-inspector.git"
11 + },
12 + "bugs": {
13 + "url": "https://github.com/ipshipyard/js-libp2p-inspector/issues"
14 + },
15 + "publishConfig": {
16 + "access": "public",
17 + "provenance": true
18 + },
19 + "type": "module",
20 + "types": "./dist/src/index.d.ts",
21 + "typesVersions": {
22 + "*": {
23 + "*": [
24 + "*",
25 + "dist/*",
26 + "dist/src/*",
27 + "dist/src/*/index"
28 + ],
29 + "src/*": [
30 + "*",
31 + "dist/*",
32 + "dist/src/*",
33 + "dist/src/*/index"
34 + ]
35 + }
36 + },
37 + "files": [
38 + "dist",
39 + "public",
40 + "src",
41 + "!dist/test",
42 + "!**/*.tsbuildinfo"
43 + ],
44 + "exports": {
45 + ".": {
46 + "types": "./dist/src/index.d.ts",
47 + "import": "./dist/src/index.js"
48 + },
49 + "./index.css": {
50 + "import": "./dist/src/index.css"
51 + }
52 + },
53 + "release": {
54 + "branches": [
55 + "main"
56 + ],
57 + "plugins": [
58 + [
59 + "@semantic-release/commit-analyzer",
60 + {
61 + "preset": "conventionalcommits",
62 + "releaseRules": [
63 + {
64 + "breaking": true,
65 + "release": "major"
66 + },
67 + {
68 + "revert": true,
69 + "release": "patch"
70 + },
71 + {
72 + "type": "feat",
73 + "release": "minor"
74 + },
75 + {
76 + "type": "fix",
77 + "release": "patch"
78 + },
79 + {
80 + "type": "docs",
81 + "release": "patch"
82 + },
83 + {
84 + "type": "test",
85 + "release": "patch"
86 + },
87 + {
88 + "type": "deps",
89 + "release": "patch"
90 + },
91 + {
92 + "scope": "no-release",
93 + "release": false
94 + }
95 + ]
96 + }
97 + ],
98 + [
99 + "@semantic-release/release-notes-generator",
100 + {
101 + "preset": "conventionalcommits",
102 + "presetConfig": {
103 + "types": [
104 + {
105 + "type": "feat",
106 + "section": "Features"
107 + },
108 + {
109 + "type": "fix",
110 + "section": "Bug Fixes"
111 + },
112 + {
113 + "type": "chore",
114 + "section": "Trivial Changes"
115 + },
116 + {
117 + "type": "docs",
118 + "section": "Documentation"
119 + },
120 + {
121 + "type": "deps",
122 + "section": "Dependencies"
123 + },
124 + {
125 + "type": "test",
126 + "section": "Tests"
127 + }
128 + ]
129 + }
130 + }
131 + ],
132 + "@semantic-release/changelog",
133 + "@semantic-release/npm",
134 + "@semantic-release/github",
135 + [
136 + "@semantic-release/git",
137 + {
138 + "assets": [
139 + "CHANGELOG.md",
140 + "package.json"
141 + ]
142 + }
143 + ]
144 + ]
145 + },
146 + "scripts": {
147 + "clean": "aegir clean",
148 + "lint": "aegir lint",
149 + "build": "tsc && node copy-assets.js",
150 + "test": "echo \"Error: no test specified\" && exit 1",
151 + "dep-check": "aegir dep-check",
152 + "start:firefox": "web-ext run --source-dir . --verbose --devtools",
153 + "release": "aegir release"
154 + },
155 + "dependencies": {
156 + "@ipshipyard/libp2p-inspector-metrics": "~0.0.0",
157 + "@libp2p/interface": "^2.10.2",
158 + "@libp2p/peer-id": "^5.1.5",
159 + "@multiformats/multiaddr": "^12.4.0",
160 + "@multiformats/multiaddr-matcher": "^1.7.2",
161 + "multiformats": "^13.3.6",
162 + "react-icons": "^5.5.0",
163 + "uint8arrays": "^5.1.0"
164 + },
165 + "devDependencies": {
166 + "@types/react": "^19.1.5",
167 + "@types/react-dom": "^19.1.5",
168 + "aegir": "^47.0.13",
169 + "fast-glob": "^3.3.3"
170 + },
171 + "peerDependencies": {
172 + "react": "^19.1.0",
173 + "react-dom": "^19.1.0"
174 + }
175 +}
packages/libp2p-inspector-ui/public/img/grant-permissions-dark-chrome.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/grant-permissions-dark-chrome.png differ
packages/libp2p-inspector-ui/public/img/grant-permissions-dark-firefox.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/grant-permissions-dark-firefox.png differ
packages/libp2p-inspector-ui/public/img/grant-permissions-light-chrome.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/grant-permissions-light-chrome.png differ
packages/libp2p-inspector-ui/public/img/grant-permissions-light-firefox.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/grant-permissions-light-firefox.png differ
packages/libp2p-inspector-ui/public/img/helia.svg new
+17
@@ -0,0 +1,17 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="807" height="930" viewBox="0, 0, 807, 930">
4 + <g id="Layer_1">
5 + <g>
6 + <path d="M807,697.368 L807,232.63 L403.466,0.261 L0,232.63 L0,697.368 L403.466,929.739 L806.966,697.368 L807,697.368 z" fill="#073A53"/>
7 + <path d="M250.599,748.783 L217.869,790.127 L232.201,748.783 C232.201,748.783 250.582,749.266 250.582,748.783 L250.617,748.783 L250.599,748.783 z" fill="#F7DF1E"/>
8 + <path d="M190.272,659.205 C180.177,664.287 171.098,676.604 168.756,687.594 C166.413,698.585 162.744,719.98 147.24,725.097 C147.24,725.097 156.594,729.559 167.808,721.91 C179.023,714.279 187.894,698.189 185.862,684.718 C183.071,666.371 190.926,658.912 190.272,659.239 L190.272,659.205 z" fill="#F7DF1E"/>
9 + <path d="M470.049,583.201 C470.049,583.201 475.648,592.038 498.714,591.832 C521.764,591.591 526.69,582.34 516.372,570.488 C506.018,558.636 493.546,555.001 477.405,559.291 C461.246,563.614 445.536,575.88 455.028,594.381 C464.52,612.883 505.002,669.11 522.005,685.045 C522.005,685.045 537.112,661.134 539.731,642.116 C542.711,620.91 543.969,596.345 531.462,600.221 C518.99,604.063 481.488,614.743 470.049,583.184 L470.049,583.218 L470.049,583.201 z" fill="#F7DF1E"/>
10 + <path d="M583.107,545.509 C583.107,545.509 593.616,570.505 600.713,582.047 C607.81,593.623 611.91,597.034 632.531,604.304 C653.151,611.556 733.444,639.963 743.281,641.996 C743.281,641.996 662.712,583.856 654.96,577.982 C647.19,572.073 635.011,571.556 625.209,572.314 C609.878,573.434 595.907,565.286 583.073,545.544 L583.107,545.544 L583.107,545.509 z" fill="#F7DF1E"/>
11 + <path d="M613.822,636.845 C613.822,636.845 608.723,654.399 617.578,671.78 C626.467,689.162 636.303,691.402 646.105,692.745 C655.907,694.089 672.341,699.05 676.872,712.608 C676.872,712.608 682.35,695.019 667.07,682.805 C651.79,670.626 631.721,670.73 623.366,660.394 C613.667,648.387 614.684,642.685 613.84,636.81 L613.84,636.845 L613.822,636.845 z" fill="#F7DF1E"/>
12 + <path d="M560.144,690.213 L603.745,783.237 L565.881,740.928 C565.881,740.928 566.088,712.935 557.233,701.376 L560.11,690.213 L560.144,690.213 z" fill="#F7DF1E"/>
13 + <path d="M515.889,772.901 L510.756,810.799 L498.663,790.575 L515.889,772.901 z" fill="#F7DF1E"/>
14 + <path d="M721.954,525.957 C700.404,524.424 699.077,501.323 676.562,496.379 C654.047,491.435 632.496,499.721 608.827,492.365 L604.675,485.922 L608.706,476.758 L781.834,449.833 L608.672,422.89 C608.362,422.167 608.276,421.96 607.983,421.219 C613.65,413.226 623.16,403.304 634.667,404.992 C653.513,407.748 669.051,414.604 681.162,404.82 C693.272,395.035 694.323,377.584 716.614,383.269 C740.938,389.453 755.787,386.646 763.384,365.526 C763.384,365.526 744.986,374.208 727.897,365.887 C710.809,357.567 698.853,351.986 683.16,360.496 C667.466,369.005 659.232,367.386 652.703,365.216 C646.226,363.062 626.536,355.155 595.735,369.229 L731.015,260.288 L568.276,323.269 C568.362,313.432 570.498,299.289 581.23,294.087 C598.37,285.766 615.063,282.7 619.576,267.799 C624.09,252.898 615.149,237.894 636.769,230.038 C660.369,221.477 671.067,210.796 665.468,189.056 C665.468,189.056 655.149,206.576 636.338,209.315 C617.526,212.054 604.503,214.155 596.32,230.021 C588.138,245.887 580.403,249.177 573.805,251.055 C567.294,252.898 546.897,257.377 529.447,285.629 C529.102,285.456 529.24,285.525 528.895,285.353 L592.289,121.545 L482.16,258.324 C482.022,258.221 482.074,258.256 481.936,258.152 C497.733,228.936 491.445,209.022 489.791,202.441 C488.12,195.775 487.121,187.437 496.785,172.433 C506.449,157.428 501.781,145.094 494.787,127.419 C487.793,109.745 497.819,92.053 497.819,92.053 C476.182,98.031 472.272,112.639 476.595,137.359 C480.575,160.029 463.107,159.754 452.427,171.089 C441.763,182.424 447.431,198.428 448.757,217.429 C449.601,229.47 438.128,238.445 429.601,243.389 L402.745,70.727 L375.751,244.268 C375.182,244.233 375.406,244.25 374.838,244.199 C373.787,211.072 358.352,197.032 353.632,192.192 C348.843,187.265 343.778,180.564 344.605,162.734 C345.432,144.904 335.182,136.584 320.247,124.835 C305.311,113.087 305.105,92.742 305.105,92.742 C289.411,108.797 293.356,123.388 309.532,142.579 C324.364,160.167 309.118,168.729 305.604,183.888 C302.073,199.048 315.027,210.038 325.725,225.801 C332.357,235.568 327.327,248.712 322.538,257.291 L213.218,121.528 L276.612,285.336 C276.267,285.508 276.405,285.439 276.06,285.611 C258.61,257.36 238.214,252.898 231.702,251.038 C225.087,249.16 217.369,245.87 209.187,230.004 C201.004,214.138 187.981,212.037 169.169,209.298 C150.358,206.559 140.039,189.039 140.039,189.039 C134.44,210.779 145.138,221.46 168.739,230.021 C190.358,237.876 181.417,252.881 185.931,267.782 C190.444,282.683 207.137,285.749 224.277,294.07 C235.009,299.272 237.146,313.415 237.232,323.252 L74.492,260.271 L209.772,369.212 C178.971,355.138 159.281,363.045 152.804,365.198 C146.275,367.369 138.041,368.988 122.347,360.478 C106.654,351.968 94.716,357.55 77.61,365.87 C60.521,374.191 42.123,365.508 42.123,365.508 C49.72,386.628 64.569,389.436 88.893,383.252 C111.185,377.584 112.235,395.018 124.346,404.802 C136.456,414.587 151.977,407.731 170.84,404.975 C182.348,403.286 191.857,413.209 197.524,421.202 C197.214,421.926 197.128,422.132 196.835,422.873 L23.673,449.816 L196.801,476.741 L183.777,484.751 C178.799,487.817 175.009,492.486 173.045,497.981 L170.702,504.544 C168.187,511.59 162.951,517.378 156.112,520.376 C146.654,524.51 137.317,524.458 126.275,522.322 C108.256,518.808 93.493,524.338 81.469,532.452 C73.045,538.136 66.068,554.088 59.608,559.807 C53.148,565.527 37.885,560.927 37.885,560.927 C47.842,576.603 76.714,577.482 83.743,568.903 C96.456,553.365 116.421,550.161 133.286,555.57 C148.756,560.548 168.411,552.09 179.419,541.943 C189.858,532.365 189.014,520.169 203.691,511.521 C216.508,503.959 232.563,504.131 232.563,504.131 C232.012,511.556 228.997,524.889 219.54,534.657 C208.188,546.371 74.423,657.327 74.423,657.327 L220.487,587.37 C222.4,632.211 231.271,681.376 239.385,700.842 C242.882,709.197 247.688,717.018 253.528,724.925 C269.945,747.147 290.996,765.476 314.734,779.585 L314.906,779.481 C340.126,794.503 374.579,812.367 397.939,814.21 C414.459,810.834 417.009,800.928 417.009,786.027 C417.009,771.126 410.307,766.837 403.313,766.441 C396.302,766.027 391.617,761.462 387.207,755.157 C382.779,748.852 395.269,748.215 412.461,747.939 C429.67,747.629 433.133,739.36 437.043,729.266 C440.919,719.188 425.656,718.223 394.89,721.875 C368.877,724.942 360.678,720.721 358.576,717.173 C373.063,703.977 390.066,692.022 398.938,694.313 C413.839,698.258 416.716,689.817 413.081,687.267 C409.842,685.01 394.442,669.817 387.758,665.183 C387.052,664.666 379.902,660.066 380.178,659.257 C384.795,646.061 393.029,650.867 407.276,653.882 C421.522,656.879 434.407,640.428 431.513,634.864 C427.224,626.612 423.934,631.246 416.044,615.415 C407.913,581.961 415.803,526.543 450.687,516.396 C481.178,507.507 514.477,517.309 522.332,519.945 C523.107,520.221 523.865,519.532 523.727,518.756 L515.028,470.091 L508.327,439.428 C508.534,438.515 509.378,437.86 510.325,437.998 C546.398,442.942 570.567,487.817 582.401,504.682 C594.236,521.547 616.648,539.618 635.132,544.648 C653.633,549.73 661.144,541.444 673.651,541.788 C686.157,542.133 692.962,553.916 713.117,554.019 C733.238,554.123 740.628,544.648 752.583,544.476 C761.558,544.338 770.395,549.248 770.395,549.248 C766.037,537.585 743.47,527.508 721.902,525.974 L721.954,525.957 z M265.242,611.642 C269.256,622.667 250.979,629.541 243.364,620.721 C236.543,612.762 238.351,584.734 276.405,589.007 L276.439,589.007 C276.439,589.007 260.074,597.465 265.242,611.642 z M329.291,608.662 C291.806,613.296 282.073,588.696 282.073,588.696 L282.073,588.662 C307.034,597.361 326.19,599.963 328.757,581.995 C331.323,564.028 310.462,554.347 293.253,551.453 C276.732,548.696 255.767,542.564 255.767,534.071 C255.767,525.578 267.361,512.779 310.169,512.779 C352.977,512.779 364.243,536.999 366.965,548.3 C369.704,559.635 366.759,604.028 329.273,608.662 L329.291,608.662 z" fill="#F7DF1E"/>
15 + </g>
16 + </g>
17 +</svg>
packages/libp2p-inspector-ui/public/img/icon-connect.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="16" height="16" viewBox="0, 0, 16, 16">
4 + <g id="Background">
5 + <rect x="0" y="0" width="16" height="16" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="connect">
8 + <path d="M3.933,16 C2.867,16 1.867,15.6 1.133,14.867 C0.4,14.133 0,13.133 0,12.067 C0,11 0.4,10 1.133,9.267 L3.667,6.733 L5,8.067 L6.867,6.2 L7.8,7.133 L5.933,9 L7,10.067 L8.867,8.2 L9.8,9.133 L7.933,11 L9.267,12.333 L6.8,14.867 C6,15.533 5,16 3.933,16 z M3.667,8.6 L2.133,10.133 C1.6,10.667 1.333,11.333 1.333,12 C1.333,12.667 1.6,13.333 2.133,13.867 C3.133,14.867 4.867,14.867 5.867,13.867 L7.4,12.267 L3.667,8.6 z M12.333,9.267 L6.733,3.667 L9.2,1.133 C9.933,0.4 11,-0 12,-0 C13,-0 14,0.4 14.8,1.133 C15.6,1.867 16,2.867 16,3.933 C16,5 15.6,6 14.867,6.733 L12.333,9.267 z M8.667,3.667 L12.333,7.333 L13.867,5.8 C14.4,5.267 14.667,4.667 14.667,3.933 C14.667,3.267 14.4,2.6 13.867,2.067 C12.867,1.067 11.2,1.067 10.133,2.067 L8.667,3.667 z" fill="#868686"/>
9 + </g>
10 +</svg>
packages/libp2p-inspector-ui/public/img/icon-copy.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="512" height="512" viewBox="0, 0, 512, 512">
4 + <g id="Background">
5 + <rect x="0" y="0" width="512" height="512" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M240,0 L364.1,0 C376.8,0 389,5.1 398,14.1 L465.9,82 C474.9,91 480,103.2 480,115.9 L480,336 C480,362.5 458.5,384 432,384 L240,384 C213.5,384 192,362.5 192,336 L192,48 C192,21.5 213.5,0 240,0 z M80,128 L160,128 L160,192 L96,192 L96,448 L288,448 L288,416 L352,416 L352,464 C352,490.5 330.5,512 304,512 L80,512 C53.5,512 32,490.5 32,464 L32,176 C32,149.5 53.5,128 80,128 z" fill="#868686"/>
10 + </g>
11 +</svg>
packages/libp2p-inspector-ui/public/img/icon-delete.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="512" height="512" viewBox="0, 0, 512, 512">
4 + <g id="Background">
5 + <rect x="0" y="0" width="512" height="512" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M217.198,3.484 C200.98,3.484 185.065,6.667 173.546,18.035 C162.027,29.403 158.389,45.545 158.389,62.294 L81.391,62.294 C70.706,62.294 61.99,71.009 61.99,81.694 L42.589,81.694 L42.589,120.496 L469.411,120.496 L469.411,81.694 L450.01,81.694 C450.01,71.009 441.294,62.294 430.609,62.294 L353.611,62.294 C353.611,45.545 349.973,29.403 338.454,18.035 C326.935,6.667 311.02,3.484 294.802,3.484 z M217.198,43.499 L294.802,43.499 C305.412,43.499 308.746,46 309.959,47.137 C311.171,48.273 313.597,51.456 313.597,62.294 L198.403,62.294 C198.403,51.456 200.829,48.273 202.041,47.137 C203.254,46 206.588,43.499 217.198,43.499 z M81.391,139.897 L81.391,450.313 C81.391,482.37 107.537,508.516 139.594,508.516 L372.406,508.516 C404.463,508.516 430.609,482.37 430.609,450.313 L430.609,139.897 z M158.995,198.1 L197.797,198.1 L197.797,430.912 L158.995,430.912 z M236.599,198.1 L275.401,198.1 L275.401,430.912 L236.599,430.912 z M314.203,198.1 L353.005,198.1 L353.005,430.912 L314.203,430.912 z" fill="#868686"/>
10 + </g>
11 +</svg>
packages/libp2p-inspector-ui/public/img/icon-disclosure-triangle-closed.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="32" height="32" viewBox="0, 0, 32, 32">
4 + <g id="Background">
5 + <rect x="0" y="0" width="32" height="32" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <path d="M8.252,31.493 L8.252,0.501 L23.748,15.997 L8.252,31.493 z" fill="#6C6C6C"/>
9 + </g>
10 +</svg>
packages/libp2p-inspector-ui/public/img/icon-disclosure-triangle-open.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="32" height="32" viewBox="0, 0, 32, 32">
4 + <g id="Background">
5 + <rect x="0" y="0" width="32" height="32" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <path d="M0.504,8.249 L31.496,8.249 L16,23.745 L0.504,8.249 z" fill="#6C6C6C"/>
9 + </g>
10 +</svg>
packages/libp2p-inspector-ui/public/img/icon-spinner.svg new
+8
@@ -0,0 +1,8 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
4 + <path fill="#868686" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity="0.25"/>
5 + <path fill="#868686" d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z">
6 + <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
7 + </path>
8 +</svg>
packages/libp2p-inspector-ui/public/img/image.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/image.png differ
packages/libp2p-inspector-ui/public/img/libp2p-128.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/libp2p-128.png differ
packages/libp2p-inspector-ui/public/img/libp2p-16.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/libp2p-16.png differ
packages/libp2p-inspector-ui/public/img/libp2p-48.png
Binary files /dev/null and b/packages/libp2p-inspector-ui/public/img/libp2p-48.png differ
packages/libp2p-inspector-ui/public/img/libp2p-monochrome.svg new
+38
@@ -0,0 +1,38 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="159" height="159" viewBox="0, 0, 159, 159">
4 + <g id="Background">
5 + <rect x="0" y="0" width="159" height="159" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <g>
9 + <path d="M148.689,92.902 L125.815,105.916 L125.62,106.013 L125.62,132.397 L148.689,119.286 L148.689,92.902 z" fill="#6C6C6C"/>
10 + <path d="M125.62,106.013 L102.747,119.028 L102.552,119.125 L102.552,145.509 L125.62,132.397 L125.62,106.013 z" fill="#969797"/>
11 + <path d="M33.38,132.075 L33.38,105.691 L10.311,92.451 L10.311,118.803 L33.38,132.075 z" fill="#C4C4C4"/>
12 + <path d="M56.448,145.348 L56.448,118.964 L33.38,105.691 L33.38,132.075 L56.448,145.348 z" fill="#C4C4C4"/>
13 + <path d="M79.516,158.62 L79.516,132.236 L56.448,118.964 L56.448,145.348 L79.516,158.62 z" fill="#9C9C9C"/>
14 + <path d="M102.552,119.124 L79.712,132.139 L79.516,132.236 L79.516,158.62 L102.552,145.509 L102.552,119.124 z" fill="#969797"/>
15 + <path d="M148.689,66.582 L125.615,79.666 L125.588,106.045 L125.62,106.045 L148.655,92.966 L148.689,66.582 z" fill="#7D7D7D"/>
16 + <path d="M125.62,79.661 L102.747,92.676 L102.552,92.773 L102.552,119.157 L125.62,106.046 L125.62,79.661 L125.62,79.694 L125.62,79.661 z" fill="#7D7D7D"/>
17 + <path d="M33.38,105.691 L33.38,79.339 L10.311,66.034 L10.311,92.418 L33.38,105.691 z" fill="#C4C4C4"/>
18 + <path d="M56.415,118.963 L56.448,92.612 L33.38,79.307 L33.38,105.691 L56.415,118.963 z" fill="#ACACAC"/>
19 + <path d="M79.483,132.268 L79.483,105.884 L56.448,92.612 L56.415,118.963 L56.448,118.996 L79.483,132.268 z" fill="#696969"/>
20 + <path d="M102.552,92.773 L79.679,105.755 L79.484,105.884 L79.484,132.268 L102.552,119.157 L102.552,92.773 z" fill="#636363"/>
21 + <path d="M79.484,26.764 L79.755,26.614 L102.552,13.653 L79.484,0.38 L56.448,13.492 L79.484,26.764 z" fill="#B6B6B6"/>
22 + <path d="M102.552,40.037 L102.747,39.908 L125.62,26.925 L102.552,13.653 L79.484,26.764 L102.552,40.037 z" fill="#A6A6A6"/>
23 + <path d="M125.62,53.31 L125.815,53.181 L148.655,40.198 L125.62,26.925 L102.552,40.037 L125.62,53.31 z" fill="#8E8E8E"/>
24 + <path d="M148.689,40.198 L125.815,53.181 L125.62,53.309 L125.62,79.661 L148.689,66.582 L148.689,66.55 L148.689,40.198 z" fill="#7D7D7D"/>
25 + <path d="M56.448,39.875 L56.676,39.746 L79.483,26.764 L56.448,13.491 L33.38,26.603 L56.448,39.875 z" fill="#C2C2C2"/>
26 + <path d="M79.484,53.148 L102.552,40.037 L79.484,26.764 L56.448,39.876 L79.484,53.148 z" fill="#C2C2C2"/>
27 + <path d="M102.552,66.421 L125.62,53.309 L102.552,39.94 L102.421,39.94 L102.552,39.94 L79.418,53.18 L102.552,66.421 z" fill="#777777"/>
28 + <path d="M125.62,53.277 L102.747,66.292 L102.552,66.421 L102.548,92.787 L125.649,79.646 L125.62,53.277 z" fill="#515151"/>
29 + <path d="M33.38,52.987 L33.575,52.858 L56.448,39.876 L33.38,26.603 L10.311,39.715 L33.38,52.987 z" fill="#D0D0D0"/>
30 + <path d="M33.38,79.339 L33.38,52.987 L10.311,39.715 L10.311,66.099 L33.38,79.339 z" fill="#C4C4C4"/>
31 + <path d="M56.48,66.296 L79.516,53.153 L56.48,39.912 L33.412,53.024 L56.48,66.296 z" fill="#BBBBBB"/>
32 + <path d="M56.448,92.612 L56.448,66.26 L33.38,52.987 L33.38,79.339 L33.38,79.371 L56.448,92.612 z" fill="#ACACAC"/>
33 + <path d="M79.516,79.5 L79.712,79.403 L102.559,66.421 L79.516,53.148 L56.448,66.26 L79.516,79.5 z" fill="#BBBBBB"/>
34 + <path d="M79.48,105.883 L79.516,79.5 L56.448,66.227 L56.448,92.579 L56.448,92.612 L79.48,105.883 z" fill="#ACACAC"/>
35 + <path d="M102.552,66.421 L79.712,79.403 L79.516,79.5 L79.516,79.532 L79.477,105.9 L102.552,92.773 L102.552,66.421 z" fill="#B6B6B6"/>
36 + </g>
37 + </g>
38 +</svg>
packages/libp2p-inspector-ui/public/img/libp2p.svg new
+38
@@ -0,0 +1,38 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="159" height="159" viewBox="0, 0, 159, 159">
4 + <g id="Background">
5 + <rect x="0" y="0" width="159" height="159" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <g>
9 + <path d="M148.689,92.902 L125.815,105.916 L125.62,106.013 L125.62,132.397 L148.689,119.286 L148.689,92.902 z" fill="#A33768"/>
10 + <path d="M125.62,106.013 L102.747,119.028 L102.552,119.125 L102.552,145.509 L125.62,132.397 L125.62,106.013 z" fill="#3DA1BD"/>
11 + <path d="M33.38,132.075 L33.38,105.691 L10.311,92.451 L10.311,118.803 L33.38,132.075 z" fill="#81D2E0"/>
12 + <path d="M56.448,145.348 L56.448,118.964 L33.38,105.691 L33.38,132.075 L56.448,145.348 z" fill="#81D2E0"/>
13 + <path d="M79.516,158.62 L79.516,132.236 L56.448,118.964 L56.448,145.348 L79.516,158.62 z" fill="#36A4CB"/>
14 + <path d="M102.552,119.124 L79.712,132.139 L79.516,132.236 L79.516,158.62 L102.552,145.509 L102.552,119.124 z" fill="#3DA1BD"/>
15 + <path d="M148.689,66.582 L125.615,79.666 L125.588,106.045 L125.62,106.045 L148.655,92.966 L148.689,66.582 z" fill="#994FA0"/>
16 + <path d="M125.62,79.661 L102.747,92.676 L102.552,92.773 L102.552,119.157 L125.62,106.046 L125.62,79.661 L125.62,79.694 L125.62,79.661 z" fill="#994FA0"/>
17 + <path d="M33.38,105.691 L33.38,79.339 L10.311,66.034 L10.311,92.418 L33.38,105.691 z" fill="#FFC41A"/>
18 + <path d="M56.415,118.963 L56.448,92.612 L33.38,79.307 L33.38,105.691 L56.415,118.963 z" fill="#F39B35"/>
19 + <path d="M79.483,132.268 L79.483,105.884 L56.448,92.612 L56.415,118.963 L56.448,118.996 L79.483,132.268 z" fill="#38727B"/>
20 + <path d="M102.552,92.773 L79.679,105.755 L79.484,105.884 L79.484,132.268 L102.552,119.157 L102.552,92.773 z" fill="#1C6B7D"/>
21 + <path d="M79.484,26.764 L79.755,26.614 L102.552,13.653 L79.484,0.38 L56.448,13.492 L79.484,26.764 z" fill="#C4CD46"/>
22 + <path d="M102.552,40.037 L102.747,39.908 L125.62,26.925 L102.552,13.653 L79.484,26.764 L102.552,40.037 z" fill="#F064A4"/>
23 + <path d="M125.62,53.31 L125.815,53.181 L148.655,40.198 L125.62,26.925 L102.552,40.037 L125.62,53.31 z" fill="#AA6AAB"/>
24 + <path d="M148.689,40.198 L125.815,53.181 L125.62,53.309 L125.62,79.661 L148.689,66.582 L148.689,66.55 L148.689,40.198 z" fill="#994FA0"/>
25 + <path d="M56.448,39.875 L56.676,39.746 L79.483,26.764 L56.448,13.491 L33.38,26.603 L56.448,39.875 z" fill="#CDDD40"/>
26 + <path d="M79.484,53.148 L102.552,40.037 L79.484,26.764 L56.448,39.876 L79.484,53.148 z" fill="#CDDD40"/>
27 + <path d="M102.552,66.421 L125.62,53.309 L102.552,39.94 L102.421,39.94 L102.552,39.94 L79.418,53.18 L102.552,66.421 z" fill="#8F499E"/>
28 + <path d="M125.62,53.277 L102.747,66.292 L102.552,66.421 L102.548,92.787 L125.649,79.646 L125.62,53.277 z" fill="#5F356B"/>
29 + <path d="M33.38,52.987 L33.575,52.858 L56.448,39.876 L33.38,26.603 L10.311,39.715 L33.38,52.987 z" fill="#FFD75A"/>
30 + <path d="M33.38,79.339 L33.38,52.987 L10.311,39.715 L10.311,66.099 L33.38,79.339 z" fill="#FFC41A"/>
31 + <path d="M56.48,66.296 L79.516,53.153 L56.48,39.912 L33.412,53.024 L56.48,66.296 z" fill="#F8B730"/>
32 + <path d="M56.448,92.612 L56.448,66.26 L33.38,52.987 L33.38,79.339 L33.38,79.371 L56.448,92.612 z" fill="#F39B35"/>
33 + <path d="M79.516,79.5 L79.712,79.403 L102.559,66.421 L79.516,53.148 L56.448,66.26 L79.516,79.5 z" fill="#F8B730"/>
34 + <path d="M79.48,105.883 L79.516,79.5 L56.448,66.227 L56.448,92.579 L56.448,92.612 L79.48,105.883 z" fill="#F39B35"/>
35 + <path d="M102.552,66.421 L79.712,79.403 L79.516,79.5 L79.516,79.532 L79.477,105.9 L102.552,92.773 L102.552,66.421 z" fill="#FAAA36"/>
36 + </g>
37 + </g>
38 +</svg>
packages/libp2p-inspector-ui/public/img/multiaddr-certified.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M498,47.75 C541.354,47.75 576.5,82.896 576.5,126.25 L576.5,244.75 L636.5,244.75 C658.591,244.75 676.5,262.659 676.5,284.75 L676.5,673.75 C676.5,695.841 658.591,713.75 636.5,713.75 L163.5,713.75 C141.409,713.75 123.5,695.841 123.5,673.75 L123.5,284.75 C123.5,262.659 141.409,244.75 163.5,244.75 L224.5,244.75 L224.5,126.25 C224.5,82.896 259.646,47.75 303,47.75 L498,47.75 z M400,337.25 C358.855,337.25 325.5,370.605 325.5,411.75 C325.734,440.771 341.322,464.576 366.5,478.251 L366.5,616.25 L433.5,616.25 L433.5,477.978 C459.406,465.824 473.191,439.454 474.5,411.75 C474.5,370.605 441.145,337.25 400,337.25 z M498,124.75 L303,124.75 C302.172,124.75 301.5,125.422 301.5,126.25 L301.5,244.75 L499.5,244.75 L499.5,126.25 C499.5,125.422 498.828,124.75 498,124.75 z" fill="#A49100"/>
10 + </g>
11 +</svg>
packages/libp2p-inspector-ui/public/img/multiaddr-uncertified.svg new
+11
@@ -0,0 +1,11 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M498,47.75 C541.354,47.75 576.5,82.896 576.5,126.25 L576.5,244.75 L636.5,244.75 C658.591,244.75 676.5,262.659 676.5,284.75 L676.5,673.75 C676.5,695.841 658.591,713.75 636.5,713.75 L163.5,713.75 C141.409,713.75 123.5,695.841 123.5,673.75 L123.5,284.75 C123.5,262.659 141.409,244.75 163.5,244.75 L224.5,244.75 L224.5,126.25 C224.5,82.896 259.646,47.75 303,47.75 L498,47.75 z M400,337.25 C358.855,337.25 325.5,370.605 325.5,411.75 C325.734,440.771 341.322,464.576 366.5,478.251 L366.5,616.25 L433.5,616.25 L433.5,477.978 C459.406,465.824 473.191,439.454 474.5,411.75 C474.5,370.605 441.145,337.25 400,337.25 z M498,124.75 L303,124.75 C302.172,124.75 301.5,125.422 301.5,126.25 L301.5,244.75 L499.5,244.75 L499.5,126.25 C499.5,125.422 498.828,124.75 498,124.75 z" fill="#868686"/>
10 + </g>
11 +</svg>
packages/libp2p-inspector-ui/public/img/shipyard.svg new
+8
@@ -0,0 +1,8 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="16" height="16" viewBox="0, 0, 16, 16">
4 + <path d="M10.668,0 L10.664,10.646 L16,5.323 L16,0 z" fill="var(--text-color)"/>
5 + <path d="M15.973,10.677 L5.309,10.673 L10.641,16 L15.973,16 z" fill="var(--text-color)"/>
6 + <path d="M5.332,16 L5.336,5.354 L0,10.677 L0,16 z" fill="var(--text-color)"/>
7 + <path d="M0.027,5.323 L10.691,5.327 L5.359,0 L0.027,0 z" fill="var(--text-color)"/>
8 +</svg>
packages/libp2p-inspector-ui/public/img/transport-circuit-relay.svg new
+1
@@ -0,0 +1 @@
1 +<svg width="2500" height="1475" viewBox="0 0 256 151" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M208.615 119.908H96.683c-10.537 0-19.11-8.572-19.11-19.107 0-10.539 8.572-19.111 19.11-19.111h61.129c17.84 0 32.353-14.513 32.353-32.353 0-17.84-14.512-32.354-32.353-32.354H47.223C44.17 7.143 34.995 0 24.152 0 10.811 0 0 10.81 0 24.152c0 13.341 10.812 24.152 24.153 24.152 11.239 0 20.687-7.677 23.381-18.076h110.279c10.535 0 19.107 8.573 19.107 19.11 0 10.536-8.572 19.109-19.107 19.109h-61.13c-17.839 0-32.353 14.513-32.353 32.354 0 17.84 14.513 32.352 32.354 32.352h111.93c2.878 10.121 12.189 17.53 23.234 17.53 13.341 0 24.152-10.81 24.152-24.151 0-13.342-10.81-24.152-24.152-24.152-11.044 0-20.354 7.408-23.233 17.528z" fill="#F26B00"/></svg>
\ No newline at end of file
packages/libp2p-inspector-ui/public/img/transport-quic.svg new
+3
@@ -0,0 +1,3 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
2 +<path d="M23.056 30.85L52.33 47.76c-.024.008-.136.104-.255.176L34.512 58.1c-1.732.998-3.465.894-5.173-.08L2.722 42.602C.894 41.557 0 39.984 0 37.876v-12.07c0-2.475 1.692-4.43 4.127-4.8 1.062-.152 2.06.072 2.986.615l4.263 2.483c.295.176.52.208.822 0l1.764-1.03-7.09-4.07c1.1-.703 2.355-.974 3.696-.623.52.152 1.022.36 1.485.63l4.143 2.395a.57.57 0 0 0 .679 0l1.836-1.07-7.17-4.087 3.105-1.804 14.697-8.478c1.764-1.022 3.537-1.022 5.31 0l26.8 15.496c1.676.966 2.54 2.467 2.54 4.4v12.167c0 1.924-.806 3.433-2.475 4.383-1.637.934-3.29.806-4.926-.144L30.29 27.06c-.255-.152-.44-.152-.68 0L27.8 28.12l29.235 16.893a4.1 4.1 0 0 1-3.018.711 6.38 6.38 0 0 1-2.14-.742l-26.21-15.144c-.32-.184-.55-.216-.862 0-.535.35-1.1.647-1.74 1.014zm18.002 12.86l-.543-.335-5.804-3.37c-.447-.27-.806-.303-1.23.024-.36.263-.742.48-1.142.663-.184.08-.47.112-.63.024l-14.673-8.47-.27-.224.295-.192 14.617-8.47c.287-.16.487-.104.742.048l14.92 8.63-2.028 1.182.088.136 6.6 3.816c.112.064.335.032.455-.04l1.892-1.102c.144-.104.232-.255.255-.43l.008-6.57c.008-.64-.184-.974-.742-1.3L32.74 15.507c-.55-.32-.926-.32-1.453 0l-8.19 4.742-13.308 7.68c-.295.176-.43.36-.415.726v6.77a.66.66 0 0 0 .367.655l21.882 12.66c.247.16.56.16.8-.008l2.427-1.405z" fill="#26465b"/><path d="M41.058 43.712l-8.662 5.014a.67.67 0 0 1-.79.008L9.724 36.072c-.27-.152-.367-.335-.367-.655v-6.77c0-.36.12-.55.415-.726l13.308-7.68 8.19-4.742c.527-.32.918-.32 1.453 0l21.14 12.222c.56.32.766.663.742 1.3l-.008 6.57c-.024.176-.112.327-.255.43l-1.892 1.102c-.12.072-.343.104-.455.04l-6.6-3.816c-.032-.024-.04-.048-.088-.136l2.02-1.174-14.905-8.638c-.255-.152-.447-.216-.742-.048l-14.617 8.47-.295.192c.08.08.176.152.27.224l14.673 8.47c.16.088.447.064.63-.024.407-.176.782-.4 1.142-.663.43-.327.774-.287 1.23-.024l5.804 3.37.55.343z" fill="#38c2e4"/>
3 +</svg>
\ No newline at end of file
packages/libp2p-inspector-ui/public/img/transport-tcp.svg new
+18
@@ -0,0 +1,18 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Background"/>
8 + <g id="Layer_1">
9 + <path d="M601.376,550.452 L700.688,550.452 L700.688,311.998 L588.798,200.108 L518.572,270.334 L601.376,353.138 L601.376,550.452 L601.376,550.452 z M700.95,600.239 L556.305,600.239 L354.537,600.239 L271.733,517.435 L306.846,482.322 L375.237,550.714 L515.952,550.714 L377.334,411.834 L412.709,376.459 L551.327,515.077 L551.327,374.363 L483.197,306.233 L518.048,271.382 L345.889,98.437 L176.089,98.437 L176.089,98.437 L0,98.437 L99.05,197.488 L99.05,197.75 L99.574,197.75 L304.487,197.75 L377.072,270.334 L270.947,376.459 L198.362,303.875 L198.362,247.537 L99.05,247.537 L99.05,345.015 L270.947,516.911 L200.983,586.875 L312.873,698.765 L482.673,698.765 L800,698.765 L800,698.765 L700.95,600.239 L700.95,600.239 z" fill="#868686" display="none"/>
10 + <path d="M734.164,22.5 C761.412,22.5 783.5,44.588 783.5,71.836 L783.5,734.164 C783.5,761.412 761.412,783.5 734.165,783.5 L71.836,783.5 C44.588,783.5 22.5,761.412 22.5,734.164 L22.5,71.836 C22.5,44.588 44.588,22.5 71.836,22.5 L734.164,22.5 z M674.802,140.905 L131.198,140.905 L131.198,565.249 L266.406,565.249 L266.406,665.095 L539.594,665.095 L539.594,565.249 L674.802,565.249 L674.802,140.905 z" fill="#868686"/>
11 + <g>
12 + <path d="M201.922,208.856 L271.259,208.856 L271.259,333.663 L201.922,333.663 L201.922,208.856 z" fill="#868686"/>
13 + <path d="M312.862,208.856 L382.199,208.856 L382.199,333.663 L312.862,333.663 L312.862,208.856 z" fill="#868686"/>
14 + <path d="M423.801,208.856 L493.138,208.856 L493.138,333.663 L423.801,333.663 L423.801,208.856 z" fill="#868686"/>
15 + <path d="M534.741,208.856 L604.078,208.856 L604.078,333.663 L534.741,333.663 L534.741,208.856 z" fill="#868686"/>
16 + </g>
17 + </g>
18 +</svg>
packages/libp2p-inspector-ui/public/img/transport-unknown.svg new
+4
@@ -0,0 +1,4 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 +</svg>
packages/libp2p-inspector-ui/public/img/transport-webrtc.svg new
+28
@@ -0,0 +1,28 @@
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3 +<svg width="800px" height="800px" viewBox="0 -3.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
4 + <g>
5 + <path d="M142.076578,191.086817 C142.076578,159.280656 116.294759,133.494615 84.4885969,133.494615 C52.6782136,133.494615 26.896394,159.280656 26.896394,191.086817 C26.896394,222.892979 52.6782136,248.67902 84.4885969,248.67902 C116.294759,248.67902 142.076578,222.892979 142.076578,191.086817" fill="#FF6600" transform="translate(84.486486, 191.086817) scale(1, -1) translate(-84.486486, -191.086817) ">
6 + </path>
7 + <path d="M255.979703,110.454356 C255.979703,78.652416 230.197884,52.862153 198.391722,52.862153 C166.581339,52.862153 140.799519,78.652416 140.799519,110.454356 C140.799519,142.260518 166.581339,168.050781 198.391722,168.050781 C230.197884,168.050781 255.979703,142.260518 255.979703,110.454356" fill="#FFCC00" transform="translate(198.389611, 110.456467) scale(1, -1) translate(-198.389611, -110.456467) ">
8 + </path>
9 + <path d="M115.200498,109.176452 C115.200498,77.3745125 89.4186786,51.5842495 57.6082953,51.5842495 C25.8063553,51.5842495 0.0203140271,77.3745125 0.0203140271,109.176452 C0.0203140271,140.982614 25.8063553,166.772877 57.6082953,166.772877 C89.4186786,166.772877 115.200498,140.982614 115.200498,109.176452" fill="#0089CC" transform="translate(57.610406, 109.178563) scale(1, -1) translate(-57.610406, -109.178563) ">
10 + </path>
11 + <path d="M230.385749,191.086817 C230.385749,159.280656 204.603929,133.494615 172.789324,133.494615 C140.987384,133.494615 115.201343,159.280656 115.201343,191.086817 C115.201343,222.892979 140.987384,248.67902 172.789324,248.67902 C204.603929,248.67902 230.385749,222.892979 230.385749,191.086817" fill="#009939" transform="translate(172.793546, 191.086817) scale(1, -1) translate(-172.793546, -191.086817) ">
12 + </path>
13 + <path d="M185.592001,57.9843213 C185.592001,26.1781597 159.805959,0.392118349 127.999798,0.392118349 C96.1936359,0.392118349 70.4075946,26.1781597 70.4075946,57.9843213 C70.4075946,89.790483 96.1936359,115.576524 127.999798,115.576524 C159.805959,115.576524 185.592001,89.790483 185.592001,57.9843213" fill="#BF0000" transform="translate(127.999798, 57.984321) scale(1, -1) translate(-127.999798, -57.984321) ">
14 + </path>
15 + <path d="M140.798675,57.9788331 C140.798675,56.76721 140.904217,55.580917 140.980207,54.3861807 C166.525612,60.2796505 185.590734,83.1189569 185.590734,110.454356 C185.590734,111.665979 185.485192,112.856494 185.409202,114.05123 C159.863796,108.153539 140.798675,85.3142322 140.798675,57.9788331" fill="#FC0007" transform="translate(163.194704, 84.218705) scale(1, -1) translate(-163.194704, -84.218705) ">
16 + </path>
17 + <path d="M148.39686,162.570614 C158.322038,145.219495 176.973434,133.495881 198.394255,133.495881 C207.124696,133.495881 215.369643,135.496959 222.787141,138.975626 C212.866185,156.326744 194.214789,168.050358 172.789746,168.050358 C164.059305,168.050358 155.814358,166.049281 148.39686,162.570614" fill="#1CD306" transform="translate(185.592001, 150.773120) scale(1, -1) translate(-185.592001, -150.773120) ">
18 + </path>
19 + <path d="M115.200498,191.086817 C115.200498,177.015947 120.258075,164.139813 128.642338,154.138646 C137.018157,164.139813 142.075734,177.015947 142.075734,191.086817 C142.075734,205.157688 137.018157,218.033822 128.642338,228.034989 C120.258075,218.033822 115.200498,205.157688 115.200498,191.086817" fill="#0F7504" transform="translate(128.638116, 191.086817) scale(1, -1) translate(-128.638116, -191.086817) ">
20 + </path>
21 + <path d="M34.806984,138.212768 C41.8023132,135.190043 49.5026635,133.497148 57.6082953,133.497148 C78.818032,133.497148 97.2963396,144.992791 107.293286,162.061056 C100.297956,165.083782 92.5933844,166.772455 84.4919743,166.772455 C63.2822376,166.772455 44.7997083,155.276811 34.806984,138.212768" fill="#0C5E87" transform="translate(71.050135, 150.134801) scale(1, -1) translate(-71.050135, -150.134801) ">
22 + </path>
23 + <path d="M70.6545631,114.036032 C70.5194692,112.431792 70.4054838,110.819109 70.4054838,109.176875 C70.4054838,81.862584 89.4410536,59.044386 114.956907,53.1255861 C115.087779,54.7298257 115.201765,56.3425087 115.201765,57.9805218 C115.201765,85.2948125 96.1704167,108.121454 70.6545631,114.036032" fill="#6B0001" transform="translate(92.803624, 83.580809) scale(1, -1) translate(-92.803624, -83.580809) ">
24 + </path>
25 + <path d="M76.0304545,111.503866 L67.0213825,111.503866 C59.0677312,111.503866 52.6001125,117.950377 52.6001125,125.88292 L52.6001125,207.428953 C52.6001125,215.361496 59.0677312,221.812228 67.0213825,221.812228 L179.989405,221.812228 C187.943056,221.812228 194.406453,215.361496 194.406453,207.428953 L194.406453,125.88292 C194.406453,117.950377 187.943056,111.503866 179.989405,111.503866 L141.50454,111.503866 L64.2899534,73.6522544 L76.0304545,111.503866 L76.0304545,111.503866 Z" fill="#FFFFFF" transform="translate(123.503283, 147.732241) scale(1, -1) translate(-123.503283, -147.732241) ">
26 + </path>
27 + </g>
28 +</svg>
\ No newline at end of file
packages/libp2p-inspector-ui/public/img/transport-websocket.svg new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="800" height="800" viewBox="0, 0, 800, 800">
4 + <g id="Background">
5 + <rect x="0" y="0" width="800" height="800" fill="#000000" fill-opacity="0"/>
6 + </g>
7 + <g id="Layer_1">
8 + <path d="M601.376,550.452 L700.688,550.452 L700.688,311.998 L588.798,200.108 L518.572,270.334 L601.376,353.138 L601.376,550.452 L601.376,550.452 z M700.95,600.239 L556.305,600.239 L354.537,600.239 L271.733,517.435 L306.846,482.322 L375.237,550.714 L515.952,550.714 L377.334,411.834 L412.709,376.459 L551.327,515.077 L551.327,374.363 L483.197,306.233 L518.048,271.382 L345.889,98.437 L176.089,98.437 L176.089,98.437 L0,98.437 L99.05,197.488 L99.05,197.75 L99.574,197.75 L304.487,197.75 L377.072,270.334 L270.947,376.459 L198.362,303.875 L198.362,247.537 L99.05,247.537 L99.05,345.015 L270.947,516.911 L200.983,586.875 L312.873,698.765 L482.673,698.765 L800,698.765 L800,698.765 L700.95,600.239 L700.95,600.239 z" fill="#868686"/>
9 + </g>
10 +</svg>
packages/libp2p-inspector-ui/public/img/transport-webtransport.svg new
+13
@@ -0,0 +1,13 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="154" height="154" viewBox="0, 0, 154, 154">
4 + <g id="Background">
5 + <rect x="0" y="0" width="154" height="154" fill="#1A5E9A"/>
6 + </g>
7 + <g id="Layer_1">
8 + <g>
9 + <path d="M137.665,46 L137.665,35.814 L96.703,35.814 L86.473,35.814 L86.162,35.814 L71.59,85.331 L57.027,35.814 L46.475,35.814 L51.3,51.988 L41.451,85.331 L26.884,35.814 L16.335,35.814 L40.446,117.175 L41.451,117.175 L56.519,66.756 L71.59,117.175 L72.593,117.175 L93.237,47.512 L94.208,45.86 z" fill="#FFFFFF"/>
10 + <path d="M110.094,42.668 L119.718,42.668 L119.718,118.186 L110.094,118.186 L110.094,42.668 z" fill="#FFFFFF"/>
11 + </g>
12 + </g>
13 +</svg>
packages/libp2p-inspector-ui/public/panel.html new
+11
@@ -0,0 +1,11 @@
1 +<!DOCTYPE html>
2 +<html>
3 + <head>
4 + <meta name="viewport" content="width=device-width, initial-scale=1">
5 + <link rel="stylesheet" href="/dist/app.css"/>
6 + </head>
7 + <body class="sans-serif f5">
8 + <div id="app"></div>
9 + <script type="text/javascript" src="/dist/app.js"></script>
10 + </body>
11 +</html>
packages/libp2p-inspector-ui/src/index.css new
+135
@@ -0,0 +1,135 @@
1 +@import url('reset.css');
2 +
3 +body {
4 + background: #F6F6F6;
5 +
6 + color: #252525;
7 + --text-color: #252525;
8 +
9 + --header-text-color: #4A4A4A;
10 + --header-help-color: #a0a0a0;
11 +
12 + --menu-text-color: #777777;
13 + --menu-background-color: #F6F6F6;
14 + --menu-border-color: #DFDFDF;
15 + --menu-selected-text-color: #0B57D0;
16 + --menu-selected-border-color: #0B57D0;
17 +
18 + --panel-background-color: #F2F2F2;
19 + --panel-border-color: #E5E5E5;
20 +
21 +
22 +
23 +
24 + --dark-text-color: #8F8F8F;
25 + --dark-background-color: #dddddd;
26 +
27 + --warning-text-color: #8a0000;
28 +
29 + --dark-warning-color: #4a0000;
30 + --dark-warning-color-hover: #854e4e;
31 + --dark-warning-color-active: #714242;
32 +
33 + --button-text-color: #7a91b5;
34 + --button-border-color: #7a91b5;
35 + --button-background-color-hover: #aacbff;
36 + --button-background-color-active: #047DB7;
37 +
38 + --primary-button-text-color: #062E6F;
39 + --primary-button-background-color: #A8C7FA;
40 + --primary-button-background-color-hover: #A0BDED;
41 + --primary-button-background-color-active: #839FCC;
42 +
43 + --warning-button-text-color: #8a0000;
44 + --warning-button-background-color: #ffbcbc;
45 + --warning-button-background-color-hover: #fa9f9f;
46 + --warning-button-background-color-active: #ff6161;
47 +
48 + --success-text-color: #2c5639;
49 + --success-background-color: #9fdab1;
50 +}
51 +
52 +@media (prefers-color-scheme: dark) {
53 + body {
54 + background: #282828;
55 +
56 + color: #DFDFDF;
57 + --text-color: #DFDFDF;
58 +
59 + --header-text-color: #E9E9E9;
60 + --header-help-color: #626262;
61 +
62 + --menu-text-color: #B4B4B4;
63 + --menu-background-color: #383838;
64 + --menu-border-color: #5e5e5e;
65 + --menu-selected-text-color: #a8c7fa;
66 + --menu-selected-border-color: #a8c7fa;
67 +
68 + --panel-background-color: #2B2B2B;
69 + --panel-border-color: #464646;
70 +
71 +
72 +
73 + --dark-text-color: #8F8F8F;
74 + --dark-background-color: #111111;
75 +
76 + --warning-text-color: #F2B8B5;
77 + --warning-background-color: #734444;
78 +
79 +
80 +
81 + --button-text-color: #95b2e0;
82 + --button-border-color: #95b2e0;
83 + --button-background-color-hover: #003852;
84 + --button-background-color-active: #047DB7;
85 +
86 + --warning-button-text-color: #ffc2c2;
87 + --warning-button-background-color: #773333;
88 + --warning-button-background-color-hover: #854e4e;
89 + --warning-button-background-color-active: #714242;
90 +
91 + --success-text-color: #6DD58C;
92 + --success-background-color: #2c5639;
93 + }
94 +}
95 +
96 +body {
97 + font-family: Arial, Tahoma, sans-serif;
98 + font-size: 16px;
99 + line-height: 1.2;
100 +}
101 +
102 +h1, h2, h3, h4, h5, h6 {
103 + display: block;
104 + font-size: 16px;
105 + margin-bottom: 24px;
106 +}
107 +
108 +p, li, small, form {
109 + display: block;
110 + margin-bottom: 12px;
111 +}
112 +
113 +li {
114 + margin-left: 12px;
115 +}
116 +
117 +code {
118 + font-family: 'Courier New', Courier, monospace, Tahoma, sans-serif;
119 +}
120 +
121 +.dark {
122 + background-color: var(--background-color);
123 + color: var(--text-color);
124 +}
125 +.dark small {
126 + color: var(--dark-text-color);
127 +}
128 +
129 +.light {
130 + background-color: var(--background-color);
131 + color: #1f1f1f;
132 +}
133 +.light small {
134 + color: var(--dark-text-color);
135 +}
packages/libp2p-inspector-ui/src/index.ts new
+20
@@ -0,0 +1,20 @@
1 +import './index.css'
2 +
3 +export { Inspector } from './panels/inspector.js'
4 +export { FloatingPanel } from './panels/floating-panel.js'
5 +export { Button } from './panels/button.js'
6 +export { TextInput } from './panels/text-input.js'
7 +export { Heading } from './panels/heading.js'
8 +export { Group } from './panels/group.js'
9 +export * from './panels/status.js'
10 +export { ErrorPanel } from './panels/error.tsx'
11 +export { FatalErrorPanel } from './panels/fatal-error.tsx'
12 +export { Footer } from './panels/footer.tsx'
13 +
14 +// icons
15 +export type { IconProps } from './panels/icons/index.js'
16 +export { CopyIcon } from './panels/icons/icon-copy.js'
17 +export { DeleteIcon } from './panels/icons/icon-delete.js'
18 +export { Libp2pIcon } from './panels/icons/icon-libp2p.js'
19 +export { SpinnerIcon } from './panels/icons/icon-spinner.js'
20 +export { ShipyardIcon } from './panels/icons/icon-shipyard.tsx'
packages/libp2p-inspector-ui/src/panels/button.css new
+42
@@ -0,0 +1,42 @@
1 +.Button {
2 + border: 1px solid var(--button-border-color);
3 + border-radius: 10px;
4 + background-color: transparent;
5 + color: var(--button-text-color);
6 + margin-right: 6px;
7 +}
8 +
9 +.Button:hover {
10 + background-color: var(--button-background-color-hover);
11 +}
12 +.Button:active {
13 + background-color: var(--button-background-color-active);
14 +}
15 +
16 +.Button.primary {
17 + border: none;
18 + background-color: var(--primary-button-background-color);
19 + color: var(--primary-button-text-color);
20 +}
21 +.Button.primary:hover {
22 + background-color: var(--primary-button-background-color-hover);
23 +}
24 +.Button.primary:active {
25 + background-color: var(--primary-button-background-color-active);
26 +}
27 +
28 +.Button.secondary {
29 +
30 +}
31 +
32 +.Button.danger {
33 + border: none;
34 + background-color: var(--warning-button-background-color);
35 + color: var(--warning-button-text-color);
36 +}
37 +.Button.danger:hover {
38 + background-color: var(--warning-button-background-color-hover);
39 +}
40 +.Button.danger:active {
41 + background-color: var(--warning-button-background-color-active);
42 +}
\ No newline at end of file
packages/libp2p-inspector-ui/src/panels/button.tsx new
+20
@@ -0,0 +1,20 @@
1 +import 'react'
2 +import type { JSX, MouseEventHandler } from 'react'
3 +import './button.css'
4 +
5 +interface ButtonProps {
6 + children?: any[] | string
7 + onClick: MouseEventHandler
8 + primary?: boolean
9 + secondary?: boolean
10 + danger?: boolean
11 + disabled?: boolean
12 +}
13 +
14 +export function Button ({ children, onClick, primary, secondary, danger, disabled }: ButtonProps): JSX.Element {
15 + return (
16 + <button disabled={disabled} type='button' onClick={onClick} className={`Button ${primary === true ? 'primary' : ''} ${secondary === true ? 'secondary' : ''} ${danger === true ? 'danger' : ''}`}>
17 + {children}
18 + </button>
19 + )
20 +}
packages/libp2p-inspector-ui/src/panels/console.css new
+8
@@ -0,0 +1,8 @@
1 +.Console {
2 + background-color: var(--dark-background-color);
3 + color: var(---dark-text-color);
4 + padding: 12px;
5 + border-radius: 10px;
6 + overflow: scroll;
7 + white-space: nowrap;
8 +}
packages/libp2p-inspector-ui/src/panels/console.tsx new
+15
@@ -0,0 +1,15 @@
1 +import { Panel } from './panel.tsx'
2 +import type { JSX } from 'react'
3 +import './console.css'
4 +
5 +interface ConsolePanelProps {
6 + children: any[] | string
7 +}
8 +
9 +export function ConsolePanel ({ children }: ConsolePanelProps): JSX.Element {
10 + return (
11 + <Panel className='Console'>
12 + <pre><code>{children}</code></pre>
13 + </Panel>
14 + )
15 +}
packages/libp2p-inspector-ui/src/panels/debug.tsx new
+44
@@ -0,0 +1,44 @@
1 +import { useState } from 'react'
2 +import { Button } from './button.js'
3 +import { Panel } from './panel.js'
4 +import { SmallError, SmallSuccess } from './status.js'
5 +import { TextInput } from './text-input.js'
6 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
7 +import type { ReactElement } from 'react'
8 +
9 +export interface DebugProps {
10 + metrics: MetricsRPC
11 + debug: string
12 +}
13 +
14 +export function Debug ({ metrics, debug }: DebugProps): ReactElement {
15 + const [namespace, setNamespace] = useState(debug)
16 + const [result, setResult] = useState(<small>E.g. `libp2p:*`, `libp2p:kad-dht*,*:trace`, etc</small>)
17 +
18 + function sendDebug (evt: { preventDefault(): void }, namespace: string): boolean {
19 + evt.preventDefault()
20 +
21 + metrics.setDebug(namespace)
22 + .then(() => {
23 + setNamespace(namespace)
24 + setResult(<SmallSuccess message='Update successful' />)
25 + }, (err: any) => {
26 + setResult(<SmallError error={err} />)
27 + })
28 +
29 + return false
30 + }
31 +
32 + return (
33 + <Panel>
34 + <h2>Debug</h2>
35 + <p>Enter the name of one or more libp2p components to enable logging in the console tab or disable it completely</p>
36 + <form onSubmit={(evt) => sendDebug(evt, namespace)}>
37 + <TextInput type='text' value={namespace} placeholder='libp2p:*' onChange={(e) => { setNamespace(e.target.value) }} />
38 + <Button onClick={(evt) => sendDebug(evt, namespace)} primary>Go</Button>
39 + <Button onClick={(evt) => sendDebug(evt, '')} danger>Disable</Button>
40 + </form>
41 + {result}
42 + </Panel>
43 + )
44 +}
packages/libp2p-inspector-ui/src/panels/detecting.tsx new
+13
@@ -0,0 +1,13 @@
1 +import { FloatingPanel } from './floating-panel.js'
2 +import type { JSX } from 'react'
3 +
4 +export const DetectingPanel = (): JSX.Element => {
5 + return (
6 + <>
7 + <FloatingPanel>
8 + <h2>Please wait</h2>
9 + <p>Connecting to libp2p...</p>
10 + </FloatingPanel>
11 + </>
12 + )
13 +}
packages/libp2p-inspector-ui/src/panels/error.css new
+8
@@ -0,0 +1,8 @@
1 +.Error {
2 + background-color: var(--warning-button-background-color);
3 + color: var(--warning-button-text-color);
4 + padding: 12px;
5 + border-radius: 10px;
6 + overflow: scroll;
7 + white-space: nowrap;
8 +}
packages/libp2p-inspector-ui/src/panels/error.tsx new
+15
@@ -0,0 +1,15 @@
1 +import { Panel } from './panel.tsx'
2 +import type { JSX } from 'react'
3 +import './error.css'
4 +
5 +interface ErrorPanelProps {
6 + error: Error
7 +}
8 +
9 +export function ErrorPanel ({ error }: ErrorPanelProps): JSX.Element {
10 + return (
11 + <Panel className='Error'>
12 + <pre><code>{error.stack ?? error.toString()}</code></pre>
13 + </Panel>
14 + )
15 +}
packages/libp2p-inspector-ui/src/panels/fatal-error.css new
+3
@@ -0,0 +1,3 @@
1 +.FatalErrorPanel button {
2 + margin-top: 24px;
3 +}
packages/libp2p-inspector-ui/src/panels/fatal-error.tsx new
+22
@@ -0,0 +1,22 @@
1 +import './fatal-error.css'
2 +import { Button } from './button.tsx'
3 +import { ErrorPanel } from './error.tsx'
4 +import { FloatingPanel } from './floating-panel.tsx'
5 +import type { JSX } from 'react'
6 +
7 +interface FatalErrorPanelProps {
8 + error: Error
9 + onBack?(evt: React.UIEvent): void
10 +}
11 +
12 +export function FatalErrorPanel ({ error, onBack }: FatalErrorPanelProps): JSX.Element {
13 + return (
14 + <>
15 + <FloatingPanel className='FatalErrorPanel'>
16 + <h2>Error</h2>
17 + <ErrorPanel error={error} />
18 + {onBack != null ? <Button onClick={onBack}>Back</Button> : ''}
19 + </FloatingPanel>
20 + </>
21 + )
22 +}
packages/libp2p-inspector-ui/src/panels/floating-panel.css new
+8
@@ -0,0 +1,8 @@
1 +.FloatingPanel {
2 + background-color: var(---panel-background-color);
3 + padding: 20px;
4 + margin: 50px auto auto auto;
5 + width: 600px;
6 + border: 2px solid var(--panel-border-color);
7 + border-radius: 10px;
8 +}
packages/libp2p-inspector-ui/src/panels/floating-panel.tsx new
+16
@@ -0,0 +1,16 @@
1 +import 'react'
2 +import './floating-panel.css'
3 +import type { JSX } from 'react'
4 +
5 +interface FloatingPanelProps {
6 + children?: any
7 + className?: string
8 +}
9 +
10 +export function FloatingPanel ({ children, className }: FloatingPanelProps): JSX.Element {
11 + return (
12 + <div className={`FloatingPanel ${className ?? ''}`}>
13 + {children}
14 + </div>
15 + )
16 +}
packages/libp2p-inspector-ui/src/panels/footer.css new
+15
@@ -0,0 +1,15 @@
1 +.Footer small {
2 + margin: 0;
3 + text-align: right;
4 + color: var(--dark-text-color);
5 +}
6 +.Footer small a, .Footer small a:link, .Footer small a:visited {
7 + color: var(--dark-text-color);
8 + text-decoration: none;
9 +}
10 +.Footer small a:hover {
11 + color: var(--text-color);
12 +}
13 +.Footer small svg path {
14 + color: var(--text-color);
15 +}
packages/libp2p-inspector-ui/src/panels/footer.tsx new
+14
@@ -0,0 +1,14 @@
1 +import './footer.css'
2 +import type { JSX } from 'react'
3 +
4 +export interface FooterProps {
5 + children: any
6 +}
7 +
8 +export function Footer ({ children }: FooterProps): JSX.Element {
9 + return (
10 + <div className='Footer'>
11 + {children}
12 + </div>
13 + )
14 +}
packages/libp2p-inspector-ui/src/panels/grant-permissions.css new
+23
@@ -0,0 +1,23 @@
1 +.GrantPermissions {
2 + display: block;
3 + height: 375px;
4 + width: 100%;
5 + background-size: contain;
6 + background-repeat: no-repeat;
7 +}
8 +
9 +.chrome.dark .GrantPermissions {
10 + background-image: url(../../public/img/grant-permissions-dark-chrome.png);
11 +}
12 +
13 +.chrome.light .GrantPermissions {
14 + background-image: url(../../public/img/grant-permissions-dark-chrome.png);
15 +}
16 +
17 +.firefox.dark .GrantPermissions {
18 + background-image: url(../../public/img/grant-permissions-dark-firefox.png);
19 +}
20 +
21 +.firefox.light .GrantPermissions {
22 + background-image: url(../../public/img/grant-permissions-dark-firefox.png);
23 +}
packages/libp2p-inspector-ui/src/panels/grant-permissions.tsx new
+8
@@ -0,0 +1,8 @@
1 +import './grant-permissions.css'
2 +import type { JSX } from 'react'
3 +
4 +export function GrantPermissions (): JSX.Element {
5 + return (
6 + <div className='GrantPermissions' />
7 + )
8 +}
packages/libp2p-inspector-ui/src/panels/group.css new
+5
@@ -0,0 +1,5 @@
1 +.Group:not(:last-child) {
2 + margin-bottom: 24px;
3 + padding-bottom: 12px;
4 + border-bottom: 1px solid var(--panel-border-color)
5 +}
packages/libp2p-inspector-ui/src/panels/group.tsx new
+14
@@ -0,0 +1,14 @@
1 +import './group.css'
2 +import type { JSX } from 'react'
3 +
4 +export interface GroupProps {
5 + children: any
6 +}
7 +
8 +export function Group ({ children }: GroupProps): JSX.Element {
9 + return (
10 + <div className='Group'>
11 + {children}
12 + </div>
13 + )
14 +}
packages/libp2p-inspector-ui/src/panels/heading.css new
+18
@@ -0,0 +1,18 @@
1 +.Heading {
2 + display: flex;
3 + gap: 6px;
4 + align-items: baseline;
5 +}
6 +
7 +.Heading h1, .Heading h2, .Heading h3, .Heading h4, .Heading h5, .Heading h6 {
8 + display: inline-block;
9 + flex-grow: 1;
10 +}
11 +.Heading small {
12 + display: inline-block;
13 + overflow: hidden;
14 + white-space: nowrap;
15 + text-overflow: ellipsis;
16 + text-align: right;
17 + color: var(--header-help-color);
18 +}
packages/libp2p-inspector-ui/src/panels/heading.tsx new
+18
@@ -0,0 +1,18 @@
1 +import './heading.css'
2 +import type { JSX } from 'react'
3 +
4 +export interface HeadingProps {
5 + children: any
6 + help?: string
7 +}
8 +
9 +export function Heading ({ children, help }: HeadingProps): JSX.Element {
10 + const sub = help == null ? undefined : <small>{help}</small>
11 +
12 + return (
13 + <div className='Heading'>
14 + {children}
15 + {sub}
16 + </div>
17 + )
18 +}
packages/libp2p-inspector-ui/src/panels/icons/icon-copy.tsx new
+18
@@ -0,0 +1,18 @@
1 +import 'react'
2 +import './icon.css'
3 +import src from '../../../public/img/icon-copy.svg'
4 +import { Icon } from './icon.js'
5 +import type { IconProps } from './index.js'
6 +import type { JSX } from 'react'
7 +
8 +export function CopyIcon (props: IconProps): JSX.Element {
9 + return (
10 + <>
11 + <Icon
12 + {...props}
13 + src={src}
14 + name='CopyIcon'
15 + />
16 + </>
17 + )
18 +}
packages/libp2p-inspector-ui/src/panels/icons/icon-delete.tsx new
+18
@@ -0,0 +1,18 @@
1 +import 'react'
2 +import './icon.css'
3 +import src from '../../../public/img/icon-delete.svg'
4 +import { Icon } from './icon.js'
5 +import type { IconProps } from './index.js'
6 +import type { JSX } from 'react'
7 +
8 +export function DeleteIcon (props: IconProps): JSX.Element {
9 + return (
10 + <>
11 + <Icon
12 + {...props}
13 + src={src}
14 + name='DeleteIcon'
15 + />
16 + </>
17 + )
18 +}
packages/libp2p-inspector-ui/src/panels/icons/icon-libp2p.tsx new
+18
@@ -0,0 +1,18 @@
1 +import 'react'
2 +import './icon.css'
3 +import src from '../../../public/img/libp2p.svg'
4 +import { Icon } from './icon.js'
5 +import type { IconProps } from './index.js'
6 +import type { JSX } from 'react'
7 +
8 +export function Libp2pIcon (props: IconProps): JSX.Element {
9 + return (
10 + <>
11 + <Icon
12 + {...props}
13 + src={src}
14 + name='Libp2pIcon'
15 + />
16 + </>
17 + )
18 +}
packages/libp2p-inspector-ui/src/panels/icons/icon-shipyard.tsx new
+41
@@ -0,0 +1,41 @@
1 +import 'react'
2 +import './icon.css'
3 +import { GenIcon } from 'react-icons'
4 +import type { JSX } from 'react'
5 +
6 +// generated from https://react-icons-json-generator.surge.sh/
7 +const icon = {
8 + tag: 'svg',
9 + attr: {
10 + version: '1.1', x: '0', y: '0', viewBox: '0, 0, 16, 16'
11 + },
12 + child: [{
13 + tag: 'path',
14 + attr: {
15 + d: 'M10.668,0 L10.664,10.646 L16,5.323 L16,0 z'
16 + },
17 + child: []
18 + }, {
19 + tag: 'path',
20 + attr: {
21 + d: 'M15.973,10.677 L5.309,10.673 L10.641,16 L15.973,16 z'
22 + },
23 + child: []
24 + }, {
25 + tag: 'path',
26 + attr: {
27 + d: 'M5.332,16 L5.336,5.354 L0,10.677 L0,16 z'
28 + },
29 + child: []
30 + }, {
31 + tag: 'path',
32 + attr: {
33 + d: 'M0.027,5.323 L10.691,5.327 L5.359,0 L0.027,0 z'
34 + },
35 + child: []
36 + }]
37 +}
38 +
39 +export function ShipyardIcon (props: any): JSX.Element {
40 + return GenIcon(icon)(props)
41 +}
packages/libp2p-inspector-ui/src/panels/icons/icon-spinner.tsx new
+18
@@ -0,0 +1,18 @@
1 +import 'react'
2 +import './icon.css'
3 +import src from '../../../public/img/icon-spinner.svg'
4 +import { Icon } from './icon.js'
5 +import type { IconProps } from './index.js'
6 +import type { JSX } from 'react'
7 +
8 +export function SpinnerIcon (props: IconProps): JSX.Element {
9 + return (
10 + <>
11 + <Icon
12 + {...props}
13 + src={src}
14 + name='SpinnerIcon'
15 + />
16 + </>
17 + )
18 +}
packages/libp2p-inspector-ui/src/panels/icons/icon.css new
+12
@@ -0,0 +1,12 @@
1 +.Icon {
2 + display: inline-block;
3 +}
4 +.Icon.ClickableIcon {
5 + cursor: pointer;
6 +}
7 +.Icon.ClickableIcon:hover {
8 + filter: brightness(1.4);
9 +}
10 +.Icon.ClickableIcon:active {
11 + filter: brightness(0.8);
12 +}
packages/libp2p-inspector-ui/src/panels/icons/icon.tsx new
+25
@@ -0,0 +1,25 @@
1 +import 'react'
2 +import './icon.css'
3 +import type { IconProps as ParentIconProps } from './index.js'
4 +import type { JSX } from 'react'
5 +
6 +interface IconProps extends ParentIconProps {
7 + src: string
8 + name: string
9 +}
10 +
11 +export const DEFAULT_HEIGHT = 16
12 +export const DEFAULT_WIDTH = 16
13 +
14 +export function Icon (props: IconProps): JSX.Element {
15 + return (
16 + <>
17 + <img
18 + {...props}
19 + height={props.height ?? DEFAULT_HEIGHT}
20 + width={props.width ?? DEFAULT_WIDTH}
21 + className={`Icon${props.onClick != null ? ' ClickableIcon' : ''} Libp2pIcon`}
22 + />
23 + </>
24 + )
25 +}
packages/libp2p-inspector-ui/src/panels/icons/index.ts new
+9
@@ -0,0 +1,9 @@
1 +import 'react'
2 +import './icon.css'
3 +
4 +export interface IconProps {
5 + height?: number
6 + width?: number
7 + onClick?: React.MouseEventHandler<HTMLImageElement>
8 + style?: React.CSSProperties
9 +}
packages/libp2p-inspector-ui/src/panels/identify.tsx new
+62
@@ -0,0 +1,62 @@
1 +import { base58btc } from 'multiformats/bases/base58'
2 +import { useState } from 'react'
3 +import { Button } from './button.tsx'
4 +import { ConsolePanel } from './console.tsx'
5 +import { ErrorPanel } from './error.tsx'
6 +import { Panel } from './panel.tsx'
7 +import { TextInput } from './text-input.tsx'
8 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
9 +import type { JSX } from 'react'
10 +
11 +interface IdentifyPanelProps {
12 + component: string
13 + metrics: MetricsRPC
14 +}
15 +
16 +export function Identify ({ component, metrics }: IdentifyPanelProps): JSX.Element {
17 + const [peerIdOrMultiaddr, setPeerIdOrMultiaddr] = useState('')
18 + const [result, setResult] = useState<JSX.Element | string>('')
19 +
20 + function identify (evt: { preventDefault(): void }): boolean {
21 + evt.preventDefault()
22 +
23 + metrics.identify(component, peerIdOrMultiaddr, {
24 + signal: AbortSignal.timeout(10_000)
25 + })
26 + .then((result) => {
27 + const data: any = {
28 + peerId: result.peerId.toString(),
29 + agentVersion: result.agentVersion,
30 + protocolVersion: result.protocolVersion,
31 + observedAddr: result.observedAddr,
32 + publicKey: result.publicKey && base58btc.encode(result.publicKey),
33 + listenAddrs: result.listenAddrs.map(ma => ma.toString()),
34 + protocols: result.protocols
35 + }
36 +
37 + if (result.signedPeerRecord != null) {
38 + data.signedPeerRecord = {
39 + seq: `${result.signedPeerRecord.seq}n`,
40 + addresses: result.signedPeerRecord.addresses.map(ma => ma.toString())
41 + }
42 + }
43 +
44 + setResult(<ConsolePanel>{JSON.stringify(data, null, 2)}</ConsolePanel>)
45 + }, (err) => {
46 + setResult(<ErrorPanel error={err} />)
47 + })
48 +
49 + return false
50 + }
51 +
52 + return (
53 + <Panel>
54 + <p>Identify a peer</p>
55 + <form onSubmit={(evt) => identify(evt)}>
56 + <TextInput type='text' value={peerIdOrMultiaddr} placeholder='Peer ID or Multiaddr' onChange={(e) => { setPeerIdOrMultiaddr(e.target.value) }} />
57 + <Button onClick={(evt) => identify(evt)} primary>Identify</Button>
58 + </form>
59 + {result}
60 + </Panel>
61 + )
62 +}
packages/libp2p-inspector-ui/src/panels/inspector.tsx new
+97
@@ -0,0 +1,97 @@
1 +import { useState } from 'react'
2 +import libp2pLogo from '../../public/img/libp2p.svg'
3 +import { Debug } from './debug.js'
4 +import { Identify } from './identify.tsx'
5 +import { Menu } from './menu.js'
6 +import { Node } from './node.js'
7 +import { Peers } from './peers/index.js'
8 +import { Ping } from './ping.tsx'
9 +import { PubSub } from './pubsub/index.js'
10 +import { Routing } from './routing/index.js'
11 +import type { MetricsRPC, Peer } from '@ipshipyard/libp2p-inspector-metrics'
12 +import type { Message } from '@libp2p/interface'
13 +import type { ReactElement } from 'react'
14 +
15 +export interface InspectorProps {
16 + self: Peer
17 + copyToClipboard(value: string): void
18 + peers: Peer[]
19 + debug: string
20 + metrics: MetricsRPC
21 + capabilities: Record<string, string[]>
22 + pubsub: Record<string, Message[]>
23 +}
24 +
25 +export function Inspector ({ self, copyToClipboard, peers, debug, metrics, capabilities, pubsub }: InspectorProps): ReactElement {
26 + const panels = ['Node', 'Peers', 'Debug', 'Routing']
27 + const [panel, setPanel] = useState(panels[0])
28 +
29 + const logo = (
30 + <img src={libp2pLogo} height={24} width={24} className='Icon' />
31 + )
32 +
33 + const tabs = [{
34 + name: 'Node',
35 + panel: (index: string) => <Node self={self} copyToClipboard={copyToClipboard} key={`panel-${index}`} />
36 + }, {
37 + name: 'Peers',
38 + panel: (index: string) => <Peers peers={peers} metrics={metrics} copyToClipboard={copyToClipboard} key={`panel-${index}`} />
39 + }, {
40 + name: 'Debug',
41 + panel: (index: string) => <Debug metrics={metrics} debug={debug} key={`panel-${index}`} />
42 + }, {
43 + name: 'Routing',
44 + panel: (index: string) => <Routing metrics={metrics} key={`panel-${index}`} />
45 + }, {
46 + capability: '@libp2p/ping',
47 + name: 'Ping',
48 + component: '',
49 + panel: (index: string, component?: string) => <Ping component={component ?? ''} metrics={metrics} key={`panel-${index}`} />
50 + }, {
51 + capability: '@libp2p/identify',
52 + name: 'Identify',
53 + component: '',
54 + panel: (index: string, component?: string) => <Identify component={component ?? ''} metrics={metrics} key={`panel-${index}`} />
55 + }, {
56 + capability: '@libp2p/pubsub',
57 + name: 'PubSub',
58 + component: '',
59 + panel: (index: string, component?: string) => <PubSub component={component ?? ''} metrics={metrics} pubsub={pubsub} key={`panel-${index}`} />
60 + }]
61 +
62 + for (const tab of tabs) {
63 + if (tab.capability == null) {
64 + continue
65 + }
66 +
67 + const component = findComponent(capabilities, tab.capability)
68 +
69 + if (component != null) {
70 + tab.component = component
71 + panels.push(tab.name)
72 + }
73 + }
74 +
75 + return (
76 + <>
77 + <Menu logo={logo} onClick={(panel) => { setPanel(panel) }} panel={panel} options={panels} />
78 + {
79 + tabs.map(tab => {
80 + if (panel === tab.name) {
81 + return tab.panel(tab.name, tab.component)
82 + }
83 +
84 + return ''
85 + })
86 + }
87 + </>
88 + )
89 +}
90 +
91 +function findComponent (capabilities: Record<string, string[]>, capability: string): string | undefined {
92 + for (const component of Object.keys(capabilities)) {
93 + if (capabilities[component].includes(capability)) {
94 + return component
95 + }
96 + }
97 +}
packages/libp2p-inspector-ui/src/panels/menu.css new
+28
@@ -0,0 +1,28 @@
1 +.Menu {
2 + border-bottom: 1px solid var(--menu-border-color);
3 +}
4 +
5 +.Menu button {
6 + border: none;
7 + background-color: transparent;
8 + font-size: 14px;
9 + padding: 7px;
10 + color: var(--text-color);
11 + display: inline-block;
12 +}
13 +
14 +.Menu button:hover {
15 + color: var(--primary-action-color-hover);
16 +}
17 +
18 +.Menu button.selected {
19 + color: var(--menu-selected-text-color);
20 + border-bottom: 1px solid var(--menu-selected-border-color);
21 +}
22 +
23 +.Menu .Icon {
24 + margin: 4px;
25 + padding: 0;
26 + display: block;
27 + float: left;
28 +}
packages/libp2p-inspector-ui/src/panels/menu.tsx new
+23
@@ -0,0 +1,23 @@
1 +import 'react'
2 +import './menu.css'
3 +import type { ReactElement, JSX } from 'react'
4 +
5 +export interface MenuProps {
6 + onClick(arg: string): void
7 + panel: string
8 + options: string[]
9 + logo?: JSX.Element
10 +}
11 +
12 +export function Menu ({ onClick, panel, options, logo }: MenuProps): ReactElement {
13 + return (
14 + <div className='Menu'>
15 + {logo}
16 + {
17 + options.map(option => (
18 + <button key={option} onClick={() => { onClick(option) }} className={panel === option ? 'selected' : ''}>{option}</button>
19 + ))
20 + }
21 + </div>
22 + )
23 +}
packages/libp2p-inspector-ui/src/panels/multiaddr-list.css new
+17
@@ -0,0 +1,17 @@
1 +.MultiaddrList {
2 + margin-bottom: 12px;
3 + margin-left: 0;
4 +}
5 +
6 +.Multiaddr {
7 + display: flex;
8 + gap: 6px;
9 + margin-bottom: 3px;
10 + margin-left: 0;
11 +}
12 +.Multiaddr code {
13 + display: inline-block;
14 + overflow: hidden;
15 + white-space: nowrap;
16 + text-overflow: ellipsis;
17 +}
packages/libp2p-inspector-ui/src/panels/multiaddr-list.tsx new
+111
@@ -0,0 +1,111 @@
1 +import './multiaddr-list.css'
2 +import 'react'
3 +import { multiaddr } from '@multiformats/multiaddr'
4 +import { WebRTC, WebSockets, WebSocketsSecure, WebTransport, Circuit, QUIC, QUICV1, TCP, WebRTCDirect } from '@multiformats/multiaddr-matcher'
5 +import certifiedMultiaddr from '../../public/img/multiaddr-certified.svg'
6 +import uncertifiedMultiaddr from '../../public/img/multiaddr-uncertified.svg'
7 +import circuitRelayTransport from '../../public/img/transport-circuit-relay.svg'
8 +import quicTransport from '../../public/img/transport-quic.svg'
9 +import tcpTransport from '../../public/img/transport-tcp.svg'
10 +import unknownTransport from '../../public/img/transport-unknown.svg'
11 +import webrtcTransport from '../../public/img/transport-webrtc.svg'
12 +import websocketTransport from '../../public/img/transport-websocket.svg'
13 +import webtransportTransport from '../../public/img/transport-webtransport.svg'
14 +import { CopyIcon } from './icons/icon-copy.js'
15 +import type { Multiaddr } from '@multiformats/multiaddr'
16 +import type { ReactElement } from 'react'
17 +
18 +export interface TransportIconProps {
19 + multiaddr: Multiaddr
20 +}
21 +
22 +function TransportIcon ({ multiaddr }: TransportIconProps): ReactElement {
23 + let src: string = unknownTransport
24 +
25 + if (WebRTC.matches(multiaddr) || WebRTCDirect.matches(multiaddr)) {
26 + src = webrtcTransport
27 + } else if (Circuit.matches(multiaddr)) {
28 + src = circuitRelayTransport
29 + } else if (WebSockets.matches(multiaddr) || WebSocketsSecure.matches(multiaddr)) {
30 + src = websocketTransport
31 + } else if (WebTransport.matches(multiaddr)) {
32 + src = webtransportTransport
33 + } else if (QUIC.matches(multiaddr) || QUICV1.matches(multiaddr)) {
34 + src = quicTransport
35 + } else if (TCP.matches(multiaddr)) {
36 + src = tcpTransport
37 + }
38 +
39 + return (
40 + <>
41 + <img src={src} height={16} width={16} className='Icon' />
42 + </>
43 + )
44 +}
45 +
46 +export interface CertifiedIconProps {
47 + isCertified?: boolean
48 +}
49 +
50 +function CertifiedIcon ({ isCertified }: CertifiedIconProps): ReactElement {
51 + return (
52 + <>
53 + <img src={isCertified === true ? certifiedMultiaddr : uncertifiedMultiaddr} height={16} width={16} className='Icon' />
54 + </>
55 + )
56 +}
57 +
58 +interface MultiaddrProps {
59 + multiaddr: Multiaddr
60 + copyToClipboard(value: string): void
61 + isCertified?: boolean
62 + key?: string
63 + includeCertification?: boolean
64 +}
65 +
66 +function MultiaddrPanel ({ multiaddr: m, isCertified, includeCertification, copyToClipboard }: MultiaddrProps): ReactElement {
67 + const ma = multiaddr(m)
68 +
69 + function onCopy (evt: any): void {
70 + evt.preventDefault()
71 +
72 + copyToClipboard(ma.toString())
73 + }
74 +
75 + return (
76 + <div className='Multiaddr'>
77 + <TransportIcon multiaddr={ma} />
78 + {
79 + includeCertification === true ? <CertifiedIcon isCertified={isCertified} /> : undefined
80 + }
81 + <CopyIcon onClick={onCopy} />
82 + <code>{m.toString()}</code>
83 + </div>
84 + )
85 +}
86 +
87 +export interface MulitaddrListProps {
88 + addresses: Array<{ multiaddr: Multiaddr, isCertified?: boolean }>
89 + copyToClipboard(value: string): void
90 + includeCertification?: boolean
91 +}
92 +
93 +export function MultiaddrList ({ addresses, includeCertification, copyToClipboard }: MulitaddrListProps): ReactElement {
94 + if (addresses.length === 0) {
95 + return (
96 + <>
97 + <p>There are no multiaddrs to display</p>
98 + </>
99 + )
100 + }
101 +
102 + return (
103 + <>
104 + <div className='MultiaddrList'>
105 + <ul>
106 + {addresses.map(({ multiaddr: m, isCertified }, index) => <MultiaddrPanel key={`ma-${index}`} multiaddr={m} includeCertification={includeCertification} isCertified={isCertified} copyToClipboard={copyToClipboard} />)}
107 + </ul>
108 + </div>
109 + </>
110 + )
111 +}
packages/libp2p-inspector-ui/src/panels/node.tsx new
+74
@@ -0,0 +1,74 @@
1 +import 'react'
2 +import { getAgent } from '../utils/get-agent.js'
3 +import { Heading } from './heading.js'
4 +import { MultiaddrList } from './multiaddr-list.js'
5 +import { Panel } from './panel.js'
6 +import type { Peer } from '@ipshipyard/libp2p-inspector-metrics'
7 +import type { ReactElement } from 'react'
8 +
9 +export interface NodeProps {
10 + self: Peer
11 + copyToClipboard(value: string): void
12 +}
13 +
14 +export function Node ({ self, copyToClipboard }: NodeProps): ReactElement {
15 + const agent = getAgent(self.metadata)
16 + let agentVersion
17 +
18 + if (agent != null) {
19 + agentVersion = (
20 + <>
21 + <Heading help='The agent is sent to peers during Identify'>
22 + <h2>Agent</h2>
23 + </Heading>
24 + <p>{agent}</p>
25 + </>
26 + )
27 + }
28 +
29 + return (
30 + <Panel>
31 + <Heading help="A PeerId is derived from a node's cryptographic key and uniquely identifies it on the network">
32 + <h2>PeerId</h2>
33 + </Heading>
34 + <p>{self.id.toString()}</p>
35 + {agentVersion}
36 + <Heading help='Multiaddrs are addresses that other nodes can use to contact this node'>
37 + <h2>Multiaddrs</h2>
38 + </Heading>
39 + <MultiaddrList
40 + copyToClipboard={copyToClipboard} addresses={self.addresses.map(address => {
41 + let multiaddr = address.multiaddr
42 +
43 + if (multiaddr.getPeerId() == null) {
44 + multiaddr = multiaddr.encapsulate(`/p2p/${self.id}`)
45 + }
46 +
47 + return { multiaddr }
48 + })}
49 + />
50 + <Heading help='This node will respond to these protocols'>
51 + <h2>Supported protocols</h2>
52 + </Heading>
53 + <Protocols protocols={self.protocols} />
54 + </Panel>
55 + )
56 +}
57 +
58 +export interface ProtocolsPanelProps {
59 + protocols: string[]
60 +}
61 +
62 +function Protocols ({ protocols }: ProtocolsPanelProps): ReactElement {
63 + if (protocols.length === 0) {
64 + return (
65 + <p>This node has does not support any protocols.</p>
66 + )
67 + }
68 +
69 + return (
70 + <ul>
71 + {protocols.map((protocol, index) => <li key={`protocol-${index}`}>{protocol}</li>)}
72 + </ul>
73 + )
74 +}
packages/libp2p-inspector-ui/src/panels/panel.css new
+3
@@ -0,0 +1,3 @@
1 +.Panel {
2 + padding: 10px 20px;
3 +}
packages/libp2p-inspector-ui/src/panels/panel.tsx new
+16
@@ -0,0 +1,16 @@
1 +import 'react'
2 +import './panel.css'
3 +import type { JSX } from 'react'
4 +
5 +interface PanelProps {
6 + children?: any[] | any
7 + className?: string
8 +}
9 +
10 +export function Panel ({ children, className }: PanelProps): JSX.Element {
11 + return (
12 + <div className={`Panel ${className ?? ''}`}>
13 + {children}
14 + </div>
15 + )
16 +}
packages/libp2p-inspector-ui/src/panels/peers/dial.css new
+32
@@ -0,0 +1,32 @@
1 +.DialEvents {
2 + margin: 5px 0;
3 +}
4 +
5 +.DialEvent {
6 + margin: 0;
7 + padding: 5px 10px;
8 + white-space: nowrap;
9 + overflow: hidden;
10 + text-overflow: ellipsis;
11 +}
12 +
13 +.DialEvent.dial-queue ol {
14 + margin: 6px;
15 +}
16 +
17 +.DialEvent.dial-queue .calculated-address {
18 + text-overflow: ellipsis;
19 + overflow: hidden;
20 +}
21 +
22 +.DialEvent.webrtc .ice-candidate {
23 + padding: 5px;
24 +}
25 +
26 +.DialEvents .Success {
27 + background-color: var(--success-background-color);
28 +}
29 +
30 +.DialEvents .Error {
31 + background-color: var(--warning-background-color);
32 +}
packages/libp2p-inspector-ui/src/panels/peers/dial.tsx new
+200
@@ -0,0 +1,200 @@
1 +import './dial.css'
2 +import { peerIdFromString } from '@libp2p/peer-id'
3 +import { multiaddr } from '@multiformats/multiaddr'
4 +import { Component } from 'react'
5 +import { Button } from '../button.js'
6 +import { Panel } from '../panel.js'
7 +import { SmallError, SmallSuccess } from '../status.js'
8 +import { TextInput } from '../text-input.js'
9 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
10 +import type { Address } from '@libp2p/interface'
11 +import type { FormEvent, ReactElement, JSX } from 'react'
12 +
13 +export interface DialPeerProps {
14 + metrics: MetricsRPC
15 +}
16 +
17 +export interface DialPeerState {
18 + target: string
19 + error: string
20 + details: JSX.Element[]
21 +}
22 +
23 +const dialEvents: Record<string, (key: string, className: string, args: any) => ReactElement> = {
24 + // internal dial events
25 + 'dial-queue:already-connected': (key, className) => <p key={key} className={className}>Already connected to this peer</p>,
26 + 'dial-queue:add-to-dial-queue': (key, className) => <p key={key} className={className}>Adding dial to queue</p>,
27 + 'dial-queue:already-in-dial-queue': (key, className) => <p key={key} className={className}>Dial to this peer already in queue</p>,
28 + 'dial-queue:start-dial': (key, className) => <p key={key} className={className}>Dialing peer</p>,
29 + 'dial-queue:calculated-addresses': (key, className, addreseses: Address[]) => (
30 + <>
31 + <p key={`${key}-1`} className={className}>Calculated addresses</p>
32 + <ol key={`${key}-2`} className={className}>
33 + {
34 + addreseses.map((address, index) => (
35 + <li key={`address-${index}`} className='calculated-address'>{address.multiaddr.toString()}</li>
36 + ))
37 + }
38 + </ol>
39 + </>
40 + ),
41 + 'transport-manager:selected-transport': (key, className, transport: string) => <p key={key} className={className}>Selected transport {transport}</p>,
42 +
43 + // webrtc
44 + 'webrtc:initiate-connection': (key, className) => <p key={key} className={className}>WebRTC initiating connection</p>,
45 + 'webrtc:dial-relay': (key, className) => <p key={key} className={className}>Dialing relay</p>,
46 + 'webrtc:reuse-relay-connection': (key, className) => <p key={key} className={className}>Already connected to relay</p>,
47 + 'webrtc:open-signaling-stream': (key, className) => <p key={key} className={className}>Open signaling stream</p>,
48 + 'webrtc:send-sdp-offer': (key, className) => <p key={key} className={className}>Sending SDP offer</p>,
49 + 'webrtc:read-sdp-answer': (key, className) => <p key={key} className={className}>Reading SDP answer</p>,
50 + 'webrtc:read-ice-candidates': (key, className) => <p key={key} className={className}>Read ICE candidates</p>,
51 + 'webrtc:add-ice-candidate': (key, className, candidate) => <p key={key} className={className}>Add ICE candidate <span className='ice-candidate'>{candidate}</span></p>,
52 + 'webrtc:end-of-ice-candidates': (key, className) => <p key={key} className={className}>End of ICE candidates</p>,
53 + 'webrtc:close-signaling-stream': (key, className) => <p key={key} className={className}>Closing signaling stream</p>,
54 +
55 + // circuit relay
56 + 'circuit-relay:open-connection': (key, className) => <p key={key} className={className}>Connecting to relay</p>,
57 + 'circuit-relay:reuse-connection': (key, className) => <p key={key} className={className}>Already connected to relay</p>,
58 + 'circuit-relay:open-hop-stream': (key, className) => <p key={key} className={className}>Opening hop stream</p>,
59 + 'circuit-relay:write-connect-message': (key, className) => <p key={key} className={className}>Sending CONNECT</p>,
60 + 'circuit-relay:read-connect-response': (key, className) => <p key={key} className={className}>Reading CONNECT response</p>,
61 +
62 + // websockets
63 + 'websockets:open-connection': (key, className) => <p key={key} className={className}>Open connection</p>,
64 +
65 + // webtransport
66 + 'webtransport:wait-for-session': (key, className) => <p key={key} className={className}>Opening session</p>,
67 + 'webtransport:open-authentication-stream': (key, className) => <p key={key} className={className}>Open authentication stream</p>,
68 + 'webtransport:secure-outbound-connection': (key, className) => <p key={key} className={className}>Perform Noise handshake</p>,
69 + 'webtransport:close-authentication-stream': (key, className) => <p key={key} className={className}>Close authentication stream</p>,
70 +
71 + // tcp
72 + 'tcp:open-connection': (key, className) => <p key={key} className={className}>Open connection</p>,
73 +
74 + // upgrader
75 + 'upgrader:encrypt-outbound-connection': (key, className) => <p key={key} className={className}>Encrypting outbound connection</p>
76 +}
77 +
78 +export class DialPeer extends Component<DialPeerProps, DialPeerState> {
79 + constructor (props: DialPeerProps) {
80 + super(props)
81 +
82 + this.state = {
83 + target: '',
84 + error: '',
85 + details: []
86 + }
87 + }
88 +
89 + componentDidMount (): void {
90 + this.setState({
91 + target: '',
92 + error: '',
93 + details: []
94 + })
95 + }
96 +
97 + private dial (evt: FormEvent | Event, target: string): boolean {
98 + evt.preventDefault()
99 +
100 + this.setState({
101 + error: '',
102 + details: []
103 + })
104 +
105 + target = target?.trim()
106 +
107 + if (target == null || target === '') {
108 + this.setState({
109 + error: 'Please enter a PeerId or Multiaddr'
110 + })
111 +
112 + return false
113 + }
114 +
115 + try {
116 + peerIdFromString(target)
117 + } catch {
118 + try {
119 + multiaddr(target)
120 + } catch {
121 + this.setState({
122 + error: 'PeerId/Multiaddr invalid'
123 + })
124 +
125 + return false
126 + }
127 + }
128 +
129 + this.props.metrics.openConnection(target, {
130 + onProgress: (event) => {
131 + const type: string = event.type
132 + const component = type.split(':')[0]
133 +
134 + if (dialEvents[type] == null) {
135 + // eslint-disable-next-line no-console
136 + console.warn('No dial event handler for', type)
137 + return
138 + }
139 +
140 + this.setState(s => {
141 + return {
142 + details: [
143 + ...s.details,
144 + dialEvents[type](`event-${s.details.length}`, `DialEvent ${component}`, event.detail)
145 + ]
146 + }
147 + })
148 + }
149 + })
150 + .then(() => {
151 + this.setState(s => {
152 + return {
153 + error: '',
154 + details: [
155 + ...s.details,
156 + <SmallSuccess className='DialEvent' key={`event-${s.details.length}`} message='Dial successful' />
157 + ]
158 + }
159 + })
160 + })
161 + .catch(err => {
162 + this.setState(s => {
163 + return {
164 + details: [
165 + ...s.details,
166 + <SmallError className='DialEvent' key={`event-${s.details.length}`} errorPrefix='Dial failed' error={err} />
167 + ]
168 + }
169 + })
170 + })
171 +
172 + return false
173 + }
174 +
175 + render (): ReactElement {
176 + return (
177 + <Panel>
178 + <p>Enter a Peer ID or multiaddr to dial:</p>
179 + <form onSubmit={(evt) => this.dial(evt, this.state.target)}>
180 + <TextInput
181 + type='text' value={this.state.target} placeholder='123Foo...' onChange={(e) => {
182 + this.setState({
183 + target: e.target.value
184 + })
185 + }}
186 + />
187 + <Button onClick={(evt) => this.dial(evt, this.state.target)} primary>Dial</Button>
188 + </form>
189 + {this.state.error !== '' ? <SmallError error={this.state.error} /> : undefined}
190 + {this.state.details != null
191 + ? (
192 + <div className='DialEvents'>
193 + {this.state.details}
194 + </div>
195 + )
196 + : undefined}
197 + </Panel>
198 + )
199 + }
200 +}
packages/libp2p-inspector-ui/src/panels/peers/index.tsx new
+24
@@ -0,0 +1,24 @@
1 +import { useState } from 'react'
2 +import { Menu } from '../menu.js'
3 +import { DialPeer } from './dial.js'
4 +import { PeerList } from './list.js'
5 +import type { MetricsRPC, Peer } from '@ipshipyard/libp2p-inspector-metrics'
6 +import type { ReactElement } from 'react'
7 +
8 +export interface PeersProps {
9 + peers: Peer[]
10 + metrics: MetricsRPC
11 + copyToClipboard(value: string): void
12 +}
13 +
14 +export function Peers ({ peers, metrics, copyToClipboard }: PeersProps): ReactElement {
15 + const [panel, setPanel] = useState('Connected')
16 +
17 + return (
18 + <>
19 + <Menu onClick={(panel) => { setPanel(panel) }} panel={panel} options={['Connected', 'Dial']} />
20 + {panel === 'Connected' ? <PeerList peers={peers} metrics={metrics} copyToClipboard={copyToClipboard} /> : undefined}
21 + {panel === 'Dial' ? <DialPeer metrics={metrics} /> : undefined}
22 + </>
23 + )
24 +}
packages/libp2p-inspector-ui/src/panels/peers/list.css new
+12
@@ -0,0 +1,12 @@
1 +.PeerId {
2 + color: var(--dark-text-color);
3 +}
4 +
5 +.PeerTag {
6 + border-radius: 10px;
7 + display: inline-block;
8 + margin-right: 6px;
9 + background-color: var(--success-background-color);
10 + color: var(--success-text-color);
11 + padding: 5px;
12 +}
packages/libp2p-inspector-ui/src/panels/peers/list.tsx new
+129
@@ -0,0 +1,129 @@
1 +import './list.css'
2 +import { useState } from 'react'
3 +import disclosureTriangleClosed from '../../../public/img/icon-disclosure-triangle-closed.svg'
4 +import disclosureTriangleOpen from '../../../public/img/icon-disclosure-triangle-open.svg'
5 +import { getAgent } from '../../utils/get-agent.js'
6 +import { DeleteIcon } from '../icons/icon-delete.js'
7 +import { SpinnerIcon } from '../icons/icon-spinner.js'
8 +import { MultiaddrList } from '../multiaddr-list.js'
9 +import { Panel } from '../panel.js'
10 +import type { Peer as PeerType, MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
11 +import type { ReactElement } from 'react'
12 +
13 +export interface PeersProps {
14 + peers: PeerType[]
15 + metrics: MetricsRPC
16 + copyToClipboard(value: string): void
17 +}
18 +
19 +export function PeerList ({ peers, metrics, copyToClipboard }: PeersProps): ReactElement {
20 + if (peers.length === 0) {
21 + return (
22 + <Panel>
23 + <p>This node is not connected to any peers.</p>
24 + </Panel>
25 + )
26 + }
27 +
28 + return (
29 + <>
30 + {
31 + peers.map(peer => <Peer key={peer.id.toString()} peer={peer} metrics={metrics} copyToClipboard={copyToClipboard} />)
32 + }
33 + </>
34 + )
35 +}
36 +
37 +interface PeerProps {
38 + peer: PeerType
39 + copyToClipboard(value: string): void
40 + metrics: MetricsRPC
41 + key?: string
42 +}
43 +
44 +function Peer ({ peer, copyToClipboard, metrics }: PeerProps): ReactElement {
45 + const [expanded, setExpanded] = useState(false)
46 + const [disconnecting, setDisconnecting] = useState(false)
47 +
48 + function closeConnection (evt: any): void {
49 + evt.preventDefault()
50 +
51 + setDisconnecting(true)
52 + metrics.closeConnection(peer.id)
53 + .catch(err => {
54 + // eslint-disable-next-line
55 + console.error('could not close connection', err)
56 + })
57 + .finally(() => {
58 + setDisconnecting(false)
59 + })
60 + }
61 +
62 + let disconnectIcon = <DeleteIcon onClick={closeConnection} />
63 +
64 + if (disconnecting) {
65 + disconnectIcon = <SpinnerIcon />
66 + }
67 +
68 + if (expanded) {
69 + return (
70 + <Panel>
71 + <h2><img src={disclosureTriangleOpen} height={12} width={12} className='DisclosureTriangle' onClick={() => { setExpanded(false) }} /> {disconnectIcon} <PeerAgent peer={peer} /> <span className='PeerId'>{peer.id.toString()}</span></h2>
72 + <p>
73 + <PeerTags peer={peer} />
74 + </p>
75 + <h3>Multiaddrs</h3>
76 + <MultiaddrList addresses={peer.addresses} includeCertification copyToClipboard={copyToClipboard} />
77 + <PeerProtocols peer={peer} />
78 + </Panel>
79 + )
80 + }
81 +
82 + return (
83 + <Panel>
84 + <h2><img src={disclosureTriangleClosed} height={12} width={12} className='DisclosureTriangle' onClick={() => { setExpanded(true) }} /> {disconnectIcon} <PeerAgent peer={peer} /> <span className='PeerId'>{peer.id.toString()}</span></h2>
85 + <p>
86 + <PeerTags peer={peer} />
87 + </p>
88 + </Panel>
89 + )
90 +}
91 +
92 +function PeerAgent ({ peer }: Pick<PeerProps, 'peer'>): ReactElement | undefined {
93 + if (peer.metadata.AgentVersion == null) {
94 + return undefined
95 + }
96 +
97 + return (
98 + <span className='PeerAgent'>{getAgent(peer.metadata)}</span>
99 + )
100 +}
101 +
102 +function PeerTags ({ peer }: Pick<PeerProps, 'peer'>): ReactElement | undefined {
103 + const entries = Object.entries(peer.tags)
104 +
105 + if (entries.length === 0) {
106 + return undefined
107 + }
108 +
109 + return (
110 + <>
111 + {entries.map(([key, value], index) => <span key={index} className='PeerTag'>{key}</span>)}
112 + </>
113 + )
114 +}
115 +
116 +function PeerProtocols ({ peer }: Pick<PeerProps, 'peer'>): ReactElement | undefined {
117 + if (peer.protocols.length === 0) {
118 + return undefined
119 + }
120 +
121 + return (
122 + <>
123 + <h3>Protocols</h3>
124 + <ul>
125 + {peer.protocols.map((proto, index) => <li key={`proto-${index}`}>{proto}</li>)}
126 + </ul>
127 + </>
128 + )
129 +}
packages/libp2p-inspector-ui/src/panels/ping.tsx new
+127
@@ -0,0 +1,127 @@
1 +import { useState } from 'react'
2 +import { delay } from '../utils/delay.ts'
3 +import { Button } from './button.tsx'
4 +import { ConsolePanel } from './console.tsx'
5 +import { ErrorPanel } from './error.tsx'
6 +import { Panel } from './panel.tsx'
7 +import { TextInput } from './text-input.tsx'
8 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
9 +import type { JSX } from 'react'
10 +
11 +interface PingPanelProps {
12 + component: string
13 + metrics: MetricsRPC
14 +}
15 +
16 +export function Ping ({ component, metrics }: PingPanelProps): JSX.Element {
17 + const [peerIdOrMultiaddr, setPeerIdOrMultiaddr] = useState('')
18 + const [result, setResult] = useState<JSX.Element | string>('')
19 +
20 + function ping (evt: { preventDefault(): void }, topic: string): void {
21 + evt.preventDefault()
22 +
23 + let remote = peerIdOrMultiaddr
24 +
25 + setResult(<ConsolePanel>{formatHeader(remote)}</ConsolePanel>)
26 +
27 + Promise.resolve()
28 + .then(async () => {
29 + const conn = await metrics.openConnection(peerIdOrMultiaddr, {
30 + signal: AbortSignal.timeout(10_000)
31 + })
32 + remote = conn.remotePeer.toString()
33 +
34 + const results: Array<string | number> = []
35 +
36 + for (let i = 0; i < 5; i++) {
37 + try {
38 + const rtt = await metrics.ping(component, peerIdOrMultiaddr, {
39 + signal: AbortSignal.timeout(10_000)
40 + })
41 +
42 + results.push(rtt)
43 +
44 + setResult(
45 + <ConsolePanel>
46 + {formatHeader(remote)}{'\n'}
47 + {formatResults(results)}
48 + </ConsolePanel>
49 + )
50 +
51 + await delay(1_000)
52 + } catch (err: any) {
53 + results.push(err.message)
54 + }
55 + }
56 +
57 + setResult((
58 + <ConsolePanel>
59 + {formatHeader(remote)}{'\n'}
60 + {formatResults(results)}{'\n'}
61 + --- {peerIdOrMultiaddr} ping statistics ---{'\n'}
62 + {calculateStats(results)}
63 + </ConsolePanel>
64 + ))
65 + })
66 + .catch(err => {
67 + setResult(<ErrorPanel error={err} />)
68 + })
69 + }
70 +
71 + return (
72 + <Panel>
73 + <p>Ping a peer</p>
74 + <form onSubmit={(evt) => ping(evt, peerIdOrMultiaddr)}>
75 + <TextInput type='text' value={peerIdOrMultiaddr} placeholder='Peer ID or Multiaddr' onChange={(e) => { setPeerIdOrMultiaddr(e.target.value) }} />
76 + <Button onClick={(evt) => ping(evt, peerIdOrMultiaddr)} primary>Ping</Button>
77 + </form>
78 + {result}
79 + </Panel>
80 + )
81 +}
82 +
83 +function formatHeader (remote: string): string {
84 + return `PING ${remote}: 32 data bytes`
85 +}
86 +
87 +function formatResults (results: Array<string | number>): string {
88 + return results.map((res, index) => {
89 + if (typeof res === 'string') {
90 + return `seq=${index} ${res}`
91 + }
92 +
93 + return `seq=${index} time=${res} ms`
94 + })
95 + .join('\n')
96 +}
97 +
98 +function calculateStats (results: Array<string | number>): string {
99 + const success = results
100 + .filter(res => typeof res !== 'string')
101 +
102 + let min = Infinity
103 + let max = 0
104 + let sum = 0
105 +
106 + for (const res of success) {
107 + if (res < min) {
108 + min = res
109 + }
110 +
111 + if (res > max) {
112 + max = res
113 + }
114 +
115 + sum += res
116 + }
117 +
118 + const mean = sum / success.length
119 + const squaredDeviations = success.map(val => Math.pow(val - mean, 2))
120 + const variance = squaredDeviations.reduce((acc, curr) => acc + curr, 0) / (success.length - 1)
121 + const stdDeviation = Math.sqrt(variance)
122 +
123 + return [
124 + `${results.length} packets transmitted, ${success.length} packets received, ${100 - Math.round((success.length / results.length) * 100)}% packet loss`,
125 + `round-trip min/avg/max/stddev = ${min}/${Math.round(mean)}/${max}/${Math.round(stdDeviation)} ms`
126 + ].join('\n')
127 +}
packages/libp2p-inspector-ui/src/panels/pubsub/index.tsx new
+157
@@ -0,0 +1,157 @@
1 +import './pubsub.css'
2 +import { useState } from 'react'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { Message } from '@libp2p/interface'
9 +import type { ReactElement } from 'react'
10 +
11 +export interface PubSubProps {
12 + component: string
13 + metrics: MetricsRPC
14 + pubsub: Record<string, Message[]>
15 +}
16 +
17 +export function PubSub ({ component, metrics, pubsub }: PubSubProps): ReactElement {
18 + const [topics, setTopics] = useState<string[]>([])
19 + const [topic, setTopic] = useState('')
20 +
21 + function loadTopics (): void {
22 + metrics.pubsub.getTopics(component)
23 + .then(list => {
24 + setTopics(list)
25 + })
26 + .catch(err => {
27 + // eslint-disable-next-line no-console
28 + console.error('could not get subs', err)
29 + })
30 + }
31 +
32 + loadTopics()
33 +
34 + return (
35 + <>
36 + <SubscribePanel component={component} metrics={metrics} subscribed={loadTopics} />
37 + <TopicList topics={topics} setTopic={setTopic} />
38 + <TopicDisplay topic={topic} component={component} metrics={metrics} messages={pubsub[topic] ?? []} />
39 + </>
40 + )
41 +}
42 +
43 +interface SubscribePanelOptions {
44 + component: string
45 + subscribed(): void
46 + metrics: MetricsRPC
47 +}
48 +
49 +function SubscribePanel ({ component, subscribed, metrics }: SubscribePanelOptions): ReactElement {
50 + const [topic, setTopic] = useState('')
51 + const [result, setResult] = useState(<small>E.g. `my-topic`, etc</small>)
52 +
53 + function subscribe (evt: { preventDefault(): void }, topic: string): boolean {
54 + evt.preventDefault()
55 +
56 + metrics.pubsub.subscribe(component, topic)
57 + .then(() => {
58 + setResult(<SmallSuccess message='Subscribed successfully' />)
59 + subscribed()
60 + }, (err) => {
61 + setResult(<SmallError error={err} />)
62 + })
63 +
64 + return false
65 + }
66 +
67 + return (
68 + <Panel>
69 + <p>Subscribe to a PubSub topic</p>
70 + <form onSubmit={(evt) => subscribe(evt, topic)}>
71 + <TextInput type='text' value={topic} placeholder='Topic' onChange={(e) => { setTopic(e.target.value) }} />
72 + <Button onClick={(evt) => subscribe(evt, topic)} primary>Subscribe</Button>
73 + </form>
74 + {result}
75 + </Panel>
76 + )
77 +}
78 +
79 +interface TopicListOptions {
80 + topics: string[]
81 + setTopic(topic: string): void
82 +}
83 +
84 +function TopicList ({ topics, setTopic }: TopicListOptions): ReactElement {
85 + return (
86 + <div>
87 + <ul>
88 + {topics.map((topic, index) => (
89 + <li key={`sub-${index}`} onClick={() => { setTopic(topic) }}>
90 + {topic}
91 + </li>
92 + ))}
93 + </ul>
94 + </div>
95 + )
96 +}
97 +
98 +interface TopicDisplayOptions {
99 + component: string
100 + topic: string
101 + metrics: MetricsRPC
102 + messages: Message[]
103 +}
104 +
105 +function TopicDisplay ({ component, topic, metrics, messages }: TopicDisplayOptions): ReactElement {
106 + if (topic === '') {
107 + return <></>
108 + }
109 +
110 + const [message, setMessage] = useState('')
111 + const [result, setResult] = useState(<small>E.g. `hello world`, etc</small>)
112 +
113 + function publish (evt: { preventDefault(): void }, topic: string): boolean {
114 + evt.preventDefault()
115 +
116 + metrics.pubsub.publish(component, topic, new TextEncoder().encode(message))
117 + .then(() => {
118 + setResult(<SmallSuccess message='Published successfully' />)
119 + }, (err) => {
120 + setResult(<SmallError error={err} />)
121 + })
122 +
123 + return false
124 + }
125 +
126 + return (
127 + <>
128 + <Panel>
129 + <p>Publish a message</p>
130 + <form onSubmit={(evt) => publish(evt, topic)}>
131 + <TextInput type='text' value={message} placeholder='Message' onChange={(e) => { setMessage(e.target.value) }} />
132 + <Button onClick={(evt) => publish(evt, topic)} primary>Publish</Button>
133 + </form>
134 + {result}
135 + </Panel>
136 + <Panel>
137 + {messages.map((message, index) => {
138 + if (message.type === 'signed') {
139 + return (
140 + <div key={`message-${index}`}>
141 + <p>From: {message.from.toString()}</p>
142 + <p>Seq: {message.sequenceNumber.toString()}</p>
143 + <p>Data: {new TextDecoder().decode(message.data)}</p>
144 + </div>
145 + )
146 + }
147 +
148 + return (
149 + <div key={`message-${index}`}>
150 + <p>Data: {new TextDecoder().decode(message.data)}</p>
151 + </div>
152 + )
153 + })}
154 + </Panel>
155 + </>
156 + )
157 +}
packages/libp2p-inspector-ui/src/panels/pubsub/pubsub.css
packages/libp2p-inspector-ui/src/panels/routing/find-peer.tsx new
+141
@@ -0,0 +1,141 @@
1 +import { peerIdFromString } from '@libp2p/peer-id'
2 +import { Component } from 'react'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { PeerId } from '@libp2p/interface'
9 +import type { FormEvent, ReactElement, JSX } from 'react'
10 +
11 +export interface FindPeerProps {
12 + metrics: MetricsRPC
13 +}
14 +
15 +export interface FindPeerState {
16 + target: string
17 + error: string
18 + details: JSX.Element[]
19 +}
20 +
21 +export class FindPeer extends Component<FindPeerProps, FindPeerState> {
22 + constructor (props: FindPeerProps) {
23 + super(props)
24 +
25 + this.state = {
26 + target: '',
27 + error: '',
28 + details: []
29 + }
30 + }
31 +
32 + componentDidMount (): void {
33 + this.setState({
34 + target: '',
35 + error: '',
36 + details: []
37 + })
38 + }
39 +
40 + private findPeer (evt: FormEvent | Event, arg0: string): boolean {
41 + evt.preventDefault()
42 +
43 + this.setState({
44 + error: '',
45 + details: []
46 + })
47 +
48 + arg0 = arg0?.trim()
49 +
50 + if (arg0 == null || arg0 === '') {
51 + this.setState({
52 + error: 'Please enter a key'
53 + })
54 +
55 + return false
56 + }
57 +
58 + let peerId: PeerId
59 +
60 + try {
61 + peerId = peerIdFromString(arg0)
62 + } catch {
63 + this.setState({
64 + error: 'PeerId invalid'
65 + })
66 +
67 + return false
68 + }
69 +
70 + this.props.metrics.peerRouting.findPeer(peerId, {
71 + onProgress: (event) => {
72 + let message: string = event.type
73 +
74 + if (event.type === 'dial:already-connected') {
75 + message = 'Already connected to this peer'
76 + } else if (event.type === 'dial:add-to-dial-queue') {
77 + message = 'Adding dial to queue'
78 + } else if (event.type === 'dial:already-in-dial-queue') {
79 + message = 'Dial to this peer already in queue'
80 + } else if (event.type === 'dial:calculate-addresses') {
81 + message = 'Calculating addresses'
82 + } else if (event.type === 'dial:selected-transport') {
83 + message = `Selected transport ${event.detail}`
84 + }
85 +
86 + this.setState(s => {
87 + return {
88 + details: [
89 + ...s.details,
90 + <p key={`event-${s.details.length}`}>{message}</p>
91 + ]
92 + }
93 + })
94 + }
95 + })
96 + .then((peerInfo) => {
97 + this.setState(s => {
98 + return {
99 + error: '',
100 + details: [
101 + ...s.details,
102 + <pre key='results'><code>{JSON.stringify(peerInfo, null, 2)}</code></pre>,
103 + <SmallSuccess key={`event-${s.details.length}`} message='Get successful' />
104 + ]
105 + }
106 + })
107 + })
108 + .catch(err => {
109 + this.setState(s => {
110 + return {
111 + details: [
112 + ...s.details,
113 + <SmallError key={`event-${s.details.length}`} error={err} />
114 + ]
115 + }
116 + })
117 + })
118 +
119 + return false
120 + }
121 +
122 + render (): ReactElement {
123 + return (
124 + <Panel>
125 + <p>Enter a PeerId:</p>
126 + <form onSubmit={(evt) => this.findPeer(evt, this.state.target)}>
127 + <TextInput
128 + type='text' value={this.state.target} placeholder='123Foo...' onChange={(e) => {
129 + this.setState({
130 + target: e.target.value
131 + })
132 + }}
133 + />
134 + <Button onClick={(evt) => this.findPeer(evt, this.state.target)} primary>Get</Button>
135 + </form>
136 + {this.state.error != null ? <SmallError error={this.state.error} /> : undefined}
137 + {this.state.details}
138 + </Panel>
139 + )
140 + }
141 +}
packages/libp2p-inspector-ui/src/panels/routing/find-providers.tsx new
+142
@@ -0,0 +1,142 @@
1 +import { bases } from 'multiformats/basics'
2 +import { Component } from 'react'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { FormEvent, JSX } from 'react'
9 +
10 +// @ts-expect-error - Not easy to combine these types.
11 +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
12 +
13 +export interface FindProvidersProps {
14 + metrics: MetricsRPC
15 +}
16 +
17 +export interface FindProvidersState {
18 + target: string
19 + error: string
20 + details: JSX.Element[]
21 +}
22 +
23 +export class FindProviders extends Component<FindProvidersProps, FindProvidersState> {
24 + constructor (props: FindProvidersProps) {
25 + super(props)
26 +
27 + this.state = {
28 + target: '',
29 + error: '',
30 + details: []
31 + }
32 + }
33 +
34 + componentDidMount (): void {
35 + this.setState({
36 + target: '',
37 + error: '',
38 + details: []
39 + })
40 + }
41 +
42 + private findProviders (evt: FormEvent | Event, target: string): boolean {
43 + evt.preventDefault()
44 +
45 + this.setState({
46 + error: '',
47 + details: []
48 + })
49 +
50 + target = target?.trim()
51 +
52 + if (target == null || target === '') {
53 + this.setState({
54 + error: 'Please enter a key'
55 + })
56 +
57 + return false
58 + }
59 +
60 + let key: Uint8Array
61 +
62 + try {
63 + key = multibaseDecoder.decode(target)
64 + } catch {
65 + this.setState({
66 + error: 'Key invalid'
67 + })
68 +
69 + return false
70 + }
71 +
72 + this.props.metrics.contentRouting.get(key, {
73 + onProgress: (event) => {
74 + let message: string = event.type
75 +
76 + if (event.type === 'dial:already-connected') {
77 + message = 'Already connected to this peer'
78 + } else if (event.type === 'dial:add-to-dial-queue') {
79 + message = 'Adding dial to queue'
80 + } else if (event.type === 'dial:already-in-dial-queue') {
81 + message = 'Dial to this peer already in queue'
82 + } else if (event.type === 'dial:calculate-addresses') {
83 + message = 'Calculating addresses'
84 + } else if (event.type === 'dial:selected-transport') {
85 + message = `Selected transport ${event.detail}`
86 + }
87 +
88 + this.setState(s => {
89 + return {
90 + details: [
91 + ...s.details,
92 + <p key={`event-${s.details.length}`}>{message}</p>
93 + ]
94 + }
95 + })
96 + }
97 + })
98 + .then(() => {
99 + this.setState(s => {
100 + return {
101 + error: '',
102 + details: [
103 + ...s.details,
104 + <SmallSuccess key={`event-${s.details.length}`} message='Get successful' />
105 + ]
106 + }
107 + })
108 + })
109 + .catch(err => {
110 + this.setState(s => {
111 + return {
112 + details: [
113 + ...s.details,
114 + <SmallError key={`event-${s.details.length}`} error={err} />
115 + ]
116 + }
117 + })
118 + })
119 +
120 + return false
121 + }
122 +
123 + render (): JSX.Element {
124 + return (
125 + <Panel>
126 + <p>Enter a CID to find providers of:</p>
127 + <form onSubmit={(evt) => this.findProviders(evt, this.state.target)}>
128 + <TextInput
129 + type='text' value={this.state.target} placeholder='bafyfoo...' onChange={(e) => {
130 + this.setState({
131 + target: e.target.value
132 + })
133 + }}
134 + />
135 + <Button onClick={(evt) => this.findProviders(evt, this.state.target)} primary>Get</Button>
136 + </form>
137 + {this.state.error ? <SmallError error={this.state.error} /> : undefined}
138 + {this.state.details}
139 + </Panel>
140 + )
141 + }
142 +}
packages/libp2p-inspector-ui/src/panels/routing/get-closest-peers.tsx new
+142
@@ -0,0 +1,142 @@
1 +import { bases } from 'multiformats/basics'
2 +import { Component } from 'react'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { FormEvent, JSX } from 'react'
9 +
10 +// @ts-expect-error - Not easy to combine these types.
11 +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
12 +
13 +export interface GetClosestPeersProps {
14 + metrics: MetricsRPC
15 +}
16 +
17 +export interface GetClosestPeersState {
18 + target: string
19 + error: string
20 + details: JSX.Element[]
21 +}
22 +
23 +export class GetClosestPeers extends Component<GetClosestPeersProps, GetClosestPeersState> {
24 + constructor (props: GetClosestPeersProps) {
25 + super(props)
26 +
27 + this.state = {
28 + target: '',
29 + error: '',
30 + details: []
31 + }
32 + }
33 +
34 + componentDidMount (): void {
35 + this.setState({
36 + target: '',
37 + error: '',
38 + details: []
39 + })
40 + }
41 +
42 + private getClosestPeers (evt: FormEvent | Event, target: string): boolean {
43 + evt.preventDefault()
44 +
45 + this.setState({
46 + error: '',
47 + details: []
48 + })
49 +
50 + target = target?.trim()
51 +
52 + if (target == null || target === '') {
53 + this.setState({
54 + error: 'Please enter a key'
55 + })
56 +
57 + return false
58 + }
59 +
60 + let key: Uint8Array
61 +
62 + try {
63 + key = multibaseDecoder.decode(target)
64 + } catch {
65 + this.setState({
66 + error: 'Key invalid'
67 + })
68 +
69 + return false
70 + }
71 +
72 + this.props.metrics.contentRouting.get(key, {
73 + onProgress: (event) => {
74 + let message: string = event.type
75 +
76 + if (event.type === 'dial:already-connected') {
77 + message = 'Already connected to this peer'
78 + } else if (event.type === 'dial:add-to-dial-queue') {
79 + message = 'Adding dial to queue'
80 + } else if (event.type === 'dial:already-in-dial-queue') {
81 + message = 'Dial to this peer already in queue'
82 + } else if (event.type === 'dial:calculate-addresses') {
83 + message = 'Calculating addresses'
84 + } else if (event.type === 'dial:selected-transport') {
85 + message = `Selected transport ${event.detail}`
86 + }
87 +
88 + this.setState(s => {
89 + return {
90 + details: [
91 + ...s.details,
92 + <p key={`event-${s.details.length}`}>{message}</p>
93 + ]
94 + }
95 + })
96 + }
97 + })
98 + .then(() => {
99 + this.setState(s => {
100 + return {
101 + error: '',
102 + details: [
103 + ...s.details,
104 + <SmallSuccess key={`event-${s.details.length}`} message='Get successful' />
105 + ]
106 + }
107 + })
108 + })
109 + .catch(err => {
110 + this.setState(s => {
111 + return {
112 + details: [
113 + ...s.details,
114 + <SmallError key={`event-${s.details.length}`} error={err} />
115 + ]
116 + }
117 + })
118 + })
119 +
120 + return false
121 + }
122 +
123 + render (): JSX.Element {
124 + return (
125 + <Panel>
126 + <p>Enter a multibase encoded key to find the closest peers to:</p>
127 + <form onSubmit={(evt) => this.getClosestPeers(evt, this.state.target)}>
128 + <TextInput
129 + type='text' value={this.state.target} placeholder='mKey...' onChange={(e) => {
130 + this.setState({
131 + target: e.target.value
132 + })
133 + }}
134 + />
135 + <Button onClick={(evt) => this.getClosestPeers(evt, this.state.target)} primary>Get</Button>
136 + </form>
137 + {this.state.error ? <SmallError error={this.state.error} /> : undefined}
138 + {this.state.details}
139 + </Panel>
140 + )
141 + }
142 +}
packages/libp2p-inspector-ui/src/panels/routing/get.tsx new
+142
@@ -0,0 +1,142 @@
1 +import { bases } from 'multiformats/basics'
2 +import { Component } from 'react'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { FormEvent, JSX } from 'react'
9 +
10 +// @ts-expect-error - Not easy to combine these types.
11 +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
12 +
13 +export interface GetProps {
14 + metrics: MetricsRPC
15 +}
16 +
17 +export interface GetState {
18 + target: string
19 + error: string
20 + details: JSX.Element[]
21 +}
22 +
23 +export class Get extends Component<GetProps, GetState> {
24 + constructor (props: GetProps) {
25 + super(props)
26 +
27 + this.state = {
28 + target: '',
29 + error: '',
30 + details: []
31 + }
32 + }
33 +
34 + componentDidMount (): void {
35 + this.setState({
36 + target: '',
37 + error: '',
38 + details: []
39 + })
40 + }
41 +
42 + private get (evt: FormEvent | Event, target: string): boolean {
43 + evt.preventDefault()
44 +
45 + this.setState({
46 + error: '',
47 + details: []
48 + })
49 +
50 + target = target?.trim()
51 +
52 + if (target == null || target === '') {
53 + this.setState({
54 + error: 'Please enter a key'
55 + })
56 +
57 + return false
58 + }
59 +
60 + let key: Uint8Array
61 +
62 + try {
63 + key = multibaseDecoder.decode(target)
64 + } catch {
65 + this.setState({
66 + error: 'Key invalid'
67 + })
68 +
69 + return false
70 + }
71 +
72 + this.props.metrics.contentRouting.get(key, {
73 + onProgress: (event) => {
74 + let message: string = event.type
75 +
76 + if (event.type === 'dial:already-connected') {
77 + message = 'Already connected to this peer'
78 + } else if (event.type === 'dial:add-to-dial-queue') {
79 + message = 'Adding dial to queue'
80 + } else if (event.type === 'dial:already-in-dial-queue') {
81 + message = 'Dial to this peer already in queue'
82 + } else if (event.type === 'dial:calculate-addresses') {
83 + message = 'Calculating addresses'
84 + } else if (event.type === 'dial:selected-transport') {
85 + message = `Selected transport ${event.detail}`
86 + }
87 +
88 + this.setState(s => {
89 + return {
90 + details: [
91 + ...s.details,
92 + <p key={`event-${s.details.length}`}>{message}</p>
93 + ]
94 + }
95 + })
96 + }
97 + })
98 + .then(() => {
99 + this.setState(s => {
100 + return {
101 + error: '',
102 + details: [
103 + ...s.details,
104 + <SmallSuccess key={`event-${s.details.length}`} message='Get successful' />
105 + ]
106 + }
107 + })
108 + })
109 + .catch(err => {
110 + this.setState(s => {
111 + return {
112 + details: [
113 + ...s.details,
114 + <SmallError key={`event-${s.details.length}`} error={err} />
115 + ]
116 + }
117 + })
118 + })
119 +
120 + return false
121 + }
122 +
123 + render (): JSX.Element {
124 + return (
125 + <Panel>
126 + <p>Enter a multibase encoded key to look up in the routing:</p>
127 + <form onSubmit={(evt) => this.get(evt, this.state.target)}>
128 + <TextInput
129 + type='text' value={this.state.target} placeholder='mKey...' onChange={(e) => {
130 + this.setState({
131 + target: e.target.value
132 + })
133 + }}
134 + />
135 + <Button onClick={(evt) => this.get(evt, this.state.target)} primary>Get</Button>
136 + </form>
137 + {this.state.error ? <SmallError error={this.state.error} /> : undefined}
138 + {this.state.details}
139 + </Panel>
140 + )
141 + }
142 +}
packages/libp2p-inspector-ui/src/panels/routing/index.tsx new
+30
@@ -0,0 +1,30 @@
1 +import { useState } from 'react'
2 +import { Menu } from '../menu.js'
3 +import { FindPeer } from './find-peer.js'
4 +import { FindProviders } from './find-providers.js'
5 +import { GetClosestPeers } from './get-closest-peers.js'
6 +import { Get } from './get.js'
7 +import { Provide } from './provide.js'
8 +import { Put } from './put.js'
9 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
10 +import type { JSX } from 'react'
11 +
12 +export interface PeersProps {
13 + metrics: MetricsRPC
14 +}
15 +
16 +export function Routing ({ metrics }: PeersProps): JSX.Element {
17 + const [panel, setPanel] = useState('Get')
18 +
19 + return (
20 + <>
21 + <Menu onClick={(panel) => setPanel(panel)} panel={panel} options={['Get', 'Put', 'Find Providers', 'Provide', 'Find Peer', 'Get Closest Peers']} />
22 + {panel === 'Get' ? <Get metrics={metrics} /> : undefined}
23 + {panel === 'Put' ? <Put metrics={metrics} /> : undefined}
24 + {panel === 'Find Providers' ? <FindProviders metrics={metrics} /> : undefined}
25 + {panel === 'Provide' ? <Provide metrics={metrics} /> : undefined}
26 + {panel === 'Find Peer' ? <FindPeer metrics={metrics} /> : undefined}
27 + {panel === 'Get Closest Peers' ? <GetClosestPeers metrics={metrics} /> : undefined}
28 + </>
29 + )
30 +}
packages/libp2p-inspector-ui/src/panels/routing/provide.tsx new
+142
@@ -0,0 +1,142 @@
1 +import { bases } from 'multiformats/basics'
2 +import { Component } from 'react'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { FormEvent, JSX } from 'react'
9 +
10 +// @ts-expect-error - Not easy to combine these types.
11 +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
12 +
13 +export interface ProvideProps {
14 + metrics: MetricsRPC
15 +}
16 +
17 +export interface ProvideState {
18 + target: string
19 + error: string
20 + details: JSX.Element[]
21 +}
22 +
23 +export class Provide extends Component<ProvideProps, ProvideState> {
24 + constructor (props: ProvideProps) {
25 + super(props)
26 +
27 + this.state = {
28 + target: '',
29 + error: '',
30 + details: []
31 + }
32 + }
33 +
34 + componentDidMount (): void {
35 + this.setState({
36 + target: '',
37 + error: '',
38 + details: []
39 + })
40 + }
41 +
42 + private provide (evt: FormEvent | Event, target: string): boolean {
43 + evt.preventDefault()
44 +
45 + this.setState({
46 + error: '',
47 + details: []
48 + })
49 +
50 + target = target?.trim()
51 +
52 + if (target == null || target === '') {
53 + this.setState({
54 + error: 'Please enter a key'
55 + })
56 +
57 + return false
58 + }
59 +
60 + let key: Uint8Array
61 +
62 + try {
63 + key = multibaseDecoder.decode(target)
64 + } catch {
65 + this.setState({
66 + error: 'Key invalid'
67 + })
68 +
69 + return false
70 + }
71 +
72 + this.props.metrics.contentRouting.get(key, {
73 + onProgress: (event) => {
74 + let message: string = event.type
75 +
76 + if (event.type === 'dial:already-connected') {
77 + message = 'Already connected to this peer'
78 + } else if (event.type === 'dial:add-to-dial-queue') {
79 + message = 'Adding dial to queue'
80 + } else if (event.type === 'dial:already-in-dial-queue') {
81 + message = 'Dial to this peer already in queue'
82 + } else if (event.type === 'dial:calculate-addresses') {
83 + message = 'Calculating addresses'
84 + } else if (event.type === 'dial:selected-transport') {
85 + message = `Selected transport ${event.detail}`
86 + }
87 +
88 + this.setState(s => {
89 + return {
90 + details: [
91 + ...s.details,
92 + <p key={`event-${s.details.length}`}>{message}</p>
93 + ]
94 + }
95 + })
96 + }
97 + })
98 + .then(() => {
99 + this.setState(s => {
100 + return {
101 + error: '',
102 + details: [
103 + ...s.details,
104 + <SmallSuccess key={`event-${s.details.length}`} message='Get successful' />
105 + ]
106 + }
107 + })
108 + })
109 + .catch(err => {
110 + this.setState(s => {
111 + return {
112 + details: [
113 + ...s.details,
114 + <SmallError key={`event-${s.details.length}`} error={err} />
115 + ]
116 + }
117 + })
118 + })
119 +
120 + return false
121 + }
122 +
123 + render (): JSX.Element {
124 + return (
125 + <Panel>
126 + <p>Enter a CID to publish provider records for:</p>
127 + <form onSubmit={(evt) => this.provide(evt, this.state.target)}>
128 + <TextInput
129 + type='text' value={this.state.target} placeholder='bafyfoo...' onChange={(e) => {
130 + this.setState({
131 + target: e.target.value
132 + })
133 + }}
134 + />
135 + <Button onClick={(evt) => this.provide(evt, this.state.target)} primary>Get</Button>
136 + </form>
137 + {this.state.error ? <SmallError error={this.state.error} /> : undefined}
138 + {this.state.details}
139 + </Panel>
140 + )
141 + }
142 +}
packages/libp2p-inspector-ui/src/panels/routing/put.tsx new
+129
@@ -0,0 +1,129 @@
1 +import { Component } from 'react'
2 +import { getBytes } from '../../utils/get-bytes.js'
3 +import { Button } from '../button.js'
4 +import { Panel } from '../panel.js'
5 +import { SmallError, SmallSuccess } from '../status.js'
6 +import { TextInput } from '../text-input.js'
7 +import type { MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
8 +import type { FormEvent, JSX } from 'react'
9 +
10 +export interface PutProps {
11 + metrics: MetricsRPC
12 +}
13 +
14 +export interface PutState {
15 + key: string
16 + value: string
17 + error: string
18 + details: JSX.Element[]
19 +}
20 +
21 +export class Put extends Component<PutProps, PutState> {
22 + constructor (props: PutProps) {
23 + super(props)
24 +
25 + this.state = {
26 + key: '',
27 + value: '',
28 + error: '',
29 + details: []
30 + }
31 + }
32 +
33 + componentDidMount (): void {
34 + this.setState({
35 + key: '',
36 + value: '',
37 + error: '',
38 + details: []
39 + })
40 + }
41 +
42 + private put (evt: FormEvent | Event, key: string, value: string): boolean {
43 + evt.preventDefault()
44 +
45 + this.setState({
46 + error: '',
47 + details: []
48 + })
49 +
50 + Promise.resolve()
51 + .then(async () => {
52 + await this.props.metrics.contentRouting.put(getBytes(key), getBytes(value), {
53 + onProgress: (event) => {
54 + let message: string = event.type
55 +
56 + if (event.type === 'dial:already-connected') {
57 + message = 'Already connected to this peer'
58 + } else if (event.type === 'dial:add-to-dial-queue') {
59 + message = 'Adding dial to queue'
60 + } else if (event.type === 'dial:already-in-dial-queue') {
61 + message = 'Dial to this peer already in queue'
62 + } else if (event.type === 'dial:calculate-addresses') {
63 + message = 'Calculating addresses'
64 + } else if (event.type === 'dial:selected-transport') {
65 + message = `Selected transport ${event.detail}`
66 + }
67 +
68 + this.setState(s => {
69 + return {
70 + details: [
71 + ...s.details,
72 + <p key={`event-${s.details.length}`}>{message}</p>
73 + ]
74 + }
75 + })
76 + }
77 + })
78 +
79 + this.setState(s => {
80 + return {
81 + error: '',
82 + details: [
83 + ...s.details,
84 + <SmallSuccess key={`event-${s.details.length}`} message='Get successful' />
85 + ]
86 + }
87 + })
88 + })
89 + .catch(err => {
90 + this.setState(s => {
91 + return {
92 + details: [
93 + ...s.details,
94 + <SmallError key={`event-${s.details.length}`} error={err} />
95 + ]
96 + }
97 + })
98 + })
99 +
100 + return false
101 + }
102 +
103 + render (): JSX.Element {
104 + return (
105 + <Panel>
106 + <p>Enter a multibase encoded key and value store in the routing:</p>
107 + <form onSubmit={(evt) => this.put(evt, this.state.key, this.state.value)}>
108 + <TextInput
109 + type='text' value={this.state.key} placeholder='mKey...' onChange={(e) => {
110 + this.setState({
111 + key: e.target.value
112 + })
113 + }}
114 + />
115 + <TextInput
116 + type='text' value={this.state.value} placeholder='mValue...' onChange={(e) => {
117 + this.setState({
118 + value: e.target.value
119 + })
120 + }}
121 + />
122 + <Button onClick={(evt) => this.put(evt, this.state.key, this.state.value)} primary>Get</Button>
123 + </form>
124 + {this.state.error ? <SmallError error={this.state.error} /> : undefined}
125 + {this.state.details}
126 + </Panel>
127 + )
128 + }
129 +}
packages/libp2p-inspector-ui/src/panels/status.css new
+9
@@ -0,0 +1,9 @@
1 +small.Error {
2 + color: var(--warning-text-color);
3 + margin-top: 6px;
4 +}
5 +
6 +small.Success {
7 + color: var(--success-text-color);
8 + margin-top: 6px;
9 +}
packages/libp2p-inspector-ui/src/panels/status.tsx new
+25
@@ -0,0 +1,25 @@
1 +import type { ReactElement } from 'react'
2 +import './status.css'
3 +
4 +export interface ErrorProps {
5 + error: Error | string
6 + className?: string
7 + errorPrefix?: string
8 +}
9 +
10 +export function SmallError ({ error, className, errorPrefix }: ErrorProps): ReactElement {
11 + return (
12 + <small className={`Error ${className ?? ''}`}>{errorPrefix != null ? `${errorPrefix}: ` : ''}{error instanceof Error ? error.message : error}</small>
13 + )
14 +}
15 +
16 +export interface SuccessProps {
17 + message: string
18 + className?: string
19 +}
20 +
21 +export function SmallSuccess ({ message, className }: SuccessProps): ReactElement {
22 + return (
23 + <small className={`Success ${className ?? ''}`}>{message}</small>
24 + )
25 +}
packages/libp2p-inspector-ui/src/panels/text-input.css new
+3
@@ -0,0 +1,3 @@
1 +.TextInput {
2 + margin-right: 6px;
3 +}
packages/libp2p-inspector-ui/src/panels/text-input.tsx new
+16
@@ -0,0 +1,16 @@
1 +import 'react'
2 +import './text-input.css'
3 +import type { ChangeEvent, JSX } from 'react'
4 +
5 +interface TextInputProps {
6 + type?: string
7 + value?: string
8 + placeholder?: string
9 + onChange(evt: ChangeEvent<HTMLInputElement>): void
10 +}
11 +
12 +export function TextInput (props: TextInputProps): JSX.Element {
13 + return (
14 + <input className='TextInput' {...props} />
15 + )
16 +}
packages/libp2p-inspector-ui/src/reset.css new
+48
@@ -0,0 +1,48 @@
1 +/* http://meyerweb.com/eric/tools/css/reset/
2 + v2.0 | 20110126
3 + License: none (public domain)
4 +*/
5 +
6 +html, body, div, span, applet, object, iframe,
7 +h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 +a, abbr, acronym, address, big, cite, code,
9 +del, dfn, em, img, ins, kbd, q, s, samp,
10 +small, strike, strong, sub, sup, tt, var,
11 +b, u, i, center,
12 +dl, dt, dd, ol, ul, li,
13 +fieldset, form, label, legend,
14 +table, caption, tbody, tfoot, thead, tr, th, td,
15 +article, aside, canvas, details, embed,
16 +figure, figcaption, footer, header, hgroup,
17 +menu, nav, output, ruby, section, summary,
18 +time, mark, audio, video {
19 + margin: 0;
20 + padding: 0;
21 + border: 0;
22 + font-size: 100%;
23 + font: inherit;
24 + vertical-align: baseline;
25 +}
26 +/* HTML5 display-role reset for older browsers */
27 +article, aside, details, figcaption, figure,
28 +footer, header, hgroup, menu, nav, section {
29 + display: block;
30 +}
31 +body {
32 + line-height: 1;
33 +}
34 +ol, ul {
35 + list-style: none;
36 +}
37 +blockquote, q {
38 + quotes: none;
39 +}
40 +blockquote:before, blockquote:after,
41 +q:before, q:after {
42 + content: '';
43 + content: none;
44 +}
45 +table {
46 + border-collapse: collapse;
47 + border-spacing: 0;
48 +}
packages/libp2p-inspector-ui/src/svg.d.ts new
+5
@@ -0,0 +1,5 @@
1 +// allows importing .svg files
2 +declare module '*.svg' {
3 + const content: string
4 + export default content
5 +}
packages/libp2p-inspector-ui/src/utils/delay.ts new
+7
@@ -0,0 +1,7 @@
1 +export async function delay (ms: number): Promise<void> {
2 + await new Promise<void>(resolve => {
3 + setTimeout(() => {
4 + resolve()
5 + }, ms)
6 + })
7 +}
packages/libp2p-inspector-ui/src/utils/get-agent.ts new
+10
@@ -0,0 +1,10 @@
1 +import { base64 } from 'multiformats/bases/base64'
2 +import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
3 +
4 +export function getAgent (metadata: Record<string, string>): string {
5 + if (metadata.AgentVersion != null) {
6 + return uint8ArrayToString(base64.decode(metadata.AgentVersion))
7 + }
8 +
9 + return ''
10 +}
packages/libp2p-inspector-ui/src/utils/get-bytes.ts new
+14
@@ -0,0 +1,14 @@
1 +import { bases } from 'multiformats/basics'
2 +
3 +// @ts-expect-error - Not easy to combine these types.
4 +const multibaseDecoder = Object.values(bases).map(b => b.decoder).reduce((d, b) => d.or(b))
5 +
6 +export function getBytes (str: string): Uint8Array {
7 + str = str?.trim()
8 +
9 + if (str == null || str === '') {
10 + throw new Error('Please enter a key')
11 + }
12 +
13 + return multibaseDecoder.decode(str)
14 +}
packages/libp2p-inspector-ui/tsconfig.json new
+25
@@ -0,0 +1,25 @@
1 +{
2 + "extends": "aegir/src/config/tsconfig.aegir.json",
3 + "compilerOptions": {
4 + "target": "ES2024",
5 + "lib": [
6 + "ES2023",
7 + "ES2023.Array",
8 + "DOM",
9 + "DOM.Iterable",
10 + "WebWorker",
11 + "ES2024.Promise"
12 + ],
13 + "outDir": "dist",
14 + "jsx": "react-jsx"
15 + },
16 + "include": [
17 + "src",
18 + "test"
19 + ],
20 + "references": [
21 + {
22 + "path": "../libp2p-inspector-metrics"
23 + }
24 + ]
25 +}
packages/libp2p-inspector-ui/typedoc.json new
+5
@@ -0,0 +1,5 @@
1 +{
2 + "entryPoints": [
3 + "./src/index.ts"
4 + ]
5 +}
packages/libp2p-inspector/.aegir.js new
+9
@@ -0,0 +1,9 @@
1 +/** @type {import("./src/types").PartialOptions} */
2 +export default {
3 + dependencyCheck: {
4 + ignore: [
5 + 'esbuild',
6 + 'fast-glob'
7 + ]
8 + }
9 +}
packages/libp2p-inspector/CODE_OF_CONDUCT.md new
+3
@@ -0,0 +1,3 @@
1 +# Contributor Code of Conduct
2 +
3 +This project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
packages/libp2p-inspector/LICENSE-APACHE new
+201
@@ -0,0 +1,201 @@
1 +Apache License
2 + Version 2.0, January 2004
3 + http://www.apache.org/licenses/
4 +
5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 +
7 + 1. Definitions.
8 +
9 + "License" shall mean the terms and conditions for use, reproduction,
10 + and distribution as defined by Sections 1 through 9 of this document.
11 +
12 + "Licensor" shall mean the copyright owner or entity authorized by
13 + the copyright owner that is granting the License.
14 +
15 + "Legal Entity" shall mean the union of the acting entity and all
16 + other entities that control, are controlled by, or are under common
17 + control with that entity. For the purposes of this definition,
18 + "control" means (i) the power, direct or indirect, to cause the
19 + direction or management of such entity, whether by contract or
20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 + outstanding shares, or (iii) beneficial ownership of such entity.
22 +
23 + "You" (or "Your") shall mean an individual or Legal Entity
24 + exercising permissions granted by this License.
25 +
26 + "Source" form shall mean the preferred form for making modifications,
27 + including but not limited to software source code, documentation
28 + source, and configuration files.
29 +
30 + "Object" form shall mean any form resulting from mechanical
31 + transformation or translation of a Source form, including but
32 + not limited to compiled object code, generated documentation,
33 + and conversions to other media types.
34 +
35 + "Work" shall mean the work of authorship, whether in Source or
36 + Object form, made available under the License, as indicated by a
37 + copyright notice that is included in or attached to the work
38 + (an example is provided in the Appendix below).
39 +
40 + "Derivative Works" shall mean any work, whether in Source or Object
41 + form, that is based on (or derived from) the Work and for which the
42 + editorial revisions, annotations, elaborations, or other modifications
43 + represent, as a whole, an original work of authorship. For the purposes
44 + of this License, Derivative Works shall not include works that remain
45 + separable from, or merely link (or bind by name) to the interfaces of,
46 + the Work and Derivative Works thereof.
47 +
48 + "Contribution" shall mean any work of authorship, including
49 + the original version of the Work and any modifications or additions
50 + to that Work or Derivative Works thereof, that is intentionally
51 + submitted to Licensor for inclusion in the Work by the copyright owner
52 + or by an individual or Legal Entity authorized to submit on behalf of
53 + the copyright owner. For the purposes of this definition, "submitted"
54 + means any form of electronic, verbal, or written communication sent
55 + to the Licensor or its representatives, including but not limited to
56 + communication on electronic mailing lists, source code control systems,
57 + and issue tracking systems that are managed by, or on behalf of, the
58 + Licensor for the purpose of discussing and improving the Work, but
59 + excluding communication that is conspicuously marked or otherwise
60 + designated in writing by the copyright owner as "Not a Contribution."
61 +
62 + "Contributor" shall mean Licensor and any individual or Legal Entity
63 + on behalf of whom a Contribution has been received by Licensor and
64 + subsequently incorporated within the Work.
65 +
66 + 2. Grant of Copyright License. Subject to the terms and conditions of
67 + this License, each Contributor hereby grants to You a perpetual,
68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 + copyright license to reproduce, prepare Derivative Works of,
70 + publicly display, publicly perform, sublicense, and distribute the
71 + Work and such Derivative Works in Source or Object form.
72 +
73 + 3. Grant of Patent License. Subject to the terms and conditions of
74 + this License, each Contributor hereby grants to You a perpetual,
75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 + (except as stated in this section) patent license to make, have made,
77 + use, offer to sell, sell, import, and otherwise transfer the Work,
78 + where such license applies only to those patent claims licensable
79 + by such Contributor that are necessarily infringed by their
80 + Contribution(s) alone or by combination of their Contribution(s)
81 + with the Work to which such Contribution(s) was submitted. If You
82 + institute patent litigation against any entity (including a
83 + cross-claim or counterclaim in a lawsuit) alleging that the Work
84 + or a Contribution incorporated within the Work constitutes direct
85 + or contributory patent infringement, then any patent licenses
86 + granted to You under this License for that Work shall terminate
87 + as of the date such litigation is filed.
88 +
89 + 4. Redistribution. You may reproduce and distribute copies of the
90 + Work or Derivative Works thereof in any medium, with or without
91 + modifications, and in Source or Object form, provided that You
92 + meet the following conditions:
93 +
94 + (a) You must give any other recipients of the Work or
95 + Derivative Works a copy of this License; and
96 +
97 + (b) You must cause any modified files to carry prominent notices
98 + stating that You changed the files; and
99 +
100 + (c) You must retain, in the Source form of any Derivative Works
101 + that You distribute, all copyright, patent, trademark, and
102 + attribution notices from the Source form of the Work,
103 + excluding those notices that do not pertain to any part of
104 + the Derivative Works; and
105 +
106 + (d) If the Work includes a "NOTICE" text file as part of its
107 + distribution, then any Derivative Works that You distribute must
108 + include a readable copy of the attribution notices contained
109 + within such NOTICE file, excluding those notices that do not
110 + pertain to any part of the Derivative Works, in at least one
111 + of the following places: within a NOTICE text file distributed
112 + as part of the Derivative Works; within the Source form or
113 + documentation, if provided along with the Derivative Works; or,
114 + within a display generated by the Derivative Works, if and
115 + wherever such third-party notices normally appear. The contents
116 + of the NOTICE file are for informational purposes only and
117 + do not modify the License. You may add Your own attribution
118 + notices within Derivative Works that You distribute, alongside
119 + or as an addendum to the NOTICE text from the Work, provided
120 + that such additional attribution notices cannot be construed
121 + as modifying the License.
122 +
123 + You may add Your own copyright statement to Your modifications and
124 + may provide additional or different license terms and conditions
125 + for use, reproduction, or distribution of Your modifications, or
126 + for any such Derivative Works as a whole, provided Your use,
127 + reproduction, and distribution of the Work otherwise complies with
128 + the conditions stated in this License.
129 +
130 + 5. Submission of Contributions. Unless You explicitly state otherwise,
131 + any Contribution intentionally submitted for inclusion in the Work
132 + by You to the Licensor shall be under the terms and conditions of
133 + this License, without any additional terms or conditions.
134 + Notwithstanding the above, nothing herein shall supersede or modify
135 + the terms of any separate license agreement you may have executed
136 + with Licensor regarding such Contributions.
137 +
138 + 6. Trademarks. This License does not grant permission to use the trade
139 + names, trademarks, service marks, or product names of the Licensor,
140 + except as required for reasonable and customary use in describing the
141 + origin of the Work and reproducing the content of the NOTICE file.
142 +
143 + 7. Disclaimer of Warranty. Unless required by applicable law or
144 + agreed to in writing, Licensor provides the Work (and each
145 + Contributor provides its Contributions) on an "AS IS" BASIS,
146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 + implied, including, without limitation, any warranties or conditions
148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 + PARTICULAR PURPOSE. You are solely responsible for determining the
150 + appropriateness of using or redistributing the Work and assume any
151 + risks associated with Your exercise of permissions under this License.
152 +
153 + 8. Limitation of Liability. In no event and under no legal theory,
154 + whether in tort (including negligence), contract, or otherwise,
155 + unless required by applicable law (such as deliberate and grossly
156 + negligent acts) or agreed to in writing, shall any Contributor be
157 + liable to You for damages, including any direct, indirect, special,
158 + incidental, or consequential damages of any character arising as a
159 + result of this License or out of the use or inability to use the
160 + Work (including but not limited to damages for loss of goodwill,
161 + work stoppage, computer failure or malfunction, or any and all
162 + other commercial damages or losses), even if such Contributor
163 + has been advised of the possibility of such damages.
164 +
165 + 9. Accepting Warranty or Additional Liability. While redistributing
166 + the Work or Derivative Works thereof, You may choose to offer,
167 + and charge a fee for, acceptance of support, warranty, indemnity,
168 + or other liability obligations and/or rights consistent with this
169 + License. However, in accepting such obligations, You may act only
170 + on Your own behalf and on Your sole responsibility, not on behalf
171 + of any other Contributor, and only if You agree to indemnify,
172 + defend, and hold each Contributor harmless for any liability
173 + incurred by, or claims asserted against, such Contributor by reason
174 + of your accepting any such warranty or additional liability.
175 +
176 + END OF TERMS AND CONDITIONS
177 +
178 + APPENDIX: How to apply the Apache License to your work.
179 +
180 + To apply the Apache License to your work, attach the following
181 + boilerplate notice, with the fields enclosed by brackets "[]"
182 + replaced with your own identifying information. (Don't include
183 + the brackets!) The text should be enclosed in the appropriate
184 + comment syntax for the file format. We also recommend that a
185 + file or class name and description of purpose be included on the
186 + same "printed page" as the copyright notice for easier
187 + identification within third-party archives.
188 +
189 + Copyright [yyyy] [name of copyright owner]
190 +
191 + Licensed under the Apache License, Version 2.0 (the "License");
192 + you may not use this file except in compliance with the License.
193 + You may obtain a copy of the License at
194 +
195 + http://www.apache.org/licenses/LICENSE-2.0
196 +
197 + Unless required by applicable law or agreed to in writing, software
198 + distributed under the License is distributed on an "AS IS" BASIS,
199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 + See the License for the specific language governing permissions and
201 + limitations under the License.
packages/libp2p-inspector/LICENSE-MIT new
+19
@@ -0,0 +1,19 @@
1 +The MIT License (MIT)
2 +
3 +Permission is hereby granted, free of charge, to any person obtaining a copy
4 +of this software and associated documentation files (the "Software"), to deal
5 +in the Software without restriction, including without limitation the rights
6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 +copies of the Software, and to permit persons to whom the Software is
8 +furnished to do so, subject to the following conditions:
9 +
10 +The above copyright notice and this permission notice shall be included in
11 +all copies or substantial portions of the Software.
12 +
13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 +THE SOFTWARE.
packages/libp2p-inspector/README.md new
+43
@@ -0,0 +1,43 @@
1 +# @ipshipyard/libp2p-inspector
2 +
3 +[![codecov](https://img.shields.io/codecov/c/github/ipshipyard/js-libp2p-inspector.svg?style=flat-square)](https://codecov.io/gh/ipshipyard/js-libp2p-inspector)
4 +[![CI](https://img.shields.io/github/actions/workflow/status/ipshipyard/js-libp2p-inspector/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipshipyard/js-libp2p-inspector/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
5 +
6 +> Inspect a running libp2p node
7 +
8 +# About
9 +
10 +<!--
11 +
12 +!IMPORTANT!
13 +
14 +Everything in this README between "# About" and "# Install" is automatically
15 +generated and will be overwritten the next time the doc generator is run.
16 +
17 +To make changes to this section, please update the @packageDocumentation section
18 +of src/index.js or src/index.ts
19 +
20 +To experiment with formatting, please run "npm run docs" from the root of this
21 +repo and examine the changes made.
22 +
23 +-->
24 +
25 +An electron app that bundles @ipshipyard/libp2p-inspector-ui for use with
26 +libp2p nodes running under Node.js or (eventually) in browsers.
27 +
28 +# Install
29 +
30 +```console
31 +$ npm i @ipshipyard/libp2p-inspector
32 +```
33 +
34 +# License
35 +
36 +Licensed under either of
37 +
38 +- Apache 2.0, ([LICENSE-APACHE](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-inspector/LICENSE-APACHE) / <http://www.apache.org/licenses/LICENSE-2.0>)
39 +- MIT ([LICENSE-MIT](https://github.com/ipshipyard/js-libp2p-inspector/blob/main/packages/libp2p-inspector/LICENSE-MIT) / <http://opensource.org/licenses/MIT>)
40 +
41 +# Contribution
42 +
43 +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
packages/libp2p-inspector/bin/libp2p-inspector.ts new
+5
@@ -0,0 +1,5 @@
1 +#! /usr/bin/env node
2 +
3 +import { execSync } from 'node:child_process'
4 +
5 +execSync('npx electron ./dist/src/main/index.js')
packages/libp2p-inspector/esbuild.js new
+31
@@ -0,0 +1,31 @@
1 +import fs from 'node:fs/promises'
2 +import path from 'node:path'
3 +import * as esbuild from 'esbuild'
4 +import fg from 'fast-glob'
5 +
6 +await esbuild.build({
7 + entryPoints: [
8 + 'src/renderer/index.tsx'
9 + ],
10 + bundle: true,
11 + sourcemap: true,
12 + outfile: 'dist/renderer.js',
13 + format: 'esm',
14 + loader: {
15 + '.png': 'file',
16 + '.svg': 'dataurl'
17 + }
18 +})
19 +
20 +// copy assets to the dist folder so imports from relative paths still work
21 +for await (const asset of fg.stream([
22 + 'src/**/*.html',
23 + 'src/**/*.css'
24 +], {})) {
25 + const dest = path.join('dist', asset)
26 + const dir = path.join('dist', path.dirname(asset))
27 + await fs.mkdir(dir, {
28 + recursive: true
29 + })
30 + await fs.cp(asset, dest)
31 +}
packages/libp2p-inspector/index.html new
+17
@@ -0,0 +1,17 @@
1 +<!DOCTYPE html>
2 +<html>
3 + <head>
4 + <meta charset="UTF-8" />
5 + <title>libp2p inspector</title>
6 + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
7 + <meta
8 + http-equiv="Content-Security-Policy"
9 + content="default-src 'self' data:; script-src 'self'"
10 + />
11 + <link rel="stylesheet" href="./dist/renderer.css" />
12 + </head>
13 + <body class="sans-serif f5">
14 + <div id="app"></div>
15 + <script src="./dist/renderer.js" type="module"></script>
16 + </body>
17 +</html>
packages/libp2p-inspector/package.json new
+173
@@ -0,0 +1,173 @@
1 +{
2 + "name": "@ipshipyard/libp2p-inspector",
3 + "version": "0.0.0",
4 + "description": "Inspect a running libp2p node",
5 + "author": "info@ipshipyard.com",
6 + "license": "Apache-2.0 OR MIT",
7 + "homepage": "https://github.com/ipshipyard/js-libp2p-inspector/tree/main/packages/libp2p-inspector#readme",
8 + "repository": {
9 + "type": "git",
10 + "url": "git+https://github.com/ipshipyard/js-libp2p-inspector.git"
11 + },
12 + "bugs": {
13 + "url": "https://github.com/ipshipyard/js-libp2p-inspector/issues"
14 + },
15 + "publishConfig": {
16 + "access": "public",
17 + "provenance": true
18 + },
19 + "bin": {
20 + "libp2p-inspector": "./dist/bin/libp2p-inspector.js"
21 + },
22 + "type": "module",
23 + "types": "./dist/src/index.d.ts",
24 + "files": [
25 + "bin",
26 + "dist",
27 + "src",
28 + "index.html",
29 + "!dist/test",
30 + "!**/*.tsbuildinfo"
31 + ],
32 + "exports": {
33 + ".": {
34 + "types": "./dist/src/index.d.ts",
35 + "import": "./dist/src/index.js"
36 + }
37 + },
38 + "release": {
39 + "branches": [
40 + "main"
41 + ],
42 + "plugins": [
43 + [
44 + "@semantic-release/commit-analyzer",
45 + {
46 + "preset": "conventionalcommits",
47 + "releaseRules": [
48 + {
49 + "breaking": true,
50 + "release": "major"
51 + },
52 + {
53 + "revert": true,
54 + "release": "patch"
55 + },
56 + {
57 + "type": "feat",
58 + "release": "minor"
59 + },
60 + {
61 + "type": "fix",
62 + "release": "patch"
63 + },
64 + {
65 + "type": "docs",
66 + "release": "patch"
67 + },
68 + {
69 + "type": "test",
70 + "release": "patch"
71 + },
72 + {
73 + "type": "deps",
74 + "release": "patch"
75 + },
76 + {
77 + "scope": "no-release",
78 + "release": false
79 + }
80 + ]
81 + }
82 + ],
83 + [
84 + "@semantic-release/release-notes-generator",
85 + {
86 + "preset": "conventionalcommits",
87 + "presetConfig": {
88 + "types": [
89 + {
90 + "type": "feat",
91 + "section": "Features"
92 + },
93 + {
94 + "type": "fix",
95 + "section": "Bug Fixes"
96 + },
97 + {
98 + "type": "chore",
99 + "section": "Trivial Changes"
100 + },
101 + {
102 + "type": "docs",
103 + "section": "Documentation"
104 + },
105 + {
106 + "type": "deps",
107 + "section": "Dependencies"
108 + },
109 + {
110 + "type": "test",
111 + "section": "Tests"
112 + }
113 + ]
114 + }
115 + }
116 + ],
117 + "@semantic-release/changelog",
118 + "@semantic-release/npm",
119 + "@semantic-release/github",
120 + [
121 + "@semantic-release/git",
122 + {
123 + "assets": [
124 + "CHANGELOG.md",
125 + "package.json"
126 + ]
127 + }
128 + ]
129 + ]
130 + },
131 + "scripts": {
132 + "clean": "aegir clean",
133 + "lint": "aegir lint",
134 + "build": "tsc && node esbuild.js",
135 + "test": "aegir test -t node",
136 + "test:electron-main": "aegir test -t node",
137 + "dep-check": "aegir dep-check",
138 + "start": "npm run build && electron --trace-warnings --unhandled-rejections=strict ./dist/src/main/index.js",
139 + "release": "aegir release"
140 + },
141 + "dependencies": {
142 + "@ipshipyard/libp2p-inspector-metrics": "~0.0.0",
143 + "@ipshipyard/libp2p-inspector-ui": "~0.0.0",
144 + "@libp2p/interface": "^2.10.2",
145 + "@libp2p/peer-id": "^5.1.5",
146 + "@libp2p/utils": "^6.6.5",
147 + "@multiformats/multiaddr": "^12.4.0",
148 + "@types/multicast-dns": "^7.2.4",
149 + "electron": "^36.3.1",
150 + "it-length-prefixed": "^10.0.1",
151 + "it-pipe": "^3.0.1",
152 + "it-pushable": "^3.2.3",
153 + "it-rpc": "^1.1.0",
154 + "multicast-dns": "^7.2.5",
155 + "multiformats": "^13.3.6",
156 + "race-signal": "^1.1.3",
157 + "react": "^19.1.0",
158 + "react-dom": "^19.1.0",
159 + "react-icons": "^5.5.0",
160 + "stream-to-it": "^1.0.1",
161 + "uint8arrays": "^5.1.0"
162 + },
163 + "devDependencies": {
164 + "@libp2p/webrtc": "^5.2.15",
165 + "@playwright/test": "^1.52.0",
166 + "@types/react": "^19.1.5",
167 + "@types/react-dom": "^19.1.5",
168 + "aegir": "^47.0.13",
169 + "fast-glob": "^3.3.3",
170 + "libp2p": "^2.8.8",
171 + "playwright-core": "^1.52.0"
172 + }
173 +}
packages/libp2p-inspector/src/index.ts new
+8
@@ -0,0 +1,8 @@
1 +/**
2 + * @packageDocumentation
3 + *
4 + * An electron app that bundles @ipshipyard/libp2p-inspector-ui for use with
5 + * libp2p nodes running under Node.js or (eventually) in browsers.
6 + */
7 +
8 +export {}
packages/libp2p-inspector/src/ipc/index.ts new
+37
@@ -0,0 +1,37 @@
1 +import type { PeerId } from '@libp2p/interface'
2 +import type { Multiaddr } from '@multiformats/multiaddr'
3 +
4 +/**
5 + * - pending - just discovered
6 + * - connecting - we are connecting to this node
7 + * - ready - node is compatible & ready to be connected to
8 + * - failed - failed to connect to this node
9 + * - incompatible - node is running an incompatible version of inspector-metrics
10 + * - connected - we have connected to this node
11 + */
12 +export type InspectTargetStatus = 'pending' | 'ready' | 'failed' | 'incompatible' | 'connecting' | 'connected'
13 +
14 +export interface InspectTarget {
15 + id: PeerId
16 + multiaddrs: Multiaddr[]
17 + status: InspectTargetStatus
18 + error?: string
19 + name: string
20 + version: string
21 + userAgent: string
22 + inspector: string
23 +}
24 +
25 +/**
26 + * The IPC API defined in ./preload.ts
27 + */
28 +export interface InspectorIPC {
29 + onTargets(callback: (targets: InspectTarget[]) => void): void
30 + onConnected(callback: (err: Error | undefined) => void): void
31 + onRPC(callback: (message: Uint8Array) => void): void
32 +
33 + connect(address: string | PeerId): void
34 + cancelConnect(): void
35 + disconnect(): void
36 + sendRPC(message: Uint8Array): void
37 +}
packages/libp2p-inspector/src/main/discovery.ts new
+52
@@ -0,0 +1,52 @@
1 +import { peerIdFromString } from '@libp2p/peer-id'
2 +import { repeatingTask } from '@libp2p/utils/repeating-task'
3 +import { multiaddr } from '@multiformats/multiaddr'
4 +import multicastDNS from 'multicast-dns'
5 +import { Target } from './target.js'
6 +import type { Events } from './events.ts'
7 +
8 +const DISCOVERY_INTERVAL = 1_000
9 +const SERVICE_TAG = '_libp2p_inspector_metrics._tcp.local'
10 +
11 +export function discovery (targets = new Map<string, Target>(), events: Events): void {
12 + const mdns = multicastDNS()
13 + mdns.on('warning', (err) => {
14 + // eslint-disable-next-line no-console
15 + console.error('mdns warning', err)
16 + })
17 + mdns.on('error', (err) => {
18 + // eslint-disable-next-line no-console
19 + console.error('mdns error', err)
20 + })
21 + mdns.on('response', (event) => {
22 + for (const answer of event.answers) {
23 + if (answer.name.endsWith(`.${SERVICE_TAG}`) && answer.type === 'TXT') {
24 + const str = answer.name.replace(`.${SERVICE_TAG}`, '')
25 + const id = peerIdFromString(str)
26 + let target = targets.get(str)
27 +
28 + if (target == null) {
29 + target = new Target(id, events)
30 + targets.set(str, target)
31 +
32 + events.updateTargets()
33 + }
34 +
35 + target.addAddress(multiaddr(answer.data[0].toString()))
36 + events.updateTargets()
37 + }
38 + }
39 + })
40 +
41 + const task = repeatingTask(() => {
42 + mdns.query({
43 + questions: [{
44 + name: SERVICE_TAG,
45 + type: 'PTR'
46 + }]
47 + })
48 + }, DISCOVERY_INTERVAL, {
49 + runImmediately: true
50 + })
51 + task.start()
52 +}
packages/libp2p-inspector/src/main/events.ts new
+34
@@ -0,0 +1,34 @@
1 +import { TypedEventEmitter } from '@libp2p/interface'
2 +
3 +export interface InspectorEvents {
4 + /**
5 + * Targets were updated and should be sent to the frontend
6 + */
7 + targets: CustomEvent
8 +
9 + /**
10 + * The current target sent an RPC message that should be sent to the frontend
11 + */
12 + rpc: CustomEvent<Uint8Array>
13 +}
14 +
15 +export class Events extends TypedEventEmitter<InspectorEvents> {
16 + private timeout?: ReturnType<typeof setTimeout>
17 +
18 + /**
19 + * Debounce telling the UI that the target info has changed
20 + */
21 + updateTargets (): void {
22 + clearTimeout(this.timeout)
23 +
24 + this.timeout = setTimeout(() => {
25 + this.safeDispatchEvent('targets')
26 + }, 1)
27 + }
28 +
29 + receiveRPC (buf: Uint8Array): void {
30 + this.safeDispatchEvent('rpc', {
31 + detail: buf
32 + })
33 + }
34 +}
packages/libp2p-inspector/src/main/index.ts new
+119
@@ -0,0 +1,119 @@
1 +import * as path from 'path'
2 +import { app, BrowserWindow, ipcMain } from 'electron'
3 +import { discovery } from './discovery.js'
4 +import { Events } from './events.ts'
5 +import type { Target } from './target.ts'
6 +
7 +let mainWindow: BrowserWindow | undefined
8 +
9 +const events = new Events()
10 +const targets = new Map<string, Target>()
11 +discovery(targets, events)
12 +
13 +events.addEventListener('targets', (evt) => {
14 + const list = []
15 +
16 + for (const [key, target] of targets.entries()) {
17 + if (target.status === 'failed') {
18 + targets.delete(key)
19 + } else {
20 + list.push(target)
21 + }
22 + }
23 +
24 + mainWindow?.webContents.send('libp2p-inspector:targets', JSON.stringify(list, null, 2))
25 +})
26 +
27 +events.addEventListener('rpc', (evt) => {
28 + mainWindow?.webContents.send('libp2p-inspector:receive-rpc', evt.detail)
29 +})
30 +
31 +ipcMain.on('libp2p-inspector:connect', (_event, value) => {
32 + try {
33 + let connected = false
34 +
35 + if (value.startsWith('/')) {
36 + // eslint-disable-next-line no-console
37 + console.log('libp2p-inspector:connect connect to multiaddr', value)
38 + throw new Error('Not implemented')
39 + } else {
40 + for (const target of targets.values()) {
41 + if (target.id.equals(value)) {
42 + connected = true
43 + target.connect()
44 + } else {
45 + target.disconnect()
46 + }
47 + }
48 + }
49 +
50 + // do not need to call back to page as updating target status will let page
51 + // know we are now connected to a node
52 + if (!connected) {
53 + throw new Error(`Could not connect to ${value}`)
54 + }
55 + } catch (err) {
56 + mainWindow?.webContents.send('libp2p-inspector:connected', err)
57 + }
58 +})
59 +
60 +ipcMain.on('libp2p-inspector:cancel-connect', (_event, value) => {
61 + // eslint-disable-next-line no-console
62 + console.log('libp2p-inspector:cancel-connect', value)
63 +})
64 +
65 +ipcMain.on('libp2p-inspector:send-rpc', (_event, value) => {
66 + for (const target of targets.values()) {
67 + if (target.status === 'connected') {
68 + target.sendMessage(value)
69 + }
70 + }
71 +})
72 +
73 +ipcMain.on('libp2p-inspector:disconnect', (_event) => {
74 + for (const target of targets.values()) {
75 + target.disconnect()
76 + }
77 +})
78 +
79 +function createWindow (): void {
80 + // Create the browser window.
81 + mainWindow = new BrowserWindow({
82 + height: 600,
83 + width: 800,
84 + webPreferences: {
85 + preload: path.join(import.meta.dirname, 'preload.cjs')
86 + }
87 + })
88 + mainWindow.loadFile(path.join(import.meta.dirname, '../../../index.html'))
89 +
90 + if (process.env.NODE_ENV !== 'test') {
91 + // mainWindow.webContents.openDevTools()
92 + }
93 +
94 + mainWindow.on('close', () => {
95 + mainWindow = undefined
96 + })
97 +}
98 +
99 +// This method will be called when Electron has finished
100 +// initialization and is ready to create browser windows.
101 +// Some APIs can only be used after this event occurs.
102 +app.whenReady().then(() => {
103 + createWindow()
104 +
105 + app.on('activate', function () {
106 + // On macOS it's common to re-create a window in the app when the
107 + // dock icon is clicked and there are no other windows open.
108 + if (BrowserWindow.getAllWindows().length === 0) { createWindow() }
109 + })
110 +})
111 +
112 +// Quit when all windows are closed, except on macOS. There, it's common
113 +// for applications and their menu bar to stay active until the user quits
114 +// explicitly with Cmd + Q.
115 +app.on('window-all-closed', () => {
116 + if (process.platform !== 'darwin') {
117 + app.quit()
118 + }
119 +})
packages/libp2p-inspector/src/main/preload.cjs new
+83
@@ -0,0 +1,83 @@
1 +const { contextBridge, ipcRenderer } = require('electron/renderer')
2 +
3 +/**
4 + * main <-> renderer process communication API
5 + */
6 +contextBridge.exposeInMainWorld('inspector', {
7 + /**
8 + * @param {(nodes: string[]) => void} callback
9 + */
10 + onTargets: (callback) => {
11 + /**
12 + * @param {any} event
13 + * @param {string} value
14 + */
15 + const onTargets = (event, value) => callback(JSON.parse(value))
16 + ipcRenderer.on('libp2p-inspector:targets', onTargets)
17 + },
18 +
19 + /**
20 + * @param {(address: string) => void} callback
21 + */
22 + onConnected: (callback) => {
23 + /**
24 + * @param {any} event
25 + * @param {string} value
26 + */
27 + const onConnected = (event, value) => callback(value)
28 + ipcRenderer.on('libp2p-inspector:connected', onConnected)
29 + },
30 +
31 + /**
32 + * @param {(message: any) => void} callback
33 + */
34 + onRPC: (callback) => {
35 + /**
36 + * @param {any} event
37 + * @param {any} message
38 + */
39 + const onRPC = (event, message) => {
40 + callback(message)
41 + }
42 + ipcRenderer.on('libp2p-inspector:receive-rpc', onRPC)
43 + },
44 +
45 + /**
46 + * @param {string} address
47 + */
48 + connect: (address) => {
49 + ipcRenderer.send('libp2p-inspector:connect', address)
50 + },
51 +
52 + cancelConnect: () => {
53 + ipcRenderer.send('libp2p-inspector:cancel-connect')
54 + },
55 +
56 + disconnect: () => {
57 + ipcRenderer.send('libp2p-inspector:disconnect')
58 + },
59 +
60 + /**
61 + * @param {any} message
62 + */
63 + sendRPC: (message) => {
64 + ipcRenderer.send('libp2p-inspector:send-rpc', message)
65 + }
66 +})
67 +
68 +/*
69 +// All of the Node.js APIs are available in the preload process.
70 +// It has the same sandbox as a Chrome extension.
71 +window.addEventListener("DOMContentLoaded", () => {
72 + const replaceText = (selector: string, text: string) => {
73 + const element = document.getElementById(selector)
74 + if (element) {
75 + element.innerText = text
76 + }
77 + }
78 +
79 + for (const type of ["chrome", "node", "electron"]) {
80 + replaceText(`${type}-version`, process.versions[type] ?? '')
81 + }
82 +})
83 +*/
packages/libp2p-inspector/src/main/target.ts new
+165
@@ -0,0 +1,165 @@
1 +import { createConnection } from 'node:net'
2 +import { SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
3 +import { Queue } from '@libp2p/utils/queue'
4 +import { encode, decode } from 'it-length-prefixed'
5 +import { pushable } from 'it-pushable'
6 +import { base64 } from 'multiformats/bases/base64'
7 +import { raceSignal } from 'race-signal'
8 +import { duplex } from 'stream-to-it'
9 +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
10 +import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
11 +import type { Events } from './events.ts'
12 +import type { InspectTargetStatus } from '../ipc/index.ts'
13 +import type { InspectorMessage } from '@ipshipyard/libp2p-inspector-metrics'
14 +import type { AbortOptions, PeerId } from '@libp2p/interface'
15 +import type { Multiaddr } from '@multiformats/multiaddr'
16 +import type { Pushable } from 'it-pushable'
17 +
18 +/**
19 + * A remote process running the inspector metrics
20 + */
21 +export class Target {
22 + public status: InspectTargetStatus
23 + public error?: string
24 + public id: PeerId
25 + public multiaddrs: Multiaddr[]
26 + public name: string
27 + public version: string
28 + public userAgent: string
29 + public inspector: string
30 + private queue: Queue
31 + private source?: Pushable<Uint8Array>
32 + private events: Events
33 +
34 + constructor (id: PeerId, events: Events) {
35 + this.status = 'pending'
36 + this.id = id
37 + this.multiaddrs = []
38 + this.name = ''
39 + this.version = ''
40 + this.userAgent = ''
41 + this.inspector = ''
42 + this.events = events
43 +
44 + this.queue = new Queue({
45 + concurrency: 1
46 + })
47 + }
48 +
49 + toJSON (): Record<string, unknown> {
50 + return {
51 + id: this.id,
52 + multiaddrs: this.multiaddrs,
53 + status: this.status,
54 + error: this.error,
55 + name: this.name,
56 + version: this.version,
57 + userAgent: this.userAgent,
58 + inspector: this.inspector
59 + }
60 + }
61 +
62 + addAddress (ma: Multiaddr): void {
63 + if (this.multiaddrs.find(addr => addr.equals(ma))) {
64 + return
65 + }
66 +
67 + this.multiaddrs.push(ma)
68 + this.queue.add(this.openConnection.bind(this, ma))
69 + }
70 +
71 + sendMessage (buf: Uint8Array): void {
72 + this.source?.push(uint8ArrayFromString(JSON.stringify({
73 + source: SOURCE_CLIENT,
74 + type: 'libp2p-rpc',
75 + message: base64.encode(buf)
76 + })))
77 + }
78 +
79 + connect (): void {
80 + this.setStatus('connected')
81 + }
82 +
83 + disconnect (): void {
84 + this.setStatus('ready')
85 + }
86 +
87 + setStatus (status: InspectTargetStatus): void {
88 + this.status = status
89 + this.events.updateTargets()
90 + }
91 +
92 + private async openConnection (ma: Multiaddr, options?: AbortOptions): Promise<void> {
93 + if (this.status !== 'pending' && this.status !== 'failed') {
94 + return
95 + }
96 +
97 + this.setStatus('connecting')
98 +
99 + try {
100 + await raceSignal(this._openConnection(ma, options), AbortSignal.timeout(5_000))
101 + } catch (err) {
102 + this.setStatus('failed')
103 + }
104 + }
105 +
106 + private async _openConnection (ma: Multiaddr, options?: AbortOptions): Promise<void> {
107 + const result = Promise.withResolvers<void>()
108 +
109 + const socket = createConnection(ma.toOptions(), () => {
110 + this.source = pushable()
111 +
112 + const duplexSocket = duplex(socket)
113 +
114 + Promise.all([
115 + duplexSocket.sink(encode(this.source)),
116 + (async () => {
117 + for await (const buf of decode(duplexSocket.source)) {
118 + try {
119 + const message: InspectorMessage = JSON.parse(uint8ArrayToString(buf.subarray()))
120 +
121 + if (message.type === 'libp2p-identify') {
122 + // got identify response
123 + this.setStatus('ready')
124 + this.name = message.name
125 + this.version = message.version
126 + this.userAgent = message.userAgent
127 + this.inspector = message.inspector
128 +
129 + result.resolve()
130 + } else if (message.type === 'libp2p-rpc') {
131 + if (this.status === 'connected') {
132 + // got RPC message
133 + this.events.receiveRPC(base64.decode(message.message))
134 + }
135 + } else {
136 + throw new Error(`Unknown message - ${message}`)
137 + }
138 + } catch (err) {
139 + // eslint-disable-next-line no-console
140 + console.error('could not parse message', buf.toString(), err)
141 + }
142 + }
143 + })()
144 + ])
145 + .catch(err => {
146 + result.reject(err)
147 + })
148 +
149 + // send identify
150 + this.source.push(uint8ArrayFromString(JSON.stringify({
151 + source: SOURCE_CLIENT,
152 + type: 'libp2p-identify'
153 + })))
154 + })
155 + socket.on('close', async () => {
156 + this.setStatus('failed')
157 + })
158 + socket.on('error', (err: any) => {
159 + this.setStatus('failed')
160 + result.reject(err)
161 + })
162 +
163 + return result.promise
164 + }
165 +}
packages/libp2p-inspector/src/renderer/index.tsx new
+252
@@ -0,0 +1,252 @@
1 +import '@ipshipyard/libp2p-inspector-ui/index.css'
2 +import { valueCodecs } from '@ipshipyard/libp2p-inspector-metrics'
3 +import { Inspector, FatalErrorPanel } from '@ipshipyard/libp2p-inspector-ui'
4 +import { TypedEventEmitter } from '@libp2p/interface'
5 +import { pipe } from 'it-pipe'
6 +import { pushable } from 'it-pushable'
7 +import { rpc } from 'it-rpc'
8 +import { Component } from 'react'
9 +import { createRoot } from 'react-dom/client'
10 +import { rendererIPC } from './ipc.js'
11 +import { ConnectPanel } from './panels/connect.js'
12 +import { NodePanel } from './panels/node.js'
13 +import type { InspectorIPC, InspectTarget } from '../ipc/index.ts'
14 +import type { InspectorRPCEvents, MetricsRPC, Peer } from '@ipshipyard/libp2p-inspector-metrics'
15 +import type { TypedEventTarget, Message, PeerId } from '@libp2p/interface'
16 +import type { RPC } from 'it-rpc'
17 +import type { ReactElement } from 'react'
18 +
19 +const platform = 'chrome'
20 +const RPC_TIMEOUT = 10_000
21 +
22 +interface ConnectingAppState {
23 + status: 'connecting'
24 + targets: InspectTarget[]
25 +}
26 +
27 +interface ErrorAppState {
28 + status: 'error'
29 + error: Error
30 + targets: InspectTarget[]
31 +}
32 +
33 +interface OnlineAppState {
34 + status: 'connected'
35 + target: InspectTarget
36 + self: Peer
37 + peers: Peer[]
38 + debug: string
39 + capabilities: Record<string, string[]>
40 + pubsub: Record<string, Message[]>
41 + targets: InspectTarget[]
42 +}
43 +
44 +type AppState = ConnectingAppState | ErrorAppState | OnlineAppState
45 +
46 +export interface AppProps {
47 +
48 +}
49 +
50 +export class App extends Component<AppProps> {
51 + state: AppState
52 + private readonly rpc: RPC
53 + private readonly metrics: MetricsRPC
54 + private readonly events: TypedEventTarget<InspectorRPCEvents>
55 + private readonly ipc: InspectorIPC
56 +
57 + constructor (props: AppProps) {
58 + super(props)
59 +
60 + this.state = {
61 + status: 'connecting',
62 + targets: []
63 + }
64 +
65 + // set up IPC
66 + this.ipc = rendererIPC()
67 +
68 + this.rpc = rpc({
69 + valueCodecs
70 + })
71 +
72 + // remote metrics instance
73 + this.metrics = this.rpc.createClient<MetricsRPC>('metrics', {
74 + timeout: RPC_TIMEOUT
75 + })
76 +
77 + // register for updates from main thread
78 + this.ipc.onTargets(this.onTargets)
79 +
80 + // called after connecting to a remote node
81 + this.ipc.onConnected(this.onConnected)
82 +
83 + // create event emitter to receive events from inspector-metrics
84 + this.events = new TypedEventEmitter<InspectorRPCEvents>()
85 + this.events.addEventListener('metrics', (evt) => {
86 + // handle incoming metrics
87 + })
88 + this.events.addEventListener('self', (evt) => {
89 + this.setState(s => ({
90 + ...s,
91 + self: evt.detail
92 + }))
93 + })
94 + this.events.addEventListener('peers', (evt) => {
95 + this.setState(s => ({
96 + ...s,
97 + peers: evt.detail
98 + }))
99 + })
100 + this.events.addEventListener('pubsub:message', (evt) => {
101 + this.setState(s => ({
102 + ...s,
103 + pubsub: {
104 + // @ts-expect-error fixme
105 + ...(s.pubsub ?? {}),
106 + [evt.detail.topic ?? '']: [
107 + // @ts-expect-error fixme
108 + ...(s.pubsub[evt.detail.topic] ?? []),
109 + evt.detail
110 + ]
111 + }
112 + }))
113 + })
114 +
115 + this.rpc.createTarget('inspector', this.events)
116 +
117 + // receive messages
118 + const ipcSource = pushable()
119 + this.ipc.onRPC((buf) => {
120 + ipcSource.push(buf)
121 + })
122 +
123 + // send RPC messages
124 + Promise.resolve()
125 + .then(async () => {
126 + await pipe(
127 + ipcSource,
128 + this.rpc,
129 + async (source) => {
130 + for await (const buf of source) {
131 + this.ipc.sendRPC(buf)
132 + }
133 + }
134 + )
135 + })
136 + .catch(err => {
137 + // eslint-disable-next-line no-console
138 + console.error('error while reading RPC messages', err)
139 + })
140 + }
141 +
142 + copyToClipboard = (value: string): void => {
143 + navigator.clipboard.writeText(value)
144 + .catch(err => {
145 + // eslint-disable-next-line no-console
146 + console.error('could not write to clipboard', err)
147 + })
148 + }
149 +
150 + onTargets = (targets: InspectTarget[]): void => {
151 + this.setState(s => ({
152 + ...s,
153 + targets
154 + }))
155 +
156 + // if the main thread says we are connected to a peer but our state says we
157 + // are not, try to init with the connected peer (this can happen after a
158 + // page refresh)
159 + if (targets.find(t => t.status === 'connected') != null && this.state.status !== 'connected') {
160 + this.onConnected()
161 + }
162 + }
163 +
164 + handleConnect = (evt: React.UIEvent, address: string | PeerId): void => {
165 + evt.preventDefault()
166 + this.ipc.connect(address)
167 + }
168 +
169 + onConnected = (err?: Error): void => {
170 + if (err != null) {
171 + this.setState({
172 + status: 'error',
173 + error: err
174 + })
175 +
176 + return
177 + }
178 +
179 + Promise.resolve().then(async () => {
180 + const { self, peers, debug, capabilities } = await this.metrics.init()
181 +
182 + this.setState((s: ConnectingAppState) => {
183 + const target = s.targets.find(n => n.id.equals(self.id))
184 +
185 + return {
186 + ...s,
187 + status: 'connected',
188 + target,
189 + self,
190 + peers,
191 + debug,
192 + capabilities,
193 + pubsub: {}
194 + }
195 + })
196 + })
197 + .catch(err => {
198 + this.setState({
199 + status: 'error',
200 + error: err
201 + })
202 + })
203 + }
204 +
205 + onCancelConnect = (evt: React.UIEvent): void => {
206 + evt.preventDefault()
207 + this.ipc.cancelConnect()
208 + }
209 +
210 + handleDisconnect = (evt: React.UIEvent): void => {
211 + evt.preventDefault()
212 + this.ipc.disconnect()
213 + this.setState(s => ({
214 + ...s,
215 + status: 'connecting'
216 + }))
217 + }
218 +
219 + render (): ReactElement {
220 + if (this.state.status === 'error') {
221 + return (
222 + <FatalErrorPanel error={this.state.error} onBack={this.handleDisconnect} />
223 + )
224 + }
225 +
226 + if (this.state.status === 'connected') {
227 + return (
228 + <>
229 + <NodePanel {...this.state} onDisconnect={this.handleDisconnect} />
230 + <Inspector {...this.state} metrics={this.metrics} copyToClipboard={this.copyToClipboard} />
231 + </>
232 + )
233 + }
234 +
235 + return (
236 + <ConnectPanel {...this.state} onConnect={this.handleConnect} />
237 + )
238 + }
239 +}
240 +
241 +const body = document.getElementsByTagName('body')[0]
242 +
243 +if (body != null) {
244 + body.className = `${body.className} ${platform}`
245 +}
246 +
247 +const app = document.getElementById('app')
248 +
249 +if (app != null) {
250 + const root = createRoot(app)
251 + root.render(<App />)
252 +}
packages/libp2p-inspector/src/renderer/ipc.ts new
+53
@@ -0,0 +1,53 @@
1 +import { peerIdFromString } from '@libp2p/peer-id'
2 +import { multiaddr } from '@multiformats/multiaddr'
3 +import type { InspectorIPC, InspectTarget } from '../ipc/index.ts'
4 +import type { PeerId } from '@libp2p/interface'
5 +
6 +declare global {
7 + var inspector: InspectorIPC
8 +}
9 +
10 +class RendererIPC implements InspectorIPC {
11 + onTargets (callback: (targets: InspectTarget[]) => void): void {
12 + // register for updates from main thread
13 + globalThis.inspector.onTargets((targets: any[]) => {
14 + callback(targets.map(node => ({
15 + ...node,
16 + id: peerIdFromString(node.id),
17 + multiaddrs: node.multiaddrs.map((ma: string) => multiaddr(ma))
18 + })))
19 + })
20 + }
21 +
22 + onConnected (callback: (err: Error | undefined) => void): void {
23 + globalThis.inspector.onConnected((err) => {
24 + callback(err)
25 + })
26 + }
27 +
28 + onRPC (callback: (message: Uint8Array) => void): void {
29 + globalThis.inspector.onRPC((message) => {
30 + callback(message)
31 + })
32 + }
33 +
34 + connect (address: string | PeerId): void {
35 + globalThis.inspector.connect(address.toString())
36 + }
37 +
38 + cancelConnect (): void {
39 + globalThis.inspector.cancelConnect()
40 + }
41 +
42 + disconnect (): void {
43 + globalThis.inspector.disconnect()
44 + }
45 +
46 + sendRPC (message: Uint8Array): void {
47 + globalThis.inspector.sendRPC(message)
48 + }
49 +}
50 +
51 +export function rendererIPC (): InspectorIPC {
52 + return new RendererIPC()
53 +}
packages/libp2p-inspector/src/renderer/panels/connect.tsx new
+37
@@ -0,0 +1,37 @@
1 +import { FloatingPanel, Heading, Group, Libp2pIcon, ShipyardIcon, Footer } from '@ipshipyard/libp2p-inspector-ui'
2 +import { TargetListPanel } from './target-list.tsx'
3 +import type { InspectTarget } from '../../ipc/index.ts'
4 +import type { PeerId } from '@libp2p/interface'
5 +import type { JSX } from 'react'
6 +
7 +export interface ConnectingPanelProps {
8 + targets: InspectTarget[]
9 + onConnect(evt: React.UIEvent, peer: string | PeerId): void
10 +}
11 +
12 +export const ConnectPanel = ({ targets, onConnect }: ConnectingPanelProps): JSX.Element => {
13 + return (
14 + <>
15 + <FloatingPanel className='ConnectPanel'>
16 + <Group>
17 + <Libp2pIcon height={64} width={64} style={{ marginBottom: 8 }} />
18 + <Heading>
19 + <h1>Connect to libp2p</h1>
20 + </Heading>
21 + {/* <p>Enter a multiaddr or choose from a detected node</p>
22 + <form onSubmit={(evt) => onConnect}>
23 + <TextInput type="text" value={address} placeholder="/ip4/127.0.0.1/tcp/1234" onChange={(e) => { setAddress(e.target.value) }} />
24 + <Button disabled={connecting} onClick={(evt) => onConnect(evt, address)} primary={true}>Connect</Button>
25 + </form>
26 + </Group>
27 + <Group>
28 + <h2>Detected nodes</h2> */}
29 + <TargetListPanel targets={targets} onConnect={onConnect} />
30 + </Group>
31 + <Footer>
32 + <small>With ❤️ from <a href='https://ipshipyard.com/' target='_blank' rel='noreferrer'><ShipyardIcon /> SHIPYARD</a></small>
33 + </Footer>
34 + </FloatingPanel>
35 + </>
36 + )
37 +}
packages/libp2p-inspector/src/renderer/panels/node.css new
+7
@@ -0,0 +1,7 @@
1 +.NodePanel {
2 + padding: 6px 12px;
3 + border-bottom: 1px solid var(--menu-border-color)
4 +}
5 +.NodePanel p {
6 + margin: 0;
7 +}
packages/libp2p-inspector/src/renderer/panels/node.tsx new
+34
@@ -0,0 +1,34 @@
1 +import { Button } from '@ipshipyard/libp2p-inspector-ui'
2 +import { FaPlug, FaPlugCircleExclamation } from 'react-icons/fa6'
3 +import type { InspectTarget, InspectTargetStatus } from '../../ipc/index.ts'
4 +import type { JSX } from 'react'
5 +import './node.css'
6 +
7 +export interface NodePanelProps {
8 + targets: InspectTarget[]
9 + onDisconnect(evt: React.UIEvent): void
10 +}
11 +
12 +function getIcon (status: InspectTargetStatus): JSX.Element {
13 + if (status === 'connected') {
14 + return <FaPlug />
15 + }
16 +
17 + return <FaPlugCircleExclamation />
18 +}
19 +
20 +export const NodePanel = ({ targets, onDisconnect }: NodePanelProps): JSX.Element => {
21 + const target = targets.find(target => target.status === 'connected')
22 +
23 + if (target == null) {
24 + throw new Error('No connected target found')
25 + }
26 +
27 + return (
28 + <>
29 + <div className='NodePanel'>
30 + <p>{getIcon(target.status)} {target.userAgent} <Button onClick={onDisconnect}>Disconnect</Button></p>
31 + </div>
32 + </>
33 + )
34 +}
packages/libp2p-inspector/src/renderer/panels/target-list.css new
+3
@@ -0,0 +1,3 @@
1 +.TargetListPanel li {
2 + margin-left: 0;
3 +}
\ No newline at end of file
packages/libp2p-inspector/src/renderer/panels/target-list.tsx new
+55
@@ -0,0 +1,55 @@
1 +import { Button, SpinnerIcon, Libp2pIcon, SmallError } from '@ipshipyard/libp2p-inspector-ui'
2 +import { FaServer } from 'react-icons/fa6'
3 +import type { InspectTarget } from '../../ipc/index.ts'
4 +import type { PeerId } from '@libp2p/interface'
5 +import type { JSX } from 'react'
6 +import './target-list.css'
7 +
8 +export interface TargetListPanelProps {
9 + targets: InspectTarget[]
10 + onConnect(evt: React.UIEvent, peer: string | PeerId): void
11 +}
12 +
13 +function getIcon (name: string): JSX.Element {
14 + if (name === 'js-libp2p') {
15 + return <Libp2pIcon />
16 + }
17 +
18 + return <FaServer />
19 +}
20 +
21 +function getAction (target: InspectTarget, onConnect: (evt: React.UIEvent, peer: string | PeerId) => void): JSX.Element {
22 + if (target.status === 'ready') {
23 + return <Button onClick={(evt) => onConnect(evt, target.id)}>Inspect</Button>
24 + }
25 +
26 + if (target.status === 'incompatible') {
27 + return <SmallError error='The remote @ipshipyard/inspector-metrics dep is outdated' />
28 + }
29 +
30 + return <SpinnerIcon />
31 +}
32 +
33 +export const TargetListPanel = ({ targets, onConnect }: TargetListPanelProps): JSX.Element => {
34 + return (
35 + <div className='TargetListPanel'>
36 + {
37 + targets.length > 0
38 + ? (
39 + <ul>
40 + {targets.map((target, index) => (
41 + <li key={`node-${index}`}>
42 + {getIcon(target.name)} {target.userAgent} {getAction(target, onConnect)}
43 + </li>
44 + ))}
45 + </ul>
46 + )
47 + : (
48 + <>
49 + <p><SpinnerIcon /> Searching...</p>
50 + </>
51 + )
52 + }
53 + </div>
54 + )
55 +}
packages/libp2p-inspector/test/fixtures/get-libp2p.ts new
+19
@@ -0,0 +1,19 @@
1 +import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
2 +import { mergeOptions } from '@libp2p/utils/merge-options'
3 +import { webRTCDirect } from '@libp2p/webrtc'
4 +import { createLibp2p } from 'libp2p'
5 +import type { Libp2p, Libp2pOptions } from 'libp2p'
6 +
7 +export function getLibp2p (config: Libp2pOptions = {}): Promise<Libp2p> {
8 + return createLibp2p(mergeOptions({
9 + metrics: inspectorMetrics(),
10 + addresses: {
11 + listen: [
12 + '/ip4/0.0.0.0/udp/0/webrtc-direct'
13 + ]
14 + },
15 + transports: [
16 + webRTCDirect()
17 + ]
18 + }, config))
19 +}
packages/libp2p-inspector/test/index.spec.ts new
+42
@@ -0,0 +1,42 @@
1 +import { expect } from '@playwright/test'
2 +import { _electron as electron } from 'playwright-core'
3 +import { getLibp2p } from './fixtures/get-libp2p.ts'
4 +import type { Libp2p } from '@libp2p/interface'
5 +import type { ElectronApplication } from 'playwright-core'
6 +
7 +const TEST_USER_AGENT = 'libp2p/test'
8 +
9 +describe('launch app', () => {
10 + let electronApp: ElectronApplication
11 + let libp2p: Libp2p
12 +
13 + beforeEach(async () => {
14 + electronApp = await electron.launch({
15 + args: [
16 + // https://github.com/electron/electron/issues/42510
17 + '--no-sandbox',
18 + './dist/src/main/index.js'
19 + ]
20 + })
21 + libp2p = await getLibp2p({
22 + nodeInfo: {
23 + userAgent: TEST_USER_AGENT
24 + }
25 + })
26 + })
27 +
28 + afterEach(async () => {
29 + await electronApp?.close()
30 + await libp2p?.stop()
31 + })
32 +
33 + it('should detect libp2p node', async () => {
34 + const targetListSelector = '.TargetListPanel'
35 + const window = await electronApp.firstWindow()
36 +
37 + await window.waitForSelector(`${targetListSelector}:has-text("${TEST_USER_AGENT}")`)
38 +
39 + const outputContent = await window.textContent(targetListSelector)
40 + expect(outputContent).toContain(TEST_USER_AGENT)
41 + })
42 +})
packages/libp2p-inspector/tsconfig.json new
+20
@@ -0,0 +1,20 @@
1 +{
2 + "extends": "aegir/src/config/tsconfig.aegir.json",
3 + "compilerOptions": {
4 + "outDir": "dist",
5 + "jsx": "react-jsx"
6 + },
7 + "include": [
8 + "bin",
9 + "src",
10 + "test"
11 + ],
12 + "references": [
13 + {
14 + "path": "../libp2p-inspector-metrics"
15 + },
16 + {
17 + "path": "../libp2p-inspector-ui"
18 + }
19 + ]
20 +}
packages/libp2p-inspector/typedoc.json new
+5
@@ -0,0 +1,5 @@
1 +{
2 + "entryPoints": [
3 + "./src/index.ts"
4 + ]
5 +}