mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-17 16:17:45 +01:00
Compare commits
42 Commits
v2024.12.0
...
v2024.13.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b61257e50 | ||
|
|
1f4eea89c5 | ||
|
|
1609e46660 | ||
|
|
28d5a2a019 | ||
|
|
40f0f5387a | ||
|
|
88bcfb9e66 | ||
|
|
2b076c90e4 | ||
|
|
0443fbdfdb | ||
|
|
36d24bdac0 | ||
|
|
d4dfc1c820 | ||
|
|
00178ad197 | ||
|
|
f8efd1a31a | ||
|
|
e1363cf151 | ||
|
|
38e0f5ede7 | ||
|
|
9663018e21 | ||
|
|
80a7c2a9c7 | ||
|
|
4687723176 | ||
|
|
41ce2df00c | ||
|
|
794967904a | ||
|
|
74a7a1a21a | ||
|
|
d9587aa314 | ||
|
|
6b208ef67c | ||
|
|
0cfec0ada6 | ||
|
|
3ecfb15c89 | ||
|
|
23c026126f | ||
|
|
ff9abab547 | ||
|
|
c9c48c77e4 | ||
|
|
83efc58f29 | ||
|
|
632e1ff091 | ||
|
|
40286756b9 | ||
|
|
1050ac5e3c | ||
|
|
6d2c3712c0 | ||
|
|
4a52095033 | ||
|
|
55b12d7329 | ||
|
|
f4240e5229 | ||
|
|
7759649963 | ||
|
|
c5e6d6f2cb | ||
|
|
ec850f2cf0 | ||
|
|
ff52ad5345 | ||
|
|
5de50c70c6 | ||
|
|
94f8949ca2 | ||
|
|
44fc3c8d2d |
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- platform: 'macos-latest' # for Intel-based Macs.
|
||||
args: '--target x86_64-apple-darwin'
|
||||
yaak_arch: 'x64'
|
||||
- platform: 'ubuntu-22.04' # for Tauri v1, you could replace this with ubuntu-20.04.
|
||||
- platform: 'ubuntu-22.04'
|
||||
args: ''
|
||||
yaak_arch: 'x64'
|
||||
- platform: 'windows-latest'
|
||||
@@ -38,10 +38,6 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -56,7 +52,7 @@ jobs:
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
src-tauri/target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-
|
||||
|
||||
@@ -66,6 +62,10 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: cargo install --force trusted-signing-cli
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: |
|
||||
npm ci
|
||||
@@ -94,16 +94,24 @@ jobs:
|
||||
env:
|
||||
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
|
||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
|
||||
# Apple signing stuff
|
||||
APPLE_CERTIFICATE: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_TEAM_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# Windows signing stuff
|
||||
AZURE_CLIENT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_CLIENT_SECRET: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_SECRET }}
|
||||
AZURE_TENANT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_TENANT_ID }}
|
||||
with:
|
||||
tagName: 'v__VERSION__'
|
||||
releaseName: 'Release __VERSION__'
|
||||
|
||||
158
package-lock.json
generated
158
package-lock.json
generated
@@ -10,15 +10,16 @@
|
||||
"workspaces": [
|
||||
"plugin-runtime",
|
||||
"plugin-runtime-types",
|
||||
"src-tauri/yaak_license",
|
||||
"src-tauri/yaak_models",
|
||||
"src-tauri/yaak_plugin_runtime",
|
||||
"src-tauri/yaak_sse",
|
||||
"src-tauri/yaak_sync",
|
||||
"src-tauri/yaak_templates",
|
||||
"src-tauri/yaak_sse",
|
||||
"src-web"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.4",
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"eslint": "^8",
|
||||
@@ -2597,9 +2598,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.59.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.6.tgz",
|
||||
"integrity": "sha512-g58YTHe4ClRrjJ50GY9fas/0zARJVozY0Hs+hcSBOmwZaeKY+to0/LX8wKnnH/EJiLYcC1sHmE11CAS3ncfZBg==",
|
||||
"version": "5.59.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.16.tgz",
|
||||
"integrity": "sha512-crHn+G3ltqb5JG0oUv6q+PMz1m1YkjpASrXTU+sYWW9pLk0t2GybUHNRqYPZWhxgjPaVGC4yp92gSFEJgYEsPw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2618,12 +2619,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.59.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.6.tgz",
|
||||
"integrity": "sha512-sGg2sNKg8cYf6aS1dzDf4weN+Vt9PfUu+0btwerrbtYysdNBbcGD4rPe9jhPgMtpDDlvi4cbLv+j1Qo814Kf+Q==",
|
||||
"version": "5.59.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.16.tgz",
|
||||
"integrity": "sha512-MuyWheG47h6ERd4PKQ6V8gDyBu3ThNG22e1fRVwvq6ap3EqsFhyuxCAwhNP/03m/mLg+DAb0upgbPaX6VB+CkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.59.6"
|
||||
"@tanstack/query-core": "5.59.16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2689,9 +2690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.4.tgz",
|
||||
"integrity": "sha512-Hl9eFXz+O366+6su9PfaSzu2EJdFe1p8K8ghkWmi40dz8VmSE7vsMTaOStD0I71ckSOkh2ICDX7FQTBgjlpjWw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.1.0.tgz",
|
||||
"integrity": "sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -2705,22 +2706,22 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.0.4",
|
||||
"@tauri-apps/cli-darwin-x64": "2.0.4",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.4",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.0.4",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.0.4",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.0.4",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.0.4",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.0.4",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.0.4",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.0.4"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.1.0",
|
||||
"@tauri-apps/cli-darwin-x64": "2.1.0",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.1.0",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.1.0",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.1.0",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.1.0",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.1.0",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.1.0",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.1.0",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.4.tgz",
|
||||
"integrity": "sha512-siH7rOHobb16rPbc11k64p1mxIpiRCkWmzs2qmL5IX21Gx9K5onI3Tk67Oqpf2uNupbYzItrOttaDT4NHFC7tw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.1.0.tgz",
|
||||
"integrity": "sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2735,9 +2736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.4.tgz",
|
||||
"integrity": "sha512-zIccfbCoZMfmUpnk6PFCV0keFyfVj1A9XV3Oiiitj/dkTZ9CQvzjhX3XC0XcK4rsTWegfr2PjSrK06aiPAROAw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.1.0.tgz",
|
||||
"integrity": "sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2752,9 +2753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.4.tgz",
|
||||
"integrity": "sha512-fgQqJzefOGWCBNg4yrVA82Rg4s1XQr5K0dc2rCxBhJfa696e8dQ1LDrnWq/AiO5r+uHfVaoQTIUvxxpFicYRSA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.1.0.tgz",
|
||||
"integrity": "sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2769,9 +2770,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.4.tgz",
|
||||
"integrity": "sha512-u8wbt5tPA9pI6j+d7jGrfOz9UVCiTp+IYzKNiIqlrDsAjqAUFaNXYHKqOUboeFWEmI4zoCWj6LgpS2OJTQ5FKg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.1.0.tgz",
|
||||
"integrity": "sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2786,9 +2787,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.4.tgz",
|
||||
"integrity": "sha512-hntF1V8e3V1hlrESm93PsghDhf3lA5pbvFrRfYxU1c+fVD/jRXGVw8BH3O1lW8MWwhEg1YdhKk01oAgsuHLuig==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.0.tgz",
|
||||
"integrity": "sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2803,9 +2804,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.4.tgz",
|
||||
"integrity": "sha512-Iq1GGJb+oT1T0ZV8izrgf0cBtlzPCJaWcNueRbf1ZXquMf+FSTyQv+/Lo8rq5T6buOIJOH7cAOTuEWWqiCZteg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.1.0.tgz",
|
||||
"integrity": "sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2820,9 +2821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.4.tgz",
|
||||
"integrity": "sha512-9NTk6Pf0bSwXqCBdAA+PDYts9HeHebZzIo8mbRzRyUbER6QngG5HZb9Ka36Z1QWtJjdRy6uxSb4zb/9NuTeHfA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.0.tgz",
|
||||
"integrity": "sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2837,9 +2838,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.4.tgz",
|
||||
"integrity": "sha512-OF2e9oxiBFR8A8wVMOhUx9QGN/I1ZkquWC7gVQBnA56nx9PabJlDT08QBy5UD8USqZFVznnfNr2ehlheQahb3g==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.1.0.tgz",
|
||||
"integrity": "sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2854,9 +2855,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.4.tgz",
|
||||
"integrity": "sha512-T+hCKB3rFP6q0saHHtR02hm6wr1ZPJ0Mkii3oRTxjPG6BBXoVzHNCYzvdgEGJPTA2sFuAQtJH764NRtNlDMifw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.1.0.tgz",
|
||||
"integrity": "sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2871,9 +2872,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.4.tgz",
|
||||
"integrity": "sha512-GVaiI3KWRFLomjJmApHqihhYlkJ+7FqhumhVfBO6Z2tWzZjQyVQgTdNp0kYEuW2WoAYEj0dKY6qd4YM33xYcUA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.1.0.tgz",
|
||||
"integrity": "sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3127,6 +3128,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/whatwg-mimetype": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
@@ -3361,6 +3369,10 @@
|
||||
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@yaakapp-internal/license": {
|
||||
"resolved": "src-tauri/yaak_license",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/models": {
|
||||
"resolved": "src-tauri/yaak_models",
|
||||
"link": true
|
||||
@@ -3377,7 +3389,7 @@
|
||||
"resolved": "src-tauri/yaak_sse",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/template": {
|
||||
"node_modules/@yaakapp-internal/templates": {
|
||||
"resolved": "src-tauri/yaak_templates",
|
||||
"link": true
|
||||
},
|
||||
@@ -4922,10 +4934,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"license": "MIT",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -9218,11 +9229,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
@@ -10482,11 +10492,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-devtools/node_modules/execa/node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -13242,6 +13251,15 @@
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
@@ -13721,6 +13739,10 @@
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"src-tauri/yaak_license": {
|
||||
"name": "@yaakapp-internal/license",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"src-tauri/yaak_models": {
|
||||
"name": "@yaakapp-internal/models",
|
||||
"version": "1.0.0"
|
||||
@@ -13734,7 +13756,7 @@
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"src-tauri/yaak_templates": {
|
||||
"name": "@yaakapp-internal/template",
|
||||
"name": "@yaakapp-internal/templates",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"src-web": {
|
||||
@@ -13751,7 +13773,7 @@
|
||||
"@lezer/lr": "^1.3.3",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.55.4",
|
||||
"@tanstack/react-query": "^5.59.16",
|
||||
"@tanstack/react-virtual": "^3.10.8",
|
||||
"@tauri-apps/api": "^2.0.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
@@ -13786,6 +13808,7 @@
|
||||
"react-use": "^17.5.1",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^10.0.0",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"xml-formatter": "^3.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -13799,6 +13822,7 @@
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"decompress": "^4.2.1",
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yaakapp/app.git"
|
||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
||||
},
|
||||
"workspaces": [
|
||||
"plugin-runtime",
|
||||
"plugin-runtime-types",
|
||||
"src-tauri/yaak_license",
|
||||
"src-tauri/yaak_models",
|
||||
"src-tauri/yaak_plugin_runtime",
|
||||
"src-tauri/yaak_sse",
|
||||
"src-tauri/yaak_sync",
|
||||
"src-tauri/yaak_templates",
|
||||
"src-tauri/yaak_sse",
|
||||
"src-web"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -31,7 +32,7 @@
|
||||
"tauri-before-dev": "npm run --workspaces --if-present dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.4",
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"eslint": "^8",
|
||||
|
||||
@@ -6,7 +6,10 @@ import { PluginHandle } from './PluginHandle';
|
||||
|
||||
const port = process.env.PORT || '50051';
|
||||
|
||||
const channel = createChannel(`localhost:${port}`);
|
||||
const channel = createChannel(`localhost:${port}`, undefined, {
|
||||
'grpc.max_receive_message_length': Number.MAX_SAFE_INTEGER,
|
||||
'grpc.max_send_message_length': Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
const client: PluginRuntimeClient = createClient(PluginRuntimeDefinition, channel);
|
||||
|
||||
const events = new EventChannel();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const decompress = require('decompress');
|
||||
const Downloader = require('nodejs-file-downloader');
|
||||
const path = require('node:path');
|
||||
const { rmSync, mkdirSync, cpSync, existsSync } = require('node:fs');
|
||||
const { rmSync, mkdirSync, cpSync, existsSync, statSync, chmodSync } = require('node:fs');
|
||||
const { execSync } = require('node:child_process');
|
||||
|
||||
const VERSION = '27.2';
|
||||
const VERSION = '28.3';
|
||||
|
||||
// `${process.platform}_${process.arch}`
|
||||
const MAC_ARM = 'darwin_arm64';
|
||||
@@ -67,6 +67,11 @@ mkdirSync(dstDir, { recursive: true });
|
||||
cpSync(includeSrc, includeDst, { recursive: true });
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
// Make binary writable, so we can sign it during release
|
||||
const stat = statSync(binDst);
|
||||
const newMode = stat.mode | 0o200;
|
||||
chmodSync(binDst, newMode);
|
||||
|
||||
console.log('Downloaded protoc to', binDst);
|
||||
})().catch((err) => console.log('Script failed:', err));
|
||||
|
||||
|
||||
360
src-tauri/Cargo.lock
generated
360
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,19 @@
|
||||
[workspace]
|
||||
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models", "yaak_sse"]
|
||||
members = [
|
||||
"yaak_grpc",
|
||||
"yaak_license",
|
||||
"yaak_models",
|
||||
"yaak_plugin_runtime",
|
||||
"yaak_sse",
|
||||
"yaak_templates",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
# Produce a library for mobile support
|
||||
[lib]
|
||||
@@ -16,7 +24,7 @@ crate-type = ["staticlib", "cdylib", "lib"]
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.2", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
@@ -27,11 +35,11 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa
|
||||
|
||||
[dependencies]
|
||||
yaak_grpc = { path = "yaak_grpc" }
|
||||
yaak_templates = { path = "yaak_templates" }
|
||||
yaak_plugin_runtime = { workspace = true }
|
||||
tauri-plugin-yaak-license = { path = "yaak_license" }
|
||||
yaak_models = { workspace = true }
|
||||
yaak_sse = { path = "yaak_sse" }
|
||||
anyhow = "1.0.86"
|
||||
yaak_plugin_runtime = { workspace = true }
|
||||
yaak_sse = { workspace = true }
|
||||
yaak_templates = { path = "yaak_templates" }
|
||||
base64 = "0.22.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
datetime = "0.5.2"
|
||||
@@ -40,12 +48,11 @@ http = "1"
|
||||
log = "0.4.21"
|
||||
rand = "0.8.5"
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.12.4", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
|
||||
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
|
||||
reqwest_cookie_store = "0.8.0"
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = { version = "1.0.116", features = ["raw_value"] }
|
||||
serde_yaml = "0.9.34"
|
||||
tauri = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = "2.0.1"
|
||||
tauri-plugin-dialog = "2.0.3"
|
||||
@@ -57,13 +64,18 @@ tauri-plugin-window-state = "2.0.1"
|
||||
tokio = { version = "1.36.0", features = ["sync"] }
|
||||
tokio-stream = "0.1.15"
|
||||
uuid = "1.7.0"
|
||||
thiserror = "1.0.61"
|
||||
mime_guess = "2.0.5"
|
||||
urlencoding = "2.1.3"
|
||||
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.13.0" }
|
||||
|
||||
[workspace.dependencies]
|
||||
yaak_models = { path = "yaak_models" }
|
||||
yaak_sse = { path = "yaak_sse" }
|
||||
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
|
||||
serde = "1.0.215"
|
||||
serde_json = "1.0.132"
|
||||
tauri-plugin-shell = "2.0.2"
|
||||
tauri = { version = "2.0.6", features = ["devtools", "protocol-asset"] }
|
||||
tauri = "2.1.1"
|
||||
thiserror = "2.0.3"
|
||||
ts-rs = "10.0.0"
|
||||
reqwest = "0.12.4"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"*"
|
||||
],
|
||||
"permissions": [
|
||||
"yaak-license:default",
|
||||
"core:event:allow-emit",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten",
|
||||
|
||||
2
src-tauri/gen/schemas/acl-manifests.json
generated
2
src-tauri/gen/schemas/acl-manifests.json
generated
File diff suppressed because one or more lines are too long
2
src-tauri/gen/schemas/capabilities.json
generated
2
src-tauri/gen/schemas/capabilities.json
generated
@@ -1 +1 @@
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["yaak-license:default","core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-toggle-maximize","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
27
src-tauri/gen/schemas/desktop-schema.json
generated
27
src-tauri/gen/schemas/desktop-schema.json
generated
@@ -84,7 +84,7 @@
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
|
||||
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionEntry"
|
||||
@@ -5141,6 +5141,31 @@
|
||||
"description": "Denies the save_window_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-save-window-state"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "yaak-license:default"
|
||||
},
|
||||
{
|
||||
"description": "Enables the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:allow-activate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:allow-check"
|
||||
},
|
||||
{
|
||||
"description": "Denies the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:deny-activate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:deny-check"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
27
src-tauri/gen/schemas/macOS-schema.json
generated
27
src-tauri/gen/schemas/macOS-schema.json
generated
@@ -84,7 +84,7 @@
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
|
||||
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionEntry"
|
||||
@@ -5141,6 +5141,31 @@
|
||||
"description": "Denies the save_window_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-save-window-state"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "yaak-license:default"
|
||||
},
|
||||
{
|
||||
"description": "Enables the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:allow-activate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:allow-check"
|
||||
},
|
||||
{
|
||||
"description": "Denies the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:deny-activate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "yaak-license:deny-check"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -243,7 +243,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
let request_body = rendered_request.body;
|
||||
if let Some(body_type) = &rendered_request.body_type {
|
||||
if request_body.contains_key("query") && request_body.contains_key("variables") {
|
||||
if body_type == "graphql" {
|
||||
let query = get_str_h(&request_body, "query");
|
||||
let variables = get_str_h(&request_body, "variables");
|
||||
let body = if variables.trim().is_empty() {
|
||||
@@ -255,9 +255,6 @@ pub async fn send_http_request<R: Runtime>(
|
||||
)
|
||||
};
|
||||
request_builder = request_builder.body(body.to_owned());
|
||||
} else if request_body.contains_key("text") {
|
||||
let body = get_str_h(&request_body, "text");
|
||||
request_builder = request_builder.body(body.to_owned());
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
&& request_body.contains_key("form")
|
||||
{
|
||||
@@ -359,6 +356,9 @@ pub async fn send_http_request<R: Runtime>(
|
||||
}
|
||||
headers.remove("Content-Type"); // reqwest will add this automatically
|
||||
request_builder = request_builder.multipart(multipart_form);
|
||||
} else if request_body.contains_key("text") {
|
||||
let body = get_str_h(&request_body, "text");
|
||||
request_builder = request_builder.body(body.to_owned());
|
||||
} else {
|
||||
warn!("Unsupported body type: {}", body_type);
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ use yaak_plugin_runtime::events::{
|
||||
};
|
||||
use yaak_plugin_runtime::plugin_handle::PluginHandle;
|
||||
use yaak_sse::sse::ServerSentEvent;
|
||||
use yaak_templates::{Parser, Tokens};
|
||||
use yaak_templates::format::format_json;
|
||||
use yaak_templates::{Parser, Tokens};
|
||||
|
||||
mod analytics;
|
||||
mod export_resources;
|
||||
@@ -174,7 +174,10 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||
) -> Result<Vec<ServiceDefinition>, String> {
|
||||
let req = get_grpc_request(&window, request_id).await.map_err(|e| e.to_string())?;
|
||||
let req = get_grpc_request(&window, request_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("Failed to find GRPC request")?;
|
||||
|
||||
let uri = safe_uri(&req.url);
|
||||
|
||||
@@ -201,7 +204,10 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
Some(id) => Some(get_environment(&window, id).await.map_err(|e| e.to_string())?),
|
||||
None => None,
|
||||
};
|
||||
let req = get_grpc_request(&window, request_id).await.map_err(|e| e.to_string())?;
|
||||
let req = get_grpc_request(&window, request_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("Failed to find GRPC request")?;
|
||||
let workspace = get_workspace(&window, &req.workspace_id).await.map_err(|e| e.to_string())?;
|
||||
let req = render_grpc_request(
|
||||
&req,
|
||||
@@ -1436,12 +1442,12 @@ async fn cmd_get_folder(id: &str, w: WebviewWindow) -> Result<Folder, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_get_grpc_request(id: &str, w: WebviewWindow) -> Result<GrpcRequest, String> {
|
||||
async fn cmd_get_grpc_request(id: &str, w: WebviewWindow) -> Result<Option<GrpcRequest>, String> {
|
||||
get_grpc_request(&w, id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_get_http_request(id: &str, w: WebviewWindow) -> Result<HttpRequest, String> {
|
||||
async fn cmd_get_http_request(id: &str, w: WebviewWindow) -> Result<Option<HttpRequest>, String> {
|
||||
get_http_request(&w, id).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -1681,6 +1687,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(yaak_models::plugin::Builder::default().build())
|
||||
.plugin(tauri_plugin_yaak_license::init())
|
||||
.plugin(yaak_plugin_runtime::plugin::init());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -2079,7 +2086,7 @@ async fn handle_plugin_event<R: Runtime>(
|
||||
}))
|
||||
}
|
||||
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
|
||||
let http_request = get_http_request(app_handle, req.id.as_str()).await.ok();
|
||||
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
|
||||
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
|
||||
http_request,
|
||||
}))
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"externalBin": [
|
||||
"vendored/protoc/yaakprotoc",
|
||||
"vendored/node/yaaknode"
|
||||
"vendored/node/yaaknode",
|
||||
"vendored/protoc/yaakprotoc"
|
||||
],
|
||||
"icon": [
|
||||
"icons/release/32x32.png",
|
||||
@@ -79,8 +79,7 @@
|
||||
"frameworks": []
|
||||
},
|
||||
"windows": {
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
"signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "yaak_grpc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tonic = "0.10.2"
|
||||
@@ -22,3 +23,4 @@ tauri = { workspace = true }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
md5 = "0.7.0"
|
||||
dunce = "1.0.4"
|
||||
async-recursion = "1.1.1"
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use async_recursion::async_recursion;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
@@ -38,9 +39,8 @@ pub async fn fill_pool_from_files(
|
||||
.expect("failed to resolve protoc include directory");
|
||||
|
||||
// HACK: Remove UNC prefix for Windows paths
|
||||
let global_import_dir = dunce::simplified(global_import_dir.as_path())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let global_import_dir =
|
||||
dunce::simplified(global_import_dir.as_path()).to_string_lossy().to_string();
|
||||
let desc_path = dunce::simplified(desc_path.as_path());
|
||||
|
||||
let mut args = vec![
|
||||
@@ -89,12 +89,9 @@ pub async fn fill_pool_from_files(
|
||||
|
||||
let bytes = fs::read(desc_path).await.map_err(|e| e.to_string())?;
|
||||
let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?;
|
||||
pool.add_file_descriptor_set(fdp)
|
||||
.map_err(|e| e.to_string())?;
|
||||
pool.add_file_descriptor_set(fdp).map_err(|e| e.to_string())?;
|
||||
|
||||
fs::remove_file(desc_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
fs::remove_file(desc_path).await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
@@ -107,6 +104,10 @@ pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, Stri
|
||||
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||
continue;
|
||||
}
|
||||
if service == "grpc.reflection.v1.ServerReflection"{
|
||||
// TODO: update reflection client to use v1
|
||||
continue;
|
||||
}
|
||||
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
|
||||
}
|
||||
|
||||
@@ -120,10 +121,7 @@ pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
|
||||
http_connector.enforce_http(false);
|
||||
http_connector
|
||||
});
|
||||
Client::builder()
|
||||
.pool_max_idle_per_host(0)
|
||||
.http2_only(true)
|
||||
.build(connector)
|
||||
Client::builder().pool_max_idle_per_host(0).http2_only(true).build(connector)
|
||||
}
|
||||
|
||||
async fn list_services(
|
||||
@@ -137,11 +135,7 @@ async fn list_services(
|
||||
_ => panic!("Expected a ListServicesResponse variant"),
|
||||
};
|
||||
|
||||
Ok(list_services_response
|
||||
.service
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect::<Vec<_>>())
|
||||
Ok(list_services_response.service.iter().map(|s| s.name.clone()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
async fn file_descriptor_set_from_service_name(
|
||||
@@ -153,14 +147,11 @@ async fn file_descriptor_set_from_service_name(
|
||||
client,
|
||||
MessageRequest::FileContainingSymbol(service_name.into()),
|
||||
)
|
||||
.await
|
||||
.await
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Error fetching file descriptor for service {}: {}",
|
||||
service_name, e
|
||||
);
|
||||
warn!("Error fetching file descriptor for service {}: {}", service_name, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -170,16 +161,37 @@ async fn file_descriptor_set_from_service_name(
|
||||
_ => panic!("Expected a FileDescriptorResponse variant"),
|
||||
};
|
||||
|
||||
for fd in file_descriptor_response.file_descriptor_proto {
|
||||
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn add_file_descriptors_to_pool(
|
||||
fds: Vec<Vec<u8>>,
|
||||
pool: &mut DescriptorPool,
|
||||
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
|
||||
) {
|
||||
let mut topo_sort = topology::SimpleTopoSort::new();
|
||||
let mut fd_mapping = std::collections::HashMap::with_capacity(fds.len());
|
||||
|
||||
for fd in fds {
|
||||
let fdp = FileDescriptorProto::decode(fd.deref()).unwrap();
|
||||
|
||||
// Add deps first or else we'll get an error
|
||||
for dep_name in fdp.clone().dependency {
|
||||
file_descriptor_set_by_filename(&dep_name, pool, client).await;
|
||||
}
|
||||
topo_sort.insert(fdp.name().to_string(), fdp.dependency.clone());
|
||||
fd_mapping.insert(fdp.name().to_string(), fdp);
|
||||
}
|
||||
|
||||
pool.add_file_descriptor_proto(fdp)
|
||||
.expect("add file descriptor proto");
|
||||
for node in topo_sort {
|
||||
match node {
|
||||
Ok(node) => {
|
||||
if let Some(fdp) = fd_mapping.remove(&node) {
|
||||
pool.add_file_descriptor_proto(fdp).expect("add file descriptor proto");
|
||||
} else {
|
||||
file_descriptor_set_by_filename(node.as_str(), pool, client).await;
|
||||
}
|
||||
}
|
||||
Err(_) => panic!("proto file got cycle!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,11 +218,8 @@ async fn file_descriptor_set_by_filename(
|
||||
}
|
||||
};
|
||||
|
||||
for fd in file_descriptor_response.file_descriptor_proto {
|
||||
let fdp = FileDescriptorProto::decode(fd.deref()).unwrap();
|
||||
pool.add_file_descriptor_proto(fdp)
|
||||
.expect("add file descriptor proto");
|
||||
}
|
||||
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_reflection_request(
|
||||
@@ -249,4 +258,137 @@ pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
|
||||
.ok_or_else(|| anyhow!("invalid method path"))
|
||||
.expect("invalid method path");
|
||||
PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path")
|
||||
}
|
||||
}
|
||||
|
||||
mod topology {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub struct SimpleTopoSort<T> {
|
||||
out_graph: HashMap<T, HashSet<T>>,
|
||||
in_graph: HashMap<T, HashSet<T>>,
|
||||
}
|
||||
|
||||
impl<T> SimpleTopoSort<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
SimpleTopoSort {
|
||||
out_graph: HashMap::new(),
|
||||
in_graph: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert<I: IntoIterator<Item = T>>(&mut self, node: T, deps: I) {
|
||||
self.out_graph.entry(node.clone()).or_insert(HashSet::new());
|
||||
for dep in deps {
|
||||
self.out_graph.entry(node.clone()).or_insert(HashSet::new()).insert(dep.clone());
|
||||
self.in_graph.entry(dep.clone()).or_insert(HashSet::new()).insert(node.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for SimpleTopoSort<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
type IntoIter = SimpleTopoSortIter<T>;
|
||||
type Item = <SimpleTopoSortIter<T> as Iterator>::Item;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
SimpleTopoSortIter::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SimpleTopoSortIter<T> {
|
||||
data: SimpleTopoSort<T>,
|
||||
zero_indegree: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> SimpleTopoSortIter<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
pub fn new(data: SimpleTopoSort<T>) -> Self {
|
||||
let mut zero_indegree = Vec::new();
|
||||
for (node, _) in data.in_graph.iter() {
|
||||
if !data.out_graph.contains_key(node) {
|
||||
zero_indegree.push(node.clone());
|
||||
}
|
||||
}
|
||||
for (node, deps) in data.out_graph.iter(){
|
||||
if deps.is_empty(){
|
||||
zero_indegree.push(node.clone());
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTopoSortIter {
|
||||
data,
|
||||
zero_indegree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Iterator for SimpleTopoSortIter<T>
|
||||
where
|
||||
T: Eq + std::hash::Hash + Clone,
|
||||
{
|
||||
type Item = Result<T, &'static str>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.zero_indegree.is_empty() {
|
||||
if self.data.out_graph.is_empty() {
|
||||
return None;
|
||||
}
|
||||
return Some(Err("Cycle detected"));
|
||||
}
|
||||
|
||||
let node = self.zero_indegree.pop().unwrap();
|
||||
if let Some(parents) = self.data.in_graph.get(&node){
|
||||
for parent in parents.iter(){
|
||||
let deps = self.data.out_graph.get_mut(parent).unwrap();
|
||||
deps.remove(&node);
|
||||
if deps.is_empty() {
|
||||
self.zero_indegree.push(parent.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.data.out_graph.remove(&node);
|
||||
|
||||
Some(Ok(node))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort(){
|
||||
{
|
||||
let mut topo_sort = SimpleTopoSort::new();
|
||||
topo_sort.insert("a", []);
|
||||
|
||||
for node in topo_sort {
|
||||
match node {
|
||||
Ok(n) => assert_eq!(n, "a"),
|
||||
Err(e) => panic!("err {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut topo_sort = SimpleTopoSort::new();
|
||||
topo_sort.insert("a", ["b"]);
|
||||
topo_sort.insert("b", []);
|
||||
|
||||
let mut iter = topo_sort.into_iter();
|
||||
match iter.next() {
|
||||
Some(Ok(n)) => assert_eq!(n, "b"),
|
||||
_ => panic!("err"),
|
||||
}
|
||||
match iter.next() {
|
||||
Some(Ok(n)) => assert_eq!(n, "a"),
|
||||
_ => panic!("err"),
|
||||
}
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
20
src-tauri/yaak_license/Cargo.toml
Normal file
20
src-tauri/yaak_license/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "tauri-plugin-yaak-license"
|
||||
links = "tauri-plugin-yaak-license"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
ts-rs = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
yaak_models = { workspace = true }
|
||||
chrono = "0.4.38"
|
||||
log = "0.4.22"
|
||||
serde_json = "1.0.132"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.3", features = ["build"] }
|
||||
11
src-tauri/yaak_license/bindings/license.ts
Normal file
11
src-tauri/yaak_license/bindings/license.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type APIErrorResponsePayload = { error: string, message: string, };
|
||||
|
||||
export type ActivateLicenseRequestPayload = { licenseKey: string, appVersion: string, appPlatform: string, };
|
||||
|
||||
export type ActivateLicenseResponsePayload = { activationId: string, };
|
||||
|
||||
export type CheckActivationResponsePayload = { active: boolean, };
|
||||
|
||||
export type LicenseCheckStatus = { "type": "personal_use" } | { "type": "commercial_use" } | { "type": "trialing", end: string, } | { "type": "trial_ended", end: string, };
|
||||
3
src-tauri/yaak_license/bindings/models.ts
Normal file
3
src-tauri/yaak_license/bindings/models.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CheckActivationRequestPayload = { activationId: string, };
|
||||
5
src-tauri/yaak_license/build.rs
Normal file
5
src-tauri/yaak_license/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
const COMMANDS: &[&str] = &["activate", "check"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS).build();
|
||||
}
|
||||
33
src-tauri/yaak_license/index.ts
Normal file
33
src-tauri/yaak_license/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useListenToTauriEvent } from '@yaakapp/app/hooks/useListenToTauriEvent';
|
||||
import { LicenseCheckStatus } from './bindings/license';
|
||||
|
||||
export * from './bindings/license';
|
||||
|
||||
export function useLicense() {
|
||||
const queryClient = useQueryClient();
|
||||
const activate = useMutation<void, string, { licenseKey: string }>({
|
||||
mutationKey: ['license.activate'],
|
||||
mutationFn: (payload) => invoke('plugin:yaak-license|activate', payload),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
|
||||
},
|
||||
});
|
||||
|
||||
// Check the license again after a license is activated
|
||||
useListenToTauriEvent('license-activated', async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: CHECK_QUERY_KEY });
|
||||
});
|
||||
|
||||
const CHECK_QUERY_KEY = ['license.check'];
|
||||
const check = useQuery<void, string, LicenseCheckStatus>({
|
||||
queryKey: CHECK_QUERY_KEY,
|
||||
queryFn: () => invoke('plugin:yaak-license|check'),
|
||||
});
|
||||
|
||||
return {
|
||||
activate,
|
||||
check,
|
||||
} as const;
|
||||
}
|
||||
6
src-tauri/yaak_license/package.json
Normal file
6
src-tauri/yaak_license/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/license",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-activate"
|
||||
description = "Enables the activate command without any pre-configured scope."
|
||||
commands.allow = ["activate"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-activate"
|
||||
description = "Denies the activate command without any pre-configured scope."
|
||||
commands.deny = ["activate"]
|
||||
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-check"
|
||||
description = "Enables the check command without any pre-configured scope."
|
||||
commands.allow = ["check"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-check"
|
||||
description = "Denies the check command without any pre-configured scope."
|
||||
commands.deny = ["check"]
|
||||
@@ -0,0 +1,68 @@
|
||||
## Default Permission
|
||||
|
||||
Default permissions for the plugin
|
||||
|
||||
- `allow-check`
|
||||
- `allow-activate`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-license:allow-activate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the activate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-license:deny-activate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the activate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-license:allow-check`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the check command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`yaak-license:deny-check`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the check command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
3
src-tauri/yaak_license/permissions/default.toml
Normal file
3
src-tauri/yaak_license/permissions/default.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = ["allow-check", "allow-activate"]
|
||||
325
src-tauri/yaak_license/permissions/schemas/schema.json
Normal file
325
src-tauri/yaak_license/permissions/schemas/schema.json
Normal file
@@ -0,0 +1,325 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-activate"
|
||||
},
|
||||
{
|
||||
"description": "Denies the activate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-activate"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-check"
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-check"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src-tauri/yaak_license/src/commands.rs
Normal file
37
src-tauri/yaak_license/src/commands.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::errors::Result;
|
||||
use crate::{activate_license, check_license, ActivateLicenseRequestPayload, LicenseCheckStatus};
|
||||
use log::{debug, info};
|
||||
use std::string::ToString;
|
||||
use tauri::{command, AppHandle, Manager, Runtime, WebviewWindow};
|
||||
|
||||
#[command]
|
||||
pub async fn check<R: Runtime>(app_handle: AppHandle<R>) -> Result<LicenseCheckStatus> {
|
||||
debug!("Checking license");
|
||||
check_license(&app_handle).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn activate<R: Runtime>(license_key: &str, window: WebviewWindow<R>) -> Result<()> {
|
||||
info!("Activating license {}", license_key);
|
||||
activate_license(
|
||||
&window,
|
||||
ActivateLicenseRequestPayload {
|
||||
license_key: license_key.to_string(),
|
||||
app_platform: get_os().to_string(),
|
||||
app_version: window.app_handle().package_info().version.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
28
src-tauri/yaak_license/src/errors.rs
Normal file
28
src-tauri/yaak_license/src/errors.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Reqwest error: {0}")]
|
||||
APIError(#[from] reqwest::Error),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("{message}")]
|
||||
ClientError { message: String, error: String },
|
||||
|
||||
#[error("Internal server error")]
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
16
src-tauri/yaak_license/src/lib.rs
Normal file
16
src-tauri/yaak_license/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use tauri::{
|
||||
generate_handler,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Runtime,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
mod errors;
|
||||
mod license;
|
||||
|
||||
use crate::commands::{activate, check};
|
||||
pub use license::*;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-license").invoke_handler(generate_handler![check, activate]).build()
|
||||
}
|
||||
155
src-tauri/yaak_license/src/license.rs
Normal file
155
src-tauri/yaak_license/src/license.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use crate::errors::Error::{ClientError, ServerError};
|
||||
use crate::errors::Result;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use log::{debug, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
use std::time::Duration;
|
||||
use tauri::{is_dev, AppHandle, Emitter, Runtime, WebviewWindow};
|
||||
use ts_rs::TS;
|
||||
|
||||
const KV_NAMESPACE: &str = "license";
|
||||
const KV_ACTIVATION_ID_KEY: &str = "activation_id";
|
||||
const TRIAL_SECONDS: u64 = 3600 * 24 * 14;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "models.ts")]
|
||||
pub struct CheckActivationRequestPayload {
|
||||
pub activation_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "license.ts")]
|
||||
pub struct CheckActivationResponsePayload {
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "license.ts")]
|
||||
pub struct ActivateLicenseRequestPayload {
|
||||
pub license_key: String,
|
||||
pub app_version: String,
|
||||
pub app_platform: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "license.ts")]
|
||||
pub struct ActivateLicenseResponsePayload {
|
||||
pub activation_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "license.ts")]
|
||||
pub struct APIErrorResponsePayload {
|
||||
pub error: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub async fn activate_license<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
p: ActivateLicenseRequestPayload,
|
||||
) -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(build_url("/activate")).json(&p).send().await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let body: APIErrorResponsePayload = response.json().await?;
|
||||
return Err(ClientError {
|
||||
message: body.message,
|
||||
error: body.error,
|
||||
});
|
||||
}
|
||||
|
||||
if response.status().is_server_error() {
|
||||
return Err(ServerError);
|
||||
}
|
||||
|
||||
let body: ActivateLicenseResponsePayload = response.json().await?;
|
||||
yaak_models::queries::set_key_value_string(
|
||||
window,
|
||||
KV_ACTIVATION_ID_KEY,
|
||||
KV_NAMESPACE,
|
||||
body.activation_id.as_str(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = window.emit("license-activated", true) {
|
||||
warn!("Failed to emit check-license event: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
#[ts(export, export_to = "license.ts")]
|
||||
pub enum LicenseCheckStatus {
|
||||
PersonalUse,
|
||||
CommercialUse,
|
||||
InvalidLicense,
|
||||
Trialing { end: NaiveDateTime },
|
||||
TrialEnded { end: NaiveDateTime },
|
||||
}
|
||||
|
||||
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<LicenseCheckStatus> {
|
||||
let activation_id = yaak_models::queries::get_key_value_string(
|
||||
app_handle,
|
||||
KV_ACTIVATION_ID_KEY,
|
||||
KV_NAMESPACE,
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
let settings = yaak_models::queries::get_or_create_settings(app_handle).await;
|
||||
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
|
||||
|
||||
debug!("Trial ending at {trial_end:?}");
|
||||
|
||||
let has_activation_id = !activation_id.is_empty();
|
||||
let trial_period_active = Utc::now().naive_utc() < trial_end;
|
||||
|
||||
match (has_activation_id, trial_period_active) {
|
||||
(false, true) => Ok(LicenseCheckStatus::Trialing { end: trial_end }),
|
||||
(false, false) => Ok(LicenseCheckStatus::TrialEnded { end: trial_end }),
|
||||
(true, _) => {
|
||||
info!("Checking license activation");
|
||||
// A license has been activated, so let's check the license server
|
||||
let client = reqwest::Client::new();
|
||||
let payload = CheckActivationRequestPayload {
|
||||
activation_id: activation_id.clone(),
|
||||
};
|
||||
let response = client.post(build_url("/check")).json(&payload).send().await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let body: APIErrorResponsePayload = response.json().await?;
|
||||
return Err(ClientError {
|
||||
message: body.message,
|
||||
error: body.error,
|
||||
});
|
||||
}
|
||||
|
||||
if response.status().is_server_error() {
|
||||
return Err(ServerError);
|
||||
}
|
||||
|
||||
let body: CheckActivationResponsePayload = response.json().await?;
|
||||
if !body.active {
|
||||
return Ok(LicenseCheckStatus::InvalidLicense);
|
||||
}
|
||||
|
||||
Ok(LicenseCheckStatus::CommercialUse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_url(path: &str) -> String {
|
||||
if is_dev() {
|
||||
format!("http://localhost:9444/licenses{path}")
|
||||
} else {
|
||||
format!("https://license.yaak.app/licenses{path}")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "yaak_models"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
@@ -11,7 +12,7 @@ sea-query-rusqlite = { version = "0.6.0", features = ["with-chrono"] }
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde_json = "1.0.122"
|
||||
thiserror = "1.0.63"
|
||||
ts-rs = { version = "10.0.0", features = ["chrono-impl", "serde-json-impl"] }
|
||||
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
|
||||
tauri = { workspace = true }
|
||||
sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||
log = "0.4.22"
|
||||
|
||||
@@ -6,8 +6,10 @@ pub enum Error {
|
||||
SqlError(#[from] rusqlite::Error),
|
||||
#[error("JSON error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("Model not found {0}")]
|
||||
ModelNotFound(String),
|
||||
#[error("unknown error")]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::fs;
|
||||
|
||||
use crate::error::Error::ModelNotFound;
|
||||
use crate::error::Result;
|
||||
use crate::models::{
|
||||
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
|
||||
@@ -299,7 +300,12 @@ pub async fn duplicate_grpc_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
id: &str,
|
||||
) -> Result<GrpcRequest> {
|
||||
let mut request = get_grpc_request(window, id).await?.clone();
|
||||
let mut request = match get_grpc_request(window, id).await? {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
return Err(ModelNotFound(id.to_string()));
|
||||
}
|
||||
};
|
||||
request.id = "".to_string();
|
||||
upsert_grpc_request(window, &request).await
|
||||
}
|
||||
@@ -308,7 +314,12 @@ pub async fn delete_grpc_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
id: &str,
|
||||
) -> Result<GrpcRequest> {
|
||||
let req = get_grpc_request(window, id).await?;
|
||||
let req = match get_grpc_request(window, id).await? {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
return Err(ModelNotFound(id.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let dbm = &*window.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
@@ -393,7 +404,10 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
||||
Ok(emit_upserted_model(window, m))
|
||||
}
|
||||
|
||||
pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcRequest> {
|
||||
pub async fn get_grpc_request<R: Runtime>(
|
||||
mgr: &impl Manager<R>,
|
||||
id: &str,
|
||||
) -> Result<Option<GrpcRequest>> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
@@ -403,7 +417,7 @@ pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Re
|
||||
.cond_where(Expr::col(GrpcRequestIden::Id).eq(id))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = db.prepare(sql.as_str())?;
|
||||
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
|
||||
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into()).optional()?)
|
||||
}
|
||||
|
||||
pub async fn list_grpc_requests<R: Runtime>(
|
||||
@@ -1083,7 +1097,10 @@ pub async fn duplicate_http_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
id: &str,
|
||||
) -> Result<HttpRequest> {
|
||||
let mut request = get_http_request(window, id).await?.clone();
|
||||
let mut request = match get_http_request(window, id).await? {
|
||||
None => return Err(ModelNotFound(id.to_string())),
|
||||
Some(r) => r,
|
||||
};
|
||||
request.id = "".to_string();
|
||||
upsert_http_request(window, request).await
|
||||
}
|
||||
@@ -1181,7 +1198,10 @@ pub async fn list_http_requests<R: Runtime>(
|
||||
Ok(items.map(|v| v.unwrap()).collect())
|
||||
}
|
||||
|
||||
pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<HttpRequest> {
|
||||
pub async fn get_http_request<R: Runtime>(
|
||||
mgr: &impl Manager<R>,
|
||||
id: &str,
|
||||
) -> Result<Option<HttpRequest>> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
@@ -1191,14 +1211,17 @@ pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Re
|
||||
.cond_where(Expr::col(HttpRequestIden::Id).eq(id))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = db.prepare(sql.as_str())?;
|
||||
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
|
||||
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into()).optional()?)
|
||||
}
|
||||
|
||||
pub async fn delete_http_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
id: &str,
|
||||
) -> Result<HttpRequest> {
|
||||
let req = get_http_request(window, id).await?;
|
||||
let req = match get_http_request(window, id).await? {
|
||||
None => return Err(ModelNotFound(id.to_string())),
|
||||
Some(r) => r,
|
||||
};
|
||||
|
||||
// DB deletes will cascade but this will delete the files
|
||||
delete_all_http_responses_for_request(window, id).await?;
|
||||
@@ -1258,7 +1281,10 @@ pub async fn create_http_response<R: Runtime>(
|
||||
delete_http_response(window, response.id.as_str()).await?;
|
||||
}
|
||||
|
||||
let req = get_http_request(window, request_id).await?;
|
||||
let req = match get_http_request(window, request_id).await? {
|
||||
None => return Err(ModelNotFound(request_id.to_string())),
|
||||
Some(r) => r,
|
||||
};
|
||||
let id = generate_model_id(ModelType::TypeHttpResponse);
|
||||
let dbm = &*window.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
name = "yaak_plugin_runtime"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
command-group = "5.0.1"
|
||||
dunce = "1.0.4"
|
||||
log = "0.4.21"
|
||||
prost = "0.13.1"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.5", features = ["stream"] }
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = "1.0.113"
|
||||
tauri = { workspace = true }
|
||||
|
||||
@@ -31,7 +31,7 @@ pub struct PluginManager {
|
||||
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
|
||||
plugins: Arc<Mutex<Vec<PluginHandle>>>,
|
||||
kill_tx: tokio::sync::watch::Sender<bool>,
|
||||
server: Arc<PluginRuntimeServerImpl>,
|
||||
grpc_service: Arc<PluginRuntimeServerImpl>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -47,13 +47,13 @@ impl PluginManager {
|
||||
|
||||
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
||||
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
||||
let server =
|
||||
let grpc_service =
|
||||
PluginRuntimeServerImpl::new(events_tx, client_disconnect_tx, client_connect_tx);
|
||||
|
||||
let plugin_manager = PluginManager {
|
||||
plugins: Arc::new(Mutex::new(Vec::new())),
|
||||
subscribers: Arc::new(Mutex::new(HashMap::new())),
|
||||
server: Arc::new(server.clone()),
|
||||
grpc_service: Arc::new(grpc_service.clone()),
|
||||
kill_tx: kill_server_tx,
|
||||
};
|
||||
|
||||
@@ -78,15 +78,15 @@ impl PluginManager {
|
||||
|
||||
info!("Starting plugin server");
|
||||
|
||||
let svc = PluginRuntimeServer::new(server.to_owned());
|
||||
let svc = PluginRuntimeServer::new(grpc_service.to_owned())
|
||||
.max_encoding_message_size(usize::MAX)
|
||||
.max_decoding_message_size(usize::MAX);
|
||||
let listen_addr = match option_env!("PORT") {
|
||||
None => "localhost:0".to_string(),
|
||||
Some(port) => format!("localhost:{port}"),
|
||||
};
|
||||
let listener = tauri::async_runtime::block_on(async move {
|
||||
TcpListener::bind(listen_addr)
|
||||
.await
|
||||
.expect("Failed to bind TCP listener")
|
||||
TcpListener::bind(listen_addr).await.expect("Failed to bind TCP listener")
|
||||
});
|
||||
let addr = listener.local_addr().expect("Failed to get local address");
|
||||
|
||||
@@ -123,9 +123,7 @@ impl PluginManager {
|
||||
|
||||
// 2. Start Node.js runtime and initialize plugins
|
||||
tauri::async_runtime::block_on(async move {
|
||||
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx)
|
||||
.await
|
||||
.unwrap();
|
||||
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx).await.unwrap();
|
||||
});
|
||||
|
||||
plugin_manager
|
||||
@@ -173,10 +171,7 @@ impl PluginManager {
|
||||
}
|
||||
|
||||
pub async fn uninstall(&self, window_context: WindowContext, dir: &str) -> Result<()> {
|
||||
let plugin = self
|
||||
.get_plugin_by_dir(dir)
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
||||
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
||||
self.remove_plugin(window_context, &plugin).await
|
||||
}
|
||||
|
||||
@@ -205,7 +200,7 @@ impl PluginManager {
|
||||
watch: bool,
|
||||
) -> Result<()> {
|
||||
info!("Adding plugin by dir {dir}");
|
||||
let maybe_tx = self.server.app_to_plugin_events_tx.lock().await;
|
||||
let maybe_tx = self.grpc_service.app_to_plugin_events_tx.lock().await;
|
||||
let tx = match &*maybe_tx {
|
||||
None => return Err(ClientNotInitializedErr),
|
||||
Some(tx) => tx,
|
||||
@@ -251,9 +246,8 @@ impl PluginManager {
|
||||
warn!("Failed to remove plugin {} {e:?}", d.dir);
|
||||
}
|
||||
}
|
||||
if let Err(e) = self
|
||||
.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch)
|
||||
.await
|
||||
if let Err(e) =
|
||||
self.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch).await
|
||||
{
|
||||
warn!("Failed to add plugin {} {e:?}", d.dir);
|
||||
}
|
||||
@@ -307,21 +301,11 @@ impl PluginManager {
|
||||
}
|
||||
|
||||
pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> {
|
||||
self.plugins
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.find(|p| p.ref_id == ref_id)
|
||||
.cloned()
|
||||
self.plugins.lock().await.iter().find(|p| p.ref_id == ref_id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> {
|
||||
self.plugins
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.find(|p| p.dir == dir)
|
||||
.cloned()
|
||||
self.plugins.lock().await.iter().find(|p| p.dir == dir).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
|
||||
@@ -340,9 +324,8 @@ impl PluginManager {
|
||||
plugin: &PluginHandle,
|
||||
payload: &InternalEventPayload,
|
||||
) -> Result<InternalEvent> {
|
||||
let events = self
|
||||
.send_to_plugins_and_wait(window_context, payload, vec![plugin.to_owned()])
|
||||
.await?;
|
||||
let events =
|
||||
self.send_to_plugins_and_wait(window_context, payload, vec![plugin.to_owned()]).await?;
|
||||
Ok(events.first().unwrap().to_owned())
|
||||
}
|
||||
|
||||
@@ -352,8 +335,7 @@ impl PluginManager {
|
||||
payload: &InternalEventPayload,
|
||||
) -> Result<Vec<InternalEvent>> {
|
||||
let plugins = { self.plugins.lock().await.clone() };
|
||||
self.send_to_plugins_and_wait(window_context, payload, plugins)
|
||||
.await
|
||||
self.send_to_plugins_and_wait(window_context, payload, plugins).await
|
||||
}
|
||||
|
||||
async fn send_to_plugins_and_wait(
|
||||
@@ -440,8 +422,7 @@ impl PluginManager {
|
||||
&self,
|
||||
window: &WebviewWindow<R>,
|
||||
) -> Result<Vec<GetTemplateFunctionsResponse>> {
|
||||
self.get_template_functions_with_context(WindowContext::from_window(window))
|
||||
.await
|
||||
self.get_template_functions_with_context(WindowContext::from_window(window)).await
|
||||
}
|
||||
|
||||
pub async fn get_template_functions_with_context(
|
||||
@@ -449,10 +430,7 @@ impl PluginManager {
|
||||
window_context: WindowContext,
|
||||
) -> Result<Vec<GetTemplateFunctionsResponse>> {
|
||||
let reply_events = self
|
||||
.send_and_wait(
|
||||
window_context,
|
||||
&InternalEventPayload::GetTemplateFunctionsRequest,
|
||||
)
|
||||
.send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest)
|
||||
.await?;
|
||||
|
||||
let mut all_actions = Vec::new();
|
||||
@@ -471,10 +449,8 @@ impl PluginManager {
|
||||
req: CallHttpRequestActionRequest,
|
||||
) -> Result<()> {
|
||||
let ref_id = req.plugin_ref_id.clone();
|
||||
let plugin = self
|
||||
.get_plugin_by_ref_id(ref_id.as_str())
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(ref_id))?;
|
||||
let plugin =
|
||||
self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?;
|
||||
let event = plugin.build_event_to_send(
|
||||
WindowContext::from_window(window),
|
||||
&InternalEventPayload::CallHttpRequestActionRequest(req),
|
||||
@@ -500,10 +476,7 @@ impl PluginManager {
|
||||
};
|
||||
|
||||
let events = self
|
||||
.send_and_wait(
|
||||
window_context,
|
||||
&InternalEventPayload::CallTemplateFunctionRequest(req),
|
||||
)
|
||||
.send_and_wait(window_context, &InternalEventPayload::CallTemplateFunctionRequest(req))
|
||||
.await?;
|
||||
|
||||
let value = events.into_iter().find_map(|e| match e.payload {
|
||||
@@ -537,9 +510,7 @@ impl PluginManager {
|
||||
});
|
||||
|
||||
match result {
|
||||
None => Err(PluginErr(
|
||||
"No importers found for file contents".to_string(),
|
||||
)),
|
||||
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())
|
||||
@@ -613,14 +584,10 @@ fn fix_windows_paths(p: &PathBuf) -> String {
|
||||
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
|
||||
|
||||
// 2. Remove the drive letter
|
||||
let safe_path = Regex::new("^[a-zA-Z]:")
|
||||
.unwrap()
|
||||
.replace(safe_path.as_str(), "");
|
||||
let safe_path = Regex::new("^[a-zA-Z]:").unwrap().replace(safe_path.as_str(), "");
|
||||
|
||||
// 3. Convert backslashes to forward
|
||||
let safe_path = PathBuf::from(safe_path.to_string())
|
||||
.to_slash_lossy()
|
||||
.to_string();
|
||||
let safe_path = PathBuf::from(safe_path.to_string()).to_slash_lossy().to_string();
|
||||
|
||||
safe_path
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ impl PluginRuntime for PluginRuntimeServerImpl {
|
||||
let plugin_to_app_events_tx = self.plugin_to_app_events_tx.clone();
|
||||
let client_disconnect_tx = self.client_disconnect_tx.clone();
|
||||
|
||||
self.client_connect_tx
|
||||
.send(true)
|
||||
.expect("Failed to send client ready event");
|
||||
self.client_connect_tx.send(true).expect("Failed to send client ready event");
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(result) = in_stream.next().await {
|
||||
@@ -96,8 +94,6 @@ impl PluginRuntime for PluginRuntimeServerImpl {
|
||||
// Write the same data that was received
|
||||
let out_stream = ReceiverStream::new(to_plugin_rx);
|
||||
|
||||
Ok(Response::new(
|
||||
Box::pin(out_stream) as Self::EventStreamStream
|
||||
))
|
||||
Ok(Response::new(Box::pin(out_stream) as Self::EventStreamStream))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
name = "yaak_sse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde_json = "1.0.122"
|
||||
ts-rs = { version = "10.0.0", features = ["serde-json-impl"] }
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
name = "yaak_templates"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
ts-rs = { version = "10.0.0" }
|
||||
tokio = { version = "1.39.3", features = ["macros", "rt"] }
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
export * from './bindings/parser';
|
||||
export type COOL = {
|
||||
bar: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/template",
|
||||
"name": "@yaakapp-internal/templates",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts"
|
||||
|
||||
@@ -71,6 +71,7 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
Some('}') => {
|
||||
new_json.push(current_char);
|
||||
new_json.push('}');
|
||||
chars.next(); // Skip }
|
||||
}
|
||||
_ => {
|
||||
depth += 1;
|
||||
@@ -83,6 +84,7 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
Some(']') => {
|
||||
new_json.push(current_char);
|
||||
new_json.push(']');
|
||||
chars.next(); // Skip ]
|
||||
}
|
||||
_ => {
|
||||
depth += 1;
|
||||
@@ -92,13 +94,19 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
}
|
||||
},
|
||||
'}' => {
|
||||
depth -= 1;
|
||||
// Guard just in case invalid JSON has more closes than opens
|
||||
if depth > 0 {
|
||||
depth -= 1;
|
||||
}
|
||||
new_json.push('\n');
|
||||
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
||||
new_json.push(current_char);
|
||||
}
|
||||
']' => {
|
||||
depth -= 1;
|
||||
// Guard just in case invalid JSON has more closes than opens
|
||||
if depth > 0 {
|
||||
depth -= 1;
|
||||
}
|
||||
new_json.push('\n');
|
||||
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
||||
new_json.push(current_char);
|
||||
@@ -151,6 +159,19 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escaped() {
|
||||
assert_eq!(
|
||||
format_json(r#"{"foo":"Hi \"world!\""}"#, " "),
|
||||
r#"
|
||||
{
|
||||
"foo": "Hi \"world!\""
|
||||
}
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_array() {
|
||||
assert_eq!(
|
||||
@@ -219,11 +240,14 @@ mod test {
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_graphql_response() {
|
||||
assert_eq!(
|
||||
format_json(r#"{"data":{"capsules":[{"landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null},{"id":"5e9e2c5bf3591882af3b2665","landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null}]}}"#, " "),
|
||||
format_json(
|
||||
r#"{"data":{"capsules":[{"landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null},{"id":"5e9e2c5bf3591882af3b2665","landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null}]}}"#,
|
||||
" "
|
||||
),
|
||||
r#"
|
||||
{
|
||||
"data": {
|
||||
@@ -249,7 +273,32 @@ mod test {
|
||||
}
|
||||
}
|
||||
"#
|
||||
.trim()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_immediate_close() {
|
||||
assert_eq!(
|
||||
format_json(r#"{"bar":[]}"#, " "),
|
||||
r#"
|
||||
{
|
||||
"bar": []
|
||||
}
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_more_closes() {
|
||||
assert_eq!(
|
||||
format_json(r#"{}}"#, " "),
|
||||
r#"
|
||||
{}
|
||||
}
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { lazy } from 'react';
|
||||
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
|
||||
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { paths, useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { DefaultLayout } from './DefaultLayout';
|
||||
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
||||
import RouteError from './RouteError';
|
||||
@@ -19,19 +19,23 @@ const router = createBrowserRouter([
|
||||
element: <RedirectToLatestWorkspace />,
|
||||
},
|
||||
{
|
||||
path: routePaths.workspaces(),
|
||||
path: paths.workspaces(),
|
||||
element: <RedirectToLatestWorkspace />,
|
||||
},
|
||||
{
|
||||
path: routePaths.workspace({
|
||||
path: paths.workspace({
|
||||
workspaceId: ':workspaceId',
|
||||
environmentId: null,
|
||||
cookieJarId: null,
|
||||
}),
|
||||
element: <LazyWorkspace />,
|
||||
},
|
||||
{
|
||||
path: routePaths.request({
|
||||
path: paths.request({
|
||||
workspaceId: ':workspaceId',
|
||||
requestId: ':requestId',
|
||||
environmentId: null,
|
||||
cookieJarId: null,
|
||||
}),
|
||||
element: <LazyWorkspace />,
|
||||
},
|
||||
@@ -40,9 +44,11 @@ const router = createBrowserRouter([
|
||||
element: <RedirectLegacyEnvironmentURLs />,
|
||||
},
|
||||
{
|
||||
path: routePaths.workspaceSettings({
|
||||
workspaceId: ':workspaceId',
|
||||
}),
|
||||
path: paths
|
||||
.workspaceSettings({
|
||||
workspaceId: ':workspaceId',
|
||||
})
|
||||
.replace(/\?.*/, ''),
|
||||
element: <LazySettings />,
|
||||
},
|
||||
],
|
||||
@@ -64,13 +70,13 @@ function RedirectLegacyEnvironmentURLs() {
|
||||
workspaceId?: string;
|
||||
environmentId?: string;
|
||||
}>();
|
||||
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
|
||||
const environmentId = (rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId) ?? null;
|
||||
|
||||
let to;
|
||||
if (workspaceId != null && requestId != null) {
|
||||
to = routes.paths.request({ workspaceId, environmentId, requestId });
|
||||
to = routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId: null });
|
||||
} else if (workspaceId != null) {
|
||||
to = routes.paths.workspace({ workspaceId, environmentId });
|
||||
to = routes.paths.workspace({ workspaceId, environmentId, cookieJarId: null });
|
||||
} else {
|
||||
to = routes.paths.workspaces();
|
||||
}
|
||||
|
||||
@@ -271,7 +271,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
||||
return routes.navigate('request', {
|
||||
workspaceId: r.workspaceId,
|
||||
requestId: r.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -313,7 +314,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
||||
workspaceCommands,
|
||||
sortedRequests,
|
||||
routes,
|
||||
activeEnvironment,
|
||||
activeEnvironment?.id,
|
||||
activeCookieJar?.id,
|
||||
sortedEnvironments,
|
||||
setActiveEnvironmentId,
|
||||
sortedWorkspaces,
|
||||
|
||||
@@ -45,6 +45,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
|
||||
const handleCreateEnvironment = async () => {
|
||||
const e = await createEnvironment.mutateAsync();
|
||||
if (e == null) return;
|
||||
setSelectedEnvironmentId(e.id);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@ import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { updateSchema } from 'cm6-graphql';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import { Button } from './core/Button';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
import { Editor, formatGraphQL } from './core/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Icon } from './core/Icon';
|
||||
import { Separator } from './core/Separator';
|
||||
import { useDialog } from './DialogContext';
|
||||
|
||||
@@ -19,18 +22,25 @@ type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> &
|
||||
|
||||
export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||
const editorViewRef = useRef<EditorView>(null);
|
||||
const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
|
||||
const [currentBody, setCurrentBody] = useState<{ query: string; variables: string | undefined }>(() => {
|
||||
// Migrate text bodies to GraphQL format
|
||||
// NOTE: This is how GraphQL used to be stored
|
||||
if ('text' in body) {
|
||||
const b = tryParseJson(body.text, {});
|
||||
const variables = JSON.stringify(b.variables || undefined, null, 2);
|
||||
return { query: b.query ?? '', variables };
|
||||
}
|
||||
|
||||
return { query: body.query ?? '', variables: body.variables ?? '' };
|
||||
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
|
||||
Record<string, boolean>
|
||||
>('graphQLAutoIntrospectDisabled', {});
|
||||
const { schema, isLoading, error, refetch, clear } = useIntrospectGraphQL(baseRequest, {
|
||||
disabled: autoIntrospectDisabled?.[baseRequest.id],
|
||||
});
|
||||
const [currentBody, setCurrentBody] = useState<{ query: string; variables: string | undefined }>(
|
||||
() => {
|
||||
// Migrate text bodies to GraphQL format
|
||||
// NOTE: This is how GraphQL used to be stored
|
||||
if ('text' in body) {
|
||||
const b = tryParseJson(body.text, {});
|
||||
const variables = JSON.stringify(b.variables || undefined, null, 2);
|
||||
return { query: b.query ?? '', variables };
|
||||
}
|
||||
|
||||
return { query: body.query ?? '', variables: body.variables ?? '' };
|
||||
},
|
||||
);
|
||||
|
||||
const handleChangeQuery = (query: string) => {
|
||||
const newBody = { query, variables: currentBody.variables || undefined };
|
||||
@@ -52,52 +62,106 @@ export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const actions = useMemo<EditorProps['actions']>(() => {
|
||||
const isValid = error || isLoading;
|
||||
if (!isValid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions: EditorProps['actions'] = [
|
||||
const actions = useMemo<EditorProps['actions']>(
|
||||
() => [
|
||||
<div key="introspection" className="!opacity-100">
|
||||
<Button
|
||||
key="introspection"
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'secondary'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full my-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="primary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>
|
||||
{schema === undefined ? null /* Initializing */ : !error ? (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'refresh',
|
||||
label: 'Refetch',
|
||||
leftSlot: <Icon icon="refresh" />,
|
||||
onSelect: refetch,
|
||||
},
|
||||
{
|
||||
key: 'clear',
|
||||
label: 'Clear',
|
||||
onSelect: clear,
|
||||
hidden: !schema,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
{ type: 'separator', label: 'Setting' },
|
||||
{
|
||||
key: 'auto_fetch',
|
||||
label: 'Automatic Introspection',
|
||||
onSelect: () => {
|
||||
setAutoIntrospectDisabled({
|
||||
...autoIntrospectDisabled,
|
||||
[baseRequest.id]: !autoIntrospectDisabled?.[baseRequest.id],
|
||||
});
|
||||
},
|
||||
leftSlot: (
|
||||
<Icon
|
||||
icon={
|
||||
autoIntrospectDisabled?.[baseRequest.id]
|
||||
? 'check_square_unchecked'
|
||||
: 'check_square_checked'
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
title="Refetch Schema"
|
||||
isLoading={isLoading}
|
||||
color={isLoading || schema ? 'default' : 'warning'}
|
||||
>
|
||||
{isLoading ? 'Introspecting' : schema ? 'Schema' : 'No Schema'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: ({ hide }) => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full my-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
hide();
|
||||
await refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="primary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Introspection Failed
|
||||
</Button>
|
||||
)}
|
||||
</div>,
|
||||
];
|
||||
|
||||
return actions;
|
||||
}, [dialog, error, isLoading, refetch]);
|
||||
],
|
||||
[
|
||||
isLoading,
|
||||
refetch,
|
||||
error,
|
||||
autoIntrospectDisabled,
|
||||
baseRequest.id,
|
||||
clear,
|
||||
schema,
|
||||
setAutoIntrospectDisabled,
|
||||
dialog,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
|
||||
import { WINDOW_CONTROLS_WIDTH, WindowControls } from './WindowControls';
|
||||
|
||||
@@ -23,6 +24,7 @@ export function HeaderSize({
|
||||
onlyXWindowControl,
|
||||
children,
|
||||
}: HeaderSizeProps) {
|
||||
const osInfo = useOsInfo();
|
||||
const settings = useSettings();
|
||||
const stoplightsVisible = useStoplightsVisible();
|
||||
return (
|
||||
@@ -31,10 +33,10 @@ export function HeaderSize({
|
||||
style={{
|
||||
...style,
|
||||
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
|
||||
paddingLeft: stoplightsVisible ? 72 / settings.interfaceScale : undefined,
|
||||
paddingLeft: (stoplightsVisible && !ignoreControlsSpacing) ? 72 / settings.interfaceScale : undefined,
|
||||
...(size === 'md' ? { height: HEADER_SIZE_MD } : {}),
|
||||
...(size === 'lg' ? { height: HEADER_SIZE_LG } : {}),
|
||||
...(stoplightsVisible || ignoreControlsSpacing
|
||||
...(osInfo.osType === 'macos' || ignoreControlsSpacing
|
||||
? { paddingRight: '2px' }
|
||||
: { paddingLeft: '2px', paddingRight: WINDOW_CONTROLS_WIDTH }),
|
||||
}}
|
||||
|
||||
@@ -28,9 +28,10 @@ export function ImportCurlButton() {
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
size="2xs"
|
||||
variant="border"
|
||||
color="primary"
|
||||
color="success"
|
||||
className="rounded-full"
|
||||
leftSlot={<Icon icon="paste" size="sm" />}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
|
||||
42
src-web/components/LicenseBadge.tsx
Normal file
42
src-web/components/LicenseBadge.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { useOpenSettings } from '../hooks/useOpenSettings';
|
||||
import { Button } from './core/Button';
|
||||
import { SettingsTab } from './Settings/Settings';
|
||||
|
||||
const labels: Record<LicenseCheckStatus['type'], string | null> = {
|
||||
commercial_use: null,
|
||||
personal_use: 'Personal Use',
|
||||
trial_ended: 'Personal Use',
|
||||
trialing: 'Active Trial',
|
||||
};
|
||||
|
||||
export function LicenseBadge() {
|
||||
const openSettings = useOpenSettings(SettingsTab.License);
|
||||
const { check } = useLicense();
|
||||
|
||||
if (check.data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = labels[check.data.type];
|
||||
if (label == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="2xs"
|
||||
variant="border"
|
||||
className="!rounded-full mx-1"
|
||||
onClick={() => openSettings.mutate()}
|
||||
color={
|
||||
check.data.type == 'trial_ended' || check.data.type === 'personal_use'
|
||||
? 'primary'
|
||||
: 'success'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -71,7 +71,11 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
|
||||
className="mr-auto min-w-[5rem]"
|
||||
onClick={() => {
|
||||
toast.hide('workspace-moved');
|
||||
routes.navigate('workspace', { workspaceId: selectedWorkspaceId });
|
||||
routes.navigate('workspace', {
|
||||
workspaceId: selectedWorkspaceId,
|
||||
cookieJarId: null,
|
||||
environmentId: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Switch to Workspace
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
@@ -20,6 +21,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [activeCookieJar] = useActiveCookieJar();
|
||||
const routes = useAppRoutes();
|
||||
const allRecentRequestIds = useRecentRequests();
|
||||
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
||||
@@ -57,8 +59,9 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
onSelect: () => {
|
||||
routes.navigate('request', {
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
workspaceId: activeWorkspace.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -76,7 +79,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
}
|
||||
|
||||
return recentRequestItems.slice(0, 20);
|
||||
}, [activeWorkspace, activeEnvironment?.id, recentRequestIds, requests, routes]);
|
||||
}, [activeWorkspace, recentRequestIds, requests, routes, activeEnvironment?.id, activeCookieJar?.id]);
|
||||
|
||||
return (
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: 'Copy to Clipboard',
|
||||
label: 'Copy Body',
|
||||
onSelect: copyResponse.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
hidden: responses.length === 0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { getRecentCookieJars } from '../hooks/useRecentCookieJars';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { getRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
@@ -20,13 +21,14 @@ export function RedirectToLatestWorkspace() {
|
||||
|
||||
(async function () {
|
||||
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0];
|
||||
const requestId = (await getRecentRequests(workspaceId))[0];
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
|
||||
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
|
||||
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
|
||||
|
||||
if (workspaceId != null && requestId != null) {
|
||||
navigate(routes.paths.request({ workspaceId, environmentId, requestId }));
|
||||
navigate(routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId }));
|
||||
} else {
|
||||
navigate(routes.paths.workspace({ workspaceId, environmentId }));
|
||||
navigate(routes.paths.workspace({ workspaceId, environmentId, cookieJarId }));
|
||||
}
|
||||
})();
|
||||
}, [navigate, recentWorkspaces, routes.paths, workspaces, workspaces.length]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import classNames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
@@ -9,25 +10,34 @@ import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
||||
import { HeaderSize } from '../HeaderSize';
|
||||
import { SettingsAppearance } from './SettingsAppearance';
|
||||
import { SettingsGeneral } from './SettingsGeneral';
|
||||
import { SettingsLicense } from './SettingsLicense';
|
||||
import { SettingsPlugins } from './SettingsPlugins';
|
||||
import {SettingsProxy} from "./SettingsProxy";
|
||||
import { SettingsProxy } from './SettingsProxy';
|
||||
|
||||
interface Props {
|
||||
hide?: () => void;
|
||||
}
|
||||
|
||||
enum Tab {
|
||||
export enum SettingsTab {
|
||||
General = 'general',
|
||||
Proxy = 'proxy',
|
||||
Appearance = 'appearance',
|
||||
Plugins = 'plugins',
|
||||
License = 'license',
|
||||
}
|
||||
|
||||
const tabs = [Tab.General, Tab.Appearance, Tab.Proxy, Tab.Plugins];
|
||||
const tabs = [
|
||||
SettingsTab.General,
|
||||
SettingsTab.Appearance,
|
||||
SettingsTab.Proxy,
|
||||
SettingsTab.Plugins,
|
||||
SettingsTab.License,
|
||||
];
|
||||
|
||||
export default function Settings({ hide }: Props) {
|
||||
const osInfo = useOsInfo();
|
||||
const [tab, setTab] = useState<string>(Tab.General);
|
||||
const [params] = useSearchParams();
|
||||
const [tab, setTab] = useState<string>(params.get('tab') ?? SettingsTab.General);
|
||||
|
||||
// Close settings window on escape
|
||||
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
|
||||
@@ -71,18 +81,21 @@ export default function Settings({ hide }: Props) {
|
||||
onChangeValue={setTab}
|
||||
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))}
|
||||
>
|
||||
<TabContent value={Tab.General} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.General} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsGeneral />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsAppearance />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsPlugins />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsProxy />
|
||||
</TabContent>
|
||||
<TabContent value={SettingsTab.License} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsLicense />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
147
src-web/components/Settings/SettingsLicense.tsx
Normal file
147
src-web/components/Settings/SettingsLicense.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import classNames from 'classnames';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import React, { useState } from 'react';
|
||||
import { useCopy } from '../../hooks/useCopy';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { useTimedBoolean } from '../../hooks/useTimedBoolean';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { Link } from '../core/Link';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
|
||||
export function SettingsLicense() {
|
||||
const { check, activate } = useLicense();
|
||||
const [key, setKey] = useState<string>('');
|
||||
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
|
||||
const settings = useSettings();
|
||||
const specialAnnouncement =
|
||||
settings.createdAt < '2024-12-03' && check.data?.type !== 'commercial_use';
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
const copy = useCopy({ disableToast: true });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{check.data?.type === 'personal_use' && <Banner color="info">You're</Banner>}
|
||||
{check.data?.type === 'commercial_use' && (
|
||||
<Banner color="success">
|
||||
<strong>License active!</strong> Enjoy using Yaak for commercial use.
|
||||
</Banner>
|
||||
)}
|
||||
{check.data?.type === 'trialing' && (
|
||||
<Banner color="success">
|
||||
<strong>Your trial ends in {formatDistanceToNow(check.data.end)}</strong>. If you're
|
||||
using Yaak for commercial use, please purchase a commercial use license.
|
||||
</Banner>
|
||||
)}
|
||||
{check.data?.type === 'trial_ended' && !specialAnnouncement && (
|
||||
<Banner color="primary">
|
||||
<strong>Your trial ended on {format(check.data.end, 'MMMM dd, yyyy')}</strong>. A
|
||||
commercial-use license is required if you use Yaak within a for-profit organization of two
|
||||
or more people.
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
{check.error && <Banner color="danger">{check.error}</Banner>}
|
||||
{activate.error && <Banner color="danger">{activate.error}</Banner>}
|
||||
|
||||
{specialAnnouncement && (
|
||||
<VStack className="max-w-lg" space={4}>
|
||||
<p>
|
||||
<strong>Thank you for being an early supporter of Yaak!</strong>
|
||||
</p>
|
||||
<p>
|
||||
To support the ongoing development of the best local-first API client, Yaak now requires
|
||||
a paid license for the commercial use of prebuilt binaries (personal use and running the
|
||||
open-source code remains free.)
|
||||
</p>
|
||||
<p>
|
||||
For details, see the{' '}
|
||||
<Link href="https://yaak.app/blog/commercial-use">Announcement Post</Link>.
|
||||
</p>
|
||||
<p>
|
||||
As a thank-you, enter code{' '}
|
||||
<button
|
||||
title="Copy coupon code"
|
||||
className="hover:text-notice"
|
||||
onClick={() => {
|
||||
setCopied();
|
||||
copy('EARLYAAK');
|
||||
}}
|
||||
>
|
||||
<InlineCode className="inline-flex items-center gap-1">
|
||||
EARLYAAK{' '}
|
||||
<Icon
|
||||
icon={copied ? 'check' : 'copy'}
|
||||
size="xs"
|
||||
className={classNames(copied && 'text-success')}
|
||||
/>
|
||||
</InlineCode>
|
||||
</button>{' '}
|
||||
at checkout for 50% off your first year of the individual plan.
|
||||
</p>
|
||||
<p>~ Greg</p>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{check.data?.type === 'commercial_use' ? (
|
||||
<HStack space={2}>
|
||||
<Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}>
|
||||
Activate Another License
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => open('https://yaak.app/dashboard')}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
>
|
||||
Direct Support
|
||||
</Button>
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack space={2}>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => open('https://yaak.app/pricing')}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
>
|
||||
Purchase
|
||||
</Button>
|
||||
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
|
||||
Activate License
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{activateFormVisible && (
|
||||
<VStack
|
||||
as="form"
|
||||
space={3}
|
||||
className="max-w-sm"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
toggleActivateFormVisible();
|
||||
activate.mutate({ licenseKey: key });
|
||||
}}
|
||||
>
|
||||
<PlainInput
|
||||
autoFocus
|
||||
label="License Key"
|
||||
name="key"
|
||||
onChange={setKey}
|
||||
placeholder="YK1-XXXXX-XXXXX-XXXXX-XXXXX"
|
||||
/>
|
||||
<Button type="submit" color="primary" size="sm" isLoading={activate.isPending}>
|
||||
Submit
|
||||
</Button>
|
||||
</VStack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
@@ -73,6 +74,7 @@ export function Sidebar({ className }: Props) {
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [activeCookieJar] = useActiveCookieJar();
|
||||
const folders = useFolders();
|
||||
const requests = useRequests();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
@@ -221,14 +223,22 @@ export function Sidebar({ className }: Props) {
|
||||
routes.navigate('request', {
|
||||
requestId: id,
|
||||
workspaceId: item.workspaceId,
|
||||
environmentId: activeEnvironment?.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
setSelectedId(id);
|
||||
setSelectedTree(tree);
|
||||
if (!opts.noFocus) focusActiveRequest({ forced: { id, tree } });
|
||||
}
|
||||
},
|
||||
[treeParentMap, collapsed, routes, activeEnvironment, focusActiveRequest],
|
||||
[
|
||||
treeParentMap,
|
||||
collapsed,
|
||||
routes,
|
||||
activeEnvironment?.id,
|
||||
activeCookieJar?.id,
|
||||
focusActiveRequest,
|
||||
],
|
||||
);
|
||||
|
||||
const handleClearSelected = useCallback(() => {
|
||||
@@ -273,8 +283,9 @@ export function Sidebar({ className }: Props) {
|
||||
e.preventDefault();
|
||||
routes.navigate('request', {
|
||||
requestId: selected.id,
|
||||
workspaceId: activeWorkspace?.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
workspaceId: activeWorkspace?.id ?? null,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -704,9 +715,15 @@ function SidebarItem({
|
||||
const handleSubmitNameEdit = useCallback(
|
||||
async (el: HTMLInputElement) => {
|
||||
if (itemModel === 'http_request') {
|
||||
await updateHttpRequest.mutateAsync({ id: itemId, update: (r) => ({ ...r, name: el.value }) });
|
||||
await updateHttpRequest.mutateAsync({
|
||||
id: itemId,
|
||||
update: (r) => ({ ...r, name: el.value }),
|
||||
});
|
||||
} else if (itemModel === 'grpc_request') {
|
||||
await updateGrpcRequest.mutateAsync({ id: itemId, update: (r) => ({ ...r, name: el.value }) });
|
||||
await updateGrpcRequest.mutateAsync({
|
||||
id: itemId,
|
||||
update: (r) => ({ ...r, name: el.value }),
|
||||
});
|
||||
}
|
||||
setEditing(false);
|
||||
},
|
||||
@@ -724,7 +741,7 @@ function SidebarItem({
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
handleSubmitNameEdit(e.currentTarget);
|
||||
await handleSubmitNameEdit(e.currentTarget);
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
@@ -741,8 +758,8 @@ function SidebarItem({
|
||||
}, [setEditing, itemModel]);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
handleSubmitNameEdit(e.currentTarget);
|
||||
async (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
await handleSubmitNameEdit(e.currentTarget);
|
||||
},
|
||||
[handleSubmitNameEdit],
|
||||
);
|
||||
@@ -903,8 +920,9 @@ function SidebarItem({
|
||||
className={classNames(
|
||||
'w-full flex gap-1.5 items-center h-xs px-1.5 rounded-md focus-visible:ring focus-visible:ring-border-focus outline-0',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
isActive && 'bg-surface-highlight text',
|
||||
!isActive && 'text-text-subtle group-hover/item:text-text active:bg-surface-highlight',
|
||||
isActive && 'bg-surface-highlight text-text',
|
||||
!isActive && 'text-text-subtle group-hover/item:text-text',
|
||||
showContextMenu && '!text-text', // Show as "active" when context menu is open
|
||||
selected && useProminentStyles && '!bg-surface-active',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
TemplateFunctionSelectArg,
|
||||
TemplateFunctionTextArg,
|
||||
} from '@yaakapp-internal/plugin';
|
||||
import type { FnArg, Tokens } from '@yaakapp-internal/template';
|
||||
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Tokens } from '@yaakapp-internal/template';
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
|
||||
import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import classNames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import { useStoplightsVisible } from '../hooks/useStoplightsVisible';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { Button } from './core/Button';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onlyX?: boolean;
|
||||
macos?: boolean;
|
||||
}
|
||||
|
||||
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
|
||||
|
||||
export function WindowControls({ className, onlyX }: Props) {
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
const stoplightsVisible = useStoplightsVisible();
|
||||
if (stoplightsVisible) {
|
||||
const osInfo = useOsInfo();
|
||||
|
||||
// Never show controls on macOS
|
||||
if (osInfo.osType === 'macos') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { ImportCurlButton } from './ImportCurlButton';
|
||||
import { LicenseBadge } from './LicenseBadge';
|
||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
@@ -22,7 +23,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'grid grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center w-full h-full',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center w-full h-full',
|
||||
)}
|
||||
>
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none">
|
||||
@@ -34,10 +35,11 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
|
||||
</HStack>
|
||||
</HStack>
|
||||
<div className="pointer-events-none w-full max-w-[30vw] mx-auto">
|
||||
<div className="pointer-events-none w-full max-w-[30vw] mx-auto flex justify-center">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-0.5">
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||
<LicenseBadge />
|
||||
<ImportCurlButton />
|
||||
<IconButton
|
||||
icon="search"
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
size === 'md' && 'h-md px-3 rounded-md',
|
||||
size === 'sm' && 'h-sm px-2.5 rounded-md',
|
||||
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
|
||||
size === '2xs' && 'h-2xs px-1 text-xs rounded',
|
||||
size === '2xs' && 'h-2xs px-2 text-xs rounded',
|
||||
|
||||
// Solids
|
||||
variant === 'solid' && 'border-transparent',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { json } from '@codemirror/lang-json';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
import {
|
||||
codeFolding,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
HighlightStyle,
|
||||
@@ -150,6 +151,49 @@ export const multiLineExtensions = [
|
||||
return el;
|
||||
},
|
||||
}),
|
||||
codeFolding({
|
||||
placeholderDOM(view, onclick, prepared) {
|
||||
const el = document.createElement('span');
|
||||
el.onclick = onclick;
|
||||
el.className = 'cm-foldPlaceholder';
|
||||
el.innerText = prepared;
|
||||
el.title = 'unfold';
|
||||
el.ariaLabel = 'folded code';
|
||||
return el;
|
||||
},
|
||||
preparePlaceholder(state, range) {
|
||||
let count: number | undefined;
|
||||
let startToken = '{';
|
||||
let endToken = '}';
|
||||
|
||||
const prevLine = state.doc.lineAt(range.from).text;
|
||||
const isArray = prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{');
|
||||
|
||||
if (isArray) {
|
||||
startToken = '[';
|
||||
endToken = ']';
|
||||
}
|
||||
|
||||
const internal = state.sliceDoc(range.from, range.to);
|
||||
const toParse = startToken + internal + endToken;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(toParse);
|
||||
count = Object.keys(parsed).length;
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
if (count !== undefined) {
|
||||
const label = isArray ? 'item' : 'prop';
|
||||
const plural = count === 1 ? '' : 's';
|
||||
|
||||
return `${count} ${label}${plural}`;
|
||||
}
|
||||
|
||||
return '…';
|
||||
},
|
||||
}),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
closeBrackets(),
|
||||
|
||||
@@ -22,6 +22,8 @@ const icons = {
|
||||
cake: lucide.CakeIcon,
|
||||
chat: lucide.MessageSquare,
|
||||
check: lucide.CheckIcon,
|
||||
check_square_checked: lucide.SquareCheckIcon,
|
||||
check_square_unchecked: lucide.SquareIcon,
|
||||
check_circle: lucide.CheckCircleIcon,
|
||||
chevron_down: lucide.ChevronDownIcon,
|
||||
chevron_right: lucide.ChevronRightIcon,
|
||||
@@ -56,6 +58,7 @@ const icons = {
|
||||
left_panel_visible: lucide.PanelLeftCloseIcon,
|
||||
magic_wand: lucide.Wand2Icon,
|
||||
minus: lucide.MinusIcon,
|
||||
minus_circle: lucide.MinusCircleIcon,
|
||||
moon: lucide.MoonIcon,
|
||||
more_vertical: lucide.MoreVerticalIcon,
|
||||
paste: lucide.ClipboardPasteIcon,
|
||||
|
||||
@@ -18,11 +18,11 @@ export function Link({ href, children, className, ...other }: Props) {
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classNames(className, 'pr-4')}
|
||||
className={classNames(className, 'pr-4 inline-flex items-center')}
|
||||
{...other}
|
||||
>
|
||||
<span className="underline">{children}</span>
|
||||
<Icon className="inline absolute right-0.5 top-0.5" size="xs" icon="external_link" />
|
||||
<Icon className="inline absolute right-0.5 top-1.5" size="xs" icon="external_link" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export function useActiveCookieJar() {
|
||||
export function useEnsureActiveCookieJar() {
|
||||
const cookieJars = useCookieJars();
|
||||
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
|
||||
|
||||
// Set the active cookie jar to the first one, if none set
|
||||
useEffect(() => {
|
||||
if (cookieJars == null) return; // Hasn't loaded yet
|
||||
if (cookieJars.find((j) => j.id === activeCookieJarId)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { RouteParamsRequest } from './useAppRoutes';
|
||||
|
||||
export function useActiveRequestId(): string | null {
|
||||
const { requestId } = useParams<RouteParamsRequest>();
|
||||
const { requestId } = useParams();
|
||||
return requestId ?? null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { RouteParamsWorkspace } from './useAppRoutes';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
export function useActiveWorkspace(): Workspace | null {
|
||||
@@ -15,6 +14,6 @@ export function useActiveWorkspace(): Workspace | null {
|
||||
}
|
||||
|
||||
function useActiveWorkspaceId(): string | null {
|
||||
const { workspaceId } = useParams<RouteParamsWorkspace>();
|
||||
const { workspaceId } = useParams();
|
||||
return workspaceId ?? null;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface AppInfo {
|
||||
appLogDir: string;
|
||||
}
|
||||
|
||||
const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
|
||||
export const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
|
||||
|
||||
export function useAppInfo() {
|
||||
return appInfo;
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SettingsTab } from '../components/Settings/Settings';
|
||||
import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar';
|
||||
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment';
|
||||
import { useActiveRequestId } from './useActiveRequestId';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
|
||||
export type RouteParamsWorkspace = {
|
||||
workspaceId: string;
|
||||
environmentId?: string;
|
||||
cookieJarId?: string;
|
||||
environmentId: string | null;
|
||||
cookieJarId: string | null;
|
||||
};
|
||||
|
||||
export type RouteParamsRequest = RouteParamsWorkspace & {
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
export const routePaths = {
|
||||
workspaces() {
|
||||
export type RouteParamsSettings = {
|
||||
workspaceId: string;
|
||||
tab?: SettingsTab;
|
||||
};
|
||||
|
||||
export const paths = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
workspaces(_ = {}) {
|
||||
return '/workspaces';
|
||||
},
|
||||
workspaceSettings({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) {
|
||||
return `/workspaces/${workspaceId}/settings`;
|
||||
workspaceSettings({ workspaceId, tab } = { workspaceId: ':workspaceId' } as RouteParamsSettings) {
|
||||
return `/workspaces/${workspaceId}/settings?tab=${tab ?? SettingsTab.General}`;
|
||||
},
|
||||
workspace(
|
||||
{ workspaceId, environmentId, cookieJarId } = {
|
||||
@@ -52,44 +56,21 @@ export const routePaths = {
|
||||
};
|
||||
|
||||
export function useAppRoutes() {
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const requestId = useActiveRequestId();
|
||||
const nav = useNavigate();
|
||||
|
||||
const navigate = useCallback(
|
||||
<T extends keyof typeof routePaths>(path: T, ...params: Parameters<(typeof routePaths)[T]>) => {
|
||||
<T extends keyof typeof paths>(path: T, ...params: Parameters<(typeof paths)[T]>) => {
|
||||
// Not sure how to make TS work here, but it's good from the
|
||||
// outside caller perspective.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resolvedPath = routePaths[path](...(params as any));
|
||||
const resolvedPath = paths[path](...(params as any));
|
||||
nav(resolvedPath);
|
||||
},
|
||||
[nav],
|
||||
);
|
||||
|
||||
const setEnvironment = useCallback(
|
||||
(environment: Environment | null) => {
|
||||
if (activeWorkspace == null) {
|
||||
navigate('workspaces');
|
||||
} else if (requestId == null) {
|
||||
navigate('workspace', {
|
||||
workspaceId: activeWorkspace.id,
|
||||
environmentId: environment == null ? undefined : environment.id,
|
||||
});
|
||||
} else {
|
||||
navigate('request', {
|
||||
workspaceId: activeWorkspace.id,
|
||||
environmentId: environment == null ? undefined : environment.id,
|
||||
requestId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[navigate, activeWorkspace, requestId],
|
||||
);
|
||||
|
||||
return {
|
||||
paths: routePaths,
|
||||
paths,
|
||||
navigate,
|
||||
setEnvironment,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { CookieJar } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import {cookieJarsAtom} from "./useCookieJars";
|
||||
import { usePrompt } from './usePrompt';
|
||||
import {updateModelList} from "./useSyncModelStores";
|
||||
|
||||
export function useCreateCookieJar() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const prompt = usePrompt();
|
||||
const setCookieJars = useSetAtom(cookieJarsAtom);
|
||||
|
||||
return useMutation<CookieJar>({
|
||||
return useMutation<CookieJar | null>({
|
||||
mutationKey: ['create_cookie_jar'],
|
||||
mutationFn: async () => {
|
||||
if (workspace === null) {
|
||||
@@ -23,8 +27,16 @@ export function useCreateCookieJar() {
|
||||
label: 'Name',
|
||||
defaultValue: 'My Jar',
|
||||
});
|
||||
if (name == null) return null;
|
||||
|
||||
return invokeCmd('cmd_create_cookie_jar', { workspaceId: workspace.id, name });
|
||||
},
|
||||
onSuccess: (cookieJar) => {
|
||||
if (cookieJar == null) return;
|
||||
|
||||
// Optimistic update
|
||||
setCookieJars(updateModelList(cookieJar));
|
||||
},
|
||||
onSettled: () => trackEvent('cookie_jar', 'create'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import {environmentsAtom} from "./useEnvironments";
|
||||
import { usePrompt } from './usePrompt';
|
||||
import {updateModelList} from "./useSyncModelStores";
|
||||
|
||||
export function useCreateEnvironment() {
|
||||
const [, setActiveEnvironmentId] = useActiveEnvironment();
|
||||
const prompt = usePrompt();
|
||||
const workspace = useActiveWorkspace();
|
||||
const setEnvironments = useSetAtom(environmentsAtom);
|
||||
|
||||
return useMutation<Environment, unknown, void>({
|
||||
return useMutation<Environment | null, unknown, void>({
|
||||
mutationKey: ['create_environment'],
|
||||
mutationFn: async () => {
|
||||
const name = await prompt({
|
||||
@@ -23,6 +27,8 @@ export function useCreateEnvironment() {
|
||||
defaultValue: 'My Environment',
|
||||
confirmText: 'Create',
|
||||
});
|
||||
if (name == null) return null;
|
||||
|
||||
return invokeCmd('cmd_create_environment', {
|
||||
name,
|
||||
variables: [],
|
||||
@@ -31,7 +37,11 @@ export function useCreateEnvironment() {
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'create'),
|
||||
onSuccess: async (environment) => {
|
||||
if (workspace == null) return;
|
||||
if (environment == null) return;
|
||||
|
||||
// Optimistic update
|
||||
setEnvironments(updateModelList(environment));
|
||||
|
||||
setActiveEnvironmentId(environment.id);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Folder } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { foldersAtom } from './useFolders';
|
||||
import { usePrompt } from './usePrompt';
|
||||
import { updateModelList } from './useSyncModelStores';
|
||||
|
||||
export function useCreateFolder() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const prompt = usePrompt();
|
||||
const setFolders = useSetAtom(foldersAtom);
|
||||
|
||||
return useMutation<void, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
|
||||
return useMutation<
|
||||
Folder | null,
|
||||
unknown,
|
||||
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
|
||||
>({
|
||||
mutationKey: ['create_folder'],
|
||||
mutationFn: async (patch) => {
|
||||
if (workspace === null) {
|
||||
@@ -25,14 +33,19 @@ export function useCreateFolder() {
|
||||
confirmText: 'Create',
|
||||
placeholder: 'Name',
|
||||
});
|
||||
if (name == null) {
|
||||
return;
|
||||
}
|
||||
if (name == null) return null;
|
||||
|
||||
patch.name = name;
|
||||
}
|
||||
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
|
||||
return await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
|
||||
},
|
||||
onSuccess: (folder) => {
|
||||
if (folder == null) return;
|
||||
|
||||
// Optimistic update
|
||||
setFolders(updateModelList(folder));
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
});
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import {useActiveCookieJar} from "./useActiveCookieJar";
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import {grpcRequestsAtom} from "./useGrpcRequests";
|
||||
import {updateModelList} from "./useSyncModelStores";
|
||||
|
||||
export function useCreateGrpcRequest() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [activeCookieJar] = useActiveCookieJar();
|
||||
const activeRequest = useActiveRequest();
|
||||
const routes = useAppRoutes();
|
||||
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
|
||||
|
||||
return useMutation<
|
||||
GrpcRequest,
|
||||
@@ -19,7 +25,7 @@ export function useCreateGrpcRequest() {
|
||||
Partial<Pick<GrpcRequest, 'name' | 'sortPriority' | 'folderId'>>
|
||||
>({
|
||||
mutationKey: ['create_grpc_request'],
|
||||
mutationFn: (patch) => {
|
||||
mutationFn: async (patch) => {
|
||||
if (workspace === null) {
|
||||
throw new Error("Cannot create grpc request when there's no active workspace");
|
||||
}
|
||||
@@ -33,7 +39,7 @@ export function useCreateGrpcRequest() {
|
||||
}
|
||||
}
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invokeCmd('cmd_create_grpc_request', {
|
||||
return invokeCmd<GrpcRequest>('cmd_create_grpc_request', {
|
||||
workspaceId: workspace.id,
|
||||
name: '',
|
||||
...patch,
|
||||
@@ -41,10 +47,14 @@ export function useCreateGrpcRequest() {
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_request', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
// Optimistic update
|
||||
setGrpcRequests(updateModelList(request));
|
||||
|
||||
routes.navigate('request', {
|
||||
workspaceId: request.workspaceId,
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai/index';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveCookieJar } from './useActiveCookieJar';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { httpRequestsAtom } from './useHttpRequests';
|
||||
import { updateModelList } from './useSyncModelStores';
|
||||
|
||||
export function useCreateHttpRequest() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [activeCookieJar] = useActiveCookieJar();
|
||||
const activeRequest = useActiveRequest();
|
||||
const routes = useAppRoutes();
|
||||
const setHttpRequests = useSetAtom(httpRequestsAtom);
|
||||
|
||||
return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({
|
||||
mutationKey: ['create_http_request'],
|
||||
mutationFn: (patch = {}) => {
|
||||
mutationFn: async (patch = {}) => {
|
||||
if (workspace === null) {
|
||||
throw new Error("Cannot create request when there's no active workspace");
|
||||
}
|
||||
if (patch.sortPriority === undefined) {
|
||||
if (activeRequest != null) {
|
||||
// Place above currently-active request
|
||||
// Place above currently active request
|
||||
patch.sortPriority = activeRequest.sortPriority - 0.0001;
|
||||
} else {
|
||||
// Place at the very top
|
||||
@@ -29,16 +35,20 @@ export function useCreateHttpRequest() {
|
||||
}
|
||||
}
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invokeCmd('cmd_create_http_request', {
|
||||
return invokeCmd<HttpRequest>('cmd_create_http_request', {
|
||||
request: { workspaceId: workspace.id, ...patch },
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
// Optimistic update
|
||||
setHttpRequests(updateModelList(request));
|
||||
|
||||
routes.navigate('request', {
|
||||
workspaceId: request.workspaceId,
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai/index';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { usePrompt } from './usePrompt';
|
||||
import { updateModelList } from './useSyncModelStores';
|
||||
import { workspacesAtom } from './useWorkspaces';
|
||||
|
||||
export function useCreateWorkspace() {
|
||||
const routes = useAppRoutes();
|
||||
const prompt = usePrompt();
|
||||
return useMutation<Workspace, void, void>({
|
||||
const setWorkspaces = useSetAtom(workspacesAtom);
|
||||
|
||||
return useMutation<Workspace | null, void, void>({
|
||||
mutationKey: ['create_workspace'],
|
||||
mutationFn: async () => {
|
||||
const name = await prompt({
|
||||
@@ -18,10 +23,22 @@ export function useCreateWorkspace() {
|
||||
placeholder: 'My Workspace',
|
||||
confirmText: 'Create',
|
||||
});
|
||||
return invokeCmd('cmd_create_workspace', { name });
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return invokeCmd<Workspace>('cmd_create_workspace', { name });
|
||||
},
|
||||
onSuccess: async (workspace) => {
|
||||
routes.navigate('workspace', { workspaceId: workspace.id });
|
||||
if (workspace == null) return;
|
||||
|
||||
// Optimistic update
|
||||
setWorkspaces(updateModelList(workspace));
|
||||
|
||||
routes.navigate('workspace', {
|
||||
workspaceId: workspace.id,
|
||||
environmentId: null,
|
||||
cookieJarId: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { getGrpcRequest } from '../lib/store';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import {grpcRequestsAtom} from "./useGrpcRequests";
|
||||
import {removeModelById} from "./useSyncModelStores";
|
||||
|
||||
export function useDeleteAnyGrpcRequest() {
|
||||
const confirm = useConfirm();
|
||||
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
|
||||
|
||||
return useMutation<GrpcRequest | null, string, string>({
|
||||
mutationKey: ['delete_any_grpc_request'],
|
||||
@@ -29,6 +33,12 @@ export function useDeleteAnyGrpcRequest() {
|
||||
if (!confirmed) return null;
|
||||
return invokeCmd('cmd_delete_grpc_request', { requestId: id });
|
||||
},
|
||||
onSuccess: (request) => {
|
||||
if (request == null) return;
|
||||
|
||||
// Optimistic update
|
||||
setGrpcRequests(removeModelById(request));
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_request', 'delete'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { getHttpRequest } from '../lib/store';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { httpRequestsAtom } from './useHttpRequests';
|
||||
import { removeModelById } from './useSyncModelStores';
|
||||
|
||||
export function useDeleteAnyHttpRequest() {
|
||||
const confirm = useConfirm();
|
||||
const setHttpRequests = useSetAtom(httpRequestsAtom);
|
||||
|
||||
return useMutation<HttpRequest | null, string, string>({
|
||||
mutationKey: ['delete_any_http_request'],
|
||||
@@ -27,7 +31,13 @@ export function useDeleteAnyHttpRequest() {
|
||||
),
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return invokeCmd('cmd_delete_http_request', { requestId: id });
|
||||
return invokeCmd<HttpRequest>('cmd_delete_http_request', { requestId: id });
|
||||
},
|
||||
onSuccess: (request) => {
|
||||
if (request == null) return;
|
||||
|
||||
// Optimistic update
|
||||
setHttpRequests(removeModelById(request));
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'delete'),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { CookieJar } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import {cookieJarsAtom} from "./useCookieJars";
|
||||
import {removeModelById} from "./useSyncModelStores";
|
||||
|
||||
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
|
||||
const confirm = useConfirm();
|
||||
const setCookieJars = useSetAtom(cookieJarsAtom);
|
||||
|
||||
return useMutation<CookieJar | null, string>({
|
||||
mutationKey: ['delete_cookie_jar', cookieJar?.id],
|
||||
@@ -25,5 +29,10 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) {
|
||||
return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('cookie_jar', 'delete'),
|
||||
onSuccess: (cookieJar) => {
|
||||
if (cookieJar == null) return;
|
||||
|
||||
setCookieJars(removeModelById(cookieJar));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Environment } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import {environmentsAtom} from "./useEnvironments";
|
||||
import {removeModelById} from "./useSyncModelStores";
|
||||
|
||||
export function useDeleteEnvironment(environment: Environment | null) {
|
||||
const confirm = useConfirm();
|
||||
const setEnvironments = useSetAtom(environmentsAtom);
|
||||
|
||||
return useMutation<Environment | null, string>({
|
||||
mutationKey: ['delete_environment', environment?.id],
|
||||
@@ -25,5 +29,10 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
return invokeCmd('cmd_delete_environment', { environmentId: environment?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'delete'),
|
||||
onSuccess: (environment) => {
|
||||
if (environment == null) return;
|
||||
|
||||
setEnvironments(removeModelById(environment));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Folder } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { getFolder } from '../lib/store';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { foldersAtom } from './useFolders';
|
||||
import { removeModelById } from './useSyncModelStores';
|
||||
|
||||
export function useDeleteFolder(id: string | null) {
|
||||
const confirm = useConfirm();
|
||||
const setFolders = useSetAtom(foldersAtom);
|
||||
|
||||
return useMutation<Folder | null, string>({
|
||||
mutationKey: ['delete_folder', id],
|
||||
@@ -27,5 +31,10 @@ export function useDeleteFolder(id: string | null) {
|
||||
return invokeCmd('cmd_delete_folder', { folderId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'delete'),
|
||||
onSuccess: (folder) => {
|
||||
if (folder == null) return;
|
||||
|
||||
setFolders(removeModelById(folder));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { GrpcConnection } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import {grpcConnectionsAtom} from "./useGrpcConnections";
|
||||
import {removeModelById} from "./useSyncModelStores";
|
||||
|
||||
export function useDeleteGrpcConnection(id: string | null) {
|
||||
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
|
||||
return useMutation<GrpcConnection>({
|
||||
mutationKey: ['delete_grpc_connection', id],
|
||||
mutationFn: async () => {
|
||||
return await invokeCmd('cmd_delete_grpc_connection', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_connection', 'delete'),
|
||||
onSuccess: (connection) => {
|
||||
if (connection == null) return;
|
||||
|
||||
setGrpcConnections(removeModelById(connection));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { grpcConnectionsAtom } from './useGrpcConnections';
|
||||
|
||||
export function useDeleteGrpcConnections(requestId?: string) {
|
||||
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
|
||||
return useMutation({
|
||||
mutationKey: ['delete_grpc_connections', requestId],
|
||||
mutationFn: async () => {
|
||||
@@ -10,5 +13,8 @@ export function useDeleteGrpcConnections(requestId?: string) {
|
||||
await invokeCmd('cmd_delete_all_grpc_connections', { requestId });
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_connection', 'delete_many'),
|
||||
onSuccess: () => {
|
||||
setGrpcConnections((all) => all.filter((r) => r.requestId !== requestId));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import {httpResponsesAtom} from "./useHttpResponses";
|
||||
import {removeModelById} from "./useSyncModelStores";
|
||||
|
||||
export function useDeleteHttpResponse(id: string | null) {
|
||||
const setHttpResponses = useSetAtom(httpResponsesAtom);
|
||||
return useMutation<HttpResponse>({
|
||||
mutationKey: ['delete_http_response', id],
|
||||
mutationFn: async () => {
|
||||
return await invokeCmd('cmd_delete_http_response', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete'),
|
||||
onSuccess: (response) => {
|
||||
setHttpResponses(removeModelById(response));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { httpResponsesAtom } from './useHttpResponses';
|
||||
|
||||
export function useDeleteHttpResponses(requestId?: string) {
|
||||
const setHttpResponses = useSetAtom(httpResponsesAtom);
|
||||
return useMutation({
|
||||
mutationKey: ['delete_http_responses', requestId],
|
||||
mutationFn: async () => {
|
||||
if (requestId === undefined) return;
|
||||
await invokeCmd('cmd_delete_all_http_responses', { requestId });
|
||||
},
|
||||
onSuccess: () => {
|
||||
setHttpResponses((all) => all.filter((r) => r.requestId !== requestId));
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete_many'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useSetAtom } from 'jotai/index';
|
||||
import { count } from '../lib/pluralize';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAlert } from './useAlert';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { useGrpcConnections } from './useGrpcConnections';
|
||||
import { useHttpResponses } from './useHttpResponses';
|
||||
import { httpResponsesAtom, useHttpResponses } from './useHttpResponses';
|
||||
|
||||
export function useDeleteSendHistory() {
|
||||
const confirm = useConfirm();
|
||||
const alert = useAlert();
|
||||
const setHttpResponses = useSetAtom(httpResponsesAtom);
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const httpResponses = useHttpResponses();
|
||||
const grpcConnections = useGrpcConnections();
|
||||
@@ -36,8 +38,15 @@ export function useDeleteSendHistory() {
|
||||
variant: 'delete',
|
||||
description: <>Delete {labels.join(' and ')}?</>,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
if (!confirmed) return false;
|
||||
|
||||
await invokeCmd('cmd_delete_send_history', { workspaceId: activeWorkspace?.id ?? 'n/a' });
|
||||
return true;
|
||||
},
|
||||
onSuccess: async (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
|
||||
setHttpResponses((all) => all.filter((r) => r.workspaceId !== activeWorkspace?.id));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Workspace } from '@yaakapp-internal/models';
|
||||
import {useSetAtom} from "jotai";
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import {removeModelById} from "./useSyncModelStores";
|
||||
import {workspacesAtom} from "./useWorkspaces";
|
||||
|
||||
export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const routes = useAppRoutes();
|
||||
const confirm = useConfirm();
|
||||
const setWorkspaces = useSetAtom(workspacesAtom);
|
||||
|
||||
return useMutation<Workspace | null, string>({
|
||||
mutationKey: ['delete_workspace', workspace?.id],
|
||||
@@ -32,6 +36,9 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
|
||||
// Optimistic update
|
||||
setWorkspaces(removeModelById(workspace));
|
||||
|
||||
const { id: workspaceId } = workspace;
|
||||
if (workspaceId === activeWorkspace?.id) {
|
||||
routes.navigate('workspaces');
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation } from '@tanstack/react-query';
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import {useActiveCookieJar} from "./useActiveCookieJar";
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
@@ -16,6 +17,7 @@ export function useDuplicateGrpcRequest({
|
||||
}) {
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [activeCookieJar] = useActiveCookieJar();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
return useMutation<GrpcRequest, string>({
|
||||
@@ -36,7 +38,8 @@ export function useDuplicateGrpcRequest({
|
||||
routes.navigate('request', {
|
||||
workspaceId: activeWorkspace.id,
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import {useActiveCookieJar} from "./useActiveCookieJar";
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
@@ -15,6 +16,7 @@ export function useDuplicateHttpRequest({
|
||||
}) {
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [activeCookieJar] = useActiveCookieJar();
|
||||
const routes = useAppRoutes();
|
||||
return useMutation<HttpRequest, string>({
|
||||
mutationKey: ['duplicate_http_request', id],
|
||||
@@ -28,7 +30,8 @@ export function useDuplicateHttpRequest({
|
||||
routes.navigate('request', {
|
||||
workspaceId: activeWorkspace.id,
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironment?.id,
|
||||
environmentId: activeEnvironment?.id ?? null,
|
||||
cookieJarId: activeCookieJar?.id ?? null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { useToast } from '../components/ToastContext';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
@@ -22,11 +23,10 @@ export function useImportCurl() {
|
||||
overwriteRequestId?: string;
|
||||
command: string;
|
||||
}) => {
|
||||
const request: Record<string, unknown> = await invokeCmd('cmd_curl_to_request', {
|
||||
const request: HttpRequest = await invokeCmd('cmd_curl_to_request', {
|
||||
command,
|
||||
workspaceId: workspace?.id,
|
||||
});
|
||||
delete request.id;
|
||||
|
||||
let verb;
|
||||
if (overwriteRequestId == null) {
|
||||
@@ -34,7 +34,19 @@ export function useImportCurl() {
|
||||
await createRequest.mutateAsync(request);
|
||||
} else {
|
||||
verb = 'Updated';
|
||||
await updateRequest.mutateAsync({ id: overwriteRequestId, update: request });
|
||||
await updateRequest.mutateAsync({
|
||||
id: overwriteRequestId,
|
||||
update: (r: HttpRequest) => ({
|
||||
...request,
|
||||
id: r.id,
|
||||
createdAt: r.createdAt,
|
||||
workspaceId: r.workspaceId,
|
||||
folderId: r.folderId,
|
||||
name: r.name,
|
||||
sortPriority: r.sortPriority,
|
||||
}),
|
||||
});
|
||||
|
||||
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ export function useImportData() {
|
||||
if (importedWorkspace != null) {
|
||||
routes.navigate('workspace', {
|
||||
workspaceId: importedWorkspace.id,
|
||||
environmentId: imported.environments[0]?.id,
|
||||
environmentId: imported.environments[0]?.id ?? null,
|
||||
cookieJarId: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,14 @@ const introspectionRequestBody = JSON.stringify({
|
||||
operationName: 'IntrospectionQuery',
|
||||
});
|
||||
|
||||
export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
export function useIntrospectGraphQL(
|
||||
baseRequest: HttpRequest,
|
||||
options: { disabled?: boolean } = {},
|
||||
) {
|
||||
// Debounce the request because it can change rapidly and we don't
|
||||
// want to send so too many requests.
|
||||
const request = useDebouncedValue(baseRequest);
|
||||
const [activeEnvironment] = useActiveEnvironment();
|
||||
const [refetchKey, setRefetchKey] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
@@ -29,10 +31,11 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
namespace: 'global',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIntrospection = async () => {
|
||||
const refetch = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const args = {
|
||||
...baseRequest,
|
||||
bodyType: 'application/json',
|
||||
@@ -44,42 +47,54 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
return setError(response.error);
|
||||
}
|
||||
|
||||
const bodyText = await getResponseBodyText(response);
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`Request failed with status ${response.status}.\n\n${bodyText}`);
|
||||
return setError(`Request failed with status ${response.status}.\n\n${bodyText}`);
|
||||
}
|
||||
|
||||
if (bodyText === null) {
|
||||
throw new Error('Empty body returned in response');
|
||||
return setError('Empty body returned in response');
|
||||
}
|
||||
|
||||
const { data } = JSON.parse(bodyText);
|
||||
console.log(`Got introspection response for ${baseRequest.url}`, data);
|
||||
await setIntrospection(data);
|
||||
};
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [activeEnvironment?.id, baseRequest, setIntrospection]);
|
||||
|
||||
fetchIntrospection()
|
||||
.catch((e) => setError(e.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
useEffect(() => {
|
||||
// Skip introspection if automatic is disabled and we already have one
|
||||
if (options.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
refetch().catch(console.error);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [request.id, request.url, request.method, refetchKey, activeEnvironment?.id]);
|
||||
}, [request.id, request.url, request.method, activeEnvironment?.id]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchKey((k) => k + 1);
|
||||
}, []);
|
||||
const clear = useCallback(async () => {
|
||||
await setIntrospection(null);
|
||||
}, [setIntrospection]);
|
||||
|
||||
const schema = useMemo(() => {
|
||||
if (introspection == null) {
|
||||
return introspection;
|
||||
}
|
||||
try {
|
||||
return introspection ? buildClientSchema(introspection) : undefined;
|
||||
return buildClientSchema(introspection);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
setError('message' in e ? e.message : String(e));
|
||||
}
|
||||
}, [introspection]);
|
||||
|
||||
return { schema, isLoading, error, refetch };
|
||||
return { schema, isLoading, error, refetch, clear };
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useKeyValue<T extends object | boolean | number | string | null>
|
||||
key,
|
||||
fallback,
|
||||
}: {
|
||||
namespace?: 'global' | 'no_sync';
|
||||
namespace?: 'global' | 'no_sync' | 'license';
|
||||
key: string | string[];
|
||||
fallback: T;
|
||||
}) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { SettingsTab } from '../components/Settings/Settings';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
|
||||
export function useOpenSettings() {
|
||||
export function useOpenSettings(tab?: SettingsTab) {
|
||||
const routes = useAppRoutes();
|
||||
const workspace = useActiveWorkspace();
|
||||
return useMutation({
|
||||
@@ -12,7 +13,7 @@ export function useOpenSettings() {
|
||||
if (workspace == null) return;
|
||||
|
||||
await invokeCmd('cmd_new_child_window', {
|
||||
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
|
||||
url: routes.paths.workspaceSettings({ workspaceId: workspace.id, tab }),
|
||||
label: 'settings',
|
||||
title: 'Yaak Settings',
|
||||
innerSize: [600, 550],
|
||||
|
||||
@@ -17,9 +17,9 @@ export function useOpenWorkspace() {
|
||||
workspaceId: string;
|
||||
inNewWindow: boolean;
|
||||
}) => {
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0];
|
||||
const requestId = (await getRecentRequests(workspaceId))[0];
|
||||
const cookieJarId = (await getRecentCookieJars(workspaceId))[0];
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
|
||||
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
|
||||
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
|
||||
const baseArgs = { workspaceId, environmentId, cookieJarId } as const;
|
||||
if (inNewWindow) {
|
||||
const path =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { Tokens } from '@yaakapp-internal/template';
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function useParseTemplate(template: string) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AnyModel } from '@yaakapp-internal/models';
|
||||
import { useSetAtom } from 'jotai/index';
|
||||
import { extractKeyValue } from '../lib/keyValueStore';
|
||||
import { modelsEq } from '../lib/model_util';
|
||||
import {useActiveWorkspace} from "./useActiveWorkspace";
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { cookieJarsAtom } from './useCookieJars';
|
||||
import { environmentsAtom } from './useEnvironments';
|
||||
import { foldersAtom } from './useFolders';
|
||||
@@ -60,31 +60,26 @@ export function useSyncModelStores() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark these models as DESC instead of ASC
|
||||
const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes(
|
||||
model.model,
|
||||
);
|
||||
|
||||
if (shouldIgnoreModel(model, windowLabel)) return;
|
||||
|
||||
if (model.model === 'workspace') {
|
||||
setWorkspaces(updateModelList(model, pushToFront));
|
||||
setWorkspaces(updateModelList(model));
|
||||
} else if (model.model === 'plugin') {
|
||||
setPlugins(updateModelList(model, pushToFront));
|
||||
setPlugins(updateModelList(model));
|
||||
} else if (model.model === 'http_request') {
|
||||
setHttpRequests(updateModelList(model, pushToFront));
|
||||
setHttpRequests(updateModelList(model));
|
||||
} else if (model.model === 'folder') {
|
||||
setFolders(updateModelList(model, pushToFront));
|
||||
setFolders(updateModelList(model));
|
||||
} else if (model.model === 'http_response') {
|
||||
setHttpResponses(updateModelList(model, pushToFront));
|
||||
setHttpResponses(updateModelList(model));
|
||||
} else if (model.model === 'grpc_request') {
|
||||
setGrpcRequests(updateModelList(model, pushToFront));
|
||||
setGrpcRequests(updateModelList(model));
|
||||
} else if (model.model === 'grpc_connection') {
|
||||
setGrpcConnections(updateModelList(model, pushToFront));
|
||||
setGrpcConnections(updateModelList(model));
|
||||
} else if (model.model === 'environment') {
|
||||
setEnvironments(updateModelList(model, pushToFront));
|
||||
setEnvironments(updateModelList(model));
|
||||
} else if (model.model === 'cookie_jar') {
|
||||
setCookieJars(updateModelList(model, pushToFront));
|
||||
setCookieJars(updateModelList(model));
|
||||
} else if (model.model === 'settings') {
|
||||
setSettings(model);
|
||||
} else if (queryKey != null) {
|
||||
@@ -96,7 +91,7 @@ export function useSyncModelStores() {
|
||||
}
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
return updateModelList(model, pushToFront)(current);
|
||||
return updateModelList(model)(current);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -106,35 +101,38 @@ export function useSyncModelStores() {
|
||||
const { model, windowLabel } = payload;
|
||||
if (shouldIgnoreModel(model, windowLabel)) return;
|
||||
|
||||
console.log('Delete model', payload.model);
|
||||
console.log('Delete model', payload);
|
||||
|
||||
if (model.model === 'workspace') {
|
||||
setWorkspaces(removeById(model));
|
||||
setWorkspaces(removeModelById(model));
|
||||
} else if (model.model === 'plugin') {
|
||||
setPlugins(removeById(model));
|
||||
setPlugins(removeModelById(model));
|
||||
} else if (model.model === 'http_request') {
|
||||
setHttpRequests(removeById(model));
|
||||
setHttpRequests(removeModelById(model));
|
||||
} else if (model.model === 'http_response') {
|
||||
setHttpResponses(removeById(model));
|
||||
setHttpResponses(removeModelById(model));
|
||||
} else if (model.model === 'folder') {
|
||||
setFolders(removeById(model));
|
||||
setFolders(removeModelById(model));
|
||||
} else if (model.model === 'environment') {
|
||||
setEnvironments(removeById(model));
|
||||
setEnvironments(removeModelById(model));
|
||||
} else if (model.model === 'grpc_request') {
|
||||
setGrpcRequests(removeById(model));
|
||||
setGrpcRequests(removeModelById(model));
|
||||
} else if (model.model === 'grpc_connection') {
|
||||
setGrpcConnections(removeById(model));
|
||||
setGrpcConnections(removeModelById(model));
|
||||
} else if (model.model === 'grpc_event') {
|
||||
queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model));
|
||||
queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model));
|
||||
} else if (model.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(model), undefined);
|
||||
} else if (model.model === 'cookie_jar') {
|
||||
setCookieJars(removeById(model));
|
||||
setCookieJars(removeModelById(model));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
|
||||
export function updateModelList<T extends AnyModel>(model: T) {
|
||||
// Mark these models as DESC instead of ASC
|
||||
const pushToFront = model.model === 'http_response' || model.model === 'grpc_connection';
|
||||
|
||||
return (current: T[] | undefined): T[] => {
|
||||
const index = current?.findIndex((v) => modelsEq(v, model)) ?? -1;
|
||||
if (index >= 0) {
|
||||
@@ -145,7 +143,7 @@ function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
function removeById<T extends { id: string }>(model: T) {
|
||||
export function removeModelById<T extends { id: string }>(model: T) {
|
||||
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { GetTemplateFunctionsResponse } from '@yaakapp-internal/plugin';
|
||||
import { useState } from 'react';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { usePluginsKey } from './usePlugins';
|
||||
|
||||
export function useTemplateFunctions() {
|
||||
const pluginsKey = usePluginsKey();
|
||||
const [numFns, setNumFns] = useState<number>(0);
|
||||
|
||||
const result = useQuery({
|
||||
queryKey: ['template_functions', pluginsKey],
|
||||
// Fetch periodically until functions are returned
|
||||
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
|
||||
// to refetch things until that's working again
|
||||
// TODO: Update plugin system to wait for plugins to initialize before sending the first event to them
|
||||
refetchInterval: numFns > 0 ? Infinity : 500,
|
||||
refetchOnMount: true,
|
||||
queryFn: async () => {
|
||||
const responses = (await invokeCmd(
|
||||
'cmd_template_functions',
|
||||
)) as GetTemplateFunctionsResponse[];
|
||||
return responses;
|
||||
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions');
|
||||
setNumFns(result.length);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { Tokens } from '@yaakapp-internal/template';
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function useTemplateTokensToString(tokens: Tokens) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user