mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-14 14:47:47 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb8153f409 | ||
|
|
80de232bec | ||
|
|
7af8c95fea | ||
|
|
2db72fe6ef | ||
|
|
d297e92a5a | ||
|
|
7e1da4395d | ||
|
|
7f8b0479e1 | ||
|
|
c8d6183456 | ||
|
|
9d5f7784c4 | ||
|
|
05ac836265 | ||
|
|
af7782c93b | ||
|
|
2b1431d041 | ||
|
|
9d8b7a5265 | ||
|
|
95c12ad291 | ||
|
|
dac2cec52f | ||
|
|
efe4eef1b7 | ||
|
|
a0e196a9e7 | ||
|
|
c6427dc724 | ||
|
|
8ce1e22b4e | ||
|
|
022d725e03 | ||
|
|
ed7fdb1b4c | ||
|
|
e510204b8c | ||
|
|
d31b4448df | ||
|
|
e420a0a45e | ||
|
|
84ecbe0cd6 | ||
|
|
6a63cc26b9 | ||
|
|
8ed0fd55c3 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
37
README.md
37
README.md
@@ -7,25 +7,24 @@ APIs. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
|
||||
## Feature Overview
|
||||
|
||||
🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
|
||||
📤 Send requests via REST, GraphQL, Server Sent Events (SSE), WebSockets, or gRPC.<br/>
|
||||
🔐 Automatically authorize requests with OAuth 2.0, JWT tokens, Basic Auth, and more.<br/>
|
||||
🔎 Filter response bodies using JSONPath or XPath queries.<br/>
|
||||
⛓️ Chain together multiple requests to dynamically reference values.<br/>
|
||||
📂 Organize requests into workspaces and nested folders.<br/>
|
||||
🧮 Use environment variables to easily switch between Prod and Dev.<br/>
|
||||
🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
|
||||
🎨 Choose from many of the included themes, or make your own.<br/>
|
||||
💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
|
||||
📜 View response history for each request.<br/>
|
||||
🔌 Create your own plugins for authentication, template tags, and more!<br/>
|
||||
🛜 Configure a proxy to access firewall-blocked APIs
|
||||
- 🪂 Import data from Postman, Insomnia, OpenAPI, Swagger, or Curl.<br/>
|
||||
- 📤 Send requests via REST, GraphQL, Server Sent Events (SSE), WebSockets, or gRPC.<br/>
|
||||
- 🔐 Automatically authorize requests with OAuth 2.0, JWT tokens, Basic Auth, and more.<br/>
|
||||
- 🔎 Filter response bodies using JSONPath or XPath queries.<br/>
|
||||
- ⛓️ Chain together multiple requests to dynamically reference values.<br/>
|
||||
- 📂 Organize requests into workspaces and nested folders.<br/>
|
||||
- 🧮 Use environment variables to easily switch between Prod and Dev.<br/>
|
||||
- 🏷️ Send dynamic values like UUIDs or timestamps using template tags.<br/>
|
||||
- 🎨 Choose from many of the included themes, or make your own.<br/>
|
||||
- 💽 Mirror workspace data to a directory for integration with Git or Dropbox.<br/>
|
||||
- 📜 View response history for each request.<br/>
|
||||
- 🔌 Create your own plugins for authentication, template tags, and more!<br/>
|
||||
- 🛜 Configure a proxy to access firewall-blocked APIs
|
||||
|
||||
## Feedback and Bug Reports
|
||||
|
||||
All feedback, bug reports, questions, and feature requests should be reported to
|
||||
[feedback.yaak.app](https://feedback.yaak.app). Issues will be duplicated
|
||||
in this repository if applicable.
|
||||
[feedback.yaak.app](https://feedback.yaak.app).
|
||||
|
||||
## Community Projects
|
||||
|
||||
@@ -34,9 +33,5 @@ in this repository if applicable.
|
||||
|
||||
## Contribution Policy
|
||||
|
||||
Yaak is open source, but only accepting contributions for bug fixes. See the
|
||||
[`good first issue`](https://github.com/yaakapp/app/labels/good%20first%20issue) label for
|
||||
issues that are more approachable for contribution.
|
||||
|
||||
To get started, visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your
|
||||
environment.
|
||||
Yaak is open source, but only accepting contributions for bug fixes. To get started,
|
||||
visit [`DEVELOPMENT.md`](DEVELOPMENT.md) for tips on setting up your environment.
|
||||
|
||||
141
package-lock.json
generated
141
package-lock.json
generated
@@ -2500,9 +2500,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/history": {
|
||||
"version": "1.95.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.95.0.tgz",
|
||||
"integrity": "sha512-w1/yWuIBqmG0Z0MPMf1OuOCce7FXyVH4L4dIA4rvpnjIUCH8qRUgloFAVg37nTMUbOmhMsY2NZDxCpKBv+CLJg==",
|
||||
"version": "1.99.13",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.99.13.tgz",
|
||||
"integrity": "sha512-JMd7USmnp8zV8BRGIjALqzPxazvKtQ7PGXQC7n39HpbqdsmfV2ePCzieO84IvN+mwsTrXErpbjI4BfKCa+ZNCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -2513,9 +2513,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.62.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.16.tgz",
|
||||
"integrity": "sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g==",
|
||||
"version": "5.66.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz",
|
||||
"integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2534,12 +2534,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.62.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.16.tgz",
|
||||
"integrity": "sha512-XJIZNj65d2IdvU8VBESmrPakfIm6FSdHDzrS1dPrAwmq3ZX+9riMh/ZfbNQHAWnhrgmq7KoXpgZSRyXnqMYT9A==",
|
||||
"version": "5.66.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.9.tgz",
|
||||
"integrity": "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.62.16"
|
||||
"@tanstack/query-core": "5.66.4"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2568,14 +2568,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router": {
|
||||
"version": "1.95.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.95.1.tgz",
|
||||
"integrity": "sha512-P5x4yNhcdkYsCEoYeGZP8Q9Jlxf0WXJa4G/xvbmM905seZc9FqJqvCSRvX3dWTPOXRABhl4g+8DHqfft0c/AvQ==",
|
||||
"version": "1.111.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.111.3.tgz",
|
||||
"integrity": "sha512-OsqAuExa4WF7+BbjENWlb4dHRousxU5jahJHUPyO0gaUcWwzaVloJKi8lTFTd1PWQ8waz5V7BedkV67hd8syUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.95.0",
|
||||
"@tanstack/history": "1.99.13",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"@tanstack/router-core": "^1.111.3",
|
||||
"jsesc": "^3.1.0",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"tiny-warning": "^1.0.3"
|
||||
},
|
||||
@@ -2587,8 +2588,8 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
"react": ">=18.0.0 || >=19.0.0",
|
||||
"react-dom": ">=18.0.0 || >=19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-store": {
|
||||
@@ -2610,12 +2611,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
|
||||
"integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz",
|
||||
"integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.11.2"
|
||||
"@tanstack/virtual-core": "3.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2626,6 +2627,23 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.111.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.111.3.tgz",
|
||||
"integrity": "sha512-q+CHuOhTgqHudVKijL89jIdLe5A00RzV8ZMMSi4qiHGnggm4nisF8eSE3dFQaic1+YFk1wR7dfFA2hvkr1hFIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.99.13",
|
||||
"@tanstack/store": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-devtools": {
|
||||
"version": "1.91.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.91.3.tgz",
|
||||
@@ -2730,9 +2748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
|
||||
"integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz",
|
||||
"integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2754,9 +2772,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.2.tgz",
|
||||
"integrity": "sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.2.0.tgz",
|
||||
"integrity": "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -6963,17 +6981,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "11.11.7",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.7.tgz",
|
||||
"integrity": "sha512-89CgILOXPeG3L7ymOTGrLmf8IiKubYLUN/QkYgQuLvehAHfqgwJbLfCnhuyRI4WTds1TXkUp67A7IJrgRY/j1w==",
|
||||
"version": "12.4.7",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz",
|
||||
"integrity": "sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.4.5",
|
||||
"motion-utils": "^12.0.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
@@ -8577,9 +8597,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
|
||||
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
@@ -10008,6 +10028,47 @@
|
||||
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/motion": {
|
||||
"version": "12.4.7",
|
||||
"resolved": "https://registry.npmjs.org/motion/-/motion-12.4.7.tgz",
|
||||
"integrity": "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.4.7",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.4.5",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.5.tgz",
|
||||
"integrity": "sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
|
||||
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -15635,10 +15696,10 @@
|
||||
"@replit/codemirror-vim": "^6.2.1",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.62.16",
|
||||
"@tanstack/react-router": "^1.95.1",
|
||||
"@tanstack/react-virtual": "^3.11.2",
|
||||
"@tauri-apps/api": "^2.0.1",
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@tanstack/react-router": "^1.111.3",
|
||||
"@tanstack/react-virtual": "^3.13.0",
|
||||
"@tauri-apps/api": "^2.2.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||
@@ -15655,7 +15716,6 @@
|
||||
"eventemitter3": "^5.0.1",
|
||||
"focus-trap-react": "^10.2.3",
|
||||
"format-graphql": "^1.5.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"fuzzbunny": "^1.0.1",
|
||||
"hexy": "^0.3.5",
|
||||
"history": "^5.3.0",
|
||||
@@ -15663,6 +15723,7 @@
|
||||
"js-md5": "^0.8.3",
|
||||
"lucide-react": "^0.474.0",
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.4.7",
|
||||
"nanoid": "^5.0.9",
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-color": "^1.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -346,7 +346,7 @@ export type ImportResponse = { resources: ImportResources, };
|
||||
|
||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
|
||||
@@ -354,7 +354,7 @@ export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* Label for the window. If not provided, a random one will be generated.
|
||||
*/
|
||||
label: string, title?: string, size?: WindowSize, };
|
||||
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
|
||||
|
||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,10 @@ export interface Context {
|
||||
};
|
||||
window: {
|
||||
openUrl(
|
||||
args: OpenWindowRequest & { onNavigate?: (args: { url: string }) => void },
|
||||
args: OpenWindowRequest & {
|
||||
onNavigate?: (args: { url: string }) => void;
|
||||
onClose: () => void;
|
||||
},
|
||||
): Promise<{ close: () => void }>;
|
||||
};
|
||||
httpRequest: {
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
"scripts": {
|
||||
"bootstrap": "npm run build",
|
||||
"build": "run-p build:*",
|
||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs",
|
||||
"build:worker": "esbuild src/index.worker.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.worker.cjs",
|
||||
"build:__main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/target/debug/vendored/plugin-runtime/index.cjs",
|
||||
"build:__worker": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/target/debug/vendored/plugin-runtime/index.worker.cjs"
|
||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../src-tauri/vendored/plugin-runtime/index.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.18.0"
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { InternalEvent } from "@yaakapp/api";
|
||||
import EventEmitter from "node:events";
|
||||
import type { InternalEvent } from '@yaakapp/api';
|
||||
|
||||
export class EventChannel {
|
||||
emitter: EventEmitter = new EventEmitter();
|
||||
#listeners = new Set<(event: InternalEvent) => void>();
|
||||
|
||||
emit(e: InternalEvent) {
|
||||
this.emitter.emit("__plugin_event__", e);
|
||||
for (const l of this.#listeners) {
|
||||
l(e);
|
||||
}
|
||||
}
|
||||
|
||||
listen(cb: (e: InternalEvent) => void) {
|
||||
this.emitter.on("__plugin_event__", cb);
|
||||
this.#listeners.add(cb);
|
||||
}
|
||||
|
||||
unlisten(cb: (e: InternalEvent) => void) {
|
||||
this.#listeners.delete(cb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,28 @@
|
||||
import type { BootRequest, InternalEvent } from '@yaakapp/api';
|
||||
import path from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import type { EventChannel } from './EventChannel';
|
||||
import type { PluginWorkerData } from './index.worker';
|
||||
import { PluginInstance, PluginWorkerData } from './PluginInstance';
|
||||
|
||||
export class PluginHandle {
|
||||
#worker: Worker;
|
||||
#instance: PluginInstance;
|
||||
|
||||
constructor(
|
||||
readonly pluginRefId: string,
|
||||
readonly bootRequest: BootRequest,
|
||||
readonly events: EventChannel,
|
||||
readonly pluginToAppEvents: EventChannel,
|
||||
) {
|
||||
this.#worker = this.#createWorker();
|
||||
}
|
||||
|
||||
sendToWorker(event: InternalEvent) {
|
||||
this.#worker.postMessage(event);
|
||||
}
|
||||
|
||||
async terminate() {
|
||||
await this.#worker.terminate();
|
||||
}
|
||||
|
||||
#createWorker(): Worker {
|
||||
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
|
||||
const workerData: PluginWorkerData = {
|
||||
pluginRefId: this.pluginRefId,
|
||||
bootRequest: this.bootRequest,
|
||||
};
|
||||
const worker = new Worker(workerPath, {
|
||||
workerData,
|
||||
});
|
||||
|
||||
worker.on('message', (e) => this.events.emit(e));
|
||||
worker.on('error', this.#handleError.bind(this));
|
||||
worker.on('exit', this.#handleExit.bind(this));
|
||||
|
||||
this.#instance = new PluginInstance(workerData, pluginToAppEvents);
|
||||
console.log('Created plugin worker for ', this.bootRequest.dir);
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
async #handleError(err: Error) {
|
||||
console.error('Plugin errored', this.bootRequest.dir, err);
|
||||
sendToWorker(event: InternalEvent) {
|
||||
this.#instance.postMessage(event);
|
||||
}
|
||||
|
||||
async #handleExit(code: number) {
|
||||
if (code === 0) {
|
||||
console.log('Plugin exited successfully', this.bootRequest.dir);
|
||||
} else {
|
||||
console.log('Plugin exited with status', code, this.bootRequest.dir);
|
||||
}
|
||||
terminate() {
|
||||
this.#instance.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
597
packages/plugin-runtime/src/PluginInstance.ts
Normal file
597
packages/plugin-runtime/src/PluginInstance.ts
Normal file
@@ -0,0 +1,597 @@
|
||||
import type {
|
||||
BootRequest,
|
||||
Context,
|
||||
DeleteKeyValueResponse,
|
||||
FindHttpResponsesResponse,
|
||||
FormInput,
|
||||
GetHttpRequestByIdResponse,
|
||||
GetKeyValueResponse,
|
||||
HttpAuthenticationAction,
|
||||
HttpRequestAction,
|
||||
InternalEvent,
|
||||
InternalEventPayload,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
PromptTextResponse,
|
||||
RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse,
|
||||
TemplateFunction,
|
||||
TemplateRenderResponse,
|
||||
WindowContext,
|
||||
} from '@yaakapp/api';
|
||||
import console from 'node:console';
|
||||
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
// import util from 'node:util';
|
||||
import { EventChannel } from './EventChannel';
|
||||
// import { interceptStdout } from './interceptStdout';
|
||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||
|
||||
export interface PluginWorkerData {
|
||||
bootRequest: BootRequest;
|
||||
pluginRefId: string;
|
||||
}
|
||||
|
||||
export class PluginInstance {
|
||||
#workerData: PluginWorkerData;
|
||||
#mod: PluginDefinition;
|
||||
#pkg: { name?: string; version?: string };
|
||||
#pluginToAppEvents: EventChannel;
|
||||
#appToPluginEvents: EventChannel;
|
||||
|
||||
constructor(workerData: PluginWorkerData, pluginEvents: EventChannel) {
|
||||
this.#workerData = workerData;
|
||||
this.#pluginToAppEvents = pluginEvents;
|
||||
this.#appToPluginEvents = new EventChannel();
|
||||
|
||||
// Forward incoming events to onMessage()
|
||||
this.#appToPluginEvents.listen(async event => {
|
||||
await this.#onMessage(event);
|
||||
})
|
||||
|
||||
// Reload plugin if the JS or package.json changes
|
||||
const windowContextNone: WindowContext = { type: 'none' };
|
||||
const fileChangeCallback = async () => {
|
||||
this.#importModule();
|
||||
return this.#sendPayload(windowContextNone, { type: 'reload_response' }, null);
|
||||
};
|
||||
|
||||
if (this.#workerData.bootRequest.watch) {
|
||||
watchFile(this.#pathMod(), fileChangeCallback);
|
||||
watchFile(this.#pathPkg(), fileChangeCallback);
|
||||
}
|
||||
|
||||
this.#mod = {};
|
||||
this.#pkg = JSON.parse(readFileSync(this.#pathPkg(), 'utf8'));
|
||||
|
||||
// TODO: Re-implement this now that we're not using workers
|
||||
// prefixStdout(`[plugin][${this.#pkg.name}] %s`);
|
||||
|
||||
this.#importModule();
|
||||
}
|
||||
|
||||
postMessage(event: InternalEvent) {
|
||||
this.#appToPluginEvents.emit(event);
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.#unimportModule();
|
||||
}
|
||||
|
||||
async #onMessage(event: InternalEvent) {
|
||||
const ctx = this.#newCtx(event);
|
||||
|
||||
const { windowContext, payload, id: replyId } = event;
|
||||
try {
|
||||
if (payload.type === 'boot_request') {
|
||||
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'boot_response',
|
||||
name: this.#pkg.name ?? 'unknown',
|
||||
version: this.#pkg.version ?? '0.0.1',
|
||||
};
|
||||
this.#sendPayload(windowContext, payload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'terminate_request') {
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'terminate_response',
|
||||
};
|
||||
this.#sendPayload(windowContext, payload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'import_request' &&
|
||||
typeof this.#mod?.importer?.onImport === 'function'
|
||||
) {
|
||||
const reply = await this.#mod.importer.onImport(ctx, {
|
||||
text: payload.content,
|
||||
});
|
||||
if (reply != null) {
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'import_response',
|
||||
// deno-lint-ignore no-explicit-any
|
||||
resources: reply.resources as any,
|
||||
};
|
||||
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
} else {
|
||||
// Continue, to send back an empty reply
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'filter_request' && typeof this.#mod?.filter?.onFilter === 'function') {
|
||||
const reply = await this.#mod.filter.onFilter(ctx, {
|
||||
filter: payload.filter,
|
||||
payload: payload.content,
|
||||
mimeType: payload.type,
|
||||
});
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'filter_response',
|
||||
content: reply.filtered,
|
||||
};
|
||||
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_http_request_actions_request' &&
|
||||
Array.isArray(this.#mod?.httpRequestActions)
|
||||
) {
|
||||
const reply: HttpRequestAction[] = this.#mod.httpRequestActions.map((a) => ({
|
||||
...a,
|
||||
// Add everything except onSelect
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_request_actions_response',
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_template_functions_request' &&
|
||||
Array.isArray(this.#mod?.templateFunctions)
|
||||
) {
|
||||
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
|
||||
return {
|
||||
...migrateTemplateFunctionSelectOptions(templateFunction),
|
||||
// Add everything except render
|
||||
onRender: undefined,
|
||||
};
|
||||
});
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_template_functions_response',
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
functions: reply,
|
||||
};
|
||||
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_http_authentication_summary_request' && this.#mod?.authentication) {
|
||||
const { name, shortLabel, label } = this.#mod.authentication;
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_authentication_summary_response',
|
||||
name,
|
||||
label,
|
||||
shortLabel,
|
||||
};
|
||||
|
||||
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
|
||||
const { args, actions } = this.#mod.authentication;
|
||||
const resolvedArgs: FormInput[] = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
let v = args[i];
|
||||
if ('dynamic' in v) {
|
||||
const dynamicAttrs = await v.dynamic(ctx, payload);
|
||||
const { dynamic, ...other } = v;
|
||||
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
|
||||
} else {
|
||||
resolvedArgs.push(v);
|
||||
}
|
||||
}
|
||||
const resolvedActions: HttpAuthenticationAction[] = [];
|
||||
for (const { onSelect, ...action } of actions ?? []) {
|
||||
resolvedActions.push(action);
|
||||
}
|
||||
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_authentication_config_response',
|
||||
args: resolvedArgs,
|
||||
actions: resolvedActions,
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
};
|
||||
|
||||
this.#sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'call_http_authentication_request' && this.#mod?.authentication) {
|
||||
const auth = this.#mod.authentication;
|
||||
if (typeof auth?.onApply === 'function') {
|
||||
applyFormInputDefaults(auth.args, payload.values);
|
||||
const result = await auth.onApply(ctx, payload);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_http_authentication_response',
|
||||
setHeaders: result.setHeaders,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_authentication_action_request' &&
|
||||
this.#mod.authentication != null
|
||||
) {
|
||||
const action = this.#mod.authentication.actions?.[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(windowContext, replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_request_action_request' &&
|
||||
Array.isArray(this.#mod.httpRequestActions)
|
||||
) {
|
||||
const action = this.#mod.httpRequestActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
this.#sendEmpty(windowContext, replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_template_function_request' &&
|
||||
Array.isArray(this.#mod?.templateFunctions)
|
||||
) {
|
||||
const action = this.#mod.templateFunctions.find((a) => a.name === payload.name);
|
||||
if (typeof action?.onRender === 'function') {
|
||||
applyFormInputDefaults(action.args, payload.args.values);
|
||||
const result = await action.onRender(ctx, payload.args);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
value: result ?? null,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'reload_request') {
|
||||
this.#importModule();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Plugin call threw exception', payload.type, err);
|
||||
this.#sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'error_response',
|
||||
error: `${err}`,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// No matches, so send back an empty response so the caller doesn't block forever
|
||||
this.#sendEmpty(windowContext, replyId);
|
||||
}
|
||||
|
||||
#pathMod() {
|
||||
return path.posix.join(this.#workerData.bootRequest.dir, 'build', 'index.js');
|
||||
}
|
||||
|
||||
#pathPkg() {
|
||||
return path.join(this.#workerData.bootRequest.dir, 'package.json');
|
||||
}
|
||||
|
||||
#unimportModule() {
|
||||
const id = require.resolve(this.#pathMod());
|
||||
delete require.cache[id];
|
||||
}
|
||||
|
||||
#importModule() {
|
||||
const id = require.resolve(this.#pathMod());
|
||||
delete require.cache[id];
|
||||
this.#mod = require(id).plugin;
|
||||
}
|
||||
|
||||
#buildEventToSend(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null = null,
|
||||
): InternalEvent {
|
||||
return {
|
||||
pluginRefId: this.#workerData.pluginRefId,
|
||||
pluginName: path.basename(this.#workerData.bootRequest.dir),
|
||||
id: genId(),
|
||||
replyId,
|
||||
payload,
|
||||
windowContext,
|
||||
};
|
||||
}
|
||||
|
||||
#sendPayload(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null,
|
||||
): string {
|
||||
const event = this.#buildEventToSend(windowContext, payload, replyId);
|
||||
this.#sendEvent(event);
|
||||
return event.id;
|
||||
}
|
||||
|
||||
#sendEvent(event: InternalEvent) {
|
||||
// if (event.payload.type !== 'empty_response') {
|
||||
// console.log('Sending event to app', this.#pkg.name, event.id, event.payload.type);
|
||||
// }
|
||||
this.#pluginToAppEvents.emit(event);
|
||||
}
|
||||
|
||||
#sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
|
||||
return this.#sendPayload(windowContext, { type: 'empty_response' }, replyId);
|
||||
}
|
||||
|
||||
#sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
): Promise<T> {
|
||||
// 1. Build event to send
|
||||
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
|
||||
|
||||
// 2. Spawn listener in background
|
||||
const promise = new Promise<T>((resolve) => {
|
||||
const cb = (event: InternalEvent) => {
|
||||
if (event.replyId === eventToSend.id) {
|
||||
this.#appToPluginEvents.unlisten(cb); // Unlisten, now that we're done
|
||||
const { type: _, ...payload } = event.payload;
|
||||
resolve(payload as T);
|
||||
}
|
||||
};
|
||||
this.#appToPluginEvents.listen(cb);
|
||||
});
|
||||
|
||||
// 3. Send the event after we start listening (to prevent race)
|
||||
this.#sendEvent(eventToSend);
|
||||
|
||||
// 4. Return the listener promise
|
||||
return promise as unknown as Promise<T>;
|
||||
}
|
||||
|
||||
#sendAndListenForEvents(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
onEvent: (event: InternalEventPayload) => void,
|
||||
): void {
|
||||
// 1. Build event to send
|
||||
const eventToSend = this.#buildEventToSend(windowContext, payload, null);
|
||||
|
||||
// 2. Listen for replies in the background
|
||||
this.#appToPluginEvents.listen((event: InternalEvent) => {
|
||||
if (event.replyId === eventToSend.id) {
|
||||
onEvent(event.payload);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Send the event after we start listening (to prevent race)
|
||||
this.#sendEvent(eventToSend);
|
||||
}
|
||||
|
||||
#newCtx(event: InternalEvent): Context {
|
||||
return {
|
||||
clipboard: {
|
||||
copyText: async (text) => {
|
||||
await this.#sendAndWaitForReply(event.windowContext, {
|
||||
type: 'copy_text_request',
|
||||
text,
|
||||
});
|
||||
},
|
||||
},
|
||||
toast: {
|
||||
show: async (args) => {
|
||||
await this.#sendAndWaitForReply(event.windowContext, {
|
||||
type: 'show_toast_request',
|
||||
...args,
|
||||
});
|
||||
},
|
||||
},
|
||||
window: {
|
||||
openUrl: async ({ onNavigate, onClose, ...args }) => {
|
||||
args.label = args.label || `${Math.random()}`;
|
||||
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
|
||||
const onEvent = (event: InternalEventPayload) => {
|
||||
if (event.type === 'window_navigate_event') {
|
||||
onNavigate?.(event);
|
||||
} else if (event.type === 'window_close_event') {
|
||||
onClose?.();
|
||||
}
|
||||
};
|
||||
this.#sendAndListenForEvents(event.windowContext, payload, onEvent);
|
||||
return {
|
||||
close: () => {
|
||||
const closePayload: InternalEventPayload = {
|
||||
type: 'close_window_request',
|
||||
label: args.label,
|
||||
};
|
||||
this.#sendPayload(event.windowContext, closePayload, null);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
text: async (args) => {
|
||||
const reply: PromptTextResponse = await this.#sendAndWaitForReply(event.windowContext, {
|
||||
type: 'prompt_text_request',
|
||||
...args,
|
||||
});
|
||||
return reply.value;
|
||||
},
|
||||
},
|
||||
httpResponse: {
|
||||
find: async (args) => {
|
||||
const payload = {
|
||||
type: 'find_http_responses_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpResponses } = await this.#sendAndWaitForReply<FindHttpResponsesResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpResponses;
|
||||
},
|
||||
},
|
||||
httpRequest: {
|
||||
getById: async (args) => {
|
||||
const payload = {
|
||||
type: 'get_http_request_by_id_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpRequest } = await this.#sendAndWaitForReply<GetHttpRequestByIdResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpRequest;
|
||||
},
|
||||
send: async (args) => {
|
||||
const payload = {
|
||||
type: 'send_http_request_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpResponse } = await this.#sendAndWaitForReply<SendHttpRequestResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpResponse;
|
||||
},
|
||||
render: async (args) => {
|
||||
const payload = {
|
||||
type: 'render_http_request_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpRequest } = await this.#sendAndWaitForReply<RenderHttpRequestResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpRequest;
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
/**
|
||||
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
||||
* (eg. object), it will be recursively rendered.
|
||||
*/
|
||||
render: async (args) => {
|
||||
const payload = { type: 'template_render_request', ...args } as const;
|
||||
const result = await this.#sendAndWaitForReply<TemplateRenderResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return result.data;
|
||||
},
|
||||
},
|
||||
store: {
|
||||
get: async <T>(key: string) => {
|
||||
const payload = { type: 'get_key_value_request', key } as const;
|
||||
const result = await this.#sendAndWaitForReply<GetKeyValueResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return result.value ? (JSON.parse(result.value) as T) : undefined;
|
||||
},
|
||||
set: async <T>(key: string, value: T) => {
|
||||
const valueStr = JSON.stringify(value);
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'set_key_value_request',
|
||||
key,
|
||||
value: valueStr,
|
||||
};
|
||||
await this.#sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
|
||||
},
|
||||
delete: async (key: string) => {
|
||||
const payload = { type: 'delete_key_value_request', key } as const;
|
||||
const result = await this.#sendAndWaitForReply<DeleteKeyValueResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return result.deleted;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function genId(len = 5): string {
|
||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Recursively apply form input defaults to a set of values */
|
||||
function applyFormInputDefaults(
|
||||
inputs: FormInput[],
|
||||
values: { [p: string]: JsonPrimitive | undefined },
|
||||
) {
|
||||
for (const input of inputs) {
|
||||
if ('inputs' in input) {
|
||||
applyFormInputDefaults(input.inputs ?? [], values);
|
||||
} else if ('defaultValue' in input && values[input.name] === undefined) {
|
||||
values[input.name] = input.defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const watchedFiles: Record<string, Stats> = {};
|
||||
|
||||
/**
|
||||
* Watch a file and trigger callback on change.
|
||||
*
|
||||
* We also track the stat for each file because fs.watch() will
|
||||
* trigger a "change" event when the access date changes
|
||||
*/
|
||||
function watchFile(filepath: string, cb: (filepath: string) => void) {
|
||||
watch(filepath, () => {
|
||||
const stat = statSync(filepath);
|
||||
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
||||
cb(filepath);
|
||||
}
|
||||
watchedFiles[filepath] = stat;
|
||||
});
|
||||
}
|
||||
|
||||
// function prefixStdout(s: string) {
|
||||
// if (!s.includes('%s')) {
|
||||
// throw new Error('Console prefix must contain a "%s" replacer');
|
||||
// }
|
||||
// interceptStdout((text: string) => {
|
||||
// const lines = text.split(/\n/);
|
||||
// let newText = '';
|
||||
// for (let i = 0; i < lines.length; i++) {
|
||||
// if (lines[i] == '') continue;
|
||||
// newText += util.format(s, lines[i]) + '\n';
|
||||
// }
|
||||
// return newText.trimEnd();
|
||||
// });
|
||||
// }
|
||||
@@ -8,7 +8,7 @@ if (!port) {
|
||||
throw new Error('Plugin runtime missing PORT')
|
||||
}
|
||||
|
||||
const events = new EventChannel();
|
||||
const pluginToAppEvents = new EventChannel();
|
||||
const plugins: Record<string, PluginHandle> = {};
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${port}`);
|
||||
@@ -25,7 +25,7 @@ ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err
|
||||
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
|
||||
|
||||
// Listen for incoming events from plugins
|
||||
events.listen((e) => {
|
||||
pluginToAppEvents.listen((e) => {
|
||||
const eventStr = JSON.stringify(e);
|
||||
ws.send(eventStr);
|
||||
});
|
||||
@@ -34,7 +34,7 @@ async function handleIncoming(msg: string) {
|
||||
const pluginEvent: InternalEvent = JSON.parse(msg);
|
||||
// Handle special event to bootstrap plugin
|
||||
if (pluginEvent.payload.type === 'boot_request') {
|
||||
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, events);
|
||||
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.payload, pluginToAppEvents);
|
||||
plugins[pluginEvent.pluginRefId] = plugin;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ async function handleIncoming(msg: string) {
|
||||
}
|
||||
|
||||
if (pluginEvent.payload.type === 'terminate_request') {
|
||||
await plugin.terminate();
|
||||
plugin.terminate();
|
||||
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
|
||||
delete plugins[pluginEvent.pluginRefId];
|
||||
}
|
||||
|
||||
@@ -1,568 +0,0 @@
|
||||
// OAuth 2.0 spec -> https://datatracker.ietf.org/doc/html/rfc6749
|
||||
|
||||
import type {
|
||||
BootRequest,
|
||||
Context,
|
||||
DeleteKeyValueResponse,
|
||||
FindHttpResponsesResponse,
|
||||
FormInput,
|
||||
GetHttpRequestByIdResponse,
|
||||
GetKeyValueResponse,
|
||||
HttpAuthenticationAction,
|
||||
HttpRequestAction,
|
||||
InternalEvent,
|
||||
InternalEventPayload,
|
||||
JsonPrimitive,
|
||||
PluginDefinition,
|
||||
PromptTextResponse,
|
||||
RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse,
|
||||
TemplateFunction,
|
||||
TemplateRenderResponse,
|
||||
WindowContext,
|
||||
} from '@yaakapp/api';
|
||||
import * as console from 'node:console';
|
||||
import type { Stats } from 'node:fs';
|
||||
import { readFileSync, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as util from 'node:util';
|
||||
import { parentPort as nullableParentPort, workerData } from 'node:worker_threads';
|
||||
import { interceptStdout } from './interceptStdout';
|
||||
import { migrateTemplateFunctionSelectOptions } from './migrations';
|
||||
|
||||
if (nullableParentPort == null) {
|
||||
throw new Error('Worker does not have access to parentPort');
|
||||
}
|
||||
|
||||
const parentPort = nullableParentPort;
|
||||
|
||||
export interface PluginWorkerData {
|
||||
bootRequest: BootRequest;
|
||||
pluginRefId: string;
|
||||
}
|
||||
|
||||
function initialize(workerData: PluginWorkerData) {
|
||||
const {
|
||||
bootRequest: { dir: pluginDir, watch: enableWatch },
|
||||
pluginRefId,
|
||||
}: PluginWorkerData = workerData;
|
||||
|
||||
const pathPkg = path.join(pluginDir, 'package.json');
|
||||
const pathMod = path.posix.join(pluginDir, 'build', 'index.js');
|
||||
|
||||
const pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
|
||||
|
||||
prefixStdout(`[plugin][${pkg.name}] %s`);
|
||||
|
||||
function buildEventToSend(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null = null,
|
||||
): InternalEvent {
|
||||
return {
|
||||
pluginRefId,
|
||||
pluginName: path.basename(pluginDir),
|
||||
id: genId(),
|
||||
replyId,
|
||||
payload,
|
||||
windowContext,
|
||||
};
|
||||
}
|
||||
|
||||
function sendEmpty(windowContext: WindowContext, replyId: string | null = null): string {
|
||||
return sendPayload(windowContext, { type: 'empty_response' }, replyId);
|
||||
}
|
||||
|
||||
function sendPayload(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null,
|
||||
): string {
|
||||
const event = buildEventToSend(windowContext, payload, replyId);
|
||||
sendEvent(event);
|
||||
return event.id;
|
||||
}
|
||||
|
||||
function sendEvent(event: InternalEvent) {
|
||||
if (event.payload.type !== 'empty_response') {
|
||||
console.log('Sending event to app', event.id, event.payload.type);
|
||||
}
|
||||
parentPort.postMessage(event);
|
||||
}
|
||||
|
||||
function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
): Promise<T> {
|
||||
// 1. Build event to send
|
||||
const eventToSend = buildEventToSend(windowContext, payload, null);
|
||||
|
||||
// 2. Spawn listener in background
|
||||
const promise = new Promise<T>((resolve) => {
|
||||
const cb = (event: InternalEvent) => {
|
||||
if (event.replyId === eventToSend.id) {
|
||||
parentPort.off('message', cb); // Unlisten, now that we're done
|
||||
const { type: _, ...payload } = event.payload;
|
||||
resolve(payload as T);
|
||||
}
|
||||
};
|
||||
parentPort.on('message', cb);
|
||||
});
|
||||
|
||||
// 3. Send the event after we start listening (to prevent race)
|
||||
sendEvent(eventToSend);
|
||||
|
||||
// 4. Return the listener promise
|
||||
return promise as unknown as Promise<T>;
|
||||
}
|
||||
|
||||
function sendAndListenForEvents(
|
||||
windowContext: WindowContext,
|
||||
payload: InternalEventPayload,
|
||||
onEvent: (event: InternalEventPayload) => void,
|
||||
): void {
|
||||
// 1. Build event to send
|
||||
const eventToSend = buildEventToSend(windowContext, payload, null);
|
||||
|
||||
// 2. Listen for replies in the background
|
||||
parentPort.on('message', (event: InternalEvent) => {
|
||||
if (event.replyId === eventToSend.id) {
|
||||
onEvent(event.payload);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Send the event after we start listening (to prevent race)
|
||||
sendEvent(eventToSend);
|
||||
}
|
||||
|
||||
// Reload plugin if the JS or package.json changes
|
||||
const windowContextNone: WindowContext = { type: 'none' };
|
||||
const fileChangeCallback = async () => {
|
||||
importModule();
|
||||
return sendPayload(windowContextNone, { type: 'reload_response' }, null);
|
||||
};
|
||||
|
||||
if (enableWatch) {
|
||||
watchFile(pathMod, fileChangeCallback);
|
||||
watchFile(pathPkg, fileChangeCallback);
|
||||
}
|
||||
|
||||
const newCtx = (event: InternalEvent): Context => ({
|
||||
clipboard: {
|
||||
async copyText(text) {
|
||||
await sendAndWaitForReply(event.windowContext, {
|
||||
type: 'copy_text_request',
|
||||
text,
|
||||
});
|
||||
},
|
||||
},
|
||||
toast: {
|
||||
async show(args) {
|
||||
await sendAndWaitForReply(event.windowContext, {
|
||||
type: 'show_toast_request',
|
||||
...args,
|
||||
});
|
||||
},
|
||||
},
|
||||
window: {
|
||||
async openUrl({ onNavigate, ...args }) {
|
||||
args.label = args.label || `${Math.random()}`;
|
||||
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
|
||||
const onEvent = (event: InternalEventPayload) => {
|
||||
if (event.type === 'window_navigate_event') {
|
||||
onNavigate?.(event);
|
||||
}
|
||||
};
|
||||
sendAndListenForEvents(event.windowContext, payload, onEvent);
|
||||
return {
|
||||
close: () => {
|
||||
const closePayload: InternalEventPayload = {
|
||||
type: 'close_window_request',
|
||||
label: args.label,
|
||||
};
|
||||
sendPayload(event.windowContext, closePayload, null);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
async text(args) {
|
||||
const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, {
|
||||
type: 'prompt_text_request',
|
||||
...args,
|
||||
});
|
||||
return reply.value;
|
||||
},
|
||||
},
|
||||
httpResponse: {
|
||||
async find(args) {
|
||||
const payload = {
|
||||
type: 'find_http_responses_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpResponses;
|
||||
},
|
||||
},
|
||||
httpRequest: {
|
||||
async getById(args) {
|
||||
const payload = {
|
||||
type: 'get_http_request_by_id_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpRequest;
|
||||
},
|
||||
async send(args) {
|
||||
const payload = {
|
||||
type: 'send_http_request_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpResponse;
|
||||
},
|
||||
async render(args) {
|
||||
const payload = {
|
||||
type: 'render_http_request_request',
|
||||
...args,
|
||||
} as const;
|
||||
const { httpRequest } = await sendAndWaitForReply<RenderHttpRequestResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return httpRequest;
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
/**
|
||||
* Invoke Yaak's template engine to render a value. If the value is a nested type
|
||||
* (eg. object), it will be recursively rendered.
|
||||
*/
|
||||
async render(args) {
|
||||
const payload = { type: 'template_render_request', ...args } as const;
|
||||
const result = await sendAndWaitForReply<TemplateRenderResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return result.data;
|
||||
},
|
||||
},
|
||||
store: {
|
||||
async get<T>(key: string) {
|
||||
const payload = { type: 'get_key_value_request', key } as const;
|
||||
const result = await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
|
||||
return result.value ? (JSON.parse(result.value) as T) : undefined;
|
||||
},
|
||||
async set<T>(key: string, value: T) {
|
||||
const valueStr = JSON.stringify(value);
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'set_key_value_request',
|
||||
key,
|
||||
value: valueStr,
|
||||
};
|
||||
await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
|
||||
},
|
||||
async delete(key: string) {
|
||||
const payload = { type: 'delete_key_value_request', key } as const;
|
||||
const result = await sendAndWaitForReply<DeleteKeyValueResponse>(
|
||||
event.windowContext,
|
||||
payload,
|
||||
);
|
||||
return result.deleted;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let plug: PluginDefinition | null = null;
|
||||
|
||||
function importModule() {
|
||||
const id = require.resolve(pathMod);
|
||||
delete require.cache[id];
|
||||
plug = require(id).plugin;
|
||||
}
|
||||
|
||||
importModule();
|
||||
|
||||
// Message comes into the plugin to be processed
|
||||
parentPort.on('message', async (event: InternalEvent) => {
|
||||
const ctx = newCtx(event);
|
||||
const { windowContext, payload, id: replyId } = event;
|
||||
try {
|
||||
if (payload.type === 'boot_request') {
|
||||
// console.log('Plugin initialized', pkg.name, { capabilities, enableWatch });
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'boot_response',
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
};
|
||||
sendPayload(windowContext, payload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'terminate_request') {
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'terminate_response',
|
||||
};
|
||||
sendPayload(windowContext, payload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'import_request' && typeof plug?.importer?.onImport === 'function') {
|
||||
const reply = await plug.importer.onImport(ctx, {
|
||||
text: payload.content,
|
||||
});
|
||||
if (reply != null) {
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'import_response',
|
||||
// deno-lint-ignore no-explicit-any
|
||||
resources: reply.resources as any,
|
||||
};
|
||||
sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
} else {
|
||||
// Continue, to send back an empty reply
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'filter_request' && typeof plug?.filter?.onFilter === 'function') {
|
||||
const reply = await plug.filter.onFilter(ctx, {
|
||||
filter: payload.filter,
|
||||
payload: payload.content,
|
||||
mimeType: payload.type,
|
||||
});
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'filter_response',
|
||||
content: reply.filtered,
|
||||
};
|
||||
sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_http_request_actions_request' &&
|
||||
Array.isArray(plug?.httpRequestActions)
|
||||
) {
|
||||
const reply: HttpRequestAction[] = plug.httpRequestActions.map((a) => ({
|
||||
...a,
|
||||
// Add everything except onSelect
|
||||
onSelect: undefined,
|
||||
}));
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_request_actions_response',
|
||||
pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_template_functions_request' &&
|
||||
Array.isArray(plug?.templateFunctions)
|
||||
) {
|
||||
const reply: TemplateFunction[] = plug.templateFunctions.map((templateFunction) => {
|
||||
return {
|
||||
...migrateTemplateFunctionSelectOptions(templateFunction),
|
||||
// Add everything except render
|
||||
onRender: undefined,
|
||||
};
|
||||
});
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_template_functions_response',
|
||||
pluginRefId,
|
||||
functions: reply,
|
||||
};
|
||||
sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_http_authentication_summary_request' && plug?.authentication) {
|
||||
const { name, shortLabel, label } = plug.authentication;
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_authentication_summary_response',
|
||||
name,
|
||||
label,
|
||||
shortLabel,
|
||||
};
|
||||
|
||||
sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'get_http_authentication_config_request' && plug?.authentication) {
|
||||
const { args, actions } = plug.authentication;
|
||||
const resolvedArgs: FormInput[] = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
let v = args[i];
|
||||
if ('dynamic' in v) {
|
||||
const dynamicAttrs = await v.dynamic(ctx, payload);
|
||||
const { dynamic, ...other } = v;
|
||||
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
|
||||
} else {
|
||||
resolvedArgs.push(v);
|
||||
}
|
||||
}
|
||||
const resolvedActions: HttpAuthenticationAction[] = [];
|
||||
for (const { onSelect, ...action } of actions ?? []) {
|
||||
resolvedActions.push(action);
|
||||
}
|
||||
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_authentication_config_response',
|
||||
args: resolvedArgs,
|
||||
actions: resolvedActions,
|
||||
pluginRefId,
|
||||
};
|
||||
|
||||
sendPayload(windowContext, replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'call_http_authentication_request' && plug?.authentication) {
|
||||
const auth = plug.authentication;
|
||||
if (typeof auth?.onApply === 'function') {
|
||||
applyFormInputDefaults(auth.args, payload.values);
|
||||
const result = await auth.onApply(ctx, payload);
|
||||
sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_http_authentication_response',
|
||||
setHeaders: result.setHeaders,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_authentication_action_request' &&
|
||||
plug?.authentication != null
|
||||
) {
|
||||
const action = plug.authentication.actions?.[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
sendEmpty(windowContext, replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_request_action_request' &&
|
||||
Array.isArray(plug?.httpRequestActions)
|
||||
) {
|
||||
const action = plug.httpRequestActions[payload.index];
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
sendEmpty(windowContext, replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_template_function_request' &&
|
||||
Array.isArray(plug?.templateFunctions)
|
||||
) {
|
||||
const action = plug.templateFunctions.find((a) => a.name === payload.name);
|
||||
if (typeof action?.onRender === 'function') {
|
||||
applyFormInputDefaults(action.args, payload.args.values);
|
||||
const result = await action.onRender(ctx, payload.args);
|
||||
sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'call_template_function_response',
|
||||
value: result ?? null,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'reload_request') {
|
||||
importModule();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Plugin call threw exception', payload.type, err);
|
||||
sendPayload(
|
||||
windowContext,
|
||||
{
|
||||
type: 'error_response',
|
||||
error: `${err}`,
|
||||
},
|
||||
replyId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// No matches, so send back an empty response so the caller doesn't block forever
|
||||
sendEmpty(windowContext, replyId);
|
||||
});
|
||||
}
|
||||
|
||||
initialize(workerData);
|
||||
|
||||
function genId(len = 5): string {
|
||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function prefixStdout(s: string) {
|
||||
if (!s.includes('%s')) {
|
||||
throw new Error('Console prefix must contain a "%s" replacer');
|
||||
}
|
||||
interceptStdout((text: string) => {
|
||||
const lines = text.split(/\n/);
|
||||
let newText = '';
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i] == '') continue;
|
||||
newText += util.format(s, lines[i]) + '\n';
|
||||
}
|
||||
return newText.trimEnd();
|
||||
});
|
||||
}
|
||||
|
||||
const watchedFiles: Record<string, Stats> = {};
|
||||
|
||||
/**
|
||||
* Watch a file and trigger callback on change.
|
||||
*
|
||||
* We also track the stat for each file because fs.watch() will
|
||||
* trigger a "change" event when the access date changes
|
||||
*/
|
||||
function watchFile(filepath: string, cb: (filepath: string) => void) {
|
||||
watch(filepath, () => {
|
||||
const stat = statSync(filepath);
|
||||
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
||||
cb(filepath);
|
||||
}
|
||||
watchedFiles[filepath] = stat;
|
||||
});
|
||||
}
|
||||
|
||||
/** Recursively apply form input defaults to a set of values */
|
||||
function applyFormInputDefaults(
|
||||
inputs: FormInput[],
|
||||
values: { [p: string]: JsonPrimitive | undefined },
|
||||
) {
|
||||
for (const input of inputs) {
|
||||
if ('inputs' in input) {
|
||||
applyFormInputDefaults(input.inputs ?? [], values);
|
||||
} else if ('defaultValue' in input && values[input.name] === undefined) {
|
||||
values[input.name] = input.defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src-tauri/Cargo.lock
generated
98
src-tauri/Cargo.lock
generated
@@ -514,6 +514,25 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
|
||||
dependencies = [
|
||||
"block-sys",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -2420,6 +2439,16 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icrate"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642"
|
||||
dependencies = [
|
||||
"block2 0.4.0",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -3292,7 +3321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-data",
|
||||
@@ -3308,7 +3337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation",
|
||||
@@ -3320,7 +3349,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -3332,7 +3361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -3343,7 +3372,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-metal",
|
||||
@@ -3355,7 +3384,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-contacts",
|
||||
"objc2-foundation",
|
||||
@@ -3374,7 +3403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"dispatch",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -3386,7 +3415,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
@@ -3399,11 +3428,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6788b04a18ea31e3dc3ab256b8546639e5bbae07c1a0dc4ea8615252bc6aee9a"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.2.2"
|
||||
@@ -3411,7 +3452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-metal",
|
||||
@@ -3434,7 +3475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
@@ -3454,7 +3495,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -3466,7 +3507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-core-location",
|
||||
"objc2-foundation",
|
||||
@@ -3479,7 +3520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
@@ -3513,9 +3554,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.66"
|
||||
version = "0.10.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
@@ -3611,6 +3652,20 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35366a452fce3f8947eb2f33226a133aaf0cacedef2af67ade348d58be7f85d0"
|
||||
dependencies = [
|
||||
"icrate",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pad"
|
||||
version = "0.1.6"
|
||||
@@ -4513,7 +4568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"gtk-sys",
|
||||
@@ -5980,17 +6035,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad3de2b9203bb00b9765e637a9878aaace34df40ae484878b8cea7a5bd5f9188"
|
||||
checksum = "ebf3da08c36fb03c98c76e5563d4e74d9a590df0f40978cbe07f39cb52833f7c"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs 5.0.1",
|
||||
"dirs 6.0.0",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"semver",
|
||||
@@ -7544,7 +7600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e644bf458e27b11b0ecafc9e5633d1304fdae82baca1d42185669752fe6ca4f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
"block2 0.5.1",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
|
||||
@@ -67,7 +67,7 @@ tauri-plugin-opener = "2.2.5"
|
||||
tauri-plugin-os = "2.2.0"
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-single-instance = "2.2.1"
|
||||
tauri-plugin-updater = "2.4.0"
|
||||
tauri-plugin-updater = "2.5.0"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
tokio = { version = "1.43.0", features = ["sync"] }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AnalyticsAction = "cancel" | "click" | "commit" | "create" | "delete" | "delete_many" | "duplicate" | "error" | "export" | "hide" | "import" | "launch" | "launch_first" | "launch_update" | "send" | "show" | "toggle" | "update" | "upsert";
|
||||
|
||||
export type AnalyticsResource = "app" | "appearance" | "button" | "checkbox" | "cookie_jar" | "dialog" | "environment" | "folder" | "grpc_connection" | "grpc_event" | "grpc_request" | "http_request" | "http_response" | "key_value" | "link" | "mutation" | "plugin" | "select" | "setting" | "sidebar" | "tab" | "theme" | "websocket_connection" | "websocket_event" | "websocket_request" | "workspace";
|
||||
@@ -1,247 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use log::{debug, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
|
||||
use ts_rs::TS;
|
||||
use yaak_models::queries::{
|
||||
generate_id, get_key_value_int, get_key_value_string, get_or_create_settings,
|
||||
set_key_value_int, set_key_value_string, UpdateSource,
|
||||
};
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
const NAMESPACE: &str = "analytics";
|
||||
const NUM_LAUNCHES_KEY: &str = "num_launches";
|
||||
|
||||
// serializable
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "analytics.ts")]
|
||||
pub enum AnalyticsResource {
|
||||
App,
|
||||
Appearance,
|
||||
Button,
|
||||
Checkbox,
|
||||
CookieJar,
|
||||
Dialog,
|
||||
Environment,
|
||||
Folder,
|
||||
GrpcConnection,
|
||||
GrpcEvent,
|
||||
GrpcRequest,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
KeyValue,
|
||||
Link,
|
||||
Mutation,
|
||||
Plugin,
|
||||
Select,
|
||||
Setting,
|
||||
Sidebar,
|
||||
Tab,
|
||||
Theme,
|
||||
WebsocketConnection,
|
||||
WebsocketEvent,
|
||||
WebsocketRequest,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
impl AnalyticsResource {
|
||||
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsResource> {
|
||||
serde_json::from_str(format!("\"{s}\"").as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AnalyticsResource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "analytics.ts")]
|
||||
pub enum AnalyticsAction {
|
||||
Cancel,
|
||||
Click,
|
||||
Commit,
|
||||
Create,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
Duplicate,
|
||||
Error,
|
||||
Export,
|
||||
Hide,
|
||||
Import,
|
||||
Launch,
|
||||
LaunchFirst,
|
||||
LaunchUpdate,
|
||||
Send,
|
||||
Show,
|
||||
Toggle,
|
||||
Update,
|
||||
Upsert,
|
||||
}
|
||||
|
||||
impl AnalyticsAction {
|
||||
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsAction> {
|
||||
serde_json::from_str(format!("\"{s}\"").as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AnalyticsAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LaunchEventInfo {
|
||||
pub current_version: String,
|
||||
pub previous_version: String,
|
||||
pub launched_after_update: bool,
|
||||
pub num_launches: i32,
|
||||
}
|
||||
|
||||
pub async fn track_launch_event<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
|
||||
let last_tracked_version_key = "last_tracked_version";
|
||||
|
||||
let mut info = LaunchEventInfo::default();
|
||||
|
||||
info.num_launches = get_num_launches(w).await + 1;
|
||||
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
|
||||
info.current_version = w.package_info().version.to_string();
|
||||
|
||||
if info.previous_version.is_empty() {
|
||||
track_event(w, AnalyticsResource::App, AnalyticsAction::LaunchFirst, None).await;
|
||||
} else {
|
||||
info.launched_after_update = info.current_version != info.previous_version;
|
||||
if info.launched_after_update {
|
||||
track_event(
|
||||
w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchUpdate,
|
||||
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
// Track a launch event in all cases
|
||||
track_event(
|
||||
w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Update key values
|
||||
|
||||
set_key_value_string(
|
||||
w,
|
||||
NAMESPACE,
|
||||
last_tracked_version_key,
|
||||
info.current_version.as_str(),
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
.await;
|
||||
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, &UpdateSource::Background)
|
||||
.await;
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub async fn track_event<R: Runtime>(
|
||||
w: &WebviewWindow<R>,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<Value>,
|
||||
) {
|
||||
let id = get_id(w).await;
|
||||
let event = format!("{}.{}", resource, action);
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = w.app_handle().package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let site = match is_dev() {
|
||||
true => "site_TkHWjoXwZPq3HfhERb",
|
||||
false => "site_zOK0d7jeBy2TLxFCnZ",
|
||||
};
|
||||
let base_url = match is_dev() {
|
||||
true => "http://localhost:7194",
|
||||
false => "https://t.yaak.app",
|
||||
};
|
||||
let params = vec![
|
||||
("u", id),
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", site.to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(w)),
|
||||
];
|
||||
let req =
|
||||
reqwest::Client::builder().build().unwrap().get(format!("{base_url}/t/e")).query(¶ms);
|
||||
|
||||
let settings = get_or_create_settings(w).await;
|
||||
if !settings.telemetry {
|
||||
info!("Track event (disabled): {}", event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable analytics actual sending in dev
|
||||
if is_dev() {
|
||||
debug!("Track event: {} {}", event, attributes_json);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = req.send().await {
|
||||
info!("Error sending analytics event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window_size<R: Runtime>(w: &WebviewWindow<R>) -> String {
|
||||
let current_monitor = match w.current_monitor() {
|
||||
Ok(Some(m)) => m,
|
||||
_ => return "unknown".to_string(),
|
||||
};
|
||||
|
||||
let scale_factor = current_monitor.scale_factor();
|
||||
let size = current_monitor.size();
|
||||
let width: f64 = size.width as f64 / scale_factor;
|
||||
let height: f64 = size.height as f64 / scale_factor;
|
||||
|
||||
format!("{}x{}", (width / 100.0).round() * 100.0, (height / 100.0).round() * 100.0)
|
||||
}
|
||||
|
||||
async fn get_id<R: Runtime>(w: &WebviewWindow<R>) -> String {
|
||||
let id = get_key_value_string(w, "analytics", "id", "").await;
|
||||
if id.is_empty() {
|
||||
let new_id = generate_id();
|
||||
set_key_value_string(w, "analytics", "id", new_id.as_str(), &UpdateSource::Background)
|
||||
.await;
|
||||
new_id
|
||||
} else {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
|
||||
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
|
||||
}
|
||||
64
src-tauri/src/history.rs
Normal file
64
src-tauri/src/history.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
|
||||
use yaak_models::queries::{
|
||||
get_key_value_int, get_key_value_string,
|
||||
set_key_value_int, set_key_value_string, UpdateSource,
|
||||
};
|
||||
|
||||
const NAMESPACE: &str = "analytics";
|
||||
const NUM_LAUNCHES_KEY: &str = "num_launches";
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LaunchEventInfo {
|
||||
pub current_version: String,
|
||||
pub previous_version: String,
|
||||
pub launched_after_update: bool,
|
||||
pub num_launches: i32,
|
||||
}
|
||||
|
||||
pub async fn store_launch_history<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
|
||||
let last_tracked_version_key = "last_tracked_version";
|
||||
|
||||
let mut info = LaunchEventInfo::default();
|
||||
|
||||
info.num_launches = get_num_launches(w).await + 1;
|
||||
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
|
||||
info.current_version = w.package_info().version.to_string();
|
||||
|
||||
if info.previous_version.is_empty() {
|
||||
} else {
|
||||
info.launched_after_update = info.current_version != info.previous_version;
|
||||
};
|
||||
|
||||
// Update key values
|
||||
|
||||
set_key_value_string(
|
||||
w,
|
||||
NAMESPACE,
|
||||
last_tracked_version_key,
|
||||
info.current_version.as_str(),
|
||||
&UpdateSource::Background,
|
||||
)
|
||||
.await;
|
||||
|
||||
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, &UpdateSource::Background)
|
||||
.await;
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
|
||||
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
extern crate core;
|
||||
#[cfg(target_os = "macos")]
|
||||
extern crate objc;
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||
use crate::encoding::read_response_body;
|
||||
use crate::grpc::metadata_to_map;
|
||||
use crate::http_request::send_http_request;
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::render::{render_grpc_request, render_template};
|
||||
use crate::updates::{UpdateMode, YaakUpdater};
|
||||
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
|
||||
use eventsource_client::{EventParser, SSE};
|
||||
use log::{debug, error, warn};
|
||||
use rand::random;
|
||||
use regex::Regex;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::path::PathBuf;
|
||||
@@ -30,8 +28,30 @@ use tokio::sync::Mutex;
|
||||
use tokio::task::block_in_place;
|
||||
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
||||
use yaak_grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
|
||||
use yaak_models::models::{CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue, ModelType, Plugin, Settings, WebsocketRequest, Workspace, WorkspaceMeta};
|
||||
use yaak_models::queries::{batch_upsert, cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, delete_all_grpc_connections, delete_all_grpc_connections_for_workspace, delete_all_http_responses_for_request, delete_all_http_responses_for_workspace, delete_all_websocket_connections_for_workspace, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_plugin, delete_workspace, duplicate_folder, duplicate_grpc_request, duplicate_http_request, ensure_base_environment, generate_model_id, get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace, get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_workspace, list_key_values_raw, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace, upsert_workspace_meta, BatchUpsertResult, UpdateSource};
|
||||
use yaak_models::models::{
|
||||
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
|
||||
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
|
||||
ModelType, Plugin, Settings, WebsocketRequest, Workspace, WorkspaceMeta,
|
||||
};
|
||||
use yaak_models::queries::{
|
||||
batch_upsert, cancel_pending_grpc_connections, cancel_pending_responses,
|
||||
create_default_http_response, delete_all_grpc_connections,
|
||||
delete_all_grpc_connections_for_workspace, delete_all_http_responses_for_request,
|
||||
delete_all_http_responses_for_workspace, delete_all_websocket_connections_for_workspace,
|
||||
delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection,
|
||||
delete_grpc_request, delete_http_request, delete_http_response, delete_plugin,
|
||||
delete_workspace, duplicate_folder, duplicate_grpc_request, duplicate_http_request,
|
||||
ensure_base_environment, generate_model_id, get_base_environment, get_cookie_jar,
|
||||
get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request,
|
||||
get_http_response, get_key_value_raw, get_or_create_settings, get_or_create_workspace_meta,
|
||||
get_plugin, get_workspace, get_workspace_export_resources, list_cookie_jars, list_environments,
|
||||
list_folders, list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests,
|
||||
list_http_requests, list_http_responses_for_workspace, list_key_values_raw, list_plugins,
|
||||
list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
|
||||
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
|
||||
upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
|
||||
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
|
||||
};
|
||||
use yaak_plugins::events::{
|
||||
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
|
||||
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
|
||||
@@ -44,9 +64,9 @@ use yaak_sse::sse::ServerSentEvent;
|
||||
use yaak_templates::format::format_json;
|
||||
use yaak_templates::{Parser, Tokens};
|
||||
|
||||
mod analytics;
|
||||
mod encoding;
|
||||
mod grpc;
|
||||
mod history;
|
||||
mod http_request;
|
||||
mod notifications;
|
||||
mod plugin_events;
|
||||
@@ -807,7 +827,7 @@ async fn cmd_import_data<R: Runtime>(
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
|
||||
let file_contents = file.as_str();
|
||||
let (import_result, plugin_name) =
|
||||
let import_result =
|
||||
plugin_manager.import_data(&window, file_contents).await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut id_map: BTreeMap<String, String> = BTreeMap::new();
|
||||
@@ -923,14 +943,6 @@ async fn cmd_import_data<R: Runtime>(
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
analytics::track_event(
|
||||
&window,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Import,
|
||||
Some(json!({ "plugin": plugin_name })),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(upserted)
|
||||
}
|
||||
|
||||
@@ -1007,17 +1019,9 @@ async fn cmd_curl_to_request<R: Runtime>(
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
workspace_id: &str,
|
||||
) -> Result<HttpRequest, String> {
|
||||
let (import_result, plugin_name) =
|
||||
let import_result =
|
||||
{ plugin_manager.import_data(&window, command).await.map_err(|e| e.to_string())? };
|
||||
|
||||
analytics::track_event(
|
||||
&window,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Import,
|
||||
Some(json!({ "plugin": plugin_name })),
|
||||
)
|
||||
.await;
|
||||
|
||||
import_result.resources.http_requests.get(0).ok_or("No curl command found".to_string()).map(
|
||||
|r| {
|
||||
let mut request = r.clone();
|
||||
@@ -1051,8 +1055,6 @@ async fn cmd_export_data(
|
||||
|
||||
f.sync_all().expect("Failed to sync");
|
||||
|
||||
analytics::track_event(&window, AnalyticsResource::App, AnalyticsAction::Export, None).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1131,28 +1133,6 @@ async fn response_err<R: Runtime>(
|
||||
response
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_track_event(
|
||||
window: WebviewWindow,
|
||||
resource: &str,
|
||||
action: &str,
|
||||
attributes: Option<Value>,
|
||||
) -> Result<(), String> {
|
||||
match (AnalyticsResource::from_str(resource), AnalyticsAction::from_str(action)) {
|
||||
(Ok(resource), Ok(action)) => {
|
||||
analytics::track_event(&window, resource, action, attributes).await;
|
||||
}
|
||||
(r, a) => {
|
||||
error!(
|
||||
"Invalid action/resource for track_event: {resource}.{action} = {:?}.{:?}",
|
||||
r, a
|
||||
);
|
||||
return Err("Invalid analytics event".to_string());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_set_update_mode(update_mode: &str, w: WebviewWindow) -> Result<KeyValue, String> {
|
||||
cmd_set_key_value("app", "update_mode", update_mode, w).await.map_err(|e| e.to_string())
|
||||
@@ -1678,8 +1658,8 @@ async fn cmd_new_child_window(
|
||||
url,
|
||||
inner_size: Some(inner_size),
|
||||
position: Some(position),
|
||||
navigation_tx: None,
|
||||
hide_titlebar: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let child_window = window::create_window(&app_handle, config);
|
||||
@@ -1739,7 +1719,12 @@ async fn cmd_check_for_updates(
|
||||
yaak_updater: State<'_, Mutex<YaakUpdater>>,
|
||||
) -> Result<bool, String> {
|
||||
let update_mode = get_update_mode(&app_handle).await;
|
||||
yaak_updater.lock().await.force_check(&app_handle, update_mode).await.map_err(|e| e.to_string())
|
||||
yaak_updater
|
||||
.lock()
|
||||
.await
|
||||
.check_now(&app_handle, update_mode, UpdateTrigger::User)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@@ -1918,7 +1903,6 @@ pub fn run() {
|
||||
cmd_set_update_mode,
|
||||
cmd_template_functions,
|
||||
cmd_template_tokens_to_string,
|
||||
cmd_track_event,
|
||||
cmd_uninstall_plugin,
|
||||
cmd_update_cookie_jar,
|
||||
cmd_update_environment,
|
||||
@@ -1941,7 +1925,7 @@ pub fn run() {
|
||||
RunEvent::Ready => {
|
||||
let w = create_main_window(app_handle, "/");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&w).await;
|
||||
let info = history::store_launch_history(&w).await;
|
||||
debug!("Launched Yaak {:?}", info);
|
||||
});
|
||||
|
||||
@@ -1961,7 +1945,7 @@ pub fn run() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let val: State<'_, Mutex<YaakUpdater>> = h.state();
|
||||
let update_mode = get_update_mode(&h).await;
|
||||
if let Err(e) = val.lock().await.check(&h, update_mode).await {
|
||||
if let Err(e) = val.lock().await.maybe_check(&h, update_mode).await {
|
||||
warn!("Failed to check for updates {e:?}");
|
||||
};
|
||||
});
|
||||
@@ -2030,8 +2014,8 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
100.0 + random::<f64>() * 20.0,
|
||||
100.0 + random::<f64>() * 20.0,
|
||||
)),
|
||||
navigation_tx: None,
|
||||
hide_titlebar: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
window::create_window(handle, config)
|
||||
@@ -2070,7 +2054,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
// We might have recursive back-and-forth calls between app and plugin, so we don't
|
||||
// want to block here
|
||||
tauri::async_runtime::spawn(async move {
|
||||
crate::plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
|
||||
plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
|
||||
});
|
||||
}
|
||||
plugin_manager.unsubscribe(rx_id.as_str()).await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::analytics::{get_num_launches, get_os};
|
||||
use crate::history::{get_num_launches, get_os};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use log::debug;
|
||||
use reqwest::Method;
|
||||
|
||||
@@ -30,7 +30,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
event: &InternalEvent,
|
||||
plugin_handle: &PluginHandle,
|
||||
) {
|
||||
// info!("Got event to app {}", event.id);
|
||||
// debug!("Got event to app {event:?}");
|
||||
let window_context = event.window_context.to_owned();
|
||||
let response_event: Option<InternalEventPayload> = match event.clone().payload {
|
||||
InternalEventPayload::CopyTextRequest(req) => {
|
||||
@@ -204,32 +204,55 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||
}
|
||||
InternalEventPayload::OpenWindowRequest(req) => {
|
||||
let label = req.label;
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(128);
|
||||
let (navigation_tx, mut navigation_rx) = tokio::sync::mpsc::channel(128);
|
||||
let (close_tx, mut close_rx) = tokio::sync::mpsc::channel(128);
|
||||
let win_config = CreateWindowConfig {
|
||||
url: &req.url,
|
||||
label: &label.clone(),
|
||||
title: &req.title.unwrap_or_default(),
|
||||
navigation_tx: Some(tx),
|
||||
navigation_tx: Some(navigation_tx),
|
||||
close_tx: Some(close_tx),
|
||||
inner_size: req.size.map(|s| (s.width, s.height)),
|
||||
position: None,
|
||||
hide_titlebar: false,
|
||||
data_dir_key: req.data_dir_key,
|
||||
..Default::default()
|
||||
};
|
||||
create_window(app_handle, win_config);
|
||||
|
||||
let event_id = event.id.clone();
|
||||
let plugin_handle = plugin_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(url) = rx.recv().await {
|
||||
let label = label.clone();
|
||||
let url = url.to_string();
|
||||
let event_to_send = plugin_handle.build_event_to_send(
|
||||
&WindowContext::Label { label },
|
||||
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
|
||||
Some(event_id.clone()),
|
||||
);
|
||||
plugin_handle.send(&event_to_send).await.unwrap();
|
||||
}
|
||||
});
|
||||
{
|
||||
let event_id = event.id.clone();
|
||||
let plugin_handle = plugin_handle.clone();
|
||||
let label = label.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(url) = navigation_rx.recv().await {
|
||||
let url = url.to_string();
|
||||
let label = label.clone();
|
||||
let event_to_send = plugin_handle.build_event_to_send(
|
||||
&WindowContext::Label { label },
|
||||
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
|
||||
Some(event_id.clone()),
|
||||
);
|
||||
plugin_handle.send(&event_to_send).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let event_id = event.id.clone();
|
||||
let plugin_handle = plugin_handle.clone();
|
||||
let label = label.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(_) = close_rx.recv().await {
|
||||
let label = label.clone();
|
||||
let event_to_send = plugin_handle.build_event_to_send(
|
||||
&WindowContext::Label { label },
|
||||
&InternalEventPayload::WindowCloseEvent,
|
||||
Some(event_id.clone()),
|
||||
);
|
||||
plugin_handle.send(&event_to_send).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
InternalEventPayload::CloseWindowRequest(req) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use tokio::task::block_in_place;
|
||||
use yaak_models::queries::get_or_create_settings;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
|
||||
use crate::is_dev;
|
||||
@@ -46,6 +47,11 @@ impl UpdateMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UpdateTrigger {
|
||||
Background,
|
||||
User,
|
||||
}
|
||||
|
||||
impl YaakUpdater {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -53,11 +59,14 @@ impl YaakUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn force_check(
|
||||
pub async fn check_now(
|
||||
&mut self,
|
||||
app_handle: &AppHandle,
|
||||
mode: UpdateMode,
|
||||
update_trigger: UpdateTrigger,
|
||||
) -> Result<bool, tauri_plugin_updater::Error> {
|
||||
let settings = get_or_create_settings(app_handle).await;
|
||||
let update_key = format!("{:x}", md5::compute(settings.id));
|
||||
self.last_update_check = SystemTime::now();
|
||||
|
||||
info!("Checking for updates mode={}", mode);
|
||||
@@ -79,6 +88,14 @@ impl YaakUpdater {
|
||||
});
|
||||
})
|
||||
.header("X-Update-Mode", mode.to_string())?
|
||||
.header("X-Update-Key", update_key)?
|
||||
.header(
|
||||
"X-Update-Trigger",
|
||||
match update_trigger {
|
||||
UpdateTrigger::Background => "background",
|
||||
UpdateTrigger::User => "user",
|
||||
},
|
||||
)?
|
||||
.build()?
|
||||
.check()
|
||||
.await;
|
||||
@@ -129,7 +146,7 @@ impl YaakUpdater {
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
pub async fn check(
|
||||
pub async fn maybe_check(
|
||||
&mut self,
|
||||
app_handle: &AppHandle,
|
||||
mode: UpdateMode,
|
||||
@@ -150,6 +167,6 @@ impl YaakUpdater {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.force_check(app_handle, mode).await
|
||||
self.check_now(app_handle, mode, UpdateTrigger::Background).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_
|
||||
use log::{info, warn};
|
||||
use std::process::exit;
|
||||
use tauri::{
|
||||
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow,
|
||||
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow, WindowEvent,
|
||||
};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -16,6 +16,8 @@ pub(crate) struct CreateWindowConfig<'s> {
|
||||
pub inner_size: Option<(f64, f64)>,
|
||||
pub position: Option<(f64, f64)>,
|
||||
pub navigation_tx: Option<mpsc::Sender<String>>,
|
||||
pub close_tx: Option<mpsc::Sender<()>>,
|
||||
pub data_dir_key: Option<String>,
|
||||
pub hide_titlebar: bool,
|
||||
}
|
||||
|
||||
@@ -41,6 +43,22 @@ pub(crate) fn create_window<R: Runtime>(
|
||||
.disable_drag_drop_handler() // Required for frontend Dnd on windows
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
|
||||
|
||||
if let Some(key) = config.data_dir_key {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
use std::fs;
|
||||
let dir = handle.path().temp_dir().unwrap().join("yaak_sessions").join(key);
|
||||
fs::create_dir_all(dir.clone()).unwrap();
|
||||
win_builder = win_builder.data_directory(dir);
|
||||
}
|
||||
|
||||
// macOS doesn't support data dir so must use this fn instead
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
win_builder = win_builder.data_store_identifier(to_fixed_hash(&key));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((w, h)) = config.inner_size {
|
||||
win_builder = win_builder.inner_size(w, h);
|
||||
} else {
|
||||
@@ -85,6 +103,18 @@ pub(crate) fn create_window<R: Runtime>(
|
||||
|
||||
let win = win_builder.build().unwrap();
|
||||
|
||||
if let Some(tx) = config.close_tx {
|
||||
win.on_window_event(move |event| match event {
|
||||
WindowEvent::CloseRequested { .. } => {
|
||||
let tx = tx.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
tx.send(()).await.unwrap();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
let webview_window = win.clone();
|
||||
win.on_menu_event(move |w, event| {
|
||||
if !w.is_focused().unwrap() {
|
||||
@@ -128,3 +158,10 @@ pub(crate) fn create_window<R: Runtime>(
|
||||
|
||||
win
|
||||
}
|
||||
|
||||
fn to_fixed_hash(s: &str) -> [u8; 16] {
|
||||
let hash = md5::compute(s.as_bytes());
|
||||
let mut fixed = [0u8; 16];
|
||||
fixed.copy_from_slice(&hash[..16]); // Take the first 16 bytes of the hash
|
||||
fixed
|
||||
}
|
||||
|
||||
@@ -102,9 +102,20 @@ async function getToken(ctx, contextId) {
|
||||
async function deleteToken(ctx, contextId) {
|
||||
return ctx.store.delete(tokenStoreKey(contextId));
|
||||
}
|
||||
async function resetDataDirKey(ctx, contextId) {
|
||||
const key = (/* @__PURE__ */ new Date()).toISOString();
|
||||
return ctx.store.set(dataDirStoreKey(contextId), key);
|
||||
}
|
||||
async function getDataDirKey(ctx, contextId) {
|
||||
const key = await ctx.store.get(dataDirStoreKey(contextId)) ?? "default";
|
||||
return `${contextId}::${key}`;
|
||||
}
|
||||
function tokenStoreKey(context_id) {
|
||||
return ["token", context_id].join("::");
|
||||
}
|
||||
function dataDirStoreKey(context_id) {
|
||||
return ["data_dir", context_id].join("::");
|
||||
}
|
||||
|
||||
// src/getOrRefreshAccessToken.ts
|
||||
async function getOrRefreshAccessToken(ctx, contextId, {
|
||||
@@ -218,9 +229,16 @@ async function getAuthorizationCode(ctx, contextId, {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const authorizationUrlStr = authorizationUrl.toString();
|
||||
console.log("Authorizing", authorizationUrlStr);
|
||||
let foundCode = false;
|
||||
let { close } = await ctx.window.openUrl({
|
||||
url: authorizationUrlStr,
|
||||
label: "oauth-authorization-url",
|
||||
dataDirKey: await getDataDirKey(ctx, contextId),
|
||||
async onClose() {
|
||||
if (!foundCode) {
|
||||
reject(new Error("Authorization window closed"));
|
||||
}
|
||||
},
|
||||
async onNavigate({ url: urlStr }) {
|
||||
const url = new URL(urlStr);
|
||||
if (url.searchParams.has("error")) {
|
||||
@@ -230,6 +248,7 @@ async function getAuthorizationCode(ctx, contextId, {
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
foundCode = true;
|
||||
close();
|
||||
const response = await getAccessToken(ctx, {
|
||||
grantType: "authorization_code",
|
||||
@@ -428,7 +447,6 @@ var plugin = {
|
||||
actions: [
|
||||
{
|
||||
label: "Copy Current Token",
|
||||
icon: "copy",
|
||||
async onSelect(ctx, { contextId }) {
|
||||
const token = await getToken(ctx, contextId);
|
||||
if (token == null) {
|
||||
@@ -441,7 +459,6 @@ var plugin = {
|
||||
},
|
||||
{
|
||||
label: "Delete Token",
|
||||
icon: "trash",
|
||||
async onSelect(ctx, { contextId }) {
|
||||
if (await deleteToken(ctx, contextId)) {
|
||||
await ctx.toast.show({ message: "Token deleted", color: "success" });
|
||||
@@ -449,6 +466,12 @@ var plugin = {
|
||||
await ctx.toast.show({ message: "No token to delete", color: "warning" });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Clear Window Session",
|
||||
async onSelect(ctx, { contextId }) {
|
||||
await resetDataDirKey(ctx, contextId);
|
||||
}
|
||||
}
|
||||
],
|
||||
args: [
|
||||
@@ -461,17 +484,24 @@ var plugin = {
|
||||
options: grantTypes
|
||||
},
|
||||
// Always-present fields
|
||||
{ type: "text", name: "clientId", label: "Client ID" },
|
||||
{
|
||||
type: "text",
|
||||
name: "clientId",
|
||||
label: "Client ID",
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "clientSecret",
|
||||
label: "Client Secret",
|
||||
optional: true,
|
||||
password: true,
|
||||
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
name: "authorizationUrl",
|
||||
optional: true,
|
||||
label: "Authorization URL",
|
||||
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
|
||||
placeholder: authorizationUrls[0],
|
||||
@@ -480,6 +510,7 @@ var plugin = {
|
||||
{
|
||||
type: "text",
|
||||
name: "accessTokenUrl",
|
||||
optional: true,
|
||||
label: "Access Token URL",
|
||||
placeholder: accessTokenUrls[0],
|
||||
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
|
||||
@@ -590,55 +621,55 @@ var plugin = {
|
||||
}
|
||||
],
|
||||
async onApply(ctx, { values, contextId }) {
|
||||
const headerPrefix = optionalString(values, "headerPrefix") ?? "";
|
||||
const grantType = requiredString(values, "grantType");
|
||||
const headerPrefix = stringArg(values, "headerPrefix");
|
||||
const grantType = stringArg(values, "grantType");
|
||||
const credentialsInBody = values.credentials === "body";
|
||||
let token;
|
||||
if (grantType === "authorization_code") {
|
||||
const authorizationUrl = requiredString(values, "authorizationUrl");
|
||||
const accessTokenUrl = requiredString(values, "accessTokenUrl");
|
||||
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getAuthorizationCode(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
||||
clientId: requiredString(values, "clientId"),
|
||||
clientSecret: requiredString(values, "clientSecret"),
|
||||
redirectUri: optionalString(values, "redirectUri"),
|
||||
scope: optionalString(values, "scope"),
|
||||
state: optionalString(values, "state"),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
state: stringArgOrNull(values, "state"),
|
||||
credentialsInBody,
|
||||
pkce: values.usePkce ? {
|
||||
challengeMethod: requiredString(values, "pkceChallengeMethod"),
|
||||
codeVerifier: optionalString(values, "pkceCodeVerifier")
|
||||
challengeMethod: stringArg(values, "pkceChallengeMethod"),
|
||||
codeVerifier: stringArgOrNull(values, "pkceCodeVerifier")
|
||||
} : null
|
||||
});
|
||||
} else if (grantType === "implicit") {
|
||||
const authorizationUrl = requiredString(values, "authorizationUrl");
|
||||
const authorizationUrl = stringArg(values, "authorizationUrl");
|
||||
token = await getImplicit(ctx, contextId, {
|
||||
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
|
||||
clientId: requiredString(values, "clientId"),
|
||||
redirectUri: optionalString(values, "redirectUri"),
|
||||
responseType: requiredString(values, "responseType"),
|
||||
scope: optionalString(values, "scope"),
|
||||
state: optionalString(values, "state")
|
||||
clientId: stringArg(values, "clientId"),
|
||||
redirectUri: stringArgOrNull(values, "redirectUri"),
|
||||
responseType: stringArg(values, "responseType"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
state: stringArgOrNull(values, "state")
|
||||
});
|
||||
} else if (grantType === "client_credentials") {
|
||||
const accessTokenUrl = requiredString(values, "accessTokenUrl");
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getClientCredentials(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||
clientId: requiredString(values, "clientId"),
|
||||
clientSecret: requiredString(values, "clientSecret"),
|
||||
scope: optionalString(values, "scope"),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
credentialsInBody
|
||||
});
|
||||
} else if (grantType === "password") {
|
||||
const accessTokenUrl = requiredString(values, "accessTokenUrl");
|
||||
const accessTokenUrl = stringArg(values, "accessTokenUrl");
|
||||
token = await getPassword(ctx, contextId, {
|
||||
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
|
||||
clientId: requiredString(values, "clientId"),
|
||||
clientSecret: requiredString(values, "clientSecret"),
|
||||
username: requiredString(values, "username"),
|
||||
password: requiredString(values, "password"),
|
||||
scope: optionalString(values, "scope"),
|
||||
clientId: stringArg(values, "clientId"),
|
||||
clientSecret: stringArg(values, "clientSecret"),
|
||||
username: stringArg(values, "username"),
|
||||
password: stringArg(values, "password"),
|
||||
scope: stringArgOrNull(values, "scope"),
|
||||
credentialsInBody
|
||||
});
|
||||
} else {
|
||||
@@ -654,14 +685,14 @@ var plugin = {
|
||||
}
|
||||
}
|
||||
};
|
||||
function optionalString(values, name) {
|
||||
function stringArgOrNull(values, name) {
|
||||
const arg = values[name];
|
||||
if (arg == null || arg == "") return null;
|
||||
return `${arg}`;
|
||||
}
|
||||
function requiredString(values, name) {
|
||||
const arg = optionalString(values, name);
|
||||
if (!arg) throw new Error(`Missing required argument ${name}`);
|
||||
function stringArg(values, name) {
|
||||
const arg = stringArgOrNull(values, name);
|
||||
if (!arg) return "";
|
||||
return arg;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SyncModel } from "./gen_models";
|
||||
import type { SyncModel } from "./gen_models.js";
|
||||
|
||||
export type GitAuthor = { name: string | null, email: string | null, };
|
||||
|
||||
|
||||
@@ -51,6 +51,14 @@ export function useGit(dir: string) {
|
||||
mutationFn: (args) => invoke('plugin:yaak-git|commit', { dir, ...args }),
|
||||
onSuccess,
|
||||
}),
|
||||
commitAndPush: useMutation<PushResult, string, { message: string }>({
|
||||
mutationKey: ['git', 'commitpush', dir],
|
||||
mutationFn: async (args) => {
|
||||
await invoke('plugin:yaak-git|commit', { dir, ...args });
|
||||
return invoke('plugin:yaak-git|push', { dir });
|
||||
},
|
||||
onSuccess,
|
||||
}),
|
||||
fetchAll: useMutation<string, string, void>({
|
||||
mutationKey: ['git', 'checkout', dir],
|
||||
mutationFn: () => invoke('plugin:yaak-git|fetch_all', { dir }),
|
||||
|
||||
@@ -26,6 +26,7 @@ export function useLicense() {
|
||||
|
||||
const CHECK_QUERY_KEY = ['license.check'];
|
||||
const check = useQuery<void, string, LicenseCheckStatus>({
|
||||
refetchInterval: 1000 * 60 * 60 * 12, // Refetch every 12 hours
|
||||
queryKey: CHECK_QUERY_KEY,
|
||||
queryFn: () => invoke('plugin:yaak-license|check'),
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
|
||||
|
||||
export type ProxySettingAuth = { user: string, password: string, };
|
||||
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, telemetry: boolean, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
|
||||
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
|
||||
|
||||
export type SyncHistory = { model: "sync_history", id: string, workspaceId: string, createdAt: string, states: Array<SyncState>, checksum: string, relPath: string, syncDir: string, };
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ pub struct Settings {
|
||||
pub interface_scale: f32,
|
||||
pub open_workspace_new_window: Option<bool>,
|
||||
pub proxy: Option<ProxySetting>,
|
||||
pub telemetry: bool,
|
||||
pub theme: String,
|
||||
pub theme_dark: String,
|
||||
pub theme_light: String,
|
||||
@@ -112,7 +111,6 @@ pub enum SettingsIden {
|
||||
InterfaceScale,
|
||||
OpenWorkspaceNewWindow,
|
||||
Proxy,
|
||||
Telemetry,
|
||||
Theme,
|
||||
ThemeDark,
|
||||
ThemeLight,
|
||||
@@ -138,7 +136,6 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
|
||||
interface_scale: r.get("interface_scale")?,
|
||||
open_workspace_new_window: r.get("open_workspace_new_window")?,
|
||||
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
|
||||
telemetry: r.get("telemetry")?,
|
||||
theme: r.get("theme")?,
|
||||
theme_dark: r.get("theme_dark")?,
|
||||
theme_light: r.get("theme_light")?,
|
||||
|
||||
@@ -1478,7 +1478,6 @@ pub async fn update_settings<R: Runtime>(
|
||||
(SettingsIden::EditorFontSize, settings.editor_font_size.into()),
|
||||
(SettingsIden::EditorKeymap, settings.editor_keymap.to_string().into()),
|
||||
(SettingsIden::EditorSoftWrap, settings.editor_soft_wrap.into()),
|
||||
(SettingsIden::Telemetry, settings.telemetry.into()),
|
||||
(SettingsIden::OpenWorkspaceNewWindow, settings.open_workspace_new_window.into()),
|
||||
(
|
||||
SettingsIden::Proxy,
|
||||
|
||||
@@ -346,7 +346,7 @@ export type ImportResponse = { resources: ImportResources, };
|
||||
|
||||
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
|
||||
@@ -354,7 +354,7 @@ export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* Label for the window. If not provided, a random one will be generated.
|
||||
*/
|
||||
label: string, title?: string, size?: WindowSize, };
|
||||
label: string, title?: string, size?: WindowSize, dataDirKey?: string, };
|
||||
|
||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::collections::HashMap;
|
||||
use tauri::{Runtime, WebviewWindow};
|
||||
use ts_rs::TS;
|
||||
|
||||
use yaak_models::models::{Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace};
|
||||
use yaak_models::models::{
|
||||
Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -108,6 +110,7 @@ pub enum InternalEventPayload {
|
||||
|
||||
OpenWindowRequest(OpenWindowRequest),
|
||||
WindowNavigateEvent(WindowNavigateEvent),
|
||||
WindowCloseEvent,
|
||||
CloseWindowRequest(CloseWindowRequest),
|
||||
|
||||
TemplateRenderRequest(TemplateRenderRequest),
|
||||
@@ -262,10 +265,15 @@ pub struct OpenWindowRequest {
|
||||
pub url: String,
|
||||
/// Label for the window. If not provided, a random one will be generated.
|
||||
pub label: String,
|
||||
|
||||
#[ts(optional)]
|
||||
pub title: Option<String>,
|
||||
|
||||
#[ts(optional)]
|
||||
pub size: Option<WindowSize>,
|
||||
|
||||
#[ts(optional)]
|
||||
pub data_dir_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -206,7 +206,7 @@ impl PluginManager {
|
||||
|
||||
// Boot the plugin
|
||||
let event = timeout(
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(2),
|
||||
self.send_to_plugin_and_wait(
|
||||
window_context,
|
||||
&plugin_handle,
|
||||
@@ -559,7 +559,7 @@ impl PluginManager {
|
||||
Some(JsonPrimitive::Boolean(v)) => v.clone(),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
||||
// Auth is disabled, so don't do anything
|
||||
if disabled {
|
||||
info!("Not applying disabled auth {:?}", auth_name);
|
||||
@@ -623,7 +623,7 @@ impl PluginManager {
|
||||
&self,
|
||||
window: &WebviewWindow<R>,
|
||||
content: &str,
|
||||
) -> Result<(ImportResponse, String)> {
|
||||
) -> Result<ImportResponse> {
|
||||
let reply_events = self
|
||||
.send_and_wait(
|
||||
&WindowContext::from_window(window),
|
||||
@@ -635,19 +635,13 @@ impl PluginManager {
|
||||
|
||||
// TODO: Don't just return the first valid response
|
||||
let result = reply_events.into_iter().find_map(|e| match e.payload {
|
||||
InternalEventPayload::ImportResponse(resp) => Some((resp, e.plugin_ref_id)),
|
||||
InternalEventPayload::ImportResponse(resp) => Some(resp),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
match result {
|
||||
None => Err(PluginErr("No importers found for file contents".to_string())),
|
||||
Some((resp, ref_id)) => {
|
||||
let plugin = self
|
||||
.get_plugin_by_ref_id(ref_id.as_str())
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(ref_id))?;
|
||||
Ok((resp, plugin.info().await.name))
|
||||
}
|
||||
Some(resp) => Ok(resp),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ impl PluginHandle {
|
||||
}
|
||||
|
||||
pub async fn set_boot_response(&self, resp: &BootResponse) {
|
||||
info!("BOOTED PLUGIN {:?}", resp);
|
||||
let mut boot_resp = self.boot_resp.lock().await;
|
||||
*boot_resp = resp.clone();
|
||||
}
|
||||
|
||||
@@ -77,5 +77,7 @@ function removeWatchKey(key: string) {
|
||||
|
||||
// On page load, unlisten to all zombie watchers
|
||||
const keys = getWatchKeys();
|
||||
console.log('Unsubscribing to zombie file watchers', keys);
|
||||
keys.forEach(unlistenToWatcher);
|
||||
if (keys.length > 0) {
|
||||
console.log('Unsubscribing to zombie file watchers', keys);
|
||||
keys.forEach(unlistenToWatcher);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::error::Result;
|
||||
use crate::models::SyncModel;
|
||||
use chrono::Utc;
|
||||
use log::{debug, warn};
|
||||
use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
@@ -164,6 +164,13 @@ pub(crate) async fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
|
||||
|
||||
let path = dir_entry.path();
|
||||
let (model, checksum) = match SyncModel::from_file(&path) {
|
||||
// TODO: Remove this once we have logic to handle environments. This it to clean
|
||||
// any existing ones from the sync dir that resulted from the 2025.1 betas.
|
||||
Ok(Some((SyncModel::Environment(e), _))) => {
|
||||
fs::remove_file(path).await?;
|
||||
info!("Cleaned up synced environment {}", e.id);
|
||||
continue;
|
||||
}
|
||||
Ok(Some(m)) => m,
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
@@ -205,9 +212,17 @@ pub(crate) fn compute_sync_ops(
|
||||
let op = match (db_map.get(k), fs_map.get(k)) {
|
||||
(None, None) => return None, // Can never happen
|
||||
(None, Some(fs)) => SyncOp::DbCreate { fs: fs.to_owned() },
|
||||
(Some(DbCandidate::Unmodified(model, sync_state)), None) => SyncOp::DbDelete {
|
||||
model: model.to_owned(),
|
||||
state: sync_state.to_owned(),
|
||||
(Some(DbCandidate::Unmodified(model, sync_state)), None) => {
|
||||
// TODO: Remove this once we have logic to handle environments. This it to
|
||||
// ignore the cleaning we did above of any environments that were written
|
||||
// to disk in the 2025.1 betas.
|
||||
if let SyncModel::Environment(_) = model {
|
||||
return None;
|
||||
}
|
||||
SyncOp::DbDelete {
|
||||
model: model.to_owned(),
|
||||
state: sync_state.to_owned(),
|
||||
}
|
||||
},
|
||||
(Some(DbCandidate::Modified(model, sync_state)), None) => SyncOp::FsUpdate {
|
||||
model: model.to_owned(),
|
||||
@@ -318,7 +333,7 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
);
|
||||
let mut sync_state_ops = Vec::new();
|
||||
let mut workspaces_to_upsert = Vec::new();
|
||||
let mut environments_to_upsert = Vec::new();
|
||||
let environments_to_upsert = Vec::new();
|
||||
let mut folders_to_upsert = Vec::new();
|
||||
let mut http_requests_to_upsert = Vec::new();
|
||||
let mut grpc_requests_to_upsert = Vec::new();
|
||||
@@ -380,11 +395,13 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
// batch upsert to make foreign keys happy
|
||||
match fs.model {
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
SyncModel::Environment(m) => environments_to_upsert.push(m),
|
||||
SyncModel::Folder(m) => folders_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
|
||||
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
|
||||
|
||||
// TODO: Handle environments in sync
|
||||
SyncModel::Environment(_) => {}
|
||||
};
|
||||
SyncStateOp::Create {
|
||||
model_id,
|
||||
@@ -397,11 +414,13 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
|
||||
// batch upsert to make foreign keys happy
|
||||
match fs.model {
|
||||
SyncModel::Workspace(m) => workspaces_to_upsert.push(m),
|
||||
SyncModel::Environment(m) => environments_to_upsert.push(m),
|
||||
SyncModel::Folder(m) => folders_to_upsert.push(m),
|
||||
SyncModel::HttpRequest(m) => http_requests_to_upsert.push(m),
|
||||
SyncModel::GrpcRequest(m) => grpc_requests_to_upsert.push(m),
|
||||
SyncModel::WebsocketRequest(m) => websocket_requests_to_upsert.push(m),
|
||||
|
||||
// TODO: Handle environments in sync
|
||||
SyncModel::Environment(_) => {}
|
||||
}
|
||||
SyncStateOp::Update {
|
||||
state: state.to_owned(),
|
||||
|
||||
@@ -5,7 +5,6 @@ import { InlineCode } from '../components/core/InlineCode';
|
||||
import { VStack } from '../components/core/Stacks';
|
||||
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirm } from '../lib/confirm';
|
||||
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
|
||||
import { pluralizeCount } from '../lib/pluralize';
|
||||
@@ -42,7 +41,6 @@ export const createFolder = createFastMutation<
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
return invokeCmd<Folder>('cmd_update_folder', { folder: { workspaceId, ...patch } });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
});
|
||||
|
||||
export const syncWorkspace = createFastMutation<
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { WebsocketConnection } from '@yaakapp-internal/models';
|
||||
import { deleteWebsocketConnection as cmdDeleteWebsocketConnection } from '@yaakapp-internal/ws';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
|
||||
export const deleteWebsocketConnection = createFastMutation({
|
||||
mutationKey: ['delete_websocket_connection'],
|
||||
mutationFn: async function (connection: WebsocketConnection) {
|
||||
return cmdDeleteWebsocketConnection(connection.id);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
trackEvent('websocket_connection', 'delete');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { deleteWebsocketConnections as cmdDeleteWebsocketConnections } from '@yaakapp-internal/ws';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
|
||||
export const deleteWebsocketConnections = createFastMutation({
|
||||
mutationKey: ['delete_websocket_connections'],
|
||||
mutationFn: async function (request: WebsocketRequest) {
|
||||
return cmdDeleteWebsocketConnections(request.id);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
trackEvent('websocket_connection', 'delete_many');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { deleteWebsocketRequest as cmdDeleteWebsocketRequest } from '@yaakapp-internal/ws';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirmDelete } from '../lib/confirm';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
|
||||
@@ -24,7 +23,4 @@ export const deleteWebsocketRequest = createFastMutation({
|
||||
|
||||
return cmdDeleteWebsocketRequest(request.id);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
trackEvent('websocket_request', 'delete');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { duplicateWebsocketRequest as cmdDuplicateWebsocketRequest } from '@yaakapp-internal/ws';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { router } from '../lib/router';
|
||||
|
||||
export const duplicateWebsocketRequest = createFastMutation({
|
||||
mutationKey: ['delete_websocket_connection'],
|
||||
mutationFn: async function (request: WebsocketRequest) {
|
||||
return cmdDuplicateWebsocketRequest(request.id);
|
||||
mutationFn: async function (requestId: string) {
|
||||
return cmdDuplicateWebsocketRequest(requestId);
|
||||
},
|
||||
onSuccess: async (request) => {
|
||||
trackEvent('websocket_request', 'duplicate');
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
params: { workspaceId: request.workspaceId },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SettingsTab } from '../components/Settings/SettingsTab';
|
||||
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { router } from '../lib/router';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
@@ -11,7 +10,6 @@ export const openSettings = createFastMutation<void, string, SettingsTab | null>
|
||||
const workspaceId = getActiveWorkspaceId();
|
||||
if (workspaceId == null) return;
|
||||
|
||||
trackEvent('dialog', 'show', { id: 'settings', tab: `${tab}` });
|
||||
const location = router.buildLocation({
|
||||
to: '/workspaces/$workspaceId/settings',
|
||||
params: { workspaceId },
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { applySync, calculateSyncFsOnly } from '@yaakapp-internal/sync';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { showSimpleAlert } from '../lib/alert';
|
||||
import { router } from '../lib/router';
|
||||
|
||||
export const openWorkspaceFromSyncDir = createFastMutation<void>({
|
||||
export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
|
||||
mutationKey: [],
|
||||
mutationFn: async () => {
|
||||
const dir = await open({
|
||||
title: 'Select Workspace Directory',
|
||||
directory: true,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
if (dir == null) return;
|
||||
|
||||
mutationFn: async (dir) => {
|
||||
const ops = await calculateSyncFsOnly(dir);
|
||||
|
||||
const workspace = ops
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { upsertWebsocketRequest as cmdUpsertWebsocketRequest } from '@yaakapp-internal/ws';
|
||||
import { differenceInMilliseconds } from 'date-fns';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { router } from '../lib/router';
|
||||
|
||||
export const upsertWebsocketRequest = createFastMutation<
|
||||
@@ -16,12 +15,11 @@ export const upsertWebsocketRequest = createFastMutation<
|
||||
const isNew = differenceInMilliseconds(new Date(), request.createdAt + 'Z') < 100;
|
||||
|
||||
if (isNew) {
|
||||
trackEvent('websocket_request', 'create');
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
params: { workspaceId: request.workspaceId },
|
||||
search: (prev) => ({ ...prev, request_id: request.id }),
|
||||
});
|
||||
} else trackEvent('websocket_request', 'update');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { differenceInMilliseconds } from 'date-fns';
|
||||
import { createFastMutation } from '../hooks/useFastMutation';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export const upsertWorkspace = createFastMutation<
|
||||
@@ -11,10 +9,4 @@ export const upsertWorkspace = createFastMutation<
|
||||
>({
|
||||
mutationKey: ['upsert_workspace'],
|
||||
mutationFn: (workspace) => invokeCmd<Workspace>('cmd_update_workspace', { workspace }),
|
||||
onSuccess: async (workspace) => {
|
||||
const isNew = differenceInMilliseconds(new Date(), workspace.createdAt + 'Z') < 100;
|
||||
|
||||
if (isNew) trackEvent('workspace', 'create');
|
||||
else trackEvent('workspace', 'update');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMemo, type ReactNode } from 'react';
|
||||
import { useSaveResponse } from '../hooks/useSaveResponse';
|
||||
import { useToggle } from '../hooks/useToggle';
|
||||
import { isProbablyTextContentType } from '../lib/contentType';
|
||||
import { getContentTypeHeader } from '../lib/model_util';
|
||||
import { getContentTypeFromHeaders } from '../lib/model_util';
|
||||
import { getResponseBodyText } from '../lib/responseBody';
|
||||
import { CopyButton } from './CopyButton';
|
||||
import { Banner } from './core/Banner';
|
||||
@@ -24,7 +24,7 @@ export function ConfirmLargeResponse({ children, response }: Props) {
|
||||
const { mutate: saveResponse } = useSaveResponse(response);
|
||||
const [showLargeResponse, toggleShowLargeResponse] = useToggle();
|
||||
const isProbablyText = useMemo(() => {
|
||||
const contentType = getContentTypeHeader(response.headers);
|
||||
const contentType = getContentTypeFromHeaders(response.headers);
|
||||
return isProbablyTextContentType(contentType);
|
||||
}, [response.headers]);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
||||
const [syncConfig, setSyncConfig] = useState<{
|
||||
filePath: string | null;
|
||||
initGit?: boolean;
|
||||
}>({ filePath: null, initGit: true });
|
||||
}>({ filePath: null, initGit: false });
|
||||
|
||||
return (
|
||||
<VStack
|
||||
@@ -62,8 +62,8 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
||||
|
||||
<SyncToFilesystemSetting
|
||||
onChange={setSyncConfig}
|
||||
onCreateNewWorkspace={hide}
|
||||
value={syncConfig}
|
||||
allowNonEmptyDirectory // Will do initial import when the workspace is created
|
||||
/>
|
||||
<Button type="submit" color="primary" className="ml-auto mt-3">
|
||||
Create Workspace
|
||||
|
||||
@@ -175,6 +175,8 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
@@ -193,6 +195,8 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
||||
inputs={input.inputs}
|
||||
setDataAttr={setDataAttr}
|
||||
stateKey={stateKey}
|
||||
useTemplating={useTemplating}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
/>
|
||||
</Banner>
|
||||
);
|
||||
|
||||
@@ -32,10 +32,13 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
const { baseEnvironment, subEnvironments, allEnvironments } = useEnvironments();
|
||||
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
|
||||
initialEnvironment?.id ?? baseEnvironment?.id ?? null,
|
||||
initialEnvironment?.id ?? null,
|
||||
);
|
||||
|
||||
const selectedEnvironment = allEnvironments.find((e) => e.id === selectedEnvironmentId);
|
||||
const selectedEnvironment =
|
||||
selectedEnvironmentId != null
|
||||
? allEnvironments.find((e) => e.id === selectedEnvironmentId)
|
||||
: baseEnvironment;
|
||||
|
||||
const handleCreateEnvironment = async () => {
|
||||
if (baseEnvironment == null) return;
|
||||
@@ -55,7 +58,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
<div className="min-w-0 h-full overflow-y-auto pt-1">
|
||||
<SidebarButton
|
||||
active={selectedEnvironment?.id == baseEnvironment?.id}
|
||||
onClick={() => setSelectedEnvironmentId(baseEnvironment?.id ?? null)}
|
||||
onClick={() => setSelectedEnvironmentId(null)}
|
||||
environment={null}
|
||||
rightSlot={
|
||||
<IconButton
|
||||
@@ -82,6 +85,11 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
environment={e}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
onDelete={() => {
|
||||
if (e.id === selectedEnvironmentId) {
|
||||
setSelectedEnvironmentId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
@@ -90,11 +98,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
</aside>
|
||||
)}
|
||||
secondSlot={() =>
|
||||
selectedEnvironmentId == null ? (
|
||||
<div className="p-3 mt-10">
|
||||
<Banner color="danger">No selected environment</Banner>
|
||||
</div>
|
||||
) : selectedEnvironment == null ? (
|
||||
selectedEnvironment == null ? (
|
||||
<div className="p-3 mt-10">
|
||||
<Banner color="danger">
|
||||
Failed to find selected environment <InlineCode>{selectedEnvironmentId}</InlineCode>
|
||||
@@ -198,6 +202,7 @@ function SidebarButton({
|
||||
className,
|
||||
active,
|
||||
onClick,
|
||||
onDelete,
|
||||
rightSlot,
|
||||
environment,
|
||||
}: {
|
||||
@@ -205,6 +210,7 @@ function SidebarButton({
|
||||
children: ReactNode;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
onDelete?: () => void;
|
||||
rightSlot?: ReactNode;
|
||||
environment: Environment | null;
|
||||
}) {
|
||||
@@ -275,7 +281,11 @@ function SidebarButton({
|
||||
color: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
onSelect: () => deleteEnvironment.mutate(),
|
||||
onSelect: () => {
|
||||
deleteEnvironment.mutate(undefined, {
|
||||
onSuccess: onDelete,
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,7 @@ interface TreeNode {
|
||||
}
|
||||
|
||||
export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
const [{ status }, { commit, add, unstage, push }] = useGit(syncDir);
|
||||
const [{ status }, { commit, commitAndPush, add, unstage, push }] = useGit(syncDir);
|
||||
const [message, setMessage] = useState<string>('');
|
||||
|
||||
const handleCreateCommit = async () => {
|
||||
@@ -53,8 +53,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
|
||||
const handleCreateCommitAndPush = async () => {
|
||||
try {
|
||||
await commit.mutateAsync({ message });
|
||||
await push.mutateAsync();
|
||||
await commitAndPush.mutateAsync({ message });
|
||||
showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' });
|
||||
onDone();
|
||||
} catch (err) {
|
||||
@@ -66,10 +65,13 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
const allEntries = [];
|
||||
const yaakEntries = [];
|
||||
const externalEntries = [];
|
||||
|
||||
for (const entry of status.data?.entries ?? []) {
|
||||
allEntries.push(entry);
|
||||
if (entry.next == null && entry.prev == null) {
|
||||
externalEntries.push(entry);
|
||||
} else if (entry.next?.model === 'environment' || entry.prev?.model === 'environment') {
|
||||
externalEntries.push(entry);
|
||||
} else {
|
||||
yaakEntries.push(entry);
|
||||
}
|
||||
@@ -184,7 +186,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
size="sm"
|
||||
onClick={handleCreateCommit}
|
||||
disabled={!hasAddedAnything}
|
||||
isLoading={push.isPending || commit.isPending}
|
||||
isLoading={push.isPending || commitAndPush.isPending || commit.isPending}
|
||||
>
|
||||
Commit
|
||||
</Button>
|
||||
@@ -193,7 +195,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
size="sm"
|
||||
disabled={!hasAddedAnything}
|
||||
onClick={handleCreateCommitAndPush}
|
||||
isLoading={push.isPending || commit.isPending}
|
||||
isLoading={push.isPending || commitAndPush.isPending || commit.isPending}
|
||||
>
|
||||
Commit and Push
|
||||
</Button>
|
||||
|
||||
@@ -336,12 +336,14 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
|
||||
label: banner,
|
||||
},
|
||||
{
|
||||
color: 'success',
|
||||
label: 'Open Workspace Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect() {
|
||||
openWorkspaceSettings.mutate({ openSyncMenu: true });
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Hide This Message',
|
||||
leftSlot: <Icon icon="eye_closed" />,
|
||||
@@ -396,8 +398,8 @@ function SetupGitDropdown({
|
||||
leftSlot: <Icon icon="magic_wand" />,
|
||||
onSelect: initRepo,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
color: 'warning',
|
||||
label: 'Hide This Message',
|
||||
leftSlot: <Icon icon="eye_closed" />,
|
||||
async onSelect() {
|
||||
|
||||
@@ -7,12 +7,10 @@ import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||
import { useImportCurl } from '../hooks/useImportCurl';
|
||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
@@ -20,7 +18,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { generateId } from '../lib/generateId';
|
||||
import {
|
||||
BODY_TYPE_BINARY,
|
||||
@@ -30,8 +27,10 @@ import {
|
||||
BODY_TYPE_JSON,
|
||||
BODY_TYPE_NONE,
|
||||
BODY_TYPE_OTHER,
|
||||
BODY_TYPE_XML,
|
||||
BODY_TYPE_XML, getContentTypeFromHeaders,
|
||||
} from '../lib/model_util';
|
||||
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { showToast } from '../lib/toast';
|
||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
@@ -83,8 +82,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const [{ urlKey }] = useRequestEditor();
|
||||
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
||||
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||
const contentType = getContentTypeFromHeaders(activeRequest.headers);
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
@@ -273,7 +272,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||
const { mutate: importCurl } = useImportCurl();
|
||||
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
(body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }),
|
||||
@@ -314,17 +312,35 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(text: string) => {
|
||||
(e: ClipboardEvent, text: string) => {
|
||||
if (text.startsWith('curl ')) {
|
||||
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
||||
} else {
|
||||
// Only import query if pasted text contains entire querystring
|
||||
importQuerystring(text);
|
||||
const data = prepareImportQuerystring(text);
|
||||
if (data != null) {
|
||||
e.preventDefault(); // Prevent input onChange
|
||||
|
||||
updateRequest({ id: activeRequestId, update: data });
|
||||
focusParamsTab();
|
||||
|
||||
// Wait for request to update, then refresh the UI
|
||||
// TODO: Somehow make this deterministic
|
||||
setTimeout(() => {
|
||||
forceUrlRefresh();
|
||||
forceParamsRefresh();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeRequestId, importCurl, importQuerystring],
|
||||
[
|
||||
activeRequestId,
|
||||
focusParamsTab,
|
||||
forceParamsRefresh,
|
||||
forceUrlRefresh,
|
||||
importCurl,
|
||||
updateRequest,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSend = useCallback(
|
||||
() => sendRequest(activeRequest.id ?? null),
|
||||
[activeRequest.id, sendRequest],
|
||||
|
||||
@@ -3,9 +3,9 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import { getContentTypeFromHeaders } from '../lib/model_util';
|
||||
import { ConfirmLargeResponse } from './ConfirmLargeResponse';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
@@ -47,7 +47,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
'responsePaneActiveTabs',
|
||||
{},
|
||||
);
|
||||
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||
const contentType = getContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clear, readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as m from 'motion/react-m';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useImportCurl } from '../hooks/useImportCurl';
|
||||
import { useWindowFocus } from '../hooks/useWindowFocus';
|
||||
@@ -22,7 +22,7 @@ export function ImportCurlButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
@@ -50,6 +50,6 @@ export function ImportCurlButton() {
|
||||
>
|
||||
Import Curl
|
||||
</Button>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import type { ReactNode } from 'react';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import { appInfo } from '../hooks/useAppInfo';
|
||||
import { useLicenseConfirmation } from '../hooks/useLicenseConfirmation';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import { Icon } from './core/Icon';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import {SettingsTab} from "./Settings/SettingsTab";
|
||||
import { SettingsTab } from './Settings/SettingsTab';
|
||||
|
||||
const details: Record<
|
||||
LicenseCheckStatus['type'] | 'dev' | 'beta',
|
||||
@@ -26,22 +26,30 @@ const details: Record<
|
||||
dev: { label: 'Develop', color: 'secondary' },
|
||||
commercial_use: null,
|
||||
invalid_license: { label: 'License Error', color: 'danger' },
|
||||
personal_use: { label: 'Personal Use', color: 'primary' },
|
||||
trialing: { label: 'Personal Use', color: 'primary' },
|
||||
personal_use: { label: 'Personal Use', color: 'success' },
|
||||
trialing: { label: 'Active Trial', color: 'success' },
|
||||
};
|
||||
|
||||
export function LicenseBadge() {
|
||||
const { check } = useLicense();
|
||||
const [licenseDetails, setLicenseDetails] = useLicenseConfirmation();
|
||||
|
||||
if (check.data == null) {
|
||||
// Hasn't loaded yet
|
||||
if (licenseDetails == null || check.data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkType = appInfo.version.includes('beta')
|
||||
? 'beta'
|
||||
: appInfo.isDev
|
||||
? 'dev'
|
||||
: check.data.type;
|
||||
// User has confirmed they are using Yaak for personal use only, so hide badge
|
||||
if (licenseDetails.confirmedPersonalUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// User is trialing but has already seen the message, so hide badge
|
||||
if (check.data.type === 'trialing' && licenseDetails.hasDismissedTrial) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkType = appInfo.version.includes('beta') ? 'beta' : check.data.type;
|
||||
const detail = details[checkType];
|
||||
if (detail == null) {
|
||||
return null;
|
||||
@@ -52,15 +60,16 @@ export function LicenseBadge() {
|
||||
size="2xs"
|
||||
variant="border"
|
||||
className="!rounded-full mx-1"
|
||||
onClick={async () => {
|
||||
if (checkType === 'beta') {
|
||||
await openUrl('https://feedback.yaak.app');
|
||||
} else {
|
||||
openSettings.mutate(SettingsTab.License);
|
||||
}
|
||||
}}
|
||||
color={detail.color}
|
||||
event={{ id: 'license-badge', status: check.data.type }}
|
||||
onClick={async () => {
|
||||
if (check.data.type === 'trialing') {
|
||||
await setLicenseDetails((v) => ({
|
||||
...v,
|
||||
hasDismissedTrial: true,
|
||||
}));
|
||||
}
|
||||
openSettings.mutate(SettingsTab.License);
|
||||
}}
|
||||
>
|
||||
{detail.label}
|
||||
</Button>
|
||||
|
||||
@@ -66,18 +66,8 @@ export function MarkdownEditor({
|
||||
onChange={setViewMode}
|
||||
value={viewMode}
|
||||
options={[
|
||||
{
|
||||
event: { id: 'md_mode', mode: 'preview' },
|
||||
icon: 'eye',
|
||||
label: 'Preview mode',
|
||||
value: 'preview',
|
||||
},
|
||||
{
|
||||
event: { id: 'md_mode', mode: 'edit' },
|
||||
icon: 'pencil',
|
||||
label: 'Edit mode',
|
||||
value: 'edit',
|
||||
},
|
||||
{ icon: 'eye', label: 'Preview mode', value: 'preview' },
|
||||
{ icon: 'pencil', label: 'Edit mode', value: 'edit' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { Portal } from './Portal';
|
||||
@@ -48,7 +48,7 @@ export function Overlay({
|
||||
<Portal name={portalName}>
|
||||
{open && (
|
||||
<FocusTrap>
|
||||
<motion.div
|
||||
<m.div
|
||||
className={classNames('fixed inset-0', zIndexes[zIndex])}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -68,7 +68,7 @@ export function Overlay({
|
||||
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
|
||||
)}
|
||||
{children}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</FocusTrap>
|
||||
)}
|
||||
</Portal>
|
||||
|
||||
@@ -96,7 +96,6 @@ export function SettingsAppearance() {
|
||||
value={`${settings.interfaceFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => updateSettings.mutate({ interfaceFontSize: parseInt(v) })}
|
||||
event="ui-font-size"
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
@@ -106,7 +105,6 @@ export function SettingsAppearance() {
|
||||
value={`${settings.editorFontSize}`}
|
||||
options={fontSizeOptions}
|
||||
onChange={(v) => updateSettings.mutate({ editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
|
||||
event="editor-font-size"
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
@@ -116,13 +114,11 @@ export function SettingsAppearance() {
|
||||
value={`${settings.editorKeymap}`}
|
||||
options={keymaps}
|
||||
onChange={(v) => updateSettings.mutate({ editorKeymap: v })}
|
||||
event="editor-keymap"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.editorSoftWrap}
|
||||
title="Wrap Editor Lines"
|
||||
onChange={(editorSoftWrap) => updateSettings.mutate({ editorSoftWrap })}
|
||||
event="editor-wrap-lines"
|
||||
/>
|
||||
|
||||
<Separator className="my-4" />
|
||||
@@ -134,7 +130,6 @@ export function SettingsAppearance() {
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={(appearance) => updateSettings.mutate({ appearance })}
|
||||
event="appearance"
|
||||
options={[
|
||||
{ label: 'Automatic', value: 'system' },
|
||||
{ label: 'Light', value: 'light' },
|
||||
@@ -152,7 +147,6 @@ export function SettingsAppearance() {
|
||||
className="flex-1"
|
||||
value={activeTheme.light.id}
|
||||
options={lightThemes}
|
||||
event="theme.light"
|
||||
onChange={(themeLight) => updateSettings.mutate({ ...settings, themeLight })}
|
||||
/>
|
||||
)}
|
||||
@@ -166,7 +160,6 @@ export function SettingsAppearance() {
|
||||
size="sm"
|
||||
value={activeTheme.dark.id}
|
||||
options={darkThemes}
|
||||
event="theme.dark"
|
||||
onChange={(themeDark) => updateSettings.mutate({ ...settings, themeDark })}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,6 @@ export function SettingsGeneral() {
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={(updateChannel) => updateSettings.mutate({ updateChannel })}
|
||||
event="update-channel"
|
||||
options={[
|
||||
{ label: 'Stable (less frequent)', value: 'stable' },
|
||||
{ label: 'Beta (more frequent)', value: 'beta' },
|
||||
@@ -59,7 +58,6 @@ export function SettingsGeneral() {
|
||||
labelPosition="left"
|
||||
labelClassName="w-[14rem]"
|
||||
size="sm"
|
||||
event="workspace-switch-behavior"
|
||||
value={
|
||||
settings.openWorkspaceNewWindow === true
|
||||
? 'new'
|
||||
@@ -81,10 +79,10 @@ export function SettingsGeneral() {
|
||||
|
||||
<Checkbox
|
||||
className="mt-3"
|
||||
checked={settings.telemetry}
|
||||
title="Send Usage Statistics"
|
||||
event="usage-statistics"
|
||||
onChange={(telemetry) => updateSettings.mutate({ telemetry })}
|
||||
checked={false}
|
||||
title="Send Usage Statistics (all tracking was removed in 2025.1.2)"
|
||||
disabled
|
||||
onChange={() => {}}
|
||||
/>
|
||||
|
||||
<Separator className="my-4" />
|
||||
@@ -115,7 +113,6 @@ export function SettingsGeneral() {
|
||||
<Checkbox
|
||||
checked={workspace.settingValidateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
event="validate-certs"
|
||||
onChange={(settingValidateCertificates) =>
|
||||
upsertWorkspace.mutate({ ...workspace, settingValidateCertificates })
|
||||
}
|
||||
@@ -124,7 +121,6 @@ export function SettingsGeneral() {
|
||||
<Checkbox
|
||||
checked={workspace.settingFollowRedirects}
|
||||
title="Follow Redirects"
|
||||
event="follow-redirects"
|
||||
onChange={(settingFollowRedirects) =>
|
||||
upsertWorkspace.mutate({
|
||||
...workspace,
|
||||
|
||||
@@ -1,60 +1,87 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { formatDistanceToNowStrict } from 'date-fns';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import React, { useState } from 'react';
|
||||
import { useLicenseConfirmation } from '../../hooks/useLicenseConfirmation';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { pluralizeCount } from '../../lib/pluralize';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { Link } from '../core/Link';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
|
||||
export function SettingsLicense() {
|
||||
const { check, activate } = useLicense();
|
||||
const [key, setKey] = useState<string>('');
|
||||
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
|
||||
const [licenseDetails, setLicenseDetails] = useLicenseConfirmation();
|
||||
const [checked, setChecked] = useState<boolean>(false);
|
||||
|
||||
if (check.isPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-6 max-w-lg">
|
||||
{check.data?.type === 'commercial_use' ? (
|
||||
<Banner color="success">
|
||||
<strong>License active!</strong> Enjoy using Yaak for commercial use.
|
||||
</Banner>
|
||||
) : (
|
||||
<Banner color="primary" className="flex flex-col gap-3 max-w-lg">
|
||||
{check.data?.type === 'trialing' && (
|
||||
<p className="select-text">
|
||||
<strong>
|
||||
You have {formatDistanceToNowStrict(check.data.end)} remaining on your trial.
|
||||
</strong>
|
||||
</p>
|
||||
)}
|
||||
) : check.data?.type == 'trialing' ? (
|
||||
<Banner color="success" className="flex flex-col gap-3 max-w-lg">
|
||||
<p className="select-text">
|
||||
A commercial license is required if using Yaak within a for-profit organization.{' '}
|
||||
<Link href="https://yaak.app/pricing" className="text-notice">
|
||||
Learn More
|
||||
</Link>
|
||||
You have{' '}
|
||||
<strong>
|
||||
{pluralizeCount('day', differenceInDays(check.data.end, new Date()))} remaining
|
||||
</strong>{' '}
|
||||
on your commercial use trial. Once the trial ends, Yaak will be limited to personal use
|
||||
until a license is activated.
|
||||
</p>
|
||||
</Banner>
|
||||
)}
|
||||
) : check.data?.type == 'personal_use' && !licenseDetails?.confirmedPersonalUse ? (
|
||||
<Banner color="success" className="flex flex-col gap-3 max-w-lg">
|
||||
<p className="select-text">
|
||||
Your 30-day trial has ended. Please activate a license or confirm how you're using
|
||||
Yaak.
|
||||
</p>
|
||||
<form
|
||||
className="flex flex-col gap-3 items-start"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await setLicenseDetails((v) => ({
|
||||
...v,
|
||||
confirmedPersonalUse: true,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
title="I am only using Yaak for personal use"
|
||||
/>
|
||||
<Button type="submit" disabled={!checked} size="xs" variant="border" color="success">
|
||||
Confirm
|
||||
</Button>
|
||||
</form>
|
||||
</Banner>
|
||||
) : null}
|
||||
|
||||
<p className="select-text">
|
||||
A commercial license is required if using Yaak within a for-profit organization.{' '}
|
||||
<Link href="https://yaak.app/pricing" className="text-notice">
|
||||
Learn More
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
{check.error && <Banner color="danger">{check.error}</Banner>}
|
||||
{activate.error && <Banner color="danger">{activate.error}</Banner>}
|
||||
|
||||
{check.data?.type === 'commercial_use' ? (
|
||||
<HStack space={2}>
|
||||
<Button
|
||||
variant="border"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={toggleActivateFormVisible}
|
||||
event="license.another"
|
||||
>
|
||||
<Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}>
|
||||
Activate Another License
|
||||
</Button>
|
||||
<Button
|
||||
@@ -62,27 +89,20 @@ export function SettingsLicense() {
|
||||
size="sm"
|
||||
onClick={() => openUrl('https://yaak.app/dashboard')}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
event="license.support"
|
||||
>
|
||||
Direct Support
|
||||
</Button>
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack space={2}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={toggleActivateFormVisible}
|
||||
event="license.activate"
|
||||
>
|
||||
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
|
||||
Activate
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => open('https://yaak.app/pricing?ref=app.yaak.desktop')}
|
||||
onClick={() => openUrl('https://yaak.app/pricing?ref=app.yaak.desktop')}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
event="license.purchase"
|
||||
>
|
||||
Purchase
|
||||
</Button>
|
||||
@@ -107,13 +127,7 @@ export function SettingsLicense() {
|
||||
onChange={setKey}
|
||||
placeholder="YK1-XXXXX-XXXXX-XXXXX-XXXXX"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="sm"
|
||||
isLoading={activate.isPending}
|
||||
event="license.submit"
|
||||
>
|
||||
<Button type="submit" color="primary" size="sm" isLoading={activate.isPending}>
|
||||
Submit
|
||||
</Button>
|
||||
</VStack>
|
||||
|
||||
@@ -66,7 +66,6 @@ export function SettingsPlugins() {
|
||||
type="submit"
|
||||
color="primary"
|
||||
className="ml-auto"
|
||||
event="plugin.add"
|
||||
>
|
||||
Add Plugin
|
||||
</Button>
|
||||
@@ -76,14 +75,12 @@ export function SettingsPlugins() {
|
||||
icon="refresh"
|
||||
title="Reload plugins"
|
||||
spin={refreshPlugins.isPending}
|
||||
event="plugin.reload"
|
||||
onClick={() => refreshPlugins.mutate()}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="help"
|
||||
title="View documentation"
|
||||
event="plugin.docs"
|
||||
onClick={() => openUrl('https://feedback.yaak.app/help/articles/6911763-quick-start')}
|
||||
/>
|
||||
</HStack>
|
||||
@@ -107,7 +104,6 @@ function PluginInfo({ plugin }: { plugin: Plugin }) {
|
||||
size="sm"
|
||||
icon="trash"
|
||||
title="Uninstall plugin"
|
||||
event="plugin.delete"
|
||||
onClick={() => deletePlugin.mutate()}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -19,7 +19,6 @@ export function SettingsProxy() {
|
||||
hideLabel
|
||||
size="sm"
|
||||
value={settings.proxy?.type ?? 'automatic'}
|
||||
event="proxy"
|
||||
onChange={(v) => {
|
||||
if (v === 'automatic') {
|
||||
updateSettings.mutate({ proxy: undefined });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { useRef } from 'react';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
@@ -12,6 +13,7 @@ import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsTab } from './Settings/SettingsTab';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
@@ -19,6 +21,7 @@ export function SettingsDropdown() {
|
||||
const appInfo = useAppInfo();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
const { check } = useLicense();
|
||||
|
||||
useListenToTauriEvent('settings', () => openSettings.mutate(null));
|
||||
|
||||
@@ -56,6 +59,13 @@ export function SettingsDropdown() {
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appInfo.version}` },
|
||||
{
|
||||
label: 'Purchase License',
|
||||
color: 'success',
|
||||
hidden: check.data == null || check.data.type === 'commercial_use',
|
||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||
onSelect: () => openSettings.mutate(SettingsTab.License),
|
||||
},
|
||||
{
|
||||
label: 'Check for Updates',
|
||||
leftSlot: <Icon icon="update" />,
|
||||
@@ -65,7 +75,7 @@ export function SettingsDropdown() {
|
||||
label: 'Feedback',
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
rightSlot: <Icon icon="external_link" />,
|
||||
onSelect: () => openUrl('https://yaak.app/roadmap'),
|
||||
onSelect: () => openUrl('https://yaak.app/feedback'),
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
|
||||
@@ -1,32 +1,54 @@
|
||||
import { readDir } from '@tauri-apps/plugin-fs';
|
||||
import { useState } from 'react';
|
||||
import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { SelectFile } from './SelectFile';
|
||||
|
||||
export interface SyncToFilesystemSettingProps {
|
||||
onChange: (args: { filePath: string | null; initGit?: boolean }) => void;
|
||||
onCreateNewWorkspace: () => void;
|
||||
value: { filePath: string | null; initGit?: boolean };
|
||||
allowNonEmptyDirectory?: boolean;
|
||||
forceOpen?: boolean;
|
||||
}
|
||||
|
||||
export function SyncToFilesystemSetting({
|
||||
onChange,
|
||||
onCreateNewWorkspace,
|
||||
value,
|
||||
allowNonEmptyDirectory,
|
||||
forceOpen,
|
||||
}: SyncToFilesystemSettingProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isNonEmpty, setIsNonEmpty] = useState<string | null>(null);
|
||||
return (
|
||||
<details open={forceOpen || value != null} className="w-full">
|
||||
<details open={forceOpen || !!value.filePath} className="w-full">
|
||||
<summary>Data directory {typeof value.initGit === 'boolean' && ' and Git'}</summary>
|
||||
<VStack className="my-2" space={3}>
|
||||
<Banner color="info">
|
||||
Sync workspace data to folder as plain text files, ideal for backup and Git collaboration.
|
||||
</Banner>
|
||||
{error && <div className="text-danger">{error}</div>}
|
||||
{isNonEmpty ? (
|
||||
<Banner color="notice" className="flex flex-col gap-1.5">
|
||||
<p>The selected directory must be empty. Did you want to open it instead?</p>
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="notice"
|
||||
size="xs"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
openWorkspaceFromSyncDir.mutate(isNonEmpty);
|
||||
onCreateNewWorkspace();
|
||||
}}
|
||||
>
|
||||
Open Workspace
|
||||
</Button>
|
||||
</div>
|
||||
</Banner>
|
||||
) : (
|
||||
<Banner color="info">
|
||||
Sync workspace data to folder as plain text files, ideal for backup and Git
|
||||
collaboration. Environments are excluded in order to keep your secrets private.
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
<SelectFile
|
||||
directory
|
||||
@@ -36,12 +58,13 @@ export function SyncToFilesystemSetting({
|
||||
onChange={async ({ filePath }) => {
|
||||
if (filePath != null) {
|
||||
const files = await readDir(filePath);
|
||||
if (files.length > 0 && !allowNonEmptyDirectory) {
|
||||
setError('The directory must be empty');
|
||||
if (files.length > 0) {
|
||||
setIsNonEmpty(filePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsNonEmpty(null);
|
||||
onChange({ ...value, filePath });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { hideToast, toastsAtom } from '../lib/toast';
|
||||
|
||||
@@ -18,7 +18,7 @@ type Props = Pick<HttpRequest, 'url'> & {
|
||||
onSend: () => void;
|
||||
onUrlChange: (url: string) => void;
|
||||
onPaste?: (v: string) => void;
|
||||
onPasteOverwrite?: (v: string) => void;
|
||||
onPasteOverwrite?: InputProps['onPasteOverwrite'];
|
||||
onCancel: () => void;
|
||||
submitIcon?: IconProps['icon'] | null;
|
||||
onMethodChange?: (method: string) => void;
|
||||
|
||||
@@ -12,17 +12,17 @@ import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||
import { requestsAtom } from '../hooks/useRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { useLatestWebsocketConnection } from '../hooks/useWebsocketConnections';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { generateId } from '../lib/generateId';
|
||||
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
@@ -66,7 +66,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
const activeRequestId = activeRequest.id;
|
||||
const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const [{ urlKey }] = useRequestEditor();
|
||||
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
|
||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||
@@ -151,8 +151,8 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
|
||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
const { mutate: updateRequest } = useUpdateAnyHttpRequest();
|
||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
|
||||
const connection = useLatestWebsocketConnection(activeRequestId);
|
||||
|
||||
const activeTab = activeTabs?.[activeRequestId];
|
||||
@@ -189,7 +189,6 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
environmentId: getActiveEnvironment()?.id ?? null,
|
||||
cookieJarId: getActiveCookieJar()?.id ?? null,
|
||||
});
|
||||
trackEvent('websocket_request', 'send');
|
||||
}, [activeRequest.id]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
@@ -198,13 +197,11 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
connectionId: connection?.id,
|
||||
environmentId: getActiveEnvironment()?.id ?? null,
|
||||
});
|
||||
trackEvent('websocket_connection', 'send');
|
||||
}, [connection]);
|
||||
|
||||
const handleCancel = useCallback(async () => {
|
||||
if (connection == null) return;
|
||||
await closeWebsocket({ connectionId: connection?.id });
|
||||
trackEvent('websocket_connection', 'cancel');
|
||||
}, [connection]);
|
||||
|
||||
const handleUrlChange = useCallback(
|
||||
@@ -212,6 +209,26 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
[activeRequest],
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(e: ClipboardEvent, text: string) => {
|
||||
const data = prepareImportQuerystring(text);
|
||||
if (data != null) {
|
||||
e.preventDefault(); // Prevent input onChange
|
||||
|
||||
updateRequest({ id: activeRequestId, update: data });
|
||||
focusParamsTab();
|
||||
|
||||
// Wait for request to update, then refresh the UI
|
||||
// TODO: Somehow make this deterministic
|
||||
setTimeout(() => {
|
||||
forceUrlRefresh();
|
||||
forceParamsRefresh();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[activeRequestId, focusParamsTab, forceParamsRefresh, forceUrlRefresh, updateRequest],
|
||||
);
|
||||
|
||||
const messageLanguage = languageFromContentType(null, activeRequest.message);
|
||||
|
||||
const isLoading = connection !== null && connection.state !== 'closed';
|
||||
@@ -242,7 +259,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
)
|
||||
}
|
||||
placeholder="wss://example.com"
|
||||
onPasteOverwrite={importQuerystring}
|
||||
onPasteOverwrite={handlePaste}
|
||||
autocomplete={autocomplete}
|
||||
onSend={isLoading ? handleSend : handleConnect}
|
||||
onCancel={cancelResponse}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {duplicateWebsocketRequest} from "../commands/duplicateWebsocketRequest";
|
||||
@@ -137,7 +137,7 @@ export function Workspace() {
|
||||
portalName="sidebar"
|
||||
onClose={() => setFloatingSidebarHidden(true)}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={classNames(
|
||||
@@ -150,7 +150,7 @@ export function Workspace() {
|
||||
<SidebarActions />
|
||||
</HeaderSize>
|
||||
<Sidebar />
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</Overlay>
|
||||
) : (
|
||||
<>
|
||||
@@ -261,7 +261,7 @@ function useGlobalWorkspaceHooks() {
|
||||
} else if (activeRequest.model === 'grpc_request') {
|
||||
await duplicateGrpcRequest.mutateAsync();
|
||||
} else if (activeRequest.model === 'websocket_request') {
|
||||
await duplicateWebsocketRequest.mutateAsync(activeRequest);
|
||||
await duplicateWebsocketRequest.mutateAsync(activeRequest.id);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
throw new Error('Failed to duplicate invalid request model: ' + (activeRequest as any).model);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {open} from "@tauri-apps/plugin-dialog";
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -75,7 +76,16 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
{
|
||||
label: 'Open Existing Workspace',
|
||||
leftSlot: <Icon icon="folder_open" />,
|
||||
onSelect: openWorkspaceFromSyncDir.mutate,
|
||||
onSelect: async () => {
|
||||
const dir = await open({
|
||||
title: 'Select Workspace Directory',
|
||||
directory: true,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
if (dir == null) return;
|
||||
openWorkspaceFromSyncDir.mutate(dir);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
icon="search"
|
||||
title="Search or execute a command"
|
||||
size="sm"
|
||||
event="search"
|
||||
iconColor="secondary"
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
|
||||
@@ -63,6 +63,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, openSyncMenu }: Pro
|
||||
<SyncToFilesystemSetting
|
||||
value={{ filePath: workspaceMeta.settingSyncDir }}
|
||||
forceOpen={openSyncMenu}
|
||||
onCreateNewWorkspace={hide}
|
||||
onChange={({ filePath }) => {
|
||||
upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir: filePath });
|
||||
}}
|
||||
|
||||
@@ -9,17 +9,19 @@ interface Props {
|
||||
|
||||
export function Banner({ children, className, color }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
`x-theme-banner--${color}`,
|
||||
'whitespace-pre-wrap',
|
||||
'border border-border bg-surface',
|
||||
'px-4 py-3 rounded-lg select-auto',
|
||||
'overflow-auto h-auto mb-auto text-text',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<div className="w-full mb-auto grid grid-rows-1 max-h-full">
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
`x-theme-banner--${color}`,
|
||||
'whitespace-pre-wrap',
|
||||
'border border-border bg-surface',
|
||||
'px-4 py-3 rounded-lg select-auto',
|
||||
'overflow-auto text-text',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { Icon } from './Icon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
|
||||
@@ -22,7 +21,6 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onC
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
hotkeyAction?: HotkeyAction;
|
||||
event?: string | { id: string; [attr: string]: number | string };
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
@@ -43,7 +41,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
hotkeyAction,
|
||||
title,
|
||||
onClick,
|
||||
event,
|
||||
...props
|
||||
}: ButtonProps,
|
||||
ref,
|
||||
@@ -107,12 +104,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
type={type}
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
onClick?.(e);
|
||||
if (event != null) {
|
||||
trackEvent('button', 'click', typeof event === 'string' ? { id: event } : event);
|
||||
}
|
||||
}}
|
||||
onClick={onClick}
|
||||
onDoubleClick={(e) => {
|
||||
// Kind of a hack? This prevents double-clicks from going through buttons. For example, when
|
||||
// double-clicking the workspace header to toggle window maximization
|
||||
@@ -136,7 +128,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
{children}
|
||||
</div>
|
||||
{rightSlot && <div className="ml-1">{rightSlot}</div>}
|
||||
{forDropdown && <Icon icon="check" size={size} className="ml-1 -mr-1" />}
|
||||
{forDropdown && <Icon icon="chevron_down" size={size} className="ml-1 -mr-1" />}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { type ReactNode } from 'react';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
@@ -13,7 +12,6 @@ export interface CheckboxProps {
|
||||
inputWrapperClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
fullWidth?: boolean;
|
||||
event?: string;
|
||||
}
|
||||
|
||||
export function Checkbox({
|
||||
@@ -25,7 +23,6 @@ export function Checkbox({
|
||||
title,
|
||||
hideLabel,
|
||||
fullWidth,
|
||||
event,
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<HStack as="label" space={2} className={classNames(className, 'text-text mr-auto')}>
|
||||
@@ -42,9 +39,6 @@ export function Checkbox({
|
||||
disabled={disabled}
|
||||
onChange={() => {
|
||||
onChange(checked === 'indeterminate' ? true : !checked);
|
||||
if (event != null) {
|
||||
trackEvent('button', 'click', { id: event, checked: checked ? 'on' : 'off' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useKey } from 'react-use';
|
||||
@@ -65,7 +65,7 @@ export function Dialog({
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={descriptionId}
|
||||
>
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ top: 5, scale: 0.97 }}
|
||||
animate={{ top: 0, scale: 1 }}
|
||||
className={classNames(
|
||||
@@ -122,7 +122,7 @@ export function Dialog({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as m from 'motion/react-m';
|
||||
import { atom } from 'jotai';
|
||||
import type {
|
||||
CSSProperties,
|
||||
@@ -55,7 +55,7 @@ export type DropdownItemDefault = {
|
||||
label: ReactNode;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
hotKeyLabelOnly?: boolean;
|
||||
color?: 'default' | 'danger' | 'info' | 'warning' | 'notice';
|
||||
color?: 'default' | 'primary' | 'danger' | 'info' | 'warning' | 'notice' | 'success';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
@@ -111,20 +111,23 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
|
||||
const setIsOpen = useCallback((o: SetStateAction<boolean>) => {
|
||||
jotaiStore.set(openAtom, (prevId) => {
|
||||
const prevIsOpen = prevId === id.current;
|
||||
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
|
||||
// Persist background color of button until we close the dropdown
|
||||
if (newIsOpen) {
|
||||
onOpen?.();
|
||||
buttonRef.current!.style.backgroundColor = window
|
||||
.getComputedStyle(buttonRef.current!)
|
||||
.getPropertyValue('background-color');
|
||||
}
|
||||
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
|
||||
});
|
||||
}, [onOpen]);
|
||||
const setIsOpen = useCallback(
|
||||
(o: SetStateAction<boolean>) => {
|
||||
jotaiStore.set(openAtom, (prevId) => {
|
||||
const prevIsOpen = prevId === id.current;
|
||||
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
|
||||
// Persist background color of button until we close the dropdown
|
||||
if (newIsOpen) {
|
||||
onOpen?.();
|
||||
buttonRef.current!.style.backgroundColor = window
|
||||
.getComputedStyle(buttonRef.current!)
|
||||
.getPropertyValue('background-color');
|
||||
}
|
||||
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
|
||||
});
|
||||
},
|
||||
[onOpen],
|
||||
);
|
||||
|
||||
// Because a different dropdown can cause ours to close, a useEffect([isOpen]) is the only method
|
||||
// we have of detecting the dropdown closed, to do cleanup.
|
||||
@@ -494,7 +497,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
)}
|
||||
{isOpen && (
|
||||
<Overlay noBackdrop open={isOpen} portalName="dropdown-menu">
|
||||
<motion.div
|
||||
<m.div
|
||||
ref={menuRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
@@ -571,7 +574,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
@@ -642,6 +645,8 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
|
||||
'focus:bg-surface-highlight focus:text rounded',
|
||||
item.color === 'danger' && '!text-danger',
|
||||
item.color === 'primary' && '!text-primary',
|
||||
item.color === 'success' && '!text-success',
|
||||
item.color === 'warning' && '!text-warning',
|
||||
item.color === 'notice' && '!text-notice',
|
||||
item.color === 'info' && '!text-info',
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface EditorProps {
|
||||
useTemplating?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onPaste?: (value: string) => void;
|
||||
onPasteOverwrite?: (value: string) => void;
|
||||
onPasteOverwrite?: (e: ClipboardEvent, value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
@@ -173,7 +173,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}, [onPaste]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPaste);
|
||||
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPasteOverwrite);
|
||||
useEffect(() => {
|
||||
handlePasteOverwrite.current = onPasteOverwrite;
|
||||
}, [onPasteOverwrite]);
|
||||
@@ -606,7 +606,7 @@ function getExtensions({
|
||||
const textData = e.clipboardData?.getData('text/plain') ?? '';
|
||||
onPaste.current?.(textData);
|
||||
if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) {
|
||||
onPasteOverwrite.current?.(textData);
|
||||
onPasteOverwrite.current?.(e, textData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import {
|
||||
crosshairCursor,
|
||||
@@ -73,6 +74,8 @@ export const syntaxHighlightStyle = HighlightStyle.define([
|
||||
|
||||
const syntaxTheme = EditorView.theme({}, { dark: true });
|
||||
|
||||
const closeBracketsExts: Extension = [closeBrackets(), keymap.of([...closeBracketsKeymap])];
|
||||
|
||||
const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSupport | null> = {
|
||||
graphql: null,
|
||||
json: json(),
|
||||
@@ -85,6 +88,8 @@ const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSup
|
||||
markdown: markdown(),
|
||||
};
|
||||
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript'];
|
||||
|
||||
export function getLanguageExtension({
|
||||
language,
|
||||
useTemplating = false,
|
||||
@@ -110,7 +115,13 @@ export function getLanguageExtension({
|
||||
return base;
|
||||
}
|
||||
|
||||
const extraExtensions = language === 'url' ? [pathParametersPlugin(onClickPathParameter)] : [];
|
||||
const extraExtensions: Extension[] =
|
||||
language === 'url' ? [pathParametersPlugin(onClickPathParameter)] : [];
|
||||
|
||||
// Only close brackets on languages that need it
|
||||
if (language && closeBracketsFor.includes(language)) {
|
||||
extraExtensions.push(closeBracketsExts);
|
||||
}
|
||||
|
||||
return twig({
|
||||
base,
|
||||
@@ -209,9 +220,8 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
}),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
closeBrackets(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLineGutter(),
|
||||
keymap.of([...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
|
||||
keymap.of([...searchKeymap, ...foldKeymap, ...lintKeymap]),
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ const icons = {
|
||||
alert_triangle: lucide.AlertTriangleIcon,
|
||||
archive: lucide.ArchiveIcon,
|
||||
arrow_big_down_dash: lucide.ArrowBigDownDashIcon,
|
||||
circle_dollar_sign: lucide.CircleDollarSignIcon,
|
||||
arrow_right_circle: lucide.ArrowRightCircleIcon,
|
||||
arrow_big_left_dash: lucide.ArrowBigLeftDashIcon,
|
||||
arrow_big_right: lucide.ArrowBigRightIcon,
|
||||
|
||||
@@ -35,7 +35,7 @@ export type InputProps = Pick<
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
onPaste?: (value: string) => void;
|
||||
onPasteOverwrite?: (value: string) => void;
|
||||
onPasteOverwrite?: EditorProps['onPasteOverwrite'];
|
||||
defaultValue?: string;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Link as RouterLink } from '@tanstack/react-router';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { Link as RouterLink } from '@tanstack/react-router';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||
href: string;
|
||||
event?: string;
|
||||
}
|
||||
|
||||
export function Link({ href, children, className, event, ...other }: Props) {
|
||||
export function Link({ href, children, className, ...other }: Props) {
|
||||
const isExternal = href.match(/^https?:\/\//);
|
||||
|
||||
className = classNames(className, 'relative underline hover:text-violet-600');
|
||||
@@ -23,9 +21,6 @@ export function Link({ href, children, className, event, ...other }: Props) {
|
||||
className={classNames(className, 'pr-4 inline-flex items-center')}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (event != null) {
|
||||
trackEvent('link', 'click', { id: event });
|
||||
}
|
||||
}}
|
||||
{...other}
|
||||
>
|
||||
|
||||
@@ -261,7 +261,6 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
variant="border"
|
||||
className="m-2"
|
||||
size="xs"
|
||||
event="pairs.reveal-more"
|
||||
>
|
||||
Show {pairs.length - MAX_INITIAL_PAIRS} More
|
||||
</Button>
|
||||
|
||||
@@ -2,18 +2,17 @@ import classNames from 'classnames';
|
||||
import { useRef } from 'react';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import type { IconProps } from './Icon';
|
||||
import type { IconButtonProps } from './IconButton';
|
||||
import { IconButton } from './IconButton';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props<T extends string> {
|
||||
options: { value: T; label: string; icon: IconProps['icon']; event?: IconButtonProps['event'] }[];
|
||||
options: { value: T; label: string; icon: IconProps['icon'] }[];
|
||||
onChange: (value: T) => void;
|
||||
value: T;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function SegmentedControl<T extends string>({ value, onChange, options, name }: Props<T>) {
|
||||
export function SegmentedControl<T extends string>({ value, onChange, options }: Props<T>) {
|
||||
const [selectedValue, setSelectedValue] = useStateWithDeps<T>(value, [value]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
@@ -45,14 +44,13 @@ export function SegmentedControl<T extends string>({ value, onChange, options, n
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="solid"
|
||||
color={isActive ? "secondary" : undefined}
|
||||
color={isActive ? 'secondary' : undefined}
|
||||
role="radio"
|
||||
event={{ id: name, value: String(o.value) }}
|
||||
tabIndex={isSelected ? 0 : -1}
|
||||
className={classNames(
|
||||
isActive && '!text-text',
|
||||
'!px-1.5 !w-auto',
|
||||
'focus:ring-border-focus',
|
||||
isActive && '!text-text',
|
||||
'!px-1.5 !w-auto',
|
||||
'focus:ring-border-focus',
|
||||
)}
|
||||
key={i}
|
||||
title={o.label}
|
||||
|
||||
@@ -2,7 +2,6 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import { Label } from './Label';
|
||||
@@ -22,7 +21,6 @@ export interface SelectProps<T extends string> {
|
||||
onChange: (value: T) => void;
|
||||
size?: ButtonProps['size'];
|
||||
className?: string;
|
||||
event?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -38,7 +36,6 @@ export function Select<T extends string>({
|
||||
leftSlot,
|
||||
onChange,
|
||||
className,
|
||||
event,
|
||||
size = 'md',
|
||||
}: SelectProps<T>) {
|
||||
const osInfo = useOsInfo();
|
||||
@@ -48,9 +45,6 @@ export function Select<T extends string>({
|
||||
|
||||
const handleChange = (value: T) => {
|
||||
onChange?.(value);
|
||||
if (event != null) {
|
||||
trackEvent('select', 'click', { id: event, value });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { trackEvent } from '../../../lib/analytics';
|
||||
import { Icon } from '../Icon';
|
||||
import type { RadioDropdownProps } from '../RadioDropdown';
|
||||
import { RadioDropdown } from '../RadioDropdown';
|
||||
@@ -104,14 +103,7 @@ export function Tabs({
|
||||
onChange={t.options.onChange}
|
||||
>
|
||||
<button
|
||||
onClick={
|
||||
isActive
|
||||
? undefined
|
||||
: () => {
|
||||
trackEvent('tab', 'click', { label, tab: t.value });
|
||||
onChangeValue(t.value);
|
||||
}
|
||||
}
|
||||
onClick={isActive ? undefined : () => onChangeValue(t.value)}
|
||||
className={btnClassName}
|
||||
>
|
||||
{option && 'shortLabel' in option && option.shortLabel
|
||||
@@ -133,14 +125,7 @@ export function Tabs({
|
||||
return (
|
||||
<button
|
||||
key={t.value}
|
||||
onClick={
|
||||
isActive
|
||||
? undefined
|
||||
: () => {
|
||||
trackEvent('tab', 'click', { label, tab: t.value });
|
||||
onChangeValue(t.value);
|
||||
}
|
||||
}
|
||||
onClick={isActive ? undefined : () => onChangeValue(t.value)}
|
||||
className={btnClassName}
|
||||
>
|
||||
{t.label}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ShowToastRequest } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { useKey } from 'react-use';
|
||||
@@ -45,7 +45,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
const toastIcon = icon ?? (color && color in ICONS && ICONS[color]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ opacity: 0, right: '-10%' }}
|
||||
animate={{ opacity: 100, right: 0 }}
|
||||
exit={{ opacity: 0, right: '-100%' }}
|
||||
@@ -80,7 +80,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
|
||||
{timeout != null && (
|
||||
<div className="w-full absolute bottom-0 left-0 right-0">
|
||||
<motion.div
|
||||
<m.div
|
||||
className="bg-surface-highlight h-[3px]"
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: '0%', opacity: 0.2 }}
|
||||
@@ -89,6 +89,6 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import { useSaveResponse } from '../../hooks/useSaveResponse';
|
||||
import { getContentTypeHeader } from '../../lib/model_util';
|
||||
import { getContentTypeFromHeaders } from '../../lib/model_util';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
|
||||
export function BinaryViewer({ response }: Props) {
|
||||
const saveResponse = useSaveResponse(response);
|
||||
const contentType = getContentTypeHeader(response.headers) ?? 'unknown';
|
||||
const contentType = getContentTypeFromHeaders(response.headers) ?? 'unknown';
|
||||
|
||||
// Wait until the response has been fully-downloaded
|
||||
if (response.state === 'closed') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
|
||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||
import { languageFromContentType } from '../../lib/contentType';
|
||||
import { getContentTypeFromHeaders } from '../../lib/model_util';
|
||||
import { BinaryViewer } from './BinaryViewer';
|
||||
import { TextViewer } from './TextViewer';
|
||||
import { WebPageViewer } from './WebPageViewer';
|
||||
@@ -14,10 +14,8 @@ interface Props {
|
||||
|
||||
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
|
||||
const rawTextBody = useResponseBodyText(response);
|
||||
const language = languageFromContentType(
|
||||
useContentTypeFromHeaders(response.headers),
|
||||
rawTextBody.data ?? '',
|
||||
);
|
||||
const contentType = getContentTypeFromHeaders(response.headers);
|
||||
const language = languageFromContentType(contentType, rawTextBody.data ?? '');
|
||||
|
||||
if (rawTextBody.isLoading || response.state === 'initialized') {
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
import { useFloatingSidebarHidden } from '../../hooks/useFloatingSidebarHidden';
|
||||
import { useShouldFloatSidebar } from '../../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../../hooks/useSidebarHidden';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { HStack } from '../core/Stacks';
|
||||
import { CreateDropdown } from '../CreateDropdown';
|
||||
@@ -22,8 +21,6 @@ export function SidebarActions() {
|
||||
<HStack className="h-full">
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
trackEvent('sidebar', 'toggle');
|
||||
|
||||
// NOTE: We're not using the (h) => !h pattern here because the data
|
||||
// might be different if another window changed it (out of sync)
|
||||
await setHidden(!hidden);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { duplicateWebsocketRequest } from '../../commands/duplicateWebsocketRequest';
|
||||
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
|
||||
import { useDeleteFolder } from '../../hooks/useDeleteFolder';
|
||||
import { useDeleteAnyRequest } from '../../hooks/useDeleteAnyRequest';
|
||||
import { useDeleteFolder } from '../../hooks/useDeleteFolder';
|
||||
import { useDuplicateFolder } from '../../hooks/useDuplicateFolder';
|
||||
import { useDuplicateGrpcRequest } from '../../hooks/useDuplicateGrpcRequest';
|
||||
import { useDuplicateHttpRequest } from '../../hooks/useDuplicateHttpRequest';
|
||||
import { useHttpRequestActions } from '../../hooks/useHttpRequestActions';
|
||||
import { getHttpRequest } from '../../hooks/useHttpRequests';
|
||||
import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace';
|
||||
import { useRenameRequest } from '../../hooks/useRenameRequest';
|
||||
import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest';
|
||||
@@ -18,7 +20,6 @@ import { ContextMenu } from '../core/Dropdown';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { FolderSettingsDialog } from '../FolderSettingsDialog';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
import { getHttpRequest } from '../../hooks/useHttpRequests';
|
||||
|
||||
interface Props {
|
||||
child: SidebarTreeNode;
|
||||
@@ -110,10 +111,17 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
hotKeyAction: 'http_request.duplicate',
|
||||
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () =>
|
||||
child.model === 'http_request'
|
||||
? duplicateHttpRequest.mutate()
|
||||
: duplicateGrpcRequest.mutate(),
|
||||
onSelect: () => {
|
||||
if (child.model === 'http_request') {
|
||||
duplicateHttpRequest.mutate();
|
||||
} else if (child.model === 'grpc_request') {
|
||||
duplicateGrpcRequest.mutate();
|
||||
} else if (child.model === 'websocket_request') {
|
||||
duplicateWebsocketRequest.mutate(child.id);
|
||||
} else {
|
||||
throw new Error('Cannot duplicate invalid model: ' + child.model);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Move',
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useFastMutation } from './useFastMutation';
|
||||
import { event } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
|
||||
export function useCancelHttpResponse(id: string | null) {
|
||||
return useFastMutation<void>({
|
||||
mutationKey: ['cancel_http_response', id],
|
||||
mutationFn: () => event.emit(`cancel_http_response_${id}`),
|
||||
onSettled: () => trackEvent('http_response', 'cancel'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { HttpResponseHeader } from '@yaakapp-internal/models';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useContentTypeFromHeaders(headers: HttpResponseHeader[] | null): string | null {
|
||||
return useMemo(
|
||||
() => headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null,
|
||||
[headers],
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CookieJar } from '@yaakapp-internal/models';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { getActiveWorkspaceId } from './useActiveWorkspace';
|
||||
@@ -25,6 +24,5 @@ export function useCreateCookieJar() {
|
||||
|
||||
return invokeCmd('cmd_create_cookie_jar', { workspaceId, name });
|
||||
},
|
||||
onSettled: () => trackEvent('cookie_jar', 'create'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
@@ -33,7 +32,6 @@ export function useCreateEnvironment() {
|
||||
environmentId: baseEnvironment.id,
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'create'),
|
||||
onSuccess: async (environment) => {
|
||||
if (environment == null) return;
|
||||
setWorkspaceSearchParams({ environment_id: environment.id });
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { getActiveRequest } from './useActiveRequest';
|
||||
@@ -36,7 +35,6 @@ export function useCreateGrpcRequest() {
|
||||
...patch,
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_request', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { router } from '../lib/router';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { getActiveRequest } from './useActiveRequest';
|
||||
@@ -30,7 +29,6 @@ export function useCreateHttpRequest() {
|
||||
request: { workspaceId, ...patch },
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
await router.navigate({
|
||||
to: '/workspaces/$workspaceId',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirmDelete } from '../lib/confirm';
|
||||
import { router } from '../lib/router';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
@@ -24,7 +23,6 @@ export function useDeleteActiveWorkspace() {
|
||||
if (!confirmed) return null;
|
||||
return invokeCmd('cmd_delete_workspace', { workspaceId: workspace?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'delete'),
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
await router.navigate({ to: '/workspaces' });
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirmDelete } from '../lib/confirm';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
@@ -24,6 +23,5 @@ export function useDeleteAnyGrpcRequest() {
|
||||
}
|
||||
return invokeCmd('cmd_delete_grpc_request', { requestId: request.id });
|
||||
},
|
||||
onSuccess: () => trackEvent('grpc_request', 'delete'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirmDelete } from '../lib/confirm';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
@@ -24,6 +23,5 @@ export function useDeleteAnyHttpRequest() {
|
||||
}
|
||||
return invokeCmd<HttpRequest>('cmd_delete_http_request', { requestId: request.id });
|
||||
},
|
||||
onSuccess: () => trackEvent('http_request', 'delete'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CookieJar } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirmDelete } from '../lib/confirm';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { cookieJarsAtom } from './useCookieJars';
|
||||
@@ -26,7 +25,6 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) {
|
||||
if (!confirmed) return null;
|
||||
return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('cookie_jar', 'delete'),
|
||||
onSuccess: (cookieJar) => {
|
||||
if (cookieJar == null) return;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { showConfirmDelete } from '../lib/confirm';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { environmentsAtom } from './useEnvironments';
|
||||
@@ -26,7 +25,6 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
if (!confirmed) return null;
|
||||
return invokeCmd('cmd_delete_environment', { environmentId: environment?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'delete'),
|
||||
onSuccess: (environment) => {
|
||||
if (environment == null) return;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user