gRPC Support (#20)

This commit is contained in:
Gregory Schier
2024-02-09 05:01:00 -08:00
committed by GitHub
parent 373915671e
commit 9f54eb77a0
162 changed files with 6670 additions and 1770 deletions

312
package-lock.json generated
View File

@@ -28,6 +28,9 @@
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"codemirror-json5": "^1.0.3",
"date-fns": "^3.3.1",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",
@@ -469,6 +472,30 @@
"node": ">=6.9.0"
}
},
"node_modules/@changesets/changelog-github": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.8.tgz",
"integrity": "sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==",
"dependencies": {
"@changesets/get-github-info": "^0.5.2",
"@changesets/types": "^5.2.1",
"dotenv": "^8.1.0"
}
},
"node_modules/@changesets/get-github-info": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz",
"integrity": "sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==",
"dependencies": {
"dataloader": "^1.4.0",
"node-fetch": "^2.5.0"
}
},
"node_modules/@changesets/types": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz",
"integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg=="
},
"node_modules/@codemirror/autocomplete": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.2.0.tgz",
@@ -511,16 +538,6 @@
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/lang-javascript/node_modules/@codemirror/view": {
"version": "6.21.4",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.21.4.tgz",
"integrity": "sha512-WKVZ7nvN0lwWPfAf05WxWqTpwjC8YN3q5goj3CsSig7//DD81LULgOx3nBegqpqP0iygBqRmW8z0KSc2QTAdAg==",
"dependencies": {
"@codemirror/state": "^6.1.4",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
@@ -556,9 +573,9 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.2.1.tgz",
"integrity": "sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.4.2.tgz",
"integrity": "sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@@ -576,17 +593,17 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
"integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA=="
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz",
"integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A=="
},
"node_modules/@codemirror/view": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.2.1.tgz",
"integrity": "sha512-r1svbtAj2Lp/86F3yy1TfDAOAtJRGLINLSEqByETyUaGo1EnLS+P+bbGCVHV62z46BzZYm16noDid69+4bzn0g==",
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.23.1.tgz",
"integrity": "sha512-J2Xnn5lFYT1ZN/5ewEoMBCmLlL71lZ3mBdb7cUEuHhX2ESoSrNEucpsDXpX22EuTGm9LOgC9v4Z0wx+Ez8QmGA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"style-mod": "^4.0.0",
"@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
@@ -1350,6 +1367,20 @@
}
}
},
"node_modules/@sagold/json-pointer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@sagold/json-pointer/-/json-pointer-5.1.1.tgz",
"integrity": "sha512-/iskWuyGNu09qy09HYmyLnvzpKryymH9T+vTBi2LdFp1TuKvERDADvPMv2ZkQKsrRklOzivmOz9QXof0dKqvgA=="
},
"node_modules/@sagold/json-query": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@sagold/json-query/-/json-query-6.1.1.tgz",
"integrity": "sha512-5/Wu0rTnXmO5Uvtm9Of16Vx3mKjSnYA0Um9LgBtyPhIucYlppKgKC4N3g8gD0Fk00a5kizQTs4gwxKPXCpmeww==",
"dependencies": {
"@sagold/json-pointer": "^5.1.1",
"ebnf": "^1.9.1"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -2174,8 +2205,7 @@
"node_modules/@types/json-schema": {
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
"integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
"dev": true
"integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw=="
},
"node_modules/@types/json5": {
"version": "0.0.29",
@@ -3537,6 +3567,53 @@
"@codemirror/view": "^6.0.0"
}
},
"node_modules/codemirror-json-schema": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/codemirror-json-schema/-/codemirror-json-schema-0.6.1.tgz",
"integrity": "sha512-QG12Jy917eStZzxurpAE9QUQxF8SS/AYJ9DDteyJZcRGH8ePaBCfQ4KLCNtY6cUKjFeNBgcd5+c6FPAri6pPQg==",
"dependencies": {
"@changesets/changelog-github": "^0.4.8",
"@sagold/json-pointer": "^5.1.1",
"@types/json-schema": "^7.0.12",
"@types/node": "^20.4.2",
"json-schema": "^0.4.0",
"json-schema-library": "^9.1.2"
},
"optionalDependencies": {
"@codemirror/lang-json": "^6.0.1",
"codemirror-json5": "^1.0.3",
"json5": "^2.2.3"
},
"peerDependencies": {
"@codemirror/language": "^6.8.0",
"@codemirror/lint": "^6.4.0",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.14.1",
"@lezer/common": "^1.0.3"
}
},
"node_modules/codemirror-json-schema/node_modules/@types/node": {
"version": "20.11.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz",
"integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/codemirror-json5": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/codemirror-json5/-/codemirror-json5-1.0.3.tgz",
"integrity": "sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"json5": "^2.2.1",
"lezer-json5": "^2.0.2"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3724,6 +3801,20 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"node_modules/dataloader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
},
"node_modules/date-fns": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -3803,6 +3894,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
@@ -3885,6 +3984,11 @@
"node": ">=8"
}
},
"node_modules/discontinuous-range": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
"integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ=="
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -3924,6 +4028,14 @@
"node": ">=4"
}
},
"node_modules/dotenv": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz",
"integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==",
"engines": {
"node": ">=10"
}
},
"node_modules/duplexer3": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz",
@@ -3936,6 +4048,14 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ebnf": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ebnf/-/ebnf-1.9.1.tgz",
"integrity": "sha512-uW2UKSsuty9ANJ3YByIQE4ANkD8nqUPO7r6Fwcc1ADKPe9FRdcPpMl3VEput4JSvKBJ4J86npIC2MLP0pYkCuw==",
"bin": {
"ebnf": "dist/bin.js"
}
},
"node_modules/electron": {
"version": "23.3.13",
"resolved": "https://registry.npmjs.org/electron/-/electron-23.3.13.tgz",
@@ -4777,6 +4897,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fast-copy": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz",
"integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -6097,6 +6222,25 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"node_modules/json-schema-library": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/json-schema-library/-/json-schema-library-9.1.2.tgz",
"integrity": "sha512-uQnFb2V+VakLl6XIGGtUQzfjkP31f/dCT5lJq9NOUdypSSpjbWL/V0R2KvoNJp3hU8VErwh9DqVoZPqlC+B3IA==",
"dependencies": {
"@sagold/json-pointer": "^5.1.1",
"@sagold/json-query": "^6.1.1",
"deepmerge": "^4.3.1",
"fast-copy": "^3.0.1",
"fast-deep-equal": "^3.1.3",
"smtp-address-parser": "1.0.10",
"valid-url": "^1.0.9"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -6120,7 +6264,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
},
@@ -6207,6 +6350,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/lezer-json5": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lezer-json5/-/lezer-json5-2.0.2.tgz",
"integrity": "sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ==",
"dependencies": {
"@lezer/lr": "^1.0.0"
}
},
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -6590,6 +6741,11 @@
"ufo": "^1.3.0"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -6654,12 +6810,57 @@
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
"dev": true
},
"node_modules/nearley": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz",
"integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==",
"dependencies": {
"commander": "^2.19.0",
"moo": "^0.5.0",
"railroad-diagrams": "^1.0.0",
"randexp": "0.4.6"
},
"bin": {
"nearley-railroad": "bin/nearley-railroad.js",
"nearley-test": "bin/nearley-test.js",
"nearley-unparse": "bin/nearley-unparse.js",
"nearleyc": "bin/nearleyc.js"
},
"funding": {
"type": "individual",
"url": "https://nearley.js.org/#give-to-nearley"
}
},
"node_modules/nearley/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@@ -7606,6 +7807,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/railroad-diagrams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
"integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A=="
},
"node_modules/randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
"integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
"dependencies": {
"discontinuous-range": "1.0.0",
"ret": "~0.1.10"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -8114,6 +8332,14 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
"node_modules/ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"engines": {
"node": ">=0.12"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -8523,6 +8749,17 @@
"node": ">=8.0.0"
}
},
"node_modules/smtp-address-parser": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz",
"integrity": "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==",
"dependencies": {
"nearley": "^2.20.1"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -9253,6 +9490,11 @@
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
@@ -9448,8 +9690,7 @@
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"devOptional": true
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unique-string": {
"version": "1.0.0",
@@ -9578,6 +9819,11 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/valid-url": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
"integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA=="
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -9780,6 +10026,20 @@
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -45,6 +45,9 @@
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"codemirror-json5": "^1.0.3",
"date-fns": "^3.3.1",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO folders (\n id,\n workspace_id,\n folder_id,\n name,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id, workspace_id, folder_id, name, url, url_parameters, method, body, body_type,\n authentication, authentication_type, headers, sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO workspaces (\n id, name, description, variables, setting_request_timeout,\n setting_follow_redirects, setting_validate_certificates\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables,\n setting_request_timeout = excluded.setting_request_timeout,\n setting_follow_redirects = excluded.setting_follow_redirects,\n setting_validate_certificates = excluded.setting_validate_certificates\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "12b265491d1ebba19e1ce8a660e458ffbcd8c0850aef16ba1f70e358623ac66a"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO environments (\n id, workspace_id, name, variables\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n variables = excluded.variables\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "13cb883199e81966174e6fda9c252bf7213fe01b5346266c0a89dc0ac89eda64"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE connection_id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "connection_id",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "message",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "is_server",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "is_info",
"ordinal": 8,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_responses (\n id, request_id, workspace_id, elapsed, elapsed_headers, url, status, status_reason,\n content_length, body_path, headers, version, remote_addr\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "2c9658a639c5e4994ae9c8ec30bd4e40a1945d640962991f879928619950ef62"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed\n FROM grpc_connections\n WHERE id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "service",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "method",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 8,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "connection_id",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "message",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "is_server",
"ordinal": 7,
"type_info": "Bool"
},
{
"name": "is_info",
"ordinal": 8,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM grpc_connections\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme, appearance, update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n url_parameters,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message, is_server, is_info\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message,\n is_server = excluded.is_server,\n is_info = excluded.is_info\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n folder_id,\n name,\n sort_priority\n FROM folders\n WHERE id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority\n FROM folders\n WHERE workspace_id = ?\n ",
"describe": {
"columns": [
{
@@ -58,5 +58,5 @@
false
]
},
"hash": "1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c"
"hash": "558e72df3c6f2635c6b3d52a199f9a5f7a3d82b379ff9af36645dcfb92548fdd"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, url, method,\n body_type, authentication_type, sort_priority,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -44,39 +44,39 @@
"type_info": "Text"
},
{
"name": "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
"name": "method",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "method",
"name": "body_type",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "body!: Json<HashMap<String, JsonValue>>",
"name": "authentication_type",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "body_type",
"name": "sort_priority",
"ordinal": 11,
"type_info": "Text"
"type_info": "Float"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"name": "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "authentication_type",
"name": "body!: Json<HashMap<String, JsonValue>>",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "sort_priority",
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 14,
"type_info": "Float"
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
@@ -97,14 +97,14 @@
false,
false,
false,
false,
false,
true,
true,
false,
true,
false,
false,
false,
false
]
},
"hash": "6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd"
"hash": "573db23160de025e5c72efb90be7fff5e3ec4619b962d149fdd4d618fe02c680"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM environments\n WHERE id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, created_at, updated_at, name,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM environments\n WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -52,5 +52,5 @@
false
]
},
"hash": "689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417"
"hash": "5765e9565a8b89c5bc2d72197e0e4a1093739e9abba69f6fe5527d453fab4db8"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE http_responses SET (\n elapsed,\n elapsed_headers,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n version,\n remote_addr,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n workspace_id,\n name,\n cookies AS \"cookies!: sqlx::types::Json<Vec<JsonValue>>\"\n FROM cookie_jars WHERE id = ?\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, workspace_id, name,\n cookies AS \"cookies!: sqlx::types::Json<Vec<JsonValue>>\"\n FROM cookie_jars WHERE workspace_id = ?\n ",
"describe": {
"columns": [
{
@@ -52,5 +52,5 @@
false
]
},
"hash": "f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299"
"hash": "612efa9ac45723dc604a88f5e7e288b4055fec4ba7d9102131bd255c037fa021"
}

View File

@@ -0,0 +1,92 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json<Vec<String>>\"\n FROM grpc_requests\n WHERE id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "folder_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 7,
"type_info": "Float"
},
{
"name": "url",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "service",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "method",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "message",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "proto_files!: sqlx::types::Json<Vec<String>>",
"ordinal": 12,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
true,
true,
false,
false
]
},
"hash": "7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42"
}

View File

@@ -0,0 +1,92 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json<Vec<String>>\"\n FROM grpc_requests\n WHERE workspace_id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "folder_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "name",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 7,
"type_info": "Float"
},
{
"name": "url",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "service",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "method",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "message",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "proto_files!: sqlx::types::Json<Vec<String>>",
"ordinal": 12,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
true,
true,
false,
false
]
},
"hash": "761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7"
}

View File

@@ -0,0 +1,68 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "service",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "method",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 8,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme,\n appearance,\n update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n name,\n description,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, name, description, setting_request_timeout,\n setting_follow_redirects, setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces\n ",
"describe": {
"columns": [
{
@@ -70,5 +70,5 @@
false
]
},
"hash": "20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470"
"hash": "8dfbae65ddec905ea3734448cc9f7029b6c78de227c6fa3a85d75d0a7f21e0e9"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO folders (\n id, workspace_id, folder_id, name, sort_priority\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "9238f94c688d91f42627e5b73c627c514bab4039ab5edadc79b77dfdfd63b208"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n name,\n description,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces WHERE id = ?\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, name, description, setting_request_timeout,\n setting_follow_redirects, setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -70,5 +70,5 @@
false
]
},
"hash": "e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f"
"hash": "9ba3f783238b77637ffded4171b2fbb5e5ad0be952a0d832448d65cc5f0effc1"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_connections (\n id, workspace_id, request_id, service, method, elapsed\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n service = excluded.service,\n method = excluded.method,\n elapsed = excluded.elapsed\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n elapsed_headers,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers,\n version,\n remote_addr\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE grpc_connections\n SET (elapsed) = (-1)\n WHERE elapsed = 0;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "a690a04cd1ebe8c3dbfd0cd98ae4ef093a1696d7b7ecaf694d12e5fafd62b685"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n folder_id,\n name,\n sort_priority\n FROM folders\n WHERE workspace_id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority\n FROM folders\n WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -58,5 +58,5 @@
false
]
},
"hash": "1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb"
"hash": "ae98a7b35a5cb80a4bcd04faa22545deac2a5e9bfb814b60191f16b98ed49796"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n theme,\n appearance,\n update_channel\n FROM settings\n WHERE id = 'default'\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance, update_channel\n FROM settings\n WHERE id = 'default'\n ",
"describe": {
"columns": [
{
@@ -52,5 +52,5 @@
false
]
},
"hash": "3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335"
"hash": "b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO cookie_jars (\n id, workspace_id, name, cookies\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n cookies = excluded.cookies\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "b3fae40a793a6724dd2286a9ca4bc0a9c000a631ee0d751a9dc4f3e76de3d57c"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO cookie_jars (\n id,\n workspace_id,\n name,\n cookies\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n cookies = excluded.cookies\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO workspaces (\n id,\n name,\n description,\n variables,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables,\n setting_request_timeout = excluded.setting_request_timeout,\n setting_follow_redirects = excluded.setting_follow_redirects,\n setting_validate_certificates = excluded.setting_validate_certificates\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO environments (\n id,\n workspace_id,\n name,\n variables\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n variables = excluded.variables\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method, body_type, authentication_type, sort_priority,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n ",
"describe": {
"columns": [
{
@@ -54,29 +54,29 @@
"type_info": "Text"
},
{
"name": "body!: Json<HashMap<String, JsonValue>>",
"name": "body_type",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "body_type",
"name": "authentication_type",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"name": "sort_priority",
"ordinal": 12,
"type_info": "Text"
"type_info": "Float"
},
{
"name": "authentication_type",
"name": "body!: Json<HashMap<String, JsonValue>>",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "sort_priority",
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 14,
"type_info": "Float"
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
@@ -98,13 +98,13 @@
false,
false,
false,
false,
true,
true,
false,
true,
false,
false,
false
]
},
"hash": "7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39"
"hash": "e61c0dddb3e86d271cb9399faa4e4443342796cb72bdd43a821fae2994ae8e2f"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE http_responses SET (\n elapsed, elapsed_headers, url, status, status_reason, content_length, body_path,\n error, headers, version, remote_addr, updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "e7124f5570076bfd65985744f48d8e12cf29d6d243fffdd62ade2ab70c7bddda"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n proto_files\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n proto_files = excluded.proto_files\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n workspace_id,\n name,\n cookies AS \"cookies!: sqlx::types::Json<Vec<JsonValue>>\"\n FROM cookie_jars WHERE workspace_id = ?\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, workspace_id, name,\n cookies AS \"cookies!: sqlx::types::Json<Vec<JsonValue>>\"\n FROM cookie_jars WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -52,5 +52,5 @@
false
]
},
"hash": "cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7"
"hash": "f5f20f3b37d932617499a0da50997edad59e4f5998b15c50ed6eae2e97064068"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM grpc_requests\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "fe0652396bc30d926cf99083651c1cbd668bcf00ebe1a5f36616700c84972b39"
}

600
src-tauri/Cargo.lock generated
View File

@@ -77,9 +77,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.75"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "async-compression"
@@ -95,6 +95,39 @@ dependencies = [
"tokio",
]
[[package]]
name = "async-stream"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "async-trait"
version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "atk"
version = "0.15.1"
@@ -128,12 +161,67 @@ dependencies = [
"num-traits",
]
[[package]]
name = "atomic-write-file"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
dependencies = [
"nix",
"rand 0.8.5",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http",
"http-body",
"hyper",
"itoa 1.0.9",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"sync_wrapper",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"mime",
"rustversion",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.69"
@@ -1073,12 +1161,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.6"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1596,6 +1684,28 @@ dependencies = [
"system-deps 6.2.0",
]
[[package]]
name = "grpc"
version = "0.1.0"
dependencies = [
"anyhow",
"hyper",
"hyper-rustls",
"log",
"once_cell",
"prost",
"prost-reflect",
"prost-types",
"protoc-bin-vendored",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tonic",
"tonic-reflection",
"uuid",
]
[[package]]
name = "gtk"
version = "0.15.5"
@@ -1761,20 +1871,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "html5ever"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
dependencies = [
"log",
"mac",
"markup5ever 0.10.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@@ -1783,7 +1879,7 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"markup5ever",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -1853,6 +1949,34 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http",
"hyper",
"log",
"rustls",
"rustls-native-certs",
"tokio",
"tokio-rustls",
]
[[package]]
name = "hyper-timeout"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
dependencies = [
"hyper",
"pin-project-lite",
"tokio",
"tokio-io-timeout",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
@@ -2233,18 +2357,6 @@ dependencies = [
"treediff",
]
[[package]]
name = "kuchiki"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358"
dependencies = [
"cssparser",
"html5ever 0.25.2",
"matches",
"selectors",
]
[[package]]
name = "kuchikiki"
version = "0.8.2"
@@ -2252,7 +2364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
dependencies = [
"cssparser",
"html5ever 0.26.0",
"html5ever",
"indexmap 1.9.3",
"matches",
"selectors",
@@ -2269,9 +2381,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.150"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libm"
@@ -2292,9 +2404,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"pkg-config",
@@ -2312,9 +2424,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.4.11"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "litemap"
@@ -2380,20 +2492,6 @@ dependencies = [
"libc",
]
[[package]]
name = "markup5ever"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
dependencies = [
"log",
"phf 0.8.0",
"phf_codegen 0.8.0",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever"
version = "0.11.0"
@@ -2423,6 +2521,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md-5"
version = "0.10.6"
@@ -2549,6 +2653,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"libc",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -2839,6 +2954,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "os_info"
version = "3.7.0"
@@ -3074,6 +3198,26 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@@ -3230,6 +3374,114 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "prost-reflect"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057237efdb71cf4b3f9396302a3d6599a92fa94063ba537b66130980ea9909f3"
dependencies = [
"base64 0.21.5",
"once_cell",
"prost",
"prost-reflect-derive",
"prost-types",
"serde",
"serde-value",
]
[[package]]
name = "prost-reflect-derive"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "172da1212c02be2c94901440cb27183cd92bff00ebacca5c323bf7520b8f9c04"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "prost-types"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e"
dependencies = [
"prost",
]
[[package]]
name = "protoc-bin-vendored"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d"
dependencies = [
"protoc-bin-vendored-linux-aarch_64",
"protoc-bin-vendored-linux-ppcle_64",
"protoc-bin-vendored-linux-x86_32",
"protoc-bin-vendored-linux-x86_64",
"protoc-bin-vendored-macos-x86_64",
"protoc-bin-vendored-win32",
]
[[package]]
name = "protoc-bin-vendored-linux-aarch_64"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435"
[[package]]
name = "protoc-bin-vendored-linux-ppcle_64"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516"
[[package]]
name = "protoc-bin-vendored-linux-x86_32"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0"
[[package]]
name = "protoc-bin-vendored-linux-x86_64"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924"
[[package]]
name = "protoc-bin-vendored-macos-x86_64"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537"
[[package]]
name = "protoc-bin-vendored-win32"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804"
[[package]]
name = "psl-types"
version = "2.0.11"
@@ -3577,15 +3829,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.21"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3594,11 +3846,24 @@ version = "0.21.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@@ -3736,18 +4001,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
@@ -3756,9 +4031,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.111"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa 1.0.9",
"ryu",
@@ -4018,9 +4293,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33"
checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -4031,9 +4306,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d"
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
dependencies = [
"ahash",
"atoi",
@@ -4076,9 +4351,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec"
checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
dependencies = [
"proc-macro2",
"quote",
@@ -4089,10 +4364,11 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc"
checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
dependencies = [
"atomic-write-file",
"dotenvy",
"either",
"heck 0.4.1",
@@ -4115,9 +4391,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db"
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
dependencies = [
"atoi",
"base64 0.21.5",
@@ -4159,9 +4435,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624"
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
dependencies = [
"atoi",
"base64 0.21.5",
@@ -4200,9 +4476,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f"
checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
dependencies = [
"atoi",
"chrono",
@@ -4220,6 +4496,7 @@ dependencies = [
"time",
"tracing",
"url",
"urlencoding",
]
[[package]]
@@ -4314,6 +4591,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.13.0"
@@ -4469,9 +4752,9 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
[[package]]
name = "tauri"
version = "1.5.2"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9"
checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af"
dependencies = [
"anyhow",
"base64 0.21.5",
@@ -4544,9 +4827,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.4.1"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb"
checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc"
dependencies = [
"base64 0.21.5",
"brotli",
@@ -4570,9 +4853,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.4.1"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af"
checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -4613,9 +4896,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.14.1"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43"
checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76"
dependencies = [
"gtk",
"http",
@@ -4634,9 +4917,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.14.1"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895"
checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158"
dependencies = [
"cocoa 0.24.1",
"gtk",
@@ -4663,7 +4946,7 @@ dependencies = [
"dunce",
"glob",
"heck 0.4.1",
"html5ever 0.26.0",
"html5ever",
"infer",
"json-patch",
"kuchikiki",
@@ -4695,15 +4978,14 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.8.1"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4829,9 +5111,31 @@ dependencies = [
"num_cpus",
"pin-project-lite",
"socket2 0.5.5",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-io-timeout"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
dependencies = [
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
@@ -4842,6 +5146,16 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.14"
@@ -4935,6 +5249,72 @@ dependencies = [
"winnow",
]
[[package]]
name = "tonic"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e"
dependencies = [
"async-stream",
"async-trait",
"axum",
"base64 0.21.5",
"bytes",
"h2",
"http",
"http-body",
"hyper",
"hyper-timeout",
"percent-encoding",
"pin-project",
"prost",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-reflection"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fa37c513df1339d197f4ba21d28c918b9ef1ac1768265f11ecb6b7f1cba1b76"
dependencies = [
"prost",
"prost-types",
"tokio",
"tokio-stream",
"tonic",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand 0.8.5",
"slab",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -5096,6 +5476,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf-8"
version = "0.7.6"
@@ -5122,9 +5508,9 @@ checksum = "64a8922555b9500e3d865caed19330172cd67cbf82203f1a3311d8c305cc9f33"
[[package]]
name = "uuid"
version = "1.5.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"getrandom 0.2.11",
]
@@ -5360,12 +5746,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.24.0"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888"
dependencies = [
"rustls-webpki",
]
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webview2-com"
@@ -5558,6 +5941,15 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -5847,9 +6239,9 @@ checksum = "dad7bb64b8ef9c0aa27b6da38b452b0ee9fd82beaf276a87dd796fb55cbae14e"
[[package]]
name = "wry"
version = "0.24.4"
version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ef04bdad49eba2e01f06e53688c8413bd6a87b0bc14b72284465cf96e3578e"
checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4"
dependencies = [
"base64 0.13.1",
"block",
@@ -5861,9 +6253,9 @@ dependencies = [
"gio",
"glib",
"gtk",
"html5ever 0.25.2",
"html5ever",
"http",
"kuchiki",
"kuchikiki",
"libc",
"log",
"objc",
@@ -5925,6 +6317,7 @@ dependencies = [
"cookie 0.18.0",
"datetime",
"futures",
"grpc",
"http",
"log",
"objc",
@@ -5940,6 +6333,7 @@ dependencies = [
"tauri-plugin-log",
"tauri-plugin-window-state",
"tokio",
"tokio-stream",
"uuid",
"window-shadows",
]

View File

@@ -1,3 +1,4 @@
workspace = { members = ["grpc"] }
[package]
name = "yaak-app"
version = "0.0.0"
@@ -32,7 +33,7 @@ reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "br
cookie = { version = "0.18.0" }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = { version = "1.0.111", features = ["raw_value"] }
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
tauri = { version = "1.5.2", features = [
"config-toml",
"devtools",
@@ -59,6 +60,8 @@ log = "0.4.20"
datetime = "0.5.2"
window-shadows = "0.2.2"
reqwest_cookie_store = "0.6.0"
grpc = { path = "./grpc" }
tokio-stream = "0.1.14"
[features]
# by default Tauri runs in production mode

22
src-tauri/grpc/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "grpc"
version = "0.1.0"
edition = "2021"
[dependencies]
tonic = "0.10.2"
prost = "0.12"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "fs"] }
tonic-reflection = "0.10.2"
tokio-stream = "0.1.14"
prost-types = "0.12.3"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
prost-reflect = { version = "0.12.0", features = ["serde", "derive"] }
log = "0.4.20"
once_cell = { version = "1.19.0", features = [] }
anyhow = "1.0.79"
hyper = { version = "0.14" }
hyper-rustls = { version = "0.24.0", features = ["http2"] }
protoc-bin-vendored = "3.0.0"
uuid = { version = "1.7.0", features = ["v4"] }

View File

@@ -0,0 +1,52 @@
use prost_reflect::prost::Message;
use prost_reflect::{DynamicMessage, MethodDescriptor};
use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder};
use tonic::Status;
#[derive(Clone)]
pub struct DynamicCodec(MethodDescriptor);
impl DynamicCodec {
#[allow(dead_code)]
pub fn new(md: MethodDescriptor) -> Self {
Self(md)
}
}
impl Codec for DynamicCodec {
type Encode = DynamicMessage;
type Decode = DynamicMessage;
type Encoder = Self;
type Decoder = Self;
fn encoder(&mut self) -> Self::Encoder {
self.clone()
}
fn decoder(&mut self) -> Self::Decoder {
self.clone()
}
}
impl Encoder for DynamicCodec {
type Item = DynamicMessage;
type Error = Status;
fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> {
item.encode(dst)
.expect("buffer is too small to decode this message");
Ok(())
}
}
impl Decoder for DynamicCodec {
type Item = DynamicMessage;
type Error = Status;
fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Self::Item>, Self::Error> {
let mut msg = DynamicMessage::new(self.0.output());
msg.merge(src)
.map_err(|err| Status::internal(err.to_string()))?;
Ok(Some(msg))
}
}

View File

@@ -0,0 +1,179 @@
use prost_reflect::{DescriptorPool, MessageDescriptor};
use prost_types::field_descriptor_proto;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Default, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct JsonSchemaEntry {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(rename = "type")]
type_: JsonType,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
properties: Option<HashMap<String, JsonSchemaEntry>>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
enum_: Option<Vec<String>>,
/// Don't allow any other properties in the object
additional_properties: bool,
/// Set all properties to required
#[serde(skip_serializing_if = "Option::is_none")]
required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
items: Option<Box<JsonSchemaEntry>>,
}
enum JsonType {
String,
Number,
Object,
Array,
Boolean,
Null,
_UNKNOWN,
}
impl Default for JsonType {
fn default() -> Self {
JsonType::_UNKNOWN
}
}
impl serde::Serialize for JsonType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
JsonType::String => serializer.serialize_str("string"),
JsonType::Number => serializer.serialize_str("number"),
JsonType::Object => serializer.serialize_str("object"),
JsonType::Array => serializer.serialize_str("array"),
JsonType::Boolean => serializer.serialize_str("boolean"),
JsonType::Null => serializer.serialize_str("null"),
JsonType::_UNKNOWN => serializer.serialize_str("unknown"),
}
}
}
impl<'de> serde::Deserialize<'de> for JsonType {
fn deserialize<D>(deserializer: D) -> Result<JsonType, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"string" => Ok(JsonType::String),
"number" => Ok(JsonType::Number),
"object" => Ok(JsonType::Object),
"array" => Ok(JsonType::Array),
"boolean" => Ok(JsonType::Boolean),
"null" => Ok(JsonType::Null),
_ => Ok(JsonType::_UNKNOWN),
}
}
}
pub fn message_to_json_schema(
pool: &DescriptorPool,
message: MessageDescriptor,
) -> JsonSchemaEntry {
let mut schema = JsonSchemaEntry {
title: Some(message.name().to_string()),
type_: JsonType::Object, // Messages are objects
..Default::default()
};
let mut properties = HashMap::new();
message.fields().for_each(|f| match f.kind() {
prost_reflect::Kind::Message(m) => {
properties.insert(f.name().to_string(), message_to_json_schema(pool, m));
}
prost_reflect::Kind::Enum(e) => {
properties.insert(
f.name().to_string(),
JsonSchemaEntry {
type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()),
enum_: Some(e.values().map(|v| v.name().to_string()).collect::<Vec<_>>()),
..Default::default()
},
);
}
_ => {
// TODO: Handle repeated label
match f.field_descriptor_proto().label() {
field_descriptor_proto::Label::Repeated => {
// TODO: Handle more complex repeated types. This just handles primitives for now
properties.insert(
f.name().to_string(),
JsonSchemaEntry {
type_: JsonType::Array,
items: Some(Box::new(JsonSchemaEntry {
type_: map_proto_type_to_json_type(
f.field_descriptor_proto().r#type(),
),
..Default::default()
})),
..Default::default()
},
);
}
_ => {
// Regular JSON field
properties.insert(
f.name().to_string(),
JsonSchemaEntry {
type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()),
..Default::default()
},
);
}
};
}
});
schema.properties = Some(properties);
// All proto 3 fields are optional, so maybe we could
// make this a setting?
// schema.required = Some(
// message
// .fields()
// .map(|f| f.name().to_string())
// .collect::<Vec<_>>(),
// );
schema
}
fn map_proto_type_to_json_type(proto_type: field_descriptor_proto::Type) -> JsonType {
match proto_type {
field_descriptor_proto::Type::Double => JsonType::Number,
field_descriptor_proto::Type::Float => JsonType::Number,
field_descriptor_proto::Type::Int64 => JsonType::Number,
field_descriptor_proto::Type::Uint64 => JsonType::Number,
field_descriptor_proto::Type::Int32 => JsonType::Number,
field_descriptor_proto::Type::Fixed64 => JsonType::Number,
field_descriptor_proto::Type::Fixed32 => JsonType::Number,
field_descriptor_proto::Type::Bool => JsonType::Boolean,
field_descriptor_proto::Type::String => JsonType::String,
field_descriptor_proto::Type::Group => JsonType::_UNKNOWN,
field_descriptor_proto::Type::Message => JsonType::Object,
field_descriptor_proto::Type::Bytes => JsonType::String,
field_descriptor_proto::Type::Uint32 => JsonType::Number,
field_descriptor_proto::Type::Enum => JsonType::String,
field_descriptor_proto::Type::Sfixed32 => JsonType::Number,
field_descriptor_proto::Type::Sfixed64 => JsonType::Number,
field_descriptor_proto::Type::Sint32 => JsonType::Number,
field_descriptor_proto::Type::Sint64 => JsonType::Number,
}
}

40
src-tauri/grpc/src/lib.rs Normal file
View File

@@ -0,0 +1,40 @@
use prost_reflect::{DynamicMessage, SerializeOptions};
use serde::{Deserialize, Serialize};
mod codec;
mod json_schema;
pub mod manager;
mod proto;
pub fn serialize_options() -> SerializeOptions {
SerializeOptions::new().skip_default_fields(false)
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct ServiceDefinition {
pub name: String,
pub methods: Vec<MethodDefinition>,
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct MethodDefinition {
pub name: String,
pub schema: String,
pub client_streaming: bool,
pub server_streaming: bool,
}
static SERIALIZE_OPTIONS: &'static SerializeOptions = &SerializeOptions::new()
.skip_default_fields(false)
.stringify_64_bit_integers(false);
pub fn serialize_message(msg: &DynamicMessage) -> Result<String, String> {
let mut buf = Vec::new();
let mut se = serde_json::Serializer::pretty(&mut buf);
msg.serialize_with_options(&mut se, SERIALIZE_OPTIONS)
.map_err(|e| e.to_string())?;
let s = String::from_utf8(buf).expect("serde_json to emit valid utf8");
Ok(s)
}

View File

@@ -0,0 +1,280 @@
use std::collections::HashMap;
use std::path::PathBuf;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_rustls::HttpsConnector;
use prost_reflect::DescriptorPool;
pub use prost_reflect::DynamicMessage;
use serde_json::Deserializer;
use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::StreamExt;
use tonic::body::BoxBody;
use tonic::transport::Uri;
use tonic::{IntoRequest, IntoStreamingRequest, Streaming};
use crate::codec::DynamicCodec;
use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path};
use crate::{json_schema, MethodDefinition, ServiceDefinition};
#[derive(Clone)]
pub struct GrpcConnection {
pool: DescriptorPool,
conn: Client<HttpsConnector<HttpConnector>, BoxBody>,
pub uri: Uri,
}
impl GrpcConnection {
pub async fn unary(
&self,
service: &str,
method: &str,
message: &str,
) -> Result<DynamicMessage, String> {
let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap();
let input_message = method.input();
let mut deserializer = Deserializer::from_str(message);
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)
.map_err(|e| e.to_string())?;
deserializer.end().unwrap();
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
let req = req_message.into_request();
let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone());
client.ready().await.unwrap();
Ok(client
.unary(req, path, codec)
.await
.map_err(|e| e.to_string())?
.into_inner())
}
pub async fn streaming(
&self,
service: &str,
method: &str,
stream: ReceiverStream<String>,
) -> Result<Streaming<DynamicMessage>, String> {
let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap();
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
let method2 = method.clone();
let req = stream
.map(move |s| {
let mut deserializer = Deserializer::from_str(&s);
let req_message = DynamicMessage::deserialize(method2.input(), &mut deserializer)
.map_err(|e| e.to_string())
.unwrap();
deserializer.end().unwrap();
req_message
})
.into_streaming_request();
let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone());
client.ready().await.unwrap();
Ok(client
.streaming(req, path, codec)
.await
.map_err(|s| s.to_string())?
.into_inner())
}
pub async fn client_streaming(
&self,
service: &str,
method: &str,
stream: ReceiverStream<String>,
) -> Result<DynamicMessage, String> {
let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap();
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
let req = {
let method = method.clone();
stream
.map(move |s| {
let mut deserializer = Deserializer::from_str(&s);
let req_message =
DynamicMessage::deserialize(method.input(), &mut deserializer)
.map_err(|e| e.to_string())
.unwrap();
deserializer.end().unwrap();
req_message
})
.into_streaming_request()
};
let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone());
client.ready().await.unwrap();
Ok(client
.client_streaming(req, path, codec)
.await
.map_err(|s| s.to_string())?
.into_inner())
}
pub async fn server_streaming(
&self,
service: &str,
method: &str,
message: &str,
) -> Result<Streaming<DynamicMessage>, String> {
let service = self.pool.get_service_by_name(service).unwrap();
let method = &service.methods().find(|m| m.name() == method).unwrap();
let input_message = method.input();
let mut deserializer = Deserializer::from_str(message);
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)
.map_err(|e| e.to_string())?;
deserializer.end().unwrap();
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
let req = req_message.into_request();
let path = method_desc_to_path(method);
let codec = DynamicCodec::new(method.clone());
client.ready().await.unwrap();
Ok(client
.server_streaming(req, path, codec)
.await
.map_err(|s| s.to_string())?
.into_inner())
}
}
pub struct GrpcHandle {
pools: HashMap<String, DescriptorPool>,
}
impl Default for GrpcHandle {
fn default() -> Self {
let pools = HashMap::new();
Self { pools }
}
}
impl GrpcHandle {
pub async fn services_from_files(
&mut self,
id: &str,
uri: &Uri,
paths: Vec<PathBuf>,
) -> Result<Vec<ServiceDefinition>, String> {
let pool = fill_pool_from_files(paths).await?;
self.pools.insert(self.get_pool_key(id, uri), pool.clone());
Ok(self.services_from_pool(&pool))
}
pub async fn services_from_reflection(
&mut self,
id: &str,
uri: &Uri,
) -> Result<Vec<ServiceDefinition>, String> {
let pool = fill_pool(uri).await?;
self.pools.insert(self.get_pool_key(id, uri), pool.clone());
Ok(self.services_from_pool(&pool))
}
fn get_pool_key(&self, id: &str, uri: &Uri) -> String {
format!("{}-{}", id, uri)
}
fn services_from_pool(&self, pool: &DescriptorPool) -> Vec<ServiceDefinition> {
pool.services()
.map(|s| {
let mut def = ServiceDefinition {
name: s.full_name().to_string(),
methods: vec![],
};
for method in s.methods() {
let input_message = method.input();
def.methods.push(MethodDefinition {
name: method.name().to_string(),
server_streaming: method.is_server_streaming(),
client_streaming: method.is_client_streaming(),
schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema(
&pool,
input_message,
))
.unwrap(),
})
}
def
})
.collect::<Vec<_>>()
}
pub async fn server_streaming(
&mut self,
id: &str,
uri: Uri,
proto_files: Vec<PathBuf>,
service: &str,
method: &str,
message: &str,
) -> Result<Streaming<DynamicMessage>, String> {
self.connect(id, uri, proto_files)
.await?
.server_streaming(service, method, message)
.await
}
pub async fn client_streaming(
&mut self,
id: &str,
uri: Uri,
proto_files: Vec<PathBuf>,
service: &str,
method: &str,
stream: ReceiverStream<String>,
) -> Result<DynamicMessage, String> {
self.connect(id, uri, proto_files)
.await?
.client_streaming(service, method, stream)
.await
}
pub async fn streaming(
&mut self,
id: &str,
uri: Uri,
proto_files: Vec<PathBuf>,
service: &str,
method: &str,
stream: ReceiverStream<String>,
) -> Result<Streaming<DynamicMessage>, String> {
self.connect(id, uri, proto_files)
.await?
.streaming(service, method, stream)
.await
}
pub async fn connect(
&mut self,
id: &str,
uri: Uri,
proto_files: Vec<PathBuf>,
) -> Result<GrpcConnection, String> {
let pool = match self.pools.get(id) {
Some(p) => p.clone(),
None => match proto_files.len() {
0 => fill_pool(&uri).await?,
_ => {
let pool = fill_pool_from_files(proto_files).await?;
self.pools.insert(id.to_string(), pool.clone());
pool
}
},
};
let conn = get_transport();
let connection = GrpcConnection { pool, conn, uri };
Ok(connection)
}
}

226
src-tauri/grpc/src/proto.rs Normal file
View File

@@ -0,0 +1,226 @@
use std::env::temp_dir;
use std::ops::Deref;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use anyhow::anyhow;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use log::{debug, warn};
use prost::Message;
use prost_reflect::{DescriptorPool, MethodDescriptor};
use prost_types::{FileDescriptorProto, FileDescriptorSet};
use tokio::fs;
use tokio_stream::StreamExt;
use tonic::body::BoxBody;
use tonic::codegen::http::uri::PathAndQuery;
use tonic::transport::Uri;
use tonic::Request;
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
use tonic_reflection::pb::server_reflection_request::MessageRequest;
use tonic_reflection::pb::server_reflection_response::MessageResponse;
use tonic_reflection::pb::ServerReflectionRequest;
pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool, String> {
let mut pool = DescriptorPool::new();
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
let desc_path = temp_dir().join(random_file_name);
let bin = protoc_bin_vendored::protoc_bin_path().unwrap();
let mut cmd = Command::new(bin.clone());
cmd.arg("--include_imports")
.arg("--include_source_info")
.arg("-o")
.arg(&desc_path);
for p in paths {
if p.as_path().exists() {
cmd.arg(p.as_path().to_string_lossy().as_ref());
} else {
continue;
}
let parent = p.as_path().parent();
if let Some(parent_path) = parent {
cmd.arg("-I").arg(parent_path);
cmd.arg("-I").arg(parent_path.parent().unwrap());
} else {
debug!("ignoring {:?} since it does not exist.", parent)
}
}
let output = cmd.output().map_err(|e| e.to_string())?;
if !output.status.success() {
return Err(format!(
"protoc failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let bytes = fs::read(desc_path.as_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())?;
fs::remove_file(desc_path.as_path())
.await
.map_err(|e| e.to_string())?;
Ok(pool)
}
pub async fn fill_pool(uri: &Uri) -> Result<DescriptorPool, String> {
let mut pool = DescriptorPool::new();
let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone());
for service in list_services(&mut client).await? {
if service == "grpc.reflection.v1alpha.ServerReflection" {
continue;
}
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
}
Ok(pool)
}
pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
let connector = HttpsConnectorBuilder::new().with_native_roots();
let connector = connector.https_or_http().enable_http2().wrap_connector({
let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
http_connector
});
Client::builder()
.pool_max_idle_per_host(0)
.http2_only(true)
.build(connector)
}
async fn list_services(
reflect_client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
) -> Result<Vec<String>, String> {
let response =
send_reflection_request(reflect_client, MessageRequest::ListServices("".into())).await?;
let list_services_response = match response {
MessageResponse::ListServicesResponse(resp) => resp,
_ => panic!("Expected a ListServicesResponse variant"),
};
Ok(list_services_response
.service
.iter()
.map(|s| s.name.clone())
.collect::<Vec<_>>())
}
async fn file_descriptor_set_from_service_name(
service_name: &str,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
) {
let response = match send_reflection_request(
client,
MessageRequest::FileContainingSymbol(service_name.into()),
)
.await
{
Ok(resp) => resp,
Err(e) => {
warn!(
"Error fetching file descriptor for service {}: {}",
service_name, e
);
return;
}
};
let file_descriptor_response = match response {
MessageResponse::FileDescriptorResponse(resp) => resp,
_ => panic!("Expected a FileDescriptorResponse variant"),
};
for fd in file_descriptor_response.file_descriptor_proto {
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;
}
pool.add_file_descriptor_proto(fdp)
.expect("add file descriptor proto");
}
}
async fn file_descriptor_set_by_filename(
filename: &str,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
) {
// We already fetched this file
if let Some(_) = pool.get_file_by_name(filename) {
return;
}
let response =
send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await;
let file_descriptor_response = match response {
Ok(MessageResponse::FileDescriptorResponse(resp)) => resp,
Ok(_) => {
panic!("Expected a FileDescriptorResponse variant")
}
Err(e) => {
warn!("Error fetching file descriptor for {}: {}", filename, e);
return;
}
};
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");
}
}
async fn send_reflection_request(
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
message: MessageRequest,
) -> Result<MessageResponse, String> {
let reflection_request = ServerReflectionRequest {
host: "".into(), // Doesn't matter
message_request: Some(message),
};
let request = Request::new(tokio_stream::once(reflection_request));
client
.server_reflection_info(request)
.await
.map_err(|e| match e.code() {
tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(),
tonic::Code::Unauthenticated => "Authentication failed".to_string(),
tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(),
_ => e.to_string(),
})?
.into_inner()
.next()
.await
.expect("steamed response")
.map_err(|e| e.to_string())?
.message_response
.ok_or("No reflection response".to_string())
}
pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
let full_name = md.full_name();
let (namespace, method_name) = full_name
.rsplit_once('.')
.ok_or_else(|| anyhow!("invalid method path"))
.expect("invalid method path");
PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path")
}

View File

@@ -0,0 +1,59 @@
CREATE TABLE grpc_requests
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'grpc_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT NULL
REFERENCES folders
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
name TEXT NOT NULL,
sort_priority REAL NOT NULL,
url TEXT NOT NULL,
service TEXT NULL,
method TEXT NULL,
message TEXT NOT NULL
);
CREATE TABLE grpc_connections
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'grpc_connection' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
request_id TEXT NOT NULL
REFERENCES grpc_requests
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
service TEXT NOT NULL,
method TEXT NOT NULL,
elapsed INTEGER NOT NULL
);
CREATE TABLE grpc_messages
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'grpc_message' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
request_id TEXT NOT NULL
REFERENCES grpc_requests
ON DELETE CASCADE,
connection_id TEXT NOT NULL
REFERENCES grpc_connections
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
is_server BOOLEAN NOT NULL,
is_info BOOLEAN NOT NULL,
message TEXT NOT NULL
);

View File

@@ -0,0 +1 @@
ALTER TABLE grpc_requests ADD COLUMN proto_files TEXT DEFAULT '[]' NOT NULL;

View File

@@ -1,10 +1,8 @@
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{Pool, Sqlite};
use sqlx::types::JsonValue;
use tauri::{AppHandle, Manager, State};
use tokio::sync::Mutex;
use tauri::{AppHandle, Manager};
use crate::{is_dev, models};
@@ -16,6 +14,9 @@ pub enum AnalyticsResource {
Dialog,
Environment,
Folder,
GrpcConnection,
GrpcMessage,
GrpcRequest,
HttpRequest,
HttpResponse,
KeyValue,
@@ -31,6 +32,9 @@ impl AnalyticsResource {
"CookieJar" => Some(AnalyticsResource::CookieJar),
"Environment" => Some(AnalyticsResource::Environment),
"Folder" => Some(AnalyticsResource::Folder),
"GrpcConnection" => Some(AnalyticsResource::GrpcConnection),
"GrpcMessage" => Some(AnalyticsResource::GrpcMessage),
"GrpcRequest" => Some(AnalyticsResource::GrpcRequest),
"HttpRequest" => Some(AnalyticsResource::HttpRequest),
"HttpResponse" => Some(AnalyticsResource::HttpResponse),
"KeyValue" => Some(AnalyticsResource::KeyValue),
@@ -90,6 +94,9 @@ fn resource_name(resource: AnalyticsResource) -> &'static str {
AnalyticsResource::Dialog => "dialog",
AnalyticsResource::Environment => "environment",
AnalyticsResource::Folder => "folder",
AnalyticsResource::GrpcRequest => "grpc_request",
AnalyticsResource::GrpcConnection => "grpc_connection",
AnalyticsResource::GrpcMessage => "grpc_message",
AnalyticsResource::HttpRequest => "http_request",
AnalyticsResource::HttpResponse => "http_response",
AnalyticsResource::KeyValue => "key_value",
@@ -129,14 +136,13 @@ pub struct LaunchEventInfo {
pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo {
let namespace = "analytics";
let last_tracked_version_key = "last_tracked_version";
let db_instance: State<'_, Mutex<Pool<Sqlite>>> = app_handle.state();
let pool = &*db_instance.lock().await;
let mut info = LaunchEventInfo::default();
info.num_launches = models::get_key_value_int(namespace, "num_launches", 0, pool).await + 1;
info.num_launches =
models::get_key_value_int(app_handle, namespace, "num_launches", 0).await + 1;
info.previous_version =
models::get_key_value_string(namespace, last_tracked_version_key, "", pool).await;
models::get_key_value_string(app_handle, namespace, last_tracked_version_key, "").await;
info.current_version = app_handle.package_info().version.to_string();
if info.previous_version.is_empty() {
@@ -167,19 +173,18 @@ pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo {
AnalyticsAction::Launch,
Some(json!({ "num_launches": info.num_launches })),
)
.await;
.await;
// Update key values
models::set_key_value_string(
app_handle,
namespace,
last_tracked_version_key,
info.current_version.as_str(),
pool,
)
.await;
models::set_key_value_int(namespace, "num_launches", info.num_launches, pool).await;
models::set_key_value_int(app_handle, namespace, "num_launches", info.num_launches).await;
info
}

View File

@@ -1,5 +1,5 @@
use std::fs::{create_dir_all, File};
use std::fs;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
@@ -7,28 +7,26 @@ use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use log::{error, info, warn};
use reqwest::{multipart, Url};
use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite};
use reqwest::{multipart, Url};
use sqlx::types::{Json, JsonValue};
use tauri::{AppHandle, Wry};
use tauri::AppHandle;
use crate::{emit_side_effect, models, render, response_err};
use crate::{models, render, response_err};
pub async fn send_http_request(
app_handle: &AppHandle,
request: models::HttpRequest,
response: &models::HttpResponse,
environment: Option<models::Environment>,
cookie_jar: Option<models::CookieJar>,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
download_path: Option<PathBuf>,
) -> Result<models::HttpResponse, String> {
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
let workspace = models::get_workspace(app_handle, &request.workspace_id)
.await
.expect("Failed to get Workspace");
@@ -88,7 +86,7 @@ pub async fn send_http_request(
let url = match Url::from_str(url_string.as_str()) {
Ok(u) => u,
Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await;
return response_err(response, e.to_string(), app_handle).await;
}
};
@@ -293,7 +291,7 @@ pub async fn send_http_request(
let sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await;
return response_err(response, e.to_string(), app_handle).await;
}
};
@@ -362,12 +360,9 @@ pub async fn send_http_request(
);
}
response = models::update_response_if_id(&response, pool)
response = models::update_response_if_id(app_handle, &response)
.await
.expect("Failed to update response");
if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response);
}
// Copy response to download path, if specified
match (download_path, response.body_path.clone()) {
@@ -397,18 +392,13 @@ pub async fn send_http_request(
.collect::<Vec<_>>(),
);
cookie_jar.cookies = json_cookies;
match models::upsert_cookie_jar(pool, &cookie_jar).await {
Ok(updated_jar) => {
emit_side_effect(app_handle, "updated_model", &updated_jar);
}
Err(e) => {
error!("Failed to update cookie jar: {}", e);
}
if let Err(e) = models::upsert_cookie_jar(&app_handle, &cookie_jar).await {
error!("Failed to update cookie jar: {}", e);
};
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
Err(e) => response_err(response, e.to_string(), app_handle).await,
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
use std::time::SystemTime;
use log::info;
use tauri::{AppHandle, updater, Window, Wry};
use tauri::api::dialog;
use tauri::{updater, AppHandle, Window};
use crate::is_dev;
@@ -27,14 +27,17 @@ impl YaakUpdater {
}
pub async fn force_check(
&mut self,
app_handle: &AppHandle<Wry>,
app_handle: &AppHandle,
mode: UpdateMode,
) -> Result<bool, updater::Error> {
self.last_update_check = SystemTime::now();
let update_mode = get_update_mode_str(mode);
let enabled = !is_dev();
info!("Checking for updates mode={} enabled={}", update_mode, enabled);
info!(
"Checking for updates mode={} enabled={}",
update_mode, enabled
);
if !enabled {
return Ok(false);
@@ -89,10 +92,11 @@ impl YaakUpdater {
}
pub async fn check(
&mut self,
app_handle: &AppHandle<Wry>,
app_handle: &AppHandle,
mode: UpdateMode,
) -> Result<bool, updater::Error> {
let ignore_check = self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
let ignore_check =
self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
return Ok(false);
}

View File

@@ -1,7 +1,7 @@
import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRequests } from '../hooks/useRequests';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { GlobalHooks } from './GlobalHooks';
import Workspace from './Workspace';
import Workspaces from './Workspaces';
@@ -49,7 +49,7 @@ export function AppRouter() {
function WorkspaceOrRedirect() {
const recentRequests = useRecentRequests();
const activeEnvironmentId = useActiveEnvironmentId();
const requests = useRequests();
const requests = useHttpRequests();
const request = requests.find((r) => r.id === recentRequests[0]);
const routes = useAppRoutes();

View File

@@ -1,4 +1,4 @@
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import type { HttpRequest } from '../lib/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
@@ -9,7 +9,7 @@ interface Props {
}
export function BasicAuth({ requestId, authentication }: Props) {
const updateRequest = useUpdateRequest(requestId);
const updateRequest = useUpdateHttpRequest(requestId);
return (
<VStack className="my-2" space={2}>

View File

@@ -1,4 +1,4 @@
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import type { HttpRequest } from '../lib/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
@@ -9,7 +9,7 @@ interface Props {
}
export function BearerAuth({ requestId, authentication }: Props) {
const updateRequest = useUpdateRequest(requestId);
const updateRequest = useUpdateHttpRequest(requestId);
return (
<VStack className="my-2" space={2}>

View File

@@ -3,20 +3,23 @@ import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { requestsQueryKey } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses';
import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { useSyncAppearance } from '../hooks/useSyncAppearance';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname';
@@ -42,43 +45,20 @@ export function GlobalHooks() {
setPathname(location.pathname).catch(console.error);
}, [location.pathname]);
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
useListenToTauriEvent<Model>('upserted_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return;
const queryKey =
payload.model === 'http_request'
? requestsQueryKey(payload)
? httpRequestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
? keyValueQueryKey(payload)
: payload.model === 'settings'
? settingsQueryKey()
: payload.model === 'cookie_jar'
? cookieJarsQueryKey(payload)
: null;
if (queryKey === null) {
console.log('Unrecognized created model:', payload);
return;
}
if (!shouldIgnoreModel(payload)) {
// Order newest first
queryClient.setQueryData<Model[]>(queryKey, (values) => [payload, ...(values ?? [])]);
}
});
useListenToTauriEvent<Model>('updated_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return;
const queryKey =
payload.model === 'http_request'
? requestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
? httpResponsesQueryKey(payload)
: payload.model === 'grpc_connection'
? grpcConnectionsQueryKey(payload)
: payload.model === 'grpc_message'
? grpcMessagesQueryKey(payload)
: payload.model === 'grpc_request'
? grpcRequestsQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
@@ -98,12 +78,19 @@ export function GlobalHooks() {
wasUpdatedExternally(payload.id);
}
const pushToFront = (['http_response', 'grpc_connection'] as Model['model'][]).includes(
payload.model,
);
if (!shouldIgnoreModel(payload)) {
console.time('set query date');
queryClient.setQueryData<Model[]>(queryKey, (values) =>
values?.map((v) => (modelsEq(v, payload) ? payload : v)),
);
console.timeEnd('set query date');
queryClient.setQueryData<Model[]>(queryKey, (values = []) => {
const index = values.findIndex((v) => modelsEq(v, payload)) ?? -1;
if (index >= 0) {
return [...values.slice(0, index), payload, ...values.slice(index + 1)];
} else {
return pushToFront ? [payload, ...(values ?? [])] : [...(values ?? []), payload];
}
});
}
});
@@ -113,11 +100,17 @@ export function GlobalHooks() {
if (shouldIgnoreModel(payload)) return;
if (payload.model === 'workspace') {
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(), removeById(payload));
queryClient.setQueryData(workspacesQueryKey(), removeById(payload));
} else if (payload.model === 'http_request') {
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(payload), removeById(payload));
queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'http_response') {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
queryClient.setQueryData(httpResponsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_request') {
queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_connection') {
queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload));
} else if (payload.model === 'grpc_message') {
queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} else if (payload.model === 'cookie_jar') {

View File

@@ -72,7 +72,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
const dialog = useDialog();
return (
<div className="pb-2 h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">
<Editor
contentType="application/graphql"
defaultValue={query ?? ''}
@@ -124,19 +124,22 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
}
{...extraEditorProps}
/>
<Separator variant="primary" />
<p className="pt-1 text-gray-500 text-sm">Variables</p>
<Editor
format={tryFormatJson}
contentType="application/json"
defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto"
onChange={handleChangeVariables}
placeholder="{}"
useTemplating
autocompleteVariables
{...extraEditorProps}
/>
<div className="grid min-h-[5rem]">
<Separator variant="primary" className="pb-1">
Variables
</Separator>
<Editor
format={tryFormatJson}
contentType="application/json"
defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto"
onChange={handleChangeVariables}
placeholder="{}"
useTemplating
autocompleteVariables
{...extraEditorProps}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,119 @@
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcMessages } from '../hooks/useGrpcMessages';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { Banner } from './core/Banner';
import { HotKeyList } from './core/HotKeyList';
import { SplitLayout } from './core/SplitLayout';
import { GrpcConnectionMessagesPane } from './GrpcConnectionMessagesPane';
import { GrpcConnectionSetupPane } from './GrpcConnectionSetupPane';
interface Props {
style: CSSProperties;
}
export function GrpcConnectionLayout({ style }: Props) {
const activeRequest = useActiveRequest('grpc_request');
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const connections = useGrpcConnections(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null;
const messages = useGrpcMessages(activeConnection?.id ?? null);
const grpc = useGrpc(activeRequest, activeConnection);
const services = grpc.reflect.data ?? null;
useEffect(() => {
if (services == null || activeRequest == null) return;
const s = services.find((s) => s.name === activeRequest.service);
if (s == null) {
updateRequest.mutate({
service: services[0]?.name ?? null,
method: services[0]?.methods[0]?.name ?? null,
});
return;
}
const m = s.methods.find((m) => m.name === activeRequest.method);
if (m == null) {
updateRequest.mutate({ method: s.methods[0]?.name ?? null });
return;
}
}, [activeRequest, services, updateRequest]);
const activeMethod = useMemo(() => {
if (services == null || activeRequest == null) return null;
const s = services.find((s) => s.name === activeRequest.service);
if (s == null) return null;
return s.methods.find((m) => m.name === activeRequest.method);
}, [activeRequest, services]);
const methodType:
| 'unary'
| 'server_streaming'
| 'client_streaming'
| 'streaming'
| 'no-schema'
| 'no-method' = useMemo(() => {
if (services == null) return 'no-schema';
if (activeMethod == null) return 'no-method';
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming';
if (activeMethod.clientStreaming) return 'client_streaming';
if (activeMethod.serverStreaming) return 'server_streaming';
return 'unary';
}, [activeMethod, services]);
if (activeRequest == null) {
return null;
}
return (
<SplitLayout
name="grpc_layout"
className="p-3 gap-1.5"
style={style}
firstSlot={({ style }) => (
<GrpcConnectionSetupPane
style={style}
activeRequest={activeRequest}
methodType={methodType}
onUnary={grpc.unary.mutate}
onServerStreaming={grpc.serverStreaming.mutate}
onClientStreaming={grpc.clientStreaming.mutate}
onStreaming={grpc.streaming.mutate}
onCommit={grpc.commit.mutate}
onCancel={grpc.cancel.mutate}
onSend={grpc.send.mutate}
services={services ?? null}
reflectionError={grpc.reflect.error as string | undefined}
reflectionLoading={grpc.reflect.isFetching}
/>
)}
secondSlot={({ style }) =>
!grpc.unary.isLoading && (
<div
style={style}
className={classNames(
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
)}
>
{grpc.unary.error ? (
<Banner color="danger" className="m-2">
{grpc.unary.error}
</Banner>
) : messages.length >= 0 ? (
<GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} />
) : (
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
)}
</div>
)
}
/>
);
}

View File

@@ -0,0 +1,127 @@
import classNames from 'classnames';
import { format } from 'date-fns';
import type { CSSProperties } from 'react';
import React, { useMemo, useState } from 'react';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcMessages } from '../hooks/useGrpcMessages';
import type { GrpcRequest } from '../lib/models';
import { Icon } from './core/Icon';
import { JsonAttributeTree } from './core/JsonAttributeTree';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { HStack } from './core/Stacks';
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
interface Props {
style?: CSSProperties;
className?: string;
activeRequest: GrpcRequest;
methodType:
| 'unary'
| 'client_streaming'
| 'server_streaming'
| 'streaming'
| 'no-schema'
| 'no-method';
}
export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) {
const [activeMessageId, setActiveMessageId] = useState<string | null>(null);
const connections = useGrpcConnections(activeRequest.id ?? null);
const activeConnection = connections[0] ?? null;
const messages = useGrpcMessages(activeConnection?.id ?? null);
const activeMessage = useMemo(
() => messages.find((m) => m.id === activeMessageId) ?? null,
[activeMessageId, messages],
);
return (
<SplitLayout
forceVertical
style={style}
name={methodType === 'unary' ? 'grpc_messages_unary' : 'grpc_messages_streaming'}
defaultRatio={methodType === 'unary' ? 0.75 : 0.3}
minHeightPx={20}
firstSlot={() => (
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">
<HStack className="pl-3 mb-1 font-mono" alignItems="center">
<HStack alignItems="center" space={2}>
<span>{messages.filter((m) => !m.isInfo).length} messages</span>
{activeConnection?.elapsed === 0 && (
<Icon icon="refresh" size="sm" spin className="text-gray-600" />
)}
</HStack>
{activeConnection && (
<RecentConnectionsDropdown
connections={connections}
activeConnection={activeConnection}
onPinned={() => {
// todo
}}
/>
)}
</HStack>
<div className="overflow-y-auto h-full">
{...messages.map((m) => (
<HStack
role="button"
key={m.id}
space={2}
onClick={() => {
if (m.id === activeMessageId) setActiveMessageId(null);
else setActiveMessageId(m.id);
}}
alignItems="center"
className={classNames(
'px-2 py-1 font-mono cursor-default group',
m === activeMessage && '!bg-highlight',
)}
>
<Icon
className={
m.isInfo ? 'text-gray-600' : m.isServer ? 'text-blue-600' : 'text-green-600'
}
icon={m.isInfo ? 'info' : m.isServer ? 'arrowBigDownDash' : 'arrowBigUpDash'}
/>
<div
className={classNames(
'w-full truncate text-gray-800 text-2xs group-hover:text-gray-900',
m.id === activeMessageId && 'text-gray-900',
)}
>
{m.message}
</div>
<div
className={classNames(
'text-gray-600 text-2xs group-hover:text-gray-700',
m.id === activeMessageId && 'text-gray-700',
)}
>
{format(m.createdAt, 'HH:mm:ss')}
</div>
</HStack>
))}
</div>
</div>
)}
secondSlot={
activeMessage &&
(() => (
<div className="grid grid-rows-[auto_minmax(0,1fr)]">
<div className="pb-3 px-2">
<Separator />
</div>
<div className="pl-2 overflow-y-auto">
{activeMessage.isInfo ? (
<span>{activeMessage.message}</span>
) : (
<JsonAttributeTree attrValue={JSON.parse(activeMessage?.message ?? '{}')} />
)}
</div>
</div>
))
}
/>
);
}

View File

@@ -0,0 +1,235 @@
import useResizeObserver from '@react-hook/resize-observer';
import classNames from 'classnames';
import type { CSSProperties, FormEvent } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import type { GrpcRequest } from '../lib/models';
import { Button } from './core/Button';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { RadioDropdown } from './core/RadioDropdown';
import { HStack, VStack } from './core/Stacks';
import { GrpcEditor } from './GrpcEditor';
import { UrlBar } from './UrlBar';
interface Props {
style?: CSSProperties;
className?: string;
activeRequest: GrpcRequest;
reflectionError?: string;
reflectionLoading?: boolean;
methodType:
| 'unary'
| 'client_streaming'
| 'server_streaming'
| 'streaming'
| 'no-schema'
| 'no-method';
onUnary: () => void;
onCommit: () => void;
onCancel: () => void;
onSend: (v: { message: string }) => void;
onClientStreaming: () => void;
onServerStreaming: () => void;
onStreaming: () => void;
services: ReflectResponseService[] | null;
}
export function GrpcConnectionSetupPane({
style,
services,
methodType,
activeRequest,
reflectionError,
reflectionLoading,
onStreaming,
onClientStreaming,
onServerStreaming,
onCommit,
onCancel,
onSend,
onUnary,
}: Props) {
const connections = useGrpcConnections(activeRequest.id ?? null);
const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null);
const activeConnection = connections[0] ?? null;
const isStreaming = activeConnection?.elapsed === 0;
const [paneSize, setPaneSize] = useState(99999);
const urlContainerEl = useRef<HTMLDivElement>(null);
useResizeObserver<HTMLDivElement>(urlContainerEl.current, (entry) => {
setPaneSize(entry.contentRect.width);
});
const handleChangeUrl = useCallback(
(url: string) => updateRequest.mutateAsync({ url }),
[updateRequest],
);
const handleChangeMessage = useCallback(
(message: string) => updateRequest.mutateAsync({ message }),
[updateRequest],
);
const select = useMemo(() => {
const options =
services?.flatMap((s) =>
s.methods.map((m) => ({
label: `${s.name.split('.', 2).pop() ?? s.name}/${m.name}`,
value: `${s.name}/${m.name}`,
})),
) ?? [];
const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`;
return { value, options };
}, [activeRequest?.method, activeRequest?.service, services]);
const handleChangeService = useCallback(
async (v: string) => {
const [serviceName, methodName] = v.split('/', 2);
if (serviceName == null || methodName == null) throw new Error('Should never happen');
await updateRequest.mutateAsync({
service: serviceName,
method: methodName,
});
},
[updateRequest],
);
const handleConnect = useCallback(
async (e: FormEvent) => {
e.preventDefault();
if (activeRequest == null) return;
if (activeRequest.service == null || activeRequest.method == null) {
alert({
id: 'grpc-invalid-service-method',
title: 'Error',
body: 'Service or method not selected',
});
}
if (methodType === 'streaming') {
onStreaming();
} else if (methodType === 'server_streaming') {
onServerStreaming();
} else if (methodType === 'client_streaming') {
onClientStreaming();
} else {
onUnary();
}
},
[activeRequest, methodType, onStreaming, onServerStreaming, onClientStreaming, onUnary],
);
return (
<VStack space={2} style={style}>
<div
ref={urlContainerEl}
className={classNames(
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
paneSize < 400 && '!grid-cols-1',
)}
>
<UrlBar
url={activeRequest.url ?? ''}
method={null}
submitIcon={null}
forceUpdateKey={activeRequest?.id ?? ''}
placeholder="localhost:50051"
onSubmit={handleConnect}
onUrlChange={handleChangeUrl}
isLoading={false}
/>
<HStack space={1.5}>
<RadioDropdown
value={select.value}
onChange={handleChangeService}
items={select.options.map((o) => ({
label: o.label,
value: o.value,
type: 'default',
shortLabel: o.label,
}))}
extraItems={[
{ type: 'separator' },
{
label: 'Refresh',
type: 'default',
key: 'custom',
leftSlot: <Icon className="text-gray-600" size="sm" icon="refresh" />,
},
]}
>
<Button
size="sm"
variant="border"
rightSlot={<Icon className="text-gray-600" size="sm" icon="chevronDown" />}
disabled={isStreaming || services == null}
className={classNames(
'font-mono text-xs min-w-[5rem] !ring-0',
paneSize < 400 && 'flex-1',
)}
>
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
</Button>
</RadioDropdown>
{!isStreaming && (
<IconButton
className="border border-highlight"
size="sm"
title={methodType === 'unary' ? 'Send' : 'Connect'}
hotkeyAction={isStreaming ? undefined : 'http_request.send'}
onClick={handleConnect}
disabled={methodType === 'no-schema' || methodType === 'no-method'}
icon={
isStreaming
? 'refresh'
: methodType.includes('streaming')
? 'arrowUpDown'
: 'sendHorizontal'
}
/>
)}
{isStreaming && (
<IconButton
className="border border-highlight"
size="sm"
title="Cancel"
onClick={onCancel}
icon="x"
disabled={!isStreaming}
/>
)}
{methodType === 'client_streaming' && isStreaming && (
<IconButton
className="border border-highlight"
size="sm"
title="to-do"
onClick={onCommit}
icon="check"
/>
)}
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
<IconButton
className="border border-highlight"
size="sm"
title="to-do"
hotkeyAction="grpc_request.send"
onClick={() => onSend({ message: activeRequest.message ?? '' })}
icon="sendHorizontal"
/>
)}
</HStack>
</div>
<GrpcEditor
onChange={handleChangeMessage}
services={services}
className="bg-gray-50"
reflectionError={reflectionError}
reflectionLoading={reflectionLoading}
request={activeRequest}
/>
</VStack>
);
}

View File

@@ -0,0 +1,153 @@
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import { updateSchema } from 'codemirror-json-schema';
import { useEffect, useRef } from 'react';
import { useAlert } from '../hooks/useAlert';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { tryFormatJson } from '../lib/formatters';
import type { GrpcRequest } from '../lib/models';
import { count } from '../lib/pluralize';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor';
import { Editor } from './core/Editor';
import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { GrpcProtoSelection } from './GrpcProtoSelection';
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className'> & {
services: ReflectResponseService[] | null;
reflectionError?: string;
reflectionLoading?: boolean;
request: GrpcRequest;
};
export function GrpcEditor({
services,
reflectionError,
reflectionLoading,
request,
...extraEditorProps
}: Props) {
const editorViewRef = useRef<EditorView>(null);
const alert = useAlert();
const dialog = useDialog();
// Find the schema for the selected service and method and update the editor
useEffect(() => {
if (editorViewRef.current == null || services === null) return;
const s = services.find((s) => s.name === request.service);
if (request.service != null && s == null) {
alert({
id: 'grpc-find-service-error',
title: "Couldn't Find Service",
body: (
<>
Failed to find service <InlineCode>{request.service}</InlineCode> in schema
</>
),
});
return;
}
const schema = s?.methods.find((m) => m.name === request.method)?.schema;
if (request.method != null && schema == null) {
alert({
id: 'grpc-find-schema-error',
title: "Couldn't Find Method",
body: (
<>
Failed to find method <InlineCode>{request.method}</InlineCode> for{' '}
<InlineCode>{request.service}</InlineCode> in schema
</>
),
});
return;
}
if (schema == null) {
return;
}
try {
updateSchema(editorViewRef.current, JSON.parse(schema));
} catch (err) {
alert({
id: 'grpc-parse-schema-error',
title: 'Failed to Parse Schema',
body: (
<VStack space={4}>
<p>
For service <InlineCode>{request.service}</InlineCode> and method{' '}
<InlineCode>{request.method}</InlineCode>
</p>
<FormattedError>{String(err)}</FormattedError>
</VStack>
),
});
}
}, [alert, services, request.method, request.service]);
const reflectionUnavailable = reflectionError?.match(/unimplemented/i);
reflectionError = reflectionUnavailable ? undefined : reflectionError;
return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor
contentType="application/grpc"
forceUpdateKey={request.id}
defaultValue={request.message}
format={tryFormatJson}
heightMode="auto"
placeholder="..."
ref={editorViewRef}
actions={[
<div key="reflection" className={classNames(services == null && '!opacity-100')}>
<Button
size="xs"
color={
reflectionLoading
? 'gray'
: reflectionUnavailable
? 'secondary'
: reflectionError
? 'danger'
: 'gray'
}
isLoading={reflectionLoading}
onClick={() => {
dialog.show({
title: 'Configure Schema',
size: 'md',
id: 'reflection-failed',
render: ({ hide }) => {
return (
<VStack space={6} className="pb-5">
<GrpcProtoSelection onDone={hide} requestId={request.id} />
</VStack>
);
},
});
}}
>
{reflectionLoading
? 'Inspecting Schema'
: reflectionUnavailable
? 'Select Proto Files'
: reflectionError
? 'Server Error'
: request.protoFiles.length > 0
? count('File', request.protoFiles.length)
: services != null && request.protoFiles.length === 0
? 'Schema Detected'
: 'Select Schema'}
</Button>
</div>,
]}
{...extraEditorProps}
/>
</div>
);
}

View File

@@ -0,0 +1,148 @@
import { open } from '@tauri-apps/api/dialog';
import { useGrpc } from '../hooks/useGrpc';
import { useGrpcRequest } from '../hooks/useGrpcRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { count } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { FormattedError } from './core/FormattedError';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks';
interface Props {
requestId: string;
onDone: () => void;
}
export function GrpcProtoSelection({ requestId }: Props) {
const request = useGrpcRequest(requestId);
const grpc = useGrpc(request, null);
const updateRequest = useUpdateGrpcRequest(request?.id ?? null);
const services = grpc.reflect.data;
const serverReflection = request?.protoFiles.length === 0 && services != null;
let reflectError = grpc.reflect.error ?? null;
const reflectionUnimplemented = `${reflectError}`.match(/unimplemented/i);
if (reflectionUnimplemented) {
reflectError = null;
}
if (request == null) {
return null;
}
return (
<VStack className="flex-col-reverse" space={3}>
{/* Buttons on top so they get focus first */}
<HStack space={2} justifyContent="start" className="flex-row-reverse">
<Button
color="primary"
size="sm"
onClick={async () => {
const files = await open({
title: 'Select Proto Files',
multiple: true,
filters: [{ name: 'Proto Files', extensions: ['proto'] }],
});
if (files == null || typeof files === 'string') return;
const newFiles = files.filter((f) => !request.protoFiles.includes(f));
await updateRequest.mutateAsync({ protoFiles: [...request.protoFiles, ...newFiles] });
await grpc.reflect.refetch();
}}
>
Add File
</Button>
<Button
isLoading={grpc.reflect.isFetching}
disabled={grpc.reflect.isFetching}
color="gray"
size="sm"
onClick={() => grpc.reflect.refetch()}
>
Refresh Schema
</Button>
</HStack>
<VStack space={5}>
{!serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2">
<p>
Found services
{services?.slice(0, 5).map((s, i) => {
return (
<span key={i}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
</span>
);
})}
{services?.length > 5 && count('other', services?.length - 5)}
</p>
</Banner>
)}
{serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2">
<p>
Server reflection found services
{services?.map((s, i) => {
return (
<span key={i}>
<InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '}
</span>
);
})}
. You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{' '}
files.
</p>
</Banner>
)}
{request.protoFiles.length > 0 && (
<table className="w-full divide-y">
<thead>
<tr>
<th className="text-gray-600">
<span className="font-mono text-sm">*.proto</span> Files
</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y">
{request.protoFiles.map((f, i) => (
<tr key={f + i} className="group">
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td>
<td className="w-0 py-0.5">
<IconButton
title="Remove file"
size="sm"
icon="trash"
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
onClick={async () => {
await updateRequest.mutateAsync({
protoFiles: request.protoFiles.filter((p) => p !== f),
});
grpc.reflect.remove();
}}
/>
</td>
</tr>
))}
</tbody>
</table>
)}
{reflectError && <FormattedError>{reflectError}</FormattedError>}
{reflectionUnimplemented && request.protoFiles.length === 0 && (
<Banner>
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '}
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
Server Reflection
</Link>{' '}
. Please manually add the <InlineCode>.proto</InlineCode> file to get started.
</Banner>
)}
</VStack>
</VStack>
);
}

View File

@@ -0,0 +1,29 @@
import type { CSSProperties } from 'react';
import React from 'react';
import type { HttpRequest } from '../lib/models';
import { SplitLayout } from './core/SplitLayout';
import { RequestPane } from './RequestPane';
import { ResponsePane } from './ResponsePane';
interface Props {
activeRequest: HttpRequest;
style: CSSProperties;
}
export function HttpRequestLayout({ activeRequest, style }: Props) {
return (
<SplitLayout
name="http_layout"
className="p-3 gap-1.5"
style={style}
firstSlot={({ orientation, style }) => (
<RequestPane
style={style}
activeRequest={activeRequest}
fullHeight={orientation === 'horizontal'}
/>
)}
secondSlot={({ style }) => <ResponsePane activeRequest={activeRequest} style={style} />}
/>
);
}

View File

@@ -0,0 +1,60 @@
import { formatDistanceToNowStrict } from 'date-fns';
import { useDeleteGrpcConnection } from '../hooks/useDeleteGrpcConnection';
import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections';
import type { GrpcConnection } from '../lib/models';
import { count, pluralize } from '../lib/pluralize';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
interface Props {
connections: GrpcConnection[];
activeConnection: GrpcConnection;
onPinned: (r: GrpcConnection) => void;
}
export function RecentConnectionsDropdown({ activeConnection, connections, onPinned }: Props) {
const deleteConnection = useDeleteGrpcConnection(activeConnection?.id ?? null);
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
return (
<Dropdown
items={[
{
key: 'clear-single',
label: 'Clear Connection',
onSelect: deleteConnection.mutate,
disabled: connections.length === 0,
},
{
key: 'clear-all',
label: `Clear ${count('Connection', connections.length)}`,
onSelect: deleteAllConnections.mutate,
hidden: connections.length <= 1,
disabled: connections.length === 0,
},
{ type: 'separator', label: 'History' },
...connections.slice(0, 20).map((c) => ({
key: c.id,
label: (
<HStack space={2} alignItems="center">
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '}
<span className="font-mono text-xs">{c.elapsed}ms</span>
</HStack>
),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinned(c),
})),
]}
>
<IconButton
title="Show connection history"
icon="chevronDown"
className="ml-auto"
size="sm"
iconSize="md"
/>
</Dropdown>
);
}

View File

@@ -7,7 +7,7 @@ import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useHotKey } from '../hooks/useHotKey';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRequests } from '../hooks/useRequests';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
@@ -19,7 +19,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
const activeRequest = useActiveRequest();
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const requests = useRequests();
const requests = useHttpRequests();
const routes = useAppRoutes();
const allRecentRequestIds = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);

View File

@@ -1,5 +1,5 @@
import { useDeleteResponse } from '../hooks/useDeleteResponse';
import { useDeleteResponses } from '../hooks/useDeleteResponses';
import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse';
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses';
import type { HttpResponse } from '../lib/models';
import { Dropdown } from './core/Dropdown';
import { pluralize } from '../lib/pluralize';
@@ -19,8 +19,8 @@ export const RecentResponsesDropdown = function ResponsePane({
responses,
onPinnedResponse,
}: Props) {
const deleteResponse = useDeleteResponse(activeResponse?.id ?? null);
const deleteAllResponses = useDeleteResponses(activeResponse?.requestId);
const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null);
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
return (
<Dropdown

View File

@@ -1,10 +1,11 @@
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import type { CSSProperties, FormEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
import {
@@ -33,131 +34,131 @@ import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
interface Props {
style?: CSSProperties;
style: CSSProperties;
fullHeight: boolean;
className?: string;
activeRequest: HttpRequest;
}
const useActiveTab = createGlobalState<string>('body');
export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) {
const activeRequest = useActiveRequest();
const activeRequestId = activeRequest?.id ?? null;
const updateRequest = useUpdateRequest(activeRequestId);
export const RequestPane = memo(function RequestPane({
style,
fullHeight,
className,
activeRequest,
}: Props) {
const activeRequestId = activeRequest.id;
const updateRequest = useUpdateHttpRequest(activeRequestId);
const [activeTab, setActiveTab] = useActiveTab();
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest?.id ?? null);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
const tabs: TabItem[] = useMemo(
() =>
activeRequest === null
? []
: [
{
value: 'body',
options: {
value: activeRequest.bodyType,
items: [
{ type: 'separator', label: 'Form Data' },
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
{ type: 'separator', label: 'Text Content' },
{ label: 'JSON', value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ type: 'separator', label: 'Other' },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
],
onChange: async (bodyType) => {
const patch: Partial<HttpRequest> = { bodyType };
if (bodyType === BODY_TYPE_NONE) {
patch.headers = activeRequest?.headers.filter(
(h) => h.name.toLowerCase() !== 'content-type',
);
} else if (
bodyType === BODY_TYPE_FORM_URLENCODED ||
bodyType === BODY_TYPE_FORM_MULTIPART ||
bodyType === BODY_TYPE_JSON ||
bodyType === BODY_TYPE_XML
) {
patch.method = 'POST';
patch.headers = [
...(activeRequest?.headers.filter(
(h) => h.name.toLowerCase() !== 'content-type',
) ?? []),
{
name: 'Content-Type',
value: bodyType,
enabled: true,
},
];
} else if (bodyType == BODY_TYPE_GRAPHQL) {
patch.method = 'POST';
patch.headers = [
...(activeRequest?.headers.filter(
(h) => h.name.toLowerCase() !== 'content-type',
) ?? []),
{
name: 'Content-Type',
value: 'application/json',
enabled: true,
},
];
}
// Force update header editor so any changed headers are reflected
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
updateRequest.mutate(patch);
},
},
},
{
value: 'params',
label: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
</div>
),
},
{
value: 'headers',
label: (
<div className="flex items-center">
Headers
<CountBadge count={activeRequest.headers.filter((h) => h.name).length} />
</div>
),
},
{
value: 'auth',
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest?.authentication;
if (authenticationType === AUTH_TYPE_BASIC) {
authentication = {
username: authentication.username ?? '',
password: authentication.password ?? '',
};
} else if (authenticationType === AUTH_TYPE_BEARER) {
authentication = {
token: authentication.token ?? '',
};
}
updateRequest.mutate({ authenticationType, authentication });
},
},
},
() => [
{
value: 'body',
options: {
value: activeRequest.bodyType,
items: [
{ type: 'separator', label: 'Form Data' },
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
{ type: 'separator', label: 'Text Content' },
{ label: 'JSON', value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ type: 'separator', label: 'Other' },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
],
onChange: async (bodyType) => {
const patch: Partial<HttpRequest> = { bodyType };
if (bodyType === BODY_TYPE_NONE) {
patch.headers = activeRequest.headers.filter(
(h) => h.name.toLowerCase() !== 'content-type',
);
} else if (
bodyType === BODY_TYPE_FORM_URLENCODED ||
bodyType === BODY_TYPE_FORM_MULTIPART ||
bodyType === BODY_TYPE_JSON ||
bodyType === BODY_TYPE_XML
) {
patch.method = 'POST';
patch.headers = [
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
[]),
{
name: 'Content-Type',
value: bodyType,
enabled: true,
},
];
} else if (bodyType == BODY_TYPE_GRAPHQL) {
patch.method = 'POST';
patch.headers = [
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
[]),
{
name: 'Content-Type',
value: 'application/json',
enabled: true,
},
];
}
// Force update header editor so any changed headers are reflected
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
updateRequest.mutate(patch);
},
},
},
{
value: 'params',
label: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
</div>
),
},
{
value: 'headers',
label: (
<div className="flex items-center">
Headers
<CountBadge count={activeRequest.headers.filter((h) => h.name).length} />
</div>
),
},
{
value: 'auth',
label: 'Auth',
options: {
value: activeRequest.authenticationType,
items: [
{ label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC },
{ label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER },
{ type: 'separator' },
{ label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE },
],
onChange: async (authenticationType) => {
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
if (authenticationType === AUTH_TYPE_BASIC) {
authentication = {
username: authentication.username ?? '',
password: authentication.password ?? '',
};
} else if (authenticationType === AUTH_TYPE_BEARER) {
authentication = {
token: authentication.token ?? '',
};
}
updateRequest.mutate({ authenticationType, authentication });
},
},
},
],
[activeRequest, updateRequest],
);
@@ -178,6 +179,27 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
[updateRequest],
);
const sendRequest = useSendRequest(activeRequest.id ?? null);
const handleSend = useCallback(
async (e: FormEvent) => {
e.preventDefault();
await sendRequest.mutateAsync();
},
[sendRequest],
);
const handleMethodChange = useCallback(
(method: string) => updateRequest.mutate({ method }),
[updateRequest],
);
const handleUrlChange = useCallback(
(url: string) => updateRequest.mutate({ url }),
[updateRequest],
);
const isLoading = useIsResponseLoading(activeRequestId ?? null);
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
return (
<div
style={style}
@@ -186,10 +208,14 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
{activeRequest && (
<>
<UrlBar
key={activeRequest.id} // Force-reset the url bar when the active request changes
id={activeRequest.id}
url={activeRequest.url}
method={activeRequest.method}
placeholder="https://example.com"
onSubmit={handleSend}
onMethodChange={handleMethodChange}
onUrlChange={handleUrlChange}
forceUpdateKey={updateKey}
isLoading={isLoading}
/>
<Tabs
value={activeTab}
@@ -240,7 +266,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
placeholder="..."
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest?.body?.text ?? ''}`}
defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="application/json"
onChange={handleBodyTextChange}
format={tryFormatJson}
@@ -253,7 +279,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
placeholder="..."
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest?.body?.text ?? ''}`}
defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="text/xml"
onChange={handleBodyTextChange}
/>
@@ -262,7 +288,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
forceUpdateKey={forceUpdateKey}
baseRequest={activeRequest}
className="!bg-gray-50"
defaultValue={`${activeRequest?.body?.text ?? ''}`}
defaultValue={`${activeRequest.body?.text ?? ''}`}
onChange={handleBodyTextChange}
/>
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (

View File

@@ -1,139 +0,0 @@
import useResizeObserver from '@react-hook/resize-observer';
import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { clamp } from '../lib/clamp';
import { HotKeyList } from './core/HotKeyList';
import { RequestPane } from './RequestPane';
import { ResizeHandle } from './ResizeHandle';
import { ResponsePane } from './ResponsePane';
interface Props {
style: CSSProperties;
}
const rqst = { gridArea: 'rqst' };
const resp = { gridArea: 'resp' };
const drag = { gridArea: 'drag' };
const DEFAULT = 0.5;
const MIN_WIDTH_PX = 10;
const MIN_HEIGHT_PX = 30;
const STACK_VERTICAL_WIDTH = 700;
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const activeRequest = useActiveRequest();
const [vertical, setVertical] = useState<boolean>(false);
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
const width = widthRaw ?? DEFAULT;
const height = heightRaw ?? DEFAULT;
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null,
);
useResizeObserver(containerRef.current, ({ contentRect }) => {
setVertical(contentRect.width < STACK_VERTICAL_WIDTH);
});
const styles = useMemo<CSSProperties>(
() => ({
...style,
gridTemplate: vertical
? `
' ${rqst.gridArea}' minmax(0,${1 - height}fr)
' ${drag.gridArea}' 0
' ${resp.gridArea}' minmax(0,${height}fr)
/ 1fr
`
: `
' ${rqst.gridArea} ${drag.gridArea} ${resp.gridArea}' minmax(0,1fr)
/ ${1 - width}fr 0 ${width}fr
`,
}),
[vertical, width, height, style],
);
const unsub = () => {
if (moveState.current !== null) {
document.documentElement.removeEventListener('mousemove', moveState.current.move);
document.documentElement.removeEventListener('mouseup', moveState.current.up);
}
};
const handleReset = useCallback(
() => (vertical ? setHeight(DEFAULT) : setWidth(DEFAULT)),
[setHeight, vertical, setWidth],
);
const handleResizeStart = useCallback(
(e: ReactMouseEvent<HTMLDivElement>) => {
if (containerRef.current === null) return;
unsub();
const containerRect = containerRef.current.getBoundingClientRect();
const mouseStartX = e.clientX;
const mouseStartY = e.clientY;
const startWidth = containerRect.width * width;
const startHeight = containerRect.height * height;
moveState.current = {
move: (e: MouseEvent) => {
e.preventDefault(); // Prevent text selection and things
if (vertical) {
const maxHeightPx = containerRect.height - MIN_HEIGHT_PX;
const newHeightPx = clamp(
startHeight - (e.clientY - mouseStartY),
MIN_HEIGHT_PX,
maxHeightPx,
);
setHeight(newHeightPx / containerRect.height);
} else {
const maxWidthPx = containerRect.width - MIN_WIDTH_PX;
const newWidthPx = clamp(
startWidth - (e.clientX - mouseStartX),
MIN_WIDTH_PX,
maxWidthPx,
);
setWidth(newWidthPx / containerRect.width);
}
},
up: (e: MouseEvent) => {
e.preventDefault();
unsub();
setIsResizing(false);
},
};
document.documentElement.addEventListener('mousemove', moveState.current.move);
document.documentElement.addEventListener('mouseup', moveState.current.up);
setIsResizing(true);
},
[width, height, vertical, setHeight, setWidth],
);
if (activeRequest === null) {
return <HotKeyList hotkeys={['request.create', 'sidebar.toggle']} />;
}
return (
<div ref={containerRef} className="grid gap-1.5 w-full h-full p-3" style={styles}>
<RequestPane style={rqst} fullHeight={!vertical} />
<ResizeHandle
style={drag}
isResizing={isResizing}
className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')}
onResizeStart={handleResizeStart}
onReset={handleReset}
side={vertical ? 'top' : 'left'}
justify="center"
/>
<ResponsePane style={resp} />
</div>
);
});

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import React from 'react';
import { Separator } from './core/Separator';
interface ResizeBarProps {
style?: CSSProperties;
@@ -17,6 +18,7 @@ export function ResizeHandle({
style,
justify,
className,
barClassName,
onResizeStart,
onReset,
isResizing,
@@ -28,6 +30,8 @@ export function ResizeHandle({
aria-hidden
draggable
style={style}
onDragStart={onResizeStart}
onDoubleClick={onReset}
className={classNames(
className,
'group z-10 flex',
@@ -39,8 +43,6 @@ export function ResizeHandle({
side === 'left' && 'left-0',
side === 'top' && 'top-0',
)}
onDragStart={onResizeStart}
onDoubleClick={onReset}
>
{/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */}
{isResizing && (

View File

@@ -2,12 +2,11 @@ import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useHttpResponses } from '../hooks/useHttpResponses';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { useResponseContentType } from '../hooks/useResponseContentType';
import { useResponses } from '../hooks/useResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import type { HttpResponse } from '../lib/models';
import type { HttpRequest, HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge';
@@ -29,15 +28,15 @@ import { WebPageViewer } from './responseViewers/WebPageViewer';
interface Props {
style?: CSSProperties;
className?: string;
activeRequest: HttpRequest;
}
const useActiveTab = createGlobalState<string>('body');
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
const activeRequest = useActiveRequest();
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
const responses = useResponses(activeRequest?.id ?? null);
const latestResponse = useLatestHttpResponse(activeRequest.id);
const responses = useHttpResponses(activeRequest.id);
const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null
: latestResponse ?? null;
@@ -85,10 +84,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
[activeResponse?.headers, contentType, setViewMode, viewMode],
);
if (activeRequest === null) {
return null;
}
return (
<div
style={style}
@@ -108,7 +103,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
<>
<span />
<HotKeyList
hotkeys={['request.send', 'request.create', 'sidebar.toggle', 'urlBar.focus']}
hotkeys={['http_request.send', 'http_request.create', 'sidebar.toggle', 'urlBar.focus']}
/>
</>
)}
@@ -179,6 +174,8 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
// ) : contentType?.startsWith('application/json') ? (
// <JsonViewer response={activeResponse} />
<TextViewer response={activeResponse} pretty={viewMode === 'pretty'} />
)}
</TabContent>

View File

@@ -29,11 +29,20 @@ export const SettingsDialog = () => {
size="sm"
value={settings.appearance}
onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })}
options={{
system: 'System',
light: 'Light',
dark: 'Dark',
}}
options={[
{
label: 'System',
value: 'system',
},
{
label: 'Light',
value: 'light',
},
{
label: 'Dark',
value: 'dark',
},
]}
/>
<Select
@@ -44,10 +53,16 @@ export const SettingsDialog = () => {
size="sm"
value={settings.updateChannel}
onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })}
options={{
stable: 'Release',
beta: 'Early Bird (Beta)',
}}
options={[
{
label: 'Release',
value: 'stable',
},
{
label: 'Early Bird (Beta)',
value: 'beta',
},
]}
/>
<Separator className="my-4" />

View File

@@ -102,7 +102,7 @@ export function SettingsDropdown() {
label: 'Check for Updates',
leftSlot: <Icon icon="update" />,
onSelect: async () => {
const hasUpdate: boolean = await invoke('check_for_updates');
const hasUpdate: boolean = await invoke('cmd_check_for_updates');
if (!hasUpdate) {
alert({
id: 'no-updates',

View File

@@ -7,31 +7,37 @@ import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateFolder } from '../hooks/useCreateFolder';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useDeleteAnyGrpcRequest } from '../hooks/useDeleteAnyGrpcRequest';
import { useDeleteAnyHttpRequest } from '../hooks/useDeleteAnyHttpRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders';
import { useGrpcRequests } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { useKeyValue } from '../hooks/useKeyValue';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { usePrompt } from '../hooks/usePrompt';
import { useRequests } from '../hooks/useRequests';
import { useSendManyRequests } from '../hooks/useSendFolder';
import { useSendRequest } from '../hooks/useSendRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
import type { Folder, HttpRequest, Workspace } from '../lib/models';
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
@@ -48,7 +54,7 @@ enum ItemTypes {
}
interface TreeNode {
item: Workspace | Folder | HttpRequest;
item: Workspace | Folder | HttpRequest | GrpcRequest;
children: TreeNode[];
depth: number;
}
@@ -56,18 +62,28 @@ interface TreeNode {
export function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequestId = useActiveRequestId();
const activeRequest = useActiveRequest();
const activeEnvironmentId = useActiveEnvironmentId();
const requests = useRequests();
const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests();
const folders = useFolders();
const deleteAnyRequest = useDeleteAnyRequest();
const deleteAnyHttpRequest = useDeleteAnyHttpRequest();
const deleteAnyGrpcRequest = useDeleteAnyGrpcRequest();
const activeWorkspace = useActiveWorkspace();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const duplicateGrpcRequest = useDuplicateGrpcRequest({
id: activeRequest?.id ?? null,
navigateAfter: true,
});
const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedTree, setSelectedTree] = useState<TreeNode | null>(null);
const updateAnyRequest = useUpdateAnyRequest();
const updateAnyHttpRequest = useUpdateAnyHttpRequest();
const updateAnyGrpcRequest = useUpdateAnyGrpcRequest();
const updateAnyFolder = useUpdateAnyFolder();
const [draggingId, setDraggingId] = useState<string | null>(null);
const [hoveredTree, setHoveredTree] = useState<TreeNode | null>(null);
@@ -78,8 +94,12 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC,
});
useHotKey('request.duplicate', () => {
duplicateRequest.mutate();
useHotKey('http_request.duplicate', async () => {
if (activeRequest?.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
await duplicateGrpcRequest.mutateAsync();
}
});
const isCollapsed = useCallback(
@@ -110,7 +130,7 @@ export function Sidebar({ className }: Props) {
// Put requests and folders into a tree structure
const next = (node: TreeNode): TreeNode => {
const childItems = [...requests, ...folders].filter((f) =>
const childItems = [...httpRequests, ...grpcRequests, ...folders].filter((f) =>
node.item.model === 'workspace' ? f.folderId == null : f.folderId === node.item.id,
);
@@ -119,7 +139,7 @@ export function Sidebar({ className }: Props) {
for (const item of childItems) {
treeParentMap[item.id] = node;
node.children.push(next({ item, children: [], depth }));
if (item.model === 'http_request') {
if (item.model !== 'folder') {
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
}
}
@@ -129,7 +149,7 @@ export function Sidebar({ className }: Props) {
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
return { tree, treeParentMap, selectableRequests };
}, [activeWorkspace, requests, folders]);
}, [activeWorkspace, httpRequests, grpcRequests, folders]);
const focusActiveRequest = useCallback(
(
@@ -142,9 +162,10 @@ export function Sidebar({ className }: Props) {
} = {},
) => {
const { forced, noFocusSidebar } = args;
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
const tree = forced?.tree ?? treeParentMap[activeRequest?.id ?? 'n/a'] ?? null;
const children = tree?.children ?? [];
const id = forced?.id ?? children.find((m) => m.item.id === activeRequestId)?.item.id ?? null;
const id =
forced?.id ?? children.find((m) => m.item.id === activeRequest?.id)?.item.id ?? null;
if (id == null) {
return;
}
@@ -156,11 +177,11 @@ export function Sidebar({ className }: Props) {
sidebarRef.current?.focus();
}
},
[activeRequestId, treeParentMap],
[activeRequest, treeParentMap],
);
const handleSelect = useCallback(
(id: string) => {
async (id: string) => {
const tree = treeParentMap[id ?? 'n/a'] ?? null;
const children = tree?.children ?? [];
const node = children.find((m) => m.item.id === id) ?? null;
@@ -171,7 +192,7 @@ export function Sidebar({ className }: Props) {
const { item } = node;
if (item.model === 'folder') {
collapsed.set((c) => ({ ...c, [item.id]: !c[item.id] }));
await collapsed.set((c) => ({ ...c, [item.id]: !c[item.id] }));
} else {
routes.navigate('request', {
requestId: id,
@@ -205,9 +226,10 @@ export function Sidebar({ className }: Props) {
const selected = selectableRequests.find((r) => r.id === selectedId);
if (selected == null) return;
deleteAnyRequest.mutate(selected.id);
deleteAnyHttpRequest.mutate(selected.id);
deleteAnyGrpcRequest.mutate(selected.id);
},
[deleteAnyRequest, hasFocus, selectableRequests, selectedId],
[deleteAnyHttpRequest, deleteAnyGrpcRequest, hasFocus, selectableRequests, selectedId],
);
useKeyPressEvent('Backspace', handleDeleteKey);
@@ -226,7 +248,7 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return;
const selected = selectableRequests.find((r) => r.id === selectedId);
if (!selected || selected.id === activeRequestId || activeWorkspace == null) {
if (!selected || selected.id === activeRequest?.id || activeWorkspace == null) {
return;
}
@@ -339,9 +361,12 @@ export function Sidebar({ className }: Props) {
if (child.item.model === 'folder') {
const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId });
return updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder });
} else if (child.item.model === 'grpc_request') {
const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId });
return updateAnyGrpcRequest.mutateAsync({ id: child.item.id, update: updateRequest });
} else if (child.item.model === 'http_request') {
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
return updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest });
return updateAnyHttpRequest.mutateAsync({ id: child.item.id, update: updateRequest });
}
}),
);
@@ -350,20 +375,24 @@ export function Sidebar({ className }: Props) {
if (child.item.model === 'folder') {
const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId });
await updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder });
} else if (child.item.model === 'grpc_request') {
const updateRequest = (r: GrpcRequest) => ({ ...r, sortPriority, folderId });
await updateAnyGrpcRequest.mutateAsync({ id: child.item.id, update: updateRequest });
} else if (child.item.model === 'http_request') {
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
await updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest });
await updateAnyHttpRequest.mutateAsync({ id: child.item.id, update: updateRequest });
}
}
setDraggingId(null);
},
[
hoveredIndex,
hoveredTree,
handleClearSelected,
hoveredTree,
hoveredIndex,
treeParentMap,
updateAnyFolder,
updateAnyRequest,
updateAnyGrpcRequest,
updateAnyHttpRequest,
],
);
@@ -454,7 +483,9 @@ function SidebarItems({
itemId={child.item.id}
itemName={child.item.name}
itemFallbackName={
child.item.model === 'http_request' ? fallbackRequestName(child.item) : 'New Folder'
child.item.model === 'http_request' || child.item.model === 'grpc_request'
? fallbackRequestName(child.item)
: 'New Folder'
}
itemModel={child.item.model}
onMove={handleMove}
@@ -524,16 +555,18 @@ const SidebarItem = forwardRef(function SidebarItem(
ref: ForwardedRef<HTMLLIElement>,
) {
const activeRequest = useActiveRequest();
const createRequest = useCreateRequest();
const createRequest = useCreateHttpRequest();
const createFolder = useCreateFolder();
const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId);
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId);
const sendAndDownloadRequest = useSendRequest(itemId, { download: true });
const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateRequest(itemId);
const latestHttpResponse = useLatestHttpResponse(itemId);
const latestGrpcConnection = useLatestGrpcConnection(itemId);
const updateHttpRequest = useUpdateHttpRequest(itemId);
const updateGrpcRequest = useUpdateGrpcRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt();
const [editing, setEditing] = useState<boolean>(false);
@@ -541,10 +574,15 @@ const SidebarItem = forwardRef(function SidebarItem(
const handleSubmitNameEdit = useCallback(
(el: HTMLInputElement) => {
updateRequest.mutate((r) => ({ ...r, name: el.value }));
if (activeRequest == null) return;
if (activeRequest.model === 'http_request') {
updateHttpRequest.mutate((r) => ({ ...r, name: el.value }));
} else if (activeRequest.model === 'grpc_request') {
updateGrpcRequest.mutate((r) => ({ ...r, name: el.value }));
}
setEditing(false);
},
[updateRequest],
[activeRequest, updateGrpcRequest, updateHttpRequest],
);
const handleFocus = useCallback((el: HTMLInputElement | null) => {
@@ -570,7 +608,7 @@ const SidebarItem = forwardRef(function SidebarItem(
);
const handleStartEditing = useCallback(() => {
if (itemModel !== 'http_request') return;
if (itemModel !== 'http_request' && itemModel !== 'grpc_request') return;
setEditing(true);
}, [setEditing, itemModel]);
@@ -649,23 +687,29 @@ const SidebarItem = forwardRef(function SidebarItem(
},
]
: [
{
key: 'sendRequest',
label: 'Send',
hotKeyAction: 'request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendRequest.mutate(),
},
{ type: 'separator' },
...((itemModel === 'http_request'
? [
{
key: 'sendRequest',
label: 'Send',
hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendRequest.mutate(),
},
{ type: 'separator' },
]
: []) as DropdownItem[]),
{
key: 'duplicateRequest',
label: 'Duplicate',
hotKeyAction: 'request.duplicate',
hotKeyAction: 'http_request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => {
duplicateRequest.mutate();
itemModel === 'http_request'
? duplicateHttpRequest.mutate()
: duplicateGrpcRequest.mutate();
},
},
{
@@ -717,15 +761,21 @@ const SidebarItem = forwardRef(function SidebarItem(
) : (
<span className="truncate">{itemName || itemFallbackName}</span>
)}
{latestResponse && (
{latestGrpcConnection ? (
<div className="ml-auto">
{isResponseLoading(latestResponse) ? (
<Icon spin size="sm" icon="update" />
) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestResponse} />
{latestGrpcConnection.elapsed === 0 && (
<Icon spin size="sm" icon="update" className="text-gray-400" />
)}
</div>
)}
) : latestHttpResponse ? (
<div className="ml-auto">
{isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="update" className="text-gray-400" />
) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
)}
</div>
) : null}
</button>
</div>
{children}

View File

@@ -1,6 +1,8 @@
import { memo } from 'react';
import { Simulate } from 'react-dom/test-utils';
import { useCreateFolder } from '../hooks/useCreateFolder';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { trackEvent } from '../lib/analytics';
import { Dropdown } from './core/Dropdown';
@@ -8,16 +10,21 @@ import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
export const SidebarActions = memo(function SidebarActions() {
const createRequest = useCreateRequest();
const createHttpRequest = useCreateHttpRequest();
const createGrpcRequest = useCreateGrpcRequest();
const createFolder = useCreateFolder();
const { hidden, toggle } = useSidebarHidden();
const { hidden, show, hide } = useSidebarHidden();
return (
<HStack>
<IconButton
onClick={() => {
onClick={async () => {
trackEvent('Sidebar', 'Toggle');
toggle();
// NOTE: We're not using `toggle` because it may be out of sync
// from changes in other windows
if (hidden) await show();
else await hide();
}}
className="pointer-events-auto"
size="sm"
@@ -28,14 +35,19 @@ export const SidebarActions = memo(function SidebarActions() {
<Dropdown
items={[
{
key: 'create-request',
label: 'New Request',
hotKeyAction: 'request.create',
onSelect: () => createRequest.mutate({}),
key: 'create-http-request',
label: 'HTTP Request',
hotKeyAction: 'http_request.create',
onSelect: () => createHttpRequest.mutate({}),
},
{
key: 'create-grpc-request',
label: 'GRPC Request',
onSelect: () => createGrpcRequest.mutate({}),
},
{
key: 'create-folder',
label: 'New Folder',
label: 'Folder',
onSelect: () => createFolder.mutate({}),
},
]}

View File

@@ -1,43 +1,39 @@
import type { EditorView } from 'codemirror';
import type { FormEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import { memo, useRef, useState } from 'react';
import { useHotKey } from '../hooks/useHotKey';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models';
import type { IconProps } from './core/Icon';
import { IconButton } from './core/IconButton';
import { Input } from './core/Input';
import { RequestMethodDropdown } from './RequestMethodDropdown';
type Props = Pick<HttpRequest, 'id' | 'url' | 'method'> & {
type Props = Pick<HttpRequest, 'url'> & {
className?: string;
method: HttpRequest['method'] | null;
placeholder: string;
onSubmit: (e: FormEvent) => void;
onUrlChange: (url: string) => void;
submitIcon?: IconProps['icon'] | null;
onMethodChange?: (method: string) => void;
isLoading: boolean;
forceUpdateKey: string;
};
export const UrlBar = memo(function UrlBar({ id: requestId, url, method, className }: Props) {
export const UrlBar = memo(function UrlBar({
forceUpdateKey,
onUrlChange,
url,
method,
placeholder,
className,
onSubmit,
onMethodChange,
submitIcon = 'sendHorizontal',
isLoading,
}: Props) {
const inputRef = useRef<EditorView>(null);
const sendRequest = useSendRequest(requestId);
const updateRequest = useUpdateRequest(requestId);
const [isFocused, setIsFocused] = useState<boolean>(false);
const handleMethodChange = useCallback(
(method: string) => updateRequest.mutate({ method }),
[updateRequest],
);
const handleUrlChange = useCallback(
(url: string) => updateRequest.mutate({ url }),
[updateRequest],
);
const loading = useIsResponseLoading(requestId);
const { updateKey } = useRequestUpdateKey(requestId);
const handleSubmit = useCallback(
async (e: FormEvent) => {
e.preventDefault();
sendRequest.mutate();
},
[sendRequest],
);
useHotKey('urlBar.focus', () => {
const head = inputRef.current?.state.doc.length ?? 0;
@@ -48,7 +44,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
});
return (
<form onSubmit={handleSubmit} className={className}>
<form onSubmit={onSubmit} className={className}>
<Input
autocompleteVariables
ref={inputRef}
@@ -60,31 +56,36 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
className="px-0 py-0.5"
name="url"
label="Enter URL"
forceUpdateKey={updateKey}
forceUpdateKey={forceUpdateKey}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
containerClassName="shadow shadow-gray-100 dark:shadow-gray-50"
onChange={handleUrlChange}
onChange={onUrlChange}
defaultValue={url}
placeholder="https://example.com"
placeholder={placeholder}
leftSlot={
<RequestMethodDropdown
method={method}
onChange={handleMethodChange}
className="mx-0.5 my-0.5"
/>
method != null &&
onMethodChange != null && (
<RequestMethodDropdown
method={method}
onChange={onMethodChange}
className="mx-0.5 my-0.5"
/>
)
}
rightSlot={
<IconButton
size="xs"
iconSize="md"
title="Send Request"
type="submit"
className="w-8 mr-0.5 my-0.5"
icon={loading ? 'update' : 'sendHorizontal'}
spin={loading}
hotkeyAction="request.send"
/>
submitIcon !== null && (
<IconButton
size="xs"
iconSize="md"
title="Send Request"
type="submit"
className="w-8 mr-0.5 my-0.5"
icon={isLoading ? 'update' : submitIcon}
spin={isLoading}
hotkeyAction="http_request.send"
/>
)
}
/>
</form>

View File

@@ -8,14 +8,17 @@ import type {
} from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { useOsInfo } from '../hooks/useOsInfo';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth';
import { Button } from './core/Button';
import { HotKeyList } from './core/HotKeyList';
import { HStack } from './core/Stacks';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HttpRequestLayout } from './HttpRequestLayout';
import { Overlay } from './Overlay';
import { RequestResponse } from './RequestResponse';
import { ResizeHandle } from './ResizeHandle';
import { Sidebar } from './Sidebar';
import { SidebarActions } from './SidebarActions';
@@ -31,7 +34,7 @@ const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
export default function Workspace() {
const { setWidth, width, resetWidth } = useSidebarWidth();
const { hide, show, hidden } = useSidebarHidden();
const activeRequest = useActiveRequest();
const windowSize = useWindowSize();
const [floating, setFloating] = useState<boolean>(false);
const [isResizing, setIsResizing] = useState<boolean>(false);
@@ -44,7 +47,7 @@ export default function Workspace() {
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
if (shouldHide && !floating) {
setFloating(true);
hide();
hide().catch(console.error);
} else if (!shouldHide && floating) {
setFloating(false);
}
@@ -69,10 +72,10 @@ export default function Workspace() {
e.preventDefault(); // Prevent text selection and things
const newWidth = startWidth + (e.clientX - mouseStartX);
if (newWidth < 100) {
hide();
await hide();
resetWidth();
} else {
show();
await show();
setWidth(newWidth);
}
},
@@ -163,7 +166,13 @@ export default function Workspace() {
>
<WorkspaceHeader className="pointer-events-none" />
</HeaderSize>
<RequestResponse style={body} />
{activeRequest == null ? (
<HotKeyList hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']} />
) : activeRequest.model === 'grpc_request' ? (
<GrpcConnectionLayout style={body} />
) : (
<HttpRequestLayout activeRequest={activeRequest} style={body} />
)}
</div>
);
}

View File

@@ -51,23 +51,13 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
),
render: ({ hide }) => {
return (
<HStack space={2} justifyContent="end" alignItems="center" className="mt-4 mb-6">
<HStack
space={2}
justifyContent="start"
alignItems="center"
className="mt-4 mb-6 flex-row-reverse"
>
<Button
className="focus"
color="gray"
rightSlot={<Icon icon="externalLink" />}
onClick={async () => {
hide();
const environmentId = (await getRecentEnvironments(w.id))[0];
await invoke('new_window', {
url: routes.paths.workspace({ workspaceId: w.id, environmentId }),
});
}}
>
New Window
</Button>
<Button
autoFocus
className="focus"
color="gray"
onClick={async () => {
@@ -78,6 +68,20 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
>
This Window
</Button>
<Button
className="focus"
color="gray"
rightSlot={<Icon icon="externalLink" />}
onClick={async () => {
hide();
const environmentId = (await getRecentEnvironments(w.id))[0];
await invoke('cmd_new_window', {
url: routes.paths.workspace({ workspaceId: w.id, environmentId }),
});
}}
>
New Window
</Button>
</HStack>
);
},

View File

@@ -8,6 +8,7 @@ import { Icon } from './Icon';
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
innerClassName?: string;
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
variant?: 'border' | 'solid';
isLoading?: boolean;
size?: 'sm' | 'md' | 'xs';
justify?: 'start' | 'center';
@@ -27,10 +28,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
innerClassName,
children,
forDropdown,
color,
color = 'default',
type = 'button',
justify = 'center',
size = 'md',
variant = 'solid',
leftSlot,
rightSlot,
disabled,
@@ -53,24 +55,45 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
'flex-shrink-0 flex items-center',
'focus-visible-or-class:ring rounded-md',
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
color === 'custom' && 'ring-blue-500/50',
color === 'default' &&
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
color === 'gray' &&
'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
color === 'primary' && 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
color === 'secondary' &&
'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
color === 'warning' &&
'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
color === 'danger' && 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
size === 'md' && 'h-md px-3',
size === 'sm' && 'h-sm px-2.5 text-sm',
size === 'xs' && 'h-xs px-2 text-sm',
// Solids
variant === 'solid' && color === 'custom' && 'ring-blue-500/50',
variant === 'solid' &&
color === 'default' &&
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-500/50',
variant === 'solid' &&
color === 'gray' &&
'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-gray-400',
variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700',
variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700',
variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700',
variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700',
// Borders
variant === 'border' && 'border',
variant === 'border' &&
color === 'default' &&
'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-800 ring-blue-500/50',
variant === 'border' &&
color === 'gray' &&
'border-gray-500/70 text-gray-700 enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-800 ring-blue-500/50',
variant === 'border' &&
color === 'primary' &&
'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50',
variant === 'border' &&
color === 'secondary' &&
'border-violet-500/70 text-violet-700 enabled:hocus:border-violet-500 ring-violet-500/50',
variant === 'border' &&
color === 'warning' &&
'border-orange-500/70 text-orange-700 enabled:hocus:border-orange-500 ring-orange-500/50',
variant === 'border' &&
color === 'danger' &&
'border-red-500/70 text-red-700 enabled:hocus:border-red-500 ring-red-500/50',
),
[className, disabled, color, justify, size],
[className, disabled, justify, size, variant, color],
);
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -100,7 +123,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
) : null}
<div
className={classNames(
'max-w-[15em] truncate w-full',
'truncate w-full',
justify === 'start' ? 'text-left' : 'text-center',
innerClassName,
)}

View File

@@ -12,7 +12,7 @@ export function CountBadge({ count, className }: Props) {
aria-hidden
className={classNames(
className,
'opacity-70 border border-highlight text-3xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
'opacity-70 border border-highlight text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
)}
>
{count}

View File

@@ -399,7 +399,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
{items.map((item, i) => {
if (item.type === 'separator') {
return (
<Separator key={i} className="ml-2 my-1.5">
<Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
{item.label}
</Separator>
);
@@ -473,7 +473,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
className={classNames(
className,
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
'focus:bg-highlight focus:text-gray-900 rounded',
'focus:bg-highlight focus:text-gray-800 rounded',
item.variant === 'danger' && 'text-red-600',
item.variant === 'notify' && 'text-pink-600',
)}

View File

@@ -38,7 +38,7 @@ export interface EditorProps {
className?: string;
heightMode?: 'auto' | 'full';
contentType?: string | null;
forceUpdateKey?: string;
forceUpdateKey?: string | number;
autoFocus?: boolean;
autoSelect?: boolean;
defaultValue?: string | null;

View File

@@ -10,7 +10,6 @@ import { json } from '@codemirror/lang-json';
import { xml } from '@codemirror/lang-xml';
import type { LanguageSupport } from '@codemirror/language';
import {
bracketMatching,
foldGutter,
foldKeymap,
HighlightStyle,
@@ -32,6 +31,7 @@ import {
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
import { jsonSchema } from 'codemirror-json-schema';
import type { Environment, Workspace } from '../../../lib/models';
import type { EditorProps } from './index';
import { text } from './text/extension';
@@ -83,6 +83,7 @@ export const myHighlightStyle = HighlightStyle.define([
// ]);
const syntaxExtensions: Record<string, LanguageSupport> = {
'application/grpc': jsonSchema() as any, // TODO: Fix this
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),
'application/javascript': javascript(),
@@ -119,7 +120,6 @@ export const baseExtensions = [
history(),
dropCursor(),
drawSelection(),
bracketMatching(),
// TODO: Figure out how to debounce showing of autocomplete in a good way
// debouncedAutocompletionDisplay({ millis: 1000 }),
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),

View File

@@ -1,11 +1,11 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
interface Props {
children: string;
children: ReactNode;
}
export function FormattedError({ children }: Props) {
console.log('ERROR', children);
return (
<pre
className={classNames(

View File

@@ -5,33 +5,43 @@ import { memo } from 'react';
const icons = {
archive: lucide.ArchiveIcon,
arrowBigDownDash: lucide.ArrowBigDownDashIcon,
arrowBigUpDash: lucide.ArrowBigUpDashIcon,
arrowDown: lucide.ArrowDownIcon,
arrowDownToDot: lucide.ArrowDownToDotIcon,
arrowUp: lucide.ArrowUpIcon,
arrowUpDown: lucide.ArrowUpDownIcon,
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
box: lucide.BoxIcon,
cake: lucide.CakeIcon,
chat: lucide.MessageSquare,
check: lucide.CheckIcon,
chevronDown: lucide.ChevronDownIcon,
chevronRight: lucide.ChevronRightIcon,
cookie: lucide.CookieIcon,
code: lucide.CodeIcon,
cookie: lucide.CookieIcon,
copy: lucide.CopyIcon,
download: lucide.DownloadIcon,
folderInput: lucide.FolderInputIcon,
folderOutput: lucide.FolderOutputIcon,
externalLink: lucide.ExternalLinkIcon,
eye: lucide.EyeIcon,
eyeClosed: lucide.EyeOffIcon,
filter: lucide.FilterIcon,
flask: lucide.FlaskConicalIcon,
folderInput: lucide.FolderInputIcon,
folderOutput: lucide.FolderOutputIcon,
gripVertical: lucide.GripVerticalIcon,
info: lucide.InfoIcon,
keyboard: lucide.KeyboardIcon,
leftPanelHidden: lucide.PanelLeftOpenIcon,
leftPanelVisible: lucide.PanelLeftCloseIcon,
magicWand: lucide.Wand2Icon,
moreVertical: lucide.MoreVerticalIcon,
pencil: lucide.PencilIcon,
plug: lucide.Plug,
plus: lucide.PlusIcon,
plusCircle: lucide.PlusCircleIcon,
question: lucide.ShieldQuestionIcon,
refresh: lucide.RefreshCwIcon,
sendHorizontal: lucide.SendHorizonalIcon,
settings2: lucide.Settings2Icon,
settings: lucide.SettingsIcon,
@@ -47,7 +57,7 @@ const icons = {
export interface IconProps {
icon: keyof typeof icons;
className?: string;
size?: 'xs' | 'sm' | 'md';
size?: 'xs' | 'sm' | 'md' | 'lg';
spin?: boolean;
}
@@ -57,7 +67,8 @@ export const Icon = memo(function Icon({ icon, spin, size = 'md', className }: I
<Component
className={classNames(
className,
'text-inherit',
'text-inherit flex-shrink-0',
size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',

View File

@@ -6,7 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code
className={classNames(
className,
'font-mono text-sm bg-highlight border-0 border-gray-200 px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
'font-mono text-xs bg-highlight border-0 border-gray-200/30',
'px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
)}
{...props}
/>

View File

@@ -0,0 +1,122 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useMemo, useState } from 'react';
import { Icon } from './Icon';
interface Props {
depth?: number;
attrValue: any;
attrKey?: string | number;
attrKeyJsonPath?: string;
}
export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPath }: Props) => {
attrKeyJsonPath = attrKeyJsonPath ?? `${attrKey}`;
const [isExpanded, setIsExpanded] = useState(true);
const toggleExpanded = () => setIsExpanded((v) => !v);
const { isExpandable, children, label, labelClassName } = useMemo<{
isExpandable: boolean;
children: ReactNode;
label?: string;
labelClassName?: string;
}>(() => {
const jsonType = Object.prototype.toString.call(attrValue);
if (jsonType === '[object Object]') {
return {
children: isExpanded
? Object.keys(attrValue)
.sort((a, b) => a.localeCompare(b))
.flatMap((k) => (
<JsonAttributeTree
depth={depth + 1}
attrValue={attrValue[k]}
attrKey={k}
attrKeyJsonPath={joinObjectKey(attrKeyJsonPath, k)}
/>
))
: null,
isExpandable: true,
label: isExpanded ? '{ }' : `{⋯}`,
labelClassName: 'text-gray-600',
};
} else if (jsonType === '[object Array]') {
return {
children: isExpanded
? attrValue.flatMap((v: any, i: number) => (
<JsonAttributeTree
depth={depth + 1}
attrValue={v}
attrKey={i}
attrKeyJsonPath={joinArrayKey(attrKeyJsonPath, i)}
/>
))
: null,
isExpandable: true,
label: isExpanded ? '[ ]' : `[⋯]`,
labelClassName: 'text-gray-600',
};
} else {
return {
children: null,
isExpandable: false,
label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
labelClassName: classNames(
jsonType === '[object Boolean]' && 'text-pink-600',
jsonType === '[object Number]' && 'text-blue-600',
jsonType === '[object String]' && 'text-yellow-600',
jsonType === '[object Null]' && 'text-red-600',
),
};
}
}, [attrValue, attrKeyJsonPath, isExpanded, depth]);
const labelEl = (
<span className={classNames(labelClassName, 'select-text group-hover:text-gray-800')}>
{label}
</span>
);
return (
<div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-2xs')}>
<div className="flex items-center">
{isExpandable ? (
<button className="group relative flex items-center pl-4 w-full" onClick={toggleExpanded}>
<Icon
size="xs"
icon="chevronRight"
className={classNames(
'left-0 absolute transition-transform text-gray-600 flex items-center',
'group-hover:text-gray-900',
isExpanded ? 'rotate-90' : '',
)}
/>
<span className="text-violet-600 mr-1.5 whitespace-nowrap">
{attrKey === undefined ? '$' : attrKey}:
</span>
{labelEl}
</button>
) : (
<>
<span className="text-violet-600 mr-1.5 pl-4 whitespace-nowrap select-text">
{attrKey}:
</span>
{labelEl}
</>
)}
</div>
{children && <div className="ml-4 whitespace-nowrap">{children}</div>}
</div>
);
};
function joinObjectKey(baseKey: string | undefined, key: string): string {
const quotedKey = key.match(/^[a-z0-9_]+$/i) ? key : `\`${key}\``;
if (baseKey == null) return quotedKey;
else return `${baseKey}.${quotedKey}`;
}
function joinArrayKey(baseKey: string | undefined, index: number): string {
return `${baseKey ?? ''}[${index}]`;
}

View File

@@ -0,0 +1,35 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Icon } from './Icon';
interface Props extends HTMLAttributes<HTMLAnchorElement> {
href: string;
}
export function Link({ href, children, className, ...other }: Props) {
const isExternal = href.match(/^https?:\/\//);
className = classNames(className, 'relative underline hover:text-violet-600');
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={classNames(className, 'pr-4')}
{...other}
>
<span className="underline">{children}</span>
<Icon className="inline absolute right-0.5 top-0.5" size="xs" icon="externalLink" />
</a>
);
}
return (
<RouterLink to={href} className={className} {...other}>
{children}
</RouterLink>
);
}

View File

@@ -6,10 +6,11 @@ interface Props<T extends string> {
labelPosition?: 'top' | 'left';
labelClassName?: string;
hideLabel?: boolean;
value: string;
options: Record<T, string>;
value: T;
options: { label: string; value: T }[];
onChange: (value: T) => void;
size?: 'xs' | 'sm' | 'md' | 'lg';
className?: string;
}
export function Select<T extends string>({
@@ -21,12 +22,14 @@ export function Select<T extends string>({
value,
options,
onChange,
className,
size = 'md',
}: Props<T>) {
const id = `input-${name}`;
return (
<div
className={classNames(
className,
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'flex items-center gap-2',
@@ -48,7 +51,7 @@ export function Select<T extends string>({
style={selectBackgroundStyles}
onChange={(e) => onChange(e.target.value as T)}
className={classNames(
'font-mono text-xs border w-full px-2 outline-none bg-transparent',
'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7',
'border-highlight focus:border-focus',
size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm',
@@ -56,8 +59,8 @@ export function Select<T extends string>({
size === 'lg' && 'h-lg',
)}
>
{Object.entries<string>(options).map(([value, label]) => (
<option key={value} value={value}>
{options.map(({ label, value }) => (
<option key={label} value={value}>
{label}
</option>
))}
@@ -68,7 +71,7 @@ export function Select<T extends string>({
const selectBackgroundStyles = {
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
backgroundPosition: 'right 0.5rem center',
backgroundPosition: 'right 0.3rem center',
backgroundRepeat: 'no-repeat',
backgroundSize: '1.5em 1.5em',
};

View File

@@ -1,10 +1,11 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
interface Props {
orientation?: 'horizontal' | 'vertical';
variant?: 'primary' | 'secondary';
className?: string;
children?: string;
children?: ReactNode;
}
export function Separator({

View File

@@ -0,0 +1,169 @@
import useResizeObserver from '@react-hook/resize-observer';
import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useActiveRequestId } from '../../hooks/useActiveRequestId';
import { useActiveWorkspaceId } from '../../hooks/useActiveWorkspaceId';
import { clamp } from '../../lib/clamp';
import { ResizeHandle } from '../ResizeHandle';
import { HotKeyList } from './HotKeyList';
interface SlotProps {
orientation: 'horizontal' | 'vertical';
style: CSSProperties;
}
interface Props {
name: string;
firstSlot: (props: SlotProps) => ReactNode;
secondSlot: null | ((props: SlotProps) => ReactNode);
style?: CSSProperties;
className?: string;
defaultRatio?: number;
minHeightPx?: number;
minWidthPx?: number;
forceVertical?: boolean;
}
const areaL = { gridArea: 'left' };
const areaR = { gridArea: 'right' };
const areaD = { gridArea: 'drag' };
const STACK_VERTICAL_WIDTH = 700;
export function SplitLayout({
style,
firstSlot,
secondSlot,
className,
name,
forceVertical,
defaultRatio = 0.5,
minHeightPx = 10,
minWidthPx = 10,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [vertical, setVertical] = useState<boolean>(false);
const [widthRaw, setWidth] = useLocalStorage<number>(`${name}_width::${useActiveWorkspaceId()}`);
const [heightRaw, setHeight] = useLocalStorage<number>(
`${name}_height::${useActiveWorkspaceId()}`,
);
const width = widthRaw ?? defaultRatio;
let height = heightRaw ?? defaultRatio;
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null,
);
if (!secondSlot) {
height = 0;
minHeightPx = 0;
}
useResizeObserver(containerRef.current, ({ contentRect }) => {
setVertical(contentRect.width < STACK_VERTICAL_WIDTH);
});
const styles = useMemo<CSSProperties>(() => {
return {
...style,
gridTemplate:
forceVertical || vertical
? `
' ${areaL.gridArea}' minmax(0,${1 - height}fr)
' ${areaD.gridArea}' 0
' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr)
/ 1fr
`
: `
' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr)
/ ${1 - width}fr 0 ${width}fr
`,
};
}, [style, vertical, height, minHeightPx, width]);
const unsub = () => {
if (moveState.current !== null) {
document.documentElement.removeEventListener('mousemove', moveState.current.move);
document.documentElement.removeEventListener('mouseup', moveState.current.up);
}
};
const handleReset = useCallback(
() => (vertical ? setHeight(defaultRatio) : setWidth(defaultRatio)),
[vertical, setHeight, defaultRatio, setWidth],
);
const handleResizeStart = useCallback(
(e: ReactMouseEvent<HTMLDivElement>) => {
if (containerRef.current === null) return;
unsub();
const containerRect = containerRef.current.getBoundingClientRect();
const mouseStartX = e.clientX;
const mouseStartY = e.clientY;
const startWidth = containerRect.width * width;
const startHeight = containerRect.height * height;
moveState.current = {
move: (e: MouseEvent) => {
e.preventDefault(); // Prevent text selection and things
if (vertical) {
const maxHeightPx = containerRect.height - minHeightPx;
const newHeightPx = clamp(
startHeight - (e.clientY - mouseStartY),
minHeightPx,
maxHeightPx,
);
setHeight(newHeightPx / containerRect.height);
} else {
const maxWidthPx = containerRect.width - minWidthPx;
const newWidthPx = clamp(
startWidth - (e.clientX - mouseStartX),
minWidthPx,
maxWidthPx,
);
setWidth(newWidthPx / containerRect.width);
}
},
up: (e: MouseEvent) => {
e.preventDefault();
unsub();
setIsResizing(false);
},
};
document.documentElement.addEventListener('mousemove', moveState.current.move);
document.documentElement.addEventListener('mouseup', moveState.current.up);
setIsResizing(true);
},
[width, height, vertical, minHeightPx, setHeight, minWidthPx, setWidth],
);
const activeRequestId = useActiveRequestId();
if (activeRequestId === null) {
return <HotKeyList hotkeys={['http_request.create', 'sidebar.toggle']} />;
}
return (
<div ref={containerRef} className={classNames(className, 'grid w-full h-full')} style={styles}>
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{secondSlot && (
<>
<ResizeHandle
style={areaD}
isResizing={isResizing}
barClassName={'bg-red-300'}
className={classNames(vertical ? 'translate-y-0.5' : 'translate-x-0.5')}
onResizeStart={handleResizeStart}
onReset={handleReset}
side={vertical ? 'top' : 'left'}
justify="center"
/>
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
</>
)}
</div>
);
}

View File

@@ -6,6 +6,7 @@ const gapClasses = {
0: 'gap-0',
0.5: 'gap-0.5',
1: 'gap-1',
1.5: 'gap-1.5',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
@@ -56,7 +57,7 @@ export const VStack = forwardRef(function VStack(
type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul' | 'label' | 'form';
space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center' | 'stretch';
alignItems?: 'start' | 'center' | 'stretch' | 'end';
justifyContent?: 'start' | 'center' | 'end' | 'between';
};
@@ -75,6 +76,7 @@ const BaseStack = forwardRef(function BaseStack(
alignItems === 'center' && 'items-center',
alignItems === 'start' && 'items-start',
alignItems === 'stretch' && 'items-stretch',
alignItems === 'end' && 'items-end',
justifyContent === 'start' && 'justify-start',
justifyContent === 'center' && 'justify-center',
justifyContent === 'end' && 'justify-end',

View File

@@ -0,0 +1,25 @@
import classNames from 'classnames';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import type { HttpResponse } from '../../lib/models';
import { JsonAttributeTree } from '../core/JsonAttributeTree';
interface Props {
response: HttpResponse;
className?: string;
}
export function JsonViewer({ response, className }: Props) {
const rawBody = useResponseBodyText(response) ?? '';
let parsed = {};
try {
parsed = JSON.parse(rawBody);
} catch (e) {
// foo
}
return (
<div className={classNames(className, 'overflow-x-auto h-full')}>
<JsonAttributeTree attrValue={parsed} />
</div>
);
}

View File

@@ -30,13 +30,13 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
};
return (
<HStack space={2} justifyContent="end" className="mt-2 mb-4">
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button className="focus" color={colors[variant]} onClick={handleSuccess}>
{confirmButtonTexts[variant]}
</Button>
<Button className="focus" color="gray" onClick={handleHide}>
Cancel
</Button>
<Button autoFocus className="focus" color={colors[variant]} onClick={handleSuccess}>
{confirmButtonTexts[variant]}
</Button>
</HStack>
);
}

View File

@@ -1,9 +1,28 @@
import type { HttpRequest } from '../lib/models';
import { r } from 'vitest/dist/types-94cfe4b4';
import type { GrpcRequest, HttpRequest } from '../lib/models';
import { useActiveRequestId } from './useActiveRequestId';
import { useRequests } from './useRequests';
import { useGrpcRequests } from './useGrpcRequests';
import { useHttpRequests } from './useHttpRequests';
export function useActiveRequest(): HttpRequest | null {
const requestId = useActiveRequestId();
const requests = useRequests();
return requests.find((r) => r.id === requestId) ?? null;
interface TypeMap {
http_request: HttpRequest;
grpc_request: GrpcRequest;
}
export function useActiveRequest<T extends keyof TypeMap>(
model?: T | undefined,
): TypeMap[T] | null {
const requestId = useActiveRequestId();
const httpRequests = useHttpRequests();
const grpcRequests = useGrpcRequests();
if (model === 'http_request') {
return (httpRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null;
} else if (model === 'grpc_request') {
return (grpcRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null;
} else {
return (grpcRequests.find((r) => r.id === requestId) ??
httpRequests.find((r) => r.id === requestId) ??
null) as TypeMap[T] | null;
}
}

View File

@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import type { DialogProps } from '../components/core/Dialog';
import { useDialog } from '../components/DialogContext';
import type { AlertProps } from './Alert';
@@ -5,20 +6,16 @@ import { Alert } from './Alert';
export function useAlert() {
const dialog = useDialog();
return ({
id,
title,
body,
}: {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
}) =>
dialog.show({
id,
title,
hideX: true,
size: 'sm',
render: ({ hide }) => Alert({ onHide: hide, body }),
});
return useCallback(
({ id, title, body }: { id: string; title: DialogProps['title']; body: AlertProps['body'] }) =>
dialog.show({
id,
title,
hideX: true,
size: 'sm',
render: ({ hide }) => Alert({ onHide: hide, body }),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
}

View File

@@ -15,7 +15,7 @@ export function useCookieJars() {
queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invoke('list_cookie_jars', { workspaceId })) as CookieJar[];
return (await invoke('cmd_list_cookie_jars', { workspaceId })) as CookieJar[];
},
}).data ?? []
);

View File

@@ -1,17 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { trackEvent } from '../lib/analytics';
import type { HttpRequest } from '../lib/models';
import type { CookieJar } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { cookieJarsQueryKey } from './useCookieJars';
import { usePrompt } from './usePrompt';
import { requestsQueryKey } from './useRequests';
export function useCreateCookieJar() {
const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient();
const prompt = usePrompt();
return useMutation<HttpRequest>({
return useMutation<CookieJar>({
mutationFn: async () => {
if (workspaceId === null) {
throw new Error("Cannot create cookie jar when there's no active workspace");
@@ -23,13 +23,13 @@ export function useCreateCookieJar() {
label: 'Name',
defaultValue: 'My Jar',
});
return invoke('create_cookie_jar', { workspaceId, name });
return invoke('cmd_create_cookie_jar', { workspaceId, name });
},
onSettled: () => trackEvent('CookieJar', 'Create'),
onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>(
requestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
onSuccess: async (cookieJar) => {
queryClient.setQueryData<CookieJar[]>(
cookieJarsQueryKey({ workspaceId: cookieJar.workspaceId }),
(items) => [...(items ?? []), cookieJar],
);
},
});

Some files were not shown because too many files have changed in this diff Show More