diff --git a/package-lock.json b/package-lock.json index fd61b5c3..05ab6c60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,8 +58,8 @@ "@types/react": "^18.0.31", "@types/react-dom": "^18.0.11", "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.13", "eslint": "^8.34.0", @@ -76,7 +76,7 @@ "prettier": "^2.8.4", "react-devtools": "^4.27.2", "tailwindcss": "^3.2.7", - "typescript": "^5.0.2", + "typescript": "^5.3.3", "vite": "^5.1.1", "vite-plugin-svgr": "^4.2.0", "vite-plugin-top-level-await": "^1.4.1", @@ -2631,9 +2631,9 @@ "devOptional": true }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/uuid": { @@ -2653,32 +2653,33 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", + "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/type-utils": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2687,25 +2688,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", + "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2714,16 +2716,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2731,25 +2733,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", + "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/utils": "7.0.2", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2758,12 +2760,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2771,21 +2773,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2797,43 +2800,66 @@ } } }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "7.0.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -5051,28 +5077,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -6165,9 +6169,9 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "node_modules/is-array-buffer": { @@ -7329,12 +7333,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "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", @@ -10153,6 +10151,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-easing": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", @@ -10192,27 +10202,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index af34c249..aeba482c 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,8 @@ "@types/react": "^18.0.31", "@types/react-dom": "^18.0.11", "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^5.57.0", - "@typescript-eslint/parser": "^5.57.0", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.13", "eslint": "^8.34.0", @@ -93,7 +93,7 @@ "prettier": "^2.8.4", "react-devtools": "^4.27.2", "tailwindcss": "^3.2.7", - "typescript": "^5.0.2", + "typescript": "^5.3.3", "vite": "^5.1.1", "vite-plugin-svgr": "^4.2.0", "vite-plugin-top-level-await": "^1.4.1", diff --git a/src-tauri/src/analytics.rs b/src-tauri/src/analytics.rs index 0b05d26f..818a1ce1 100644 --- a/src-tauri/src/analytics.rs +++ b/src-tauri/src/analytics.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use log::{debug, warn}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -7,7 +8,8 @@ use tauri::{AppHandle, Manager}; use crate::{is_dev, models}; // serializable -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] pub enum AnalyticsResource { App, CookieJar, @@ -25,28 +27,21 @@ pub enum AnalyticsResource { } impl AnalyticsResource { - pub fn from_str(s: &str) -> Option { - match s { - "App" => Some(AnalyticsResource::App), - "Dialog" => Some(AnalyticsResource::Dialog), - "CookieJar" => Some(AnalyticsResource::CookieJar), - "Environment" => Some(AnalyticsResource::Environment), - "Folder" => Some(AnalyticsResource::Folder), - "GrpcConnection" => Some(AnalyticsResource::GrpcConnection), - "GrpcEvent" => Some(AnalyticsResource::GrpcEvent), - "GrpcRequest" => Some(AnalyticsResource::GrpcRequest), - "HttpRequest" => Some(AnalyticsResource::HttpRequest), - "HttpResponse" => Some(AnalyticsResource::HttpResponse), - "KeyValue" => Some(AnalyticsResource::KeyValue), - "Sidebar" => Some(AnalyticsResource::Sidebar), - "Workspace" => Some(AnalyticsResource::Workspace), - _ => None, - } + pub fn from_str(s: &str) -> serde_json::Result { + return serde_json::from_str(format!("\"{s}\"").as_str()); } } -#[derive(Serialize, Deserialize)] +impl Display for AnalyticsResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", "")) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] pub enum AnalyticsAction { + Cancel, Create, Delete, DeleteMany, @@ -65,63 +60,14 @@ pub enum AnalyticsAction { } impl AnalyticsAction { - pub fn from_str(s: &str) -> Option { - match s { - "Create" => Some(AnalyticsAction::Create), - "Delete" => Some(AnalyticsAction::Delete), - "DeleteMany" => Some(AnalyticsAction::DeleteMany), - "Duplicate" => Some(AnalyticsAction::Duplicate), - "Export" => Some(AnalyticsAction::Export), - "Hide" => Some(AnalyticsAction::Hide), - "Import" => Some(AnalyticsAction::Import), - "Launch" => Some(AnalyticsAction::Launch), - "LaunchFirst" => Some(AnalyticsAction::LaunchFirst), - "LaunchUpdate" => Some(AnalyticsAction::LaunchUpdate), - "Send" => Some(AnalyticsAction::Send), - "Show" => Some(AnalyticsAction::Show), - "Toggle" => Some(AnalyticsAction::Toggle), - "Update" => Some(AnalyticsAction::Update), - "Upsert" => Some(AnalyticsAction::Upsert), - _ => None, - } + pub fn from_str(s: &str) -> serde_json::Result { + return serde_json::from_str(format!("\"{s}\"").as_str()); } } -fn resource_name(resource: AnalyticsResource) -> &'static str { - match resource { - AnalyticsResource::App => "app", - AnalyticsResource::CookieJar => "cookie_jar", - AnalyticsResource::Dialog => "dialog", - AnalyticsResource::Environment => "environment", - AnalyticsResource::Folder => "folder", - AnalyticsResource::GrpcRequest => "grpc_request", - AnalyticsResource::GrpcConnection => "grpc_connection", - AnalyticsResource::GrpcEvent => "grpc_event", - AnalyticsResource::HttpRequest => "http_request", - AnalyticsResource::HttpResponse => "http_response", - AnalyticsResource::KeyValue => "key_value", - AnalyticsResource::Sidebar => "sidebar", - AnalyticsResource::Workspace => "workspace", - } -} - -fn action_name(action: AnalyticsAction) -> &'static str { - match action { - AnalyticsAction::Create => "create", - AnalyticsAction::Delete => "delete", - AnalyticsAction::DeleteMany => "delete_many", - AnalyticsAction::Duplicate => "duplicate", - AnalyticsAction::Export => "export", - AnalyticsAction::Hide => "hide", - AnalyticsAction::Import => "import", - AnalyticsAction::Launch => "launch", - AnalyticsAction::LaunchFirst => "launch_first", - AnalyticsAction::LaunchUpdate => "launch_update", - AnalyticsAction::Send => "send", - AnalyticsAction::Show => "show", - AnalyticsAction::Toggle => "toggle", - AnalyticsAction::Update => "update", - AnalyticsAction::Upsert => "upsert", +impl Display for AnalyticsAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", "")) } } @@ -195,7 +141,7 @@ pub async fn track_event( action: AnalyticsAction, attributes: Option, ) { - let event = format!("{}.{}", resource_name(resource), action_name(action)); + let event = format!("{}.{}", resource, action); let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string(); let info = app_handle.package_info(); let tz = datetime::sys_timezone().unwrap_or("unknown".to_string()); diff --git a/src-tauri/src/http.rs b/src-tauri/src/http.rs index 540b9470..60a668ba 100644 --- a/src-tauri/src/http.rs +++ b/src-tauri/src/http.rs @@ -7,13 +7,15 @@ use std::sync::Arc; use std::time::Duration; use base64::Engine; -use http::header::{ACCEPT, USER_AGENT}; use http::{HeaderMap, HeaderName, HeaderValue, Method}; +use http::header::{ACCEPT, USER_AGENT}; use log::{error, info, warn}; -use reqwest::redirect::Policy; use reqwest::{multipart, Url}; +use reqwest::redirect::Policy; use sqlx::types::{Json, JsonValue}; use tauri::{Manager, Window}; +use tokio::sync::oneshot; +use tokio::sync::watch::{Receiver}; use crate::{models, render, response_err}; @@ -24,6 +26,7 @@ pub async fn send_http_request( environment: Option, cookie_jar: Option, download_path: Option, + cancel_rx: &mut Receiver, ) -> Result { let environment_ref = environment.as_ref(); let workspace = models::get_workspace(window, &request.workspace_id) @@ -302,7 +305,19 @@ pub async fn send_http_request( }; let start = std::time::Instant::now(); - let raw_response = client.execute(sendable_req).await; + + let (resp_tx, resp_rx) = oneshot::channel(); + + tokio::spawn(async move { + let _ = resp_tx.send(client.execute(sendable_req).await); + }); + + let raw_response = tokio::select! { + Ok(r) = resp_rx => {r} + _ = cancel_rx.changed() => { + return response_err(response, "Request was cancelled".to_string(), window).await; + } + }; match raw_response { Ok(v) => { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6ba58c38..1e7ec010 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,40 +10,54 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; -use std::fs::{create_dir_all, read_to_string, File}; +use std::fs::{create_dir_all, File, read_to_string}; use std::path::PathBuf; use std::process::exit; use std::str::FromStr; -use ::http::uri::InvalidUri; use ::http::Uri; +use ::http::uri::InvalidUri; use base64::Engine; use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; use serde_json::{json, Value}; +use sqlx::{Pool, Sqlite, SqlitePool}; use sqlx::migrate::Migrator; use sqlx::types::Json; -use sqlx::{Pool, Sqlite, SqlitePool}; -#[cfg(target_os = "macos")] -use tauri::TitleBarStyle; use tauri::{AppHandle, RunEvent, State, Window, WindowUrl}; use tauri::{Manager, WindowEvent}; +#[cfg(target_os = "macos")] +use tauri::TitleBarStyle; use tauri_plugin_log::{fern, LogTarget}; use tauri_plugin_window_state::{StateFlags, WindowExt}; -use tokio::sync::Mutex; +use tokio::sync::{Mutex}; use tokio::time::sleep; use window_shadows::set_shadow; +use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition}; use ::grpc::manager::{DynamicMessage, GrpcHandle}; -use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition}; use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::grpc::metadata_to_map; use crate::http::send_http_request; -use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue, Settings, Workspace, WorkspaceExportResources}; -use crate::plugin::{ImportResult}; +use crate::models::{ + cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, + create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, + delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, + delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, + duplicate_http_request, Environment, EnvironmentVariable, Folder, get_cookie_jar, + get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, + get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, + get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, + HttpRequest, HttpResponse, KeyValue, list_cookie_jars, list_environments, + list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, + list_requests, list_responses, list_workspaces, set_key_value_raw, Settings, + update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, + upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources, +}; +use crate::plugin::ImportResult; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; mod analytics; @@ -640,8 +654,21 @@ async fn cmd_send_ephemeral_request( None => None, }; - // let cookie_jar_id2 = cookie_jar_id.unwrap_or("").to_string(); - send_http_request(&window, request, &response, environment, cookie_jar, None).await + let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); + window.listen_global(format!("cancel_http_response_{}", response.id), move |_event| { + let _ = cancel_tx.send(true); + }); + + send_http_request( + &window, + request, + &response, + environment, + cookie_jar, + None, + &mut cancel_rx, + ) + .await } #[tauri::command] @@ -677,7 +704,10 @@ async fn cmd_filter_response(w: Window, response_id: &str, filter: &str) -> Resu } #[tauri::command] -async fn cmd_import_data(w: Window, file_paths: Vec<&str>) -> Result { +async fn cmd_import_data( + w: Window, + file_paths: Vec<&str>, +) -> Result { let mut result: Option = None; let plugins = vec!["importer-yaak", "importer-insomnia", "importer-postman"]; for plugin_name in plugins { @@ -778,19 +808,19 @@ async fn cmd_export_data( #[tauri::command] async fn cmd_send_http_request( - w: Window, + window: Window, request_id: &str, environment_id: Option<&str>, cookie_jar_id: Option<&str>, download_dir: Option<&str>, ) -> Result { - let request = get_http_request(&w, request_id) + let request = get_http_request(&window, request_id) .await .expect("Failed to get request"); let environment = match environment_id { Some(id) => Some( - get_environment(&w, id) + get_environment(&window, id) .await .expect("Failed to get environment"), ), @@ -799,7 +829,7 @@ async fn cmd_send_http_request( let cookie_jar = match cookie_jar_id { Some(id) => Some( - get_cookie_jar(&w, id) + get_cookie_jar(&window, id) .await .expect("Failed to get cookie jar"), ), @@ -807,7 +837,7 @@ async fn cmd_send_http_request( }; let response = create_http_response( - &w, + &window, &request.id, 0, 0, @@ -829,13 +859,19 @@ async fn cmd_send_http_request( None }; + let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); + window.listen_global(format!("cancel_http_response_{}", response.id), move |_event| { + let _ = cancel_tx.send(true); + }); + send_http_request( - &w, + &window, request.clone(), &response, environment, cookie_jar, download_path, + &mut cancel_rx, ) .await } @@ -865,11 +901,13 @@ async fn cmd_track_event( AnalyticsResource::from_str(resource), AnalyticsAction::from_str(action), ) { - (Some(resource), Some(action)) => { + (Ok(resource), Ok(action)) => { analytics::track_event(&window.app_handle(), resource, action, attributes).await; } - _ => { - error!("Invalid action/resource for track_event: {action} {resource}"); + (r, a) => { + println!("HttpRequest: {:?}", serde_json::to_string(&AnalyticsResource::HttpRequest)); + println!("Send: {:?}", serde_json::to_string(&AnalyticsAction::Send)); + error!("Invalid action/resource for track_event: {resource}.{action} = {:?}.{:?}", r, a); return Err("Invalid event".to_string()); } }; diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs index 302613d2..59d651c7 100644 --- a/src-tauri/src/plugin.rs +++ b/src-tauri/src/plugin.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tauri::AppHandle; -use crate::models::{Environment, Folder, HttpRequest, Workspace, WorkspaceExportResources}; +use crate::models::{WorkspaceExportResources}; #[derive(Default, Debug, Deserialize, Serialize)] pub struct FilterResult { diff --git a/src-web/components/CreateDropdown.tsx b/src-web/components/CreateDropdown.tsx index d40d432e..3dd32205 100644 --- a/src-web/components/CreateDropdown.tsx +++ b/src-web/components/CreateDropdown.tsx @@ -1,4 +1,3 @@ -import { ReactNode } from 'react'; import { useCreateFolder } from '../hooks/useCreateFolder'; import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest'; import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest'; diff --git a/src-web/components/DialogContext.tsx b/src-web/components/DialogContext.tsx index 4106de76..d03086ee 100644 --- a/src-web/components/DialogContext.tsx +++ b/src-web/components/DialogContext.tsx @@ -27,7 +27,7 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => { const actions = useMemo( () => ({ show({ id, ...props }: DialogEntry) { - trackEvent('Dialog', 'Show', { id }); + trackEvent('dialog', 'show', { id }); setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]); }, toggle({ id, ...props }: DialogEntry) { diff --git a/src-web/components/GrpcConnectionMessagesPane.tsx b/src-web/components/GrpcConnectionMessagesPane.tsx index b3b13609..ab591413 100644 --- a/src-web/components/GrpcConnectionMessagesPane.tsx +++ b/src-web/components/GrpcConnectionMessagesPane.tsx @@ -141,74 +141,56 @@ function EventRow({ }) { const { eventType, status, createdAt, content, error } = event; return ( - +
+ +
); } - -const GRPC_CODES: Record = { - 0: 'Ok', - 1: 'Cancelled', - 2: 'Unknown', - 3: 'Invalid argument', - 4: 'Deadline exceeded', - 5: 'Not found', - 6: 'Already exists', - 7: 'Permission denied', - 8: 'Resource exhausted', - 9: 'Failed precondition', - 10: 'Aborted', - 11: 'Out of range', - 12: 'Unimplemented', - 13: 'Internal', - 14: 'Unavailable', - 15: 'Data loss', - 16: 'Unauthenticated', -}; diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx index bdd46e8d..aa82568c 100644 --- a/src-web/components/GrpcConnectionSetupPane.tsx +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -1,6 +1,6 @@ import useResizeObserver from '@react-hook/resize-observer'; import classNames from 'classnames'; -import type { CSSProperties, FormEvent } from 'react'; +import type { CSSProperties } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { createGlobalState } from 'react-use'; import type { ReflectResponseService } from '../hooks/useGrpc'; @@ -104,22 +104,18 @@ export function GrpcConnectionSetupPane({ [updateRequest], ); - const handleConnect = useCallback( - async (e: FormEvent) => { - e.preventDefault(); - if (activeRequest == null) return; + const handleConnect = useCallback(async () => { + 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', - }); - } - onGo(); - }, - [activeRequest, onGo], - ); + if (activeRequest.service == null || activeRequest.method == null) { + alert({ + id: 'grpc-invalid-service-method', + title: 'Error', + body: 'Service or method not selected', + }); + } + onGo(); + }, [activeRequest, onGo]); const tabs: TabItem[] = useMemo( () => [ @@ -176,9 +172,10 @@ export function GrpcConnectionSetupPane({ submitIcon={null} forceUpdateKey={forceUpdateKey} placeholder="localhost:50051" - onSubmit={handleConnect} + onSend={handleConnect} onUrlChange={handleChangeUrl} - isLoading={false} + onCancel={onCancel} + isLoading={isStreaming} /> { - e.preventDefault(); - await sendRequest.mutateAsync(); - }, - [sendRequest], - ); + const { activeResponse } = usePinnedHttpResponse(activeRequest); + const cancelResponse = useCancelHttpResponse(activeResponse?.id ?? null); + const handleSend = useCallback(async () => { + await sendRequest.mutateAsync(); + }, [sendRequest]); + + const handleCancel = useCallback(async () => { + await cancelResponse.mutateAsync(); + }, [cancelResponse]); const handleMethodChange = useCallback( (method: string) => updateRequest.mutate({ method }), @@ -214,7 +218,8 @@ export const RequestPane = memo(function RequestPane({ url={activeRequest.url} method={activeRequest.method} placeholder="https://example.com" - onSubmit={handleSend} + onSend={handleSend} + onCancel={handleCancel} onMethodChange={handleMethodChange} onUrlChange={handleUrlChange} forceUpdateKey={updateKey} diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index ef72b175..311c6f11 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -1,17 +1,17 @@ import classNames from 'classnames'; import type { CSSProperties } from 'react'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useMemo } from 'react'; import { createGlobalState } from 'react-use'; -import { useHttpResponses } from '../hooks/useHttpResponses'; -import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; +import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { useResponseContentType } from '../hooks/useResponseContentType'; import { useResponseViewMode } from '../hooks/useResponseViewMode'; -import type { HttpRequest, HttpResponse } from '../lib/models'; +import type { HttpRequest } from '../lib/models'; import { isResponseLoading } from '../lib/models'; import { Banner } from './core/Banner'; import { CountBadge } from './core/CountBadge'; import { DurationTag } from './core/DurationTag'; import { HotKeyList } from './core/HotKeyList'; +import { Icon } from './core/Icon'; import { SizeTag } from './core/SizeTag'; import { HStack } from './core/Stacks'; import { StatusTag } from './core/StatusTag'; @@ -34,27 +34,11 @@ interface Props { const useActiveTab = createGlobalState('body'); export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) { - const [pinnedResponseId, setPinnedResponseId] = useState(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; + const { activeResponse, setPinnedResponse, responses } = usePinnedHttpResponse(activeRequest); const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId); const [activeTab, setActiveTab] = useActiveTab(); - - // Unset pinned response when a new one comes in - useEffect(() => setPinnedResponseId(null), [responses.length]); - const contentType = useResponseContentType(activeResponse); - const handlePinnedResponse = useCallback( - (r: HttpResponse) => { - setPinnedResponseId(r.id); - }, - [setPinnedResponseId], - ); - const tabs = useMemo( () => [ { @@ -89,21 +73,21 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ style={style} className={classNames( className, - 'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1', + 'max-h-full h-full', 'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight', 'shadow shadow-gray-100 dark:shadow-gray-0 relative', )} > - {!activeResponse && ( - <> - - - - )} - {activeResponse && !isResponseLoading(activeResponse) && ( - <> + {activeResponse == null ? ( + + ) : isResponseLoading(activeResponse) ? ( +
+ +
+ ) : ( +
)} @@ -179,7 +163,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ )} - +
)} ); diff --git a/src-web/components/SettingsDialog.tsx b/src-web/components/SettingsDialog.tsx index de8b8a4e..425d35ff 100644 --- a/src-web/components/SettingsDialog.tsx +++ b/src-web/components/SettingsDialog.tsx @@ -1,11 +1,15 @@ import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppInfo } from '../hooks/useAppInfo'; +import { useCheckForUpdates } from '../hooks/useCheckForUpdates'; import { useSettings } from '../hooks/useSettings'; import { useUpdateSettings } from '../hooks/useUpdateSettings'; import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace'; +import { trackEvent } from '../lib/analytics'; import { Checkbox } from './core/Checkbox'; import { Heading } from './core/Heading'; +import { IconButton } from './core/IconButton'; import { Input } from './core/Input'; +import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { Select } from './core/Select'; import { Separator } from './core/Separator'; import { VStack } from './core/Stacks'; @@ -16,6 +20,7 @@ export const SettingsDialog = () => { const settings = useSettings(); const updateSettings = useUpdateSettings(); const appInfo = useAppInfo(); + const checkForUpdates = useCheckForUpdates(); if (settings == null || workspace == null) { return null; @@ -29,7 +34,10 @@ export const SettingsDialog = () => { labelPosition="left" size="sm" value={settings.appearance} - onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })} + onChange={async (appearance) => { + await updateSettings.mutateAsync({ ...settings, appearance }); + trackEvent('setting', 'update', { appearance }); + }} options={[ { label: 'System', @@ -46,24 +54,37 @@ export const SettingsDialog = () => { ]} /> - { + trackEvent('setting', 'update', { update_channel: updateChannel }); + await updateSettings.mutateAsync({ ...settings, updateChannel }); + }} + options={[ + { + label: 'Release', + value: 'stable', + }, + { + label: 'Early Bird (Beta)', + value: 'beta', + }, + ]} + /> + checkForUpdates.mutateAsync()} + /> + @@ -88,41 +109,33 @@ export const SettingsDialog = () => { - updateWorkspace.mutateAsync({ settingValidateCertificates }) - } + onChange={async (settingValidateCertificates) => { + trackEvent('workspace', 'update', { + validate_certificates: JSON.stringify(settingValidateCertificates), + }); + await updateWorkspace.mutateAsync({ settingValidateCertificates }); + }} /> - updateWorkspace.mutateAsync({ settingFollowRedirects }) - } + onChange={async (settingFollowRedirects) => { + trackEvent('workspace', 'update', { + follow_redirects: JSON.stringify(settingFollowRedirects), + }); + await updateWorkspace.mutateAsync({ settingFollowRedirects }); + }} /> App Info - - - - - - - {appInfo.data && ( - - - - - )} - -
Version - {appInfo.data?.version} -
Data Directory - {appInfo.data.appDataDir} -
+ + + + ); }; diff --git a/src-web/components/SettingsDropdown.tsx b/src-web/components/SettingsDropdown.tsx index f86fbd17..485c729c 100644 --- a/src-web/components/SettingsDropdown.tsx +++ b/src-web/components/SettingsDropdown.tsx @@ -5,12 +5,10 @@ import { useAppInfo } from '../hooks/useAppInfo'; import { useExportData } from '../hooks/useExportData'; import { useImportData } from '../hooks/useImportData'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; -import { Button } from './core/Button'; import type { DropdownRef } from './core/Dropdown'; import { Dropdown } from './core/Dropdown'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; -import { VStack } from './core/Stacks'; import { useDialog } from './DialogContext'; import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog'; import { SettingsDialog } from './SettingsDialog'; diff --git a/src-web/components/SidebarActions.tsx b/src-web/components/SidebarActions.tsx index c343617f..ef1954ff 100644 --- a/src-web/components/SidebarActions.tsx +++ b/src-web/components/SidebarActions.tsx @@ -12,7 +12,7 @@ export const SidebarActions = memo(function SidebarActions() { { - trackEvent('Sidebar', 'Toggle'); + trackEvent('sidebar', 'toggle'); // NOTE: We're not using `toggle` because it may be out of sync // from changes in other windows diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index 1865f785..47e22755 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -12,8 +12,9 @@ type Props = Pick & { className?: string; method: HttpRequest['method'] | null; placeholder: string; - onSubmit: (e: FormEvent) => void; + onSend: () => void; onUrlChange: (url: string) => void; + onCancel: () => void; submitIcon?: IconProps['icon'] | null; onMethodChange?: (method: string) => void; isLoading: boolean; @@ -27,7 +28,8 @@ export const UrlBar = memo(function UrlBar({ method, placeholder, className, - onSubmit, + onSend, + onCancel, onMethodChange, submitIcon = 'sendHorizontal', isLoading, @@ -43,8 +45,13 @@ export const UrlBar = memo(function UrlBar({ inputRef.current?.focus(); }); + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + isLoading ? onCancel() : onSend(); + }; + return ( -
+ ) diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 066dca1b..f0df347e 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import type { HTMLAttributes, ReactNode } from 'react'; -import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; import type { HotkeyAction } from '../../hooks/useHotKey'; import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey'; import { Icon } from './Icon'; diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 7f13640c..7b4c140f 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -1,6 +1,5 @@ import { defaultKeymap } from '@codemirror/commands'; -import { Compartment, EditorState, Transaction } from '@codemirror/state'; -import type { ViewUpdate } from '@codemirror/view'; +import { Compartment, EditorState } from '@codemirror/state'; import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view'; import classNames from 'classnames'; import { EditorView } from 'codemirror'; @@ -148,14 +147,6 @@ const _Editor = forwardRef(function Editor( view.dispatch({ effects: languageCompartment.reconfigure(ext) }); }, [contentType, autocomplete, useTemplating, environment, workspace]); - useEffect(() => { - if (cm.current === null) return; - const { view } = cm.current; - view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: defaultValue ?? '' } }); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [forceUpdateKey]); - const classList = className?.split(/\s+/) ?? []; const bgClassList = classList .filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes @@ -163,57 +154,59 @@ const _Editor = forwardRef(function Editor( .map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important // Initialize the editor when ref mounts - const initEditorRef = useCallback((container: HTMLDivElement | null) => { - if (container === null) { - cm.current?.view.destroy(); - cm.current = null; - return; - } - - let view: EditorView; - try { - const languageCompartment = new Compartment(); - const langExt = getLanguageExtension({ - contentType, - useTemplating, - autocomplete, - environment, - workspace, - }); - - const state = EditorState.create({ - doc: `${defaultValue ?? ''}`, - extensions: [ - languageCompartment.of(langExt), - placeholderCompartment.current.of([]), - wrapLinesCompartment.current.of([]), - ...getExtensions({ - container, - readOnly, - singleLine, - onChange: handleChange, - onFocus: handleFocus, - onBlur: handleBlur, - onKeyDown: handleKeyDown, - }), - ], - }); - - view = new EditorView({ state, parent: container }); - cm.current = { view, languageCompartment }; - syncGutterBg({ parent: container, bgClassList }); - if (autoFocus) { - view.focus(); + const initEditorRef = useCallback( + (container: HTMLDivElement | null) => { + if (container === null) { + cm.current?.view.destroy(); + cm.current = null; + return; } - if (autoSelect) { - view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); - } - } catch (e) { - console.log('Failed to initialize Codemirror', e); - } + let view: EditorView; + try { + const languageCompartment = new Compartment(); + const langExt = getLanguageExtension({ + contentType, + useTemplating, + autocomplete, + environment, + workspace, + }); + + const state = EditorState.create({ + doc: `${defaultValue ?? ''}`, + extensions: [ + languageCompartment.of(langExt), + placeholderCompartment.current.of([]), + wrapLinesCompartment.current.of([]), + ...getExtensions({ + container, + readOnly, + singleLine, + onChange: handleChange, + onFocus: handleFocus, + onBlur: handleBlur, + onKeyDown: handleKeyDown, + }), + ], + }); + + view = new EditorView({ state, parent: container }); + cm.current = { view, languageCompartment }; + syncGutterBg({ parent: container, bgClassList }); + if (autoFocus) { + view.focus(); + } + if (autoSelect) { + view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); + } + } catch (e) { + console.log('Failed to initialize Codemirror', e); + } + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + [forceUpdateKey], + ); // Add bg classes to actions, so they appear over the text const decoratedActions = useMemo(() => { @@ -340,29 +333,13 @@ function getExtensions({ // Handle onChange EditorView.updateListener.of((update) => { - // Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when - // changing pages (one request to another in header editor) - if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) { + if (onChange && update.docChanged) { onChange.current?.(update.state.doc.toString()); } }), ]; } -function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) { - // Make sure document has changed, ensuring user events like selections don't count. - if (viewUpdate.docChanged) { - // Check transactions for any that are direct user input, not changes from Y.js or another extension. - for (const transaction of viewUpdate.transactions) { - // Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event. - const userEventType = transaction.annotation(Transaction.userEvent); - if (userEventType) return userEventType; - } - } - - return false; -} - const syncGutterBg = ({ parent, bgClassList, diff --git a/src-web/components/core/HotKeyList.tsx b/src-web/components/core/HotKeyList.tsx index 1c3a47f2..3f525d6d 100644 --- a/src-web/components/core/HotKeyList.tsx +++ b/src-web/components/core/HotKeyList.tsx @@ -11,7 +11,7 @@ interface Props { export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => { return ( -
+
{hotkeys.map((hotkey) => ( diff --git a/src-web/components/core/KeyValueRow.tsx b/src-web/components/core/KeyValueRow.tsx index 003a91d9..0847ba01 100644 --- a/src-web/components/core/KeyValueRow.tsx +++ b/src-web/components/core/KeyValueRow.tsx @@ -1,6 +1,24 @@ import classNames from 'classnames'; -import type { ReactNode } from 'react'; -import { HStack } from './Stacks'; +import type { HTMLAttributes, ReactElement, ReactNode } from 'react'; + +export function KeyValueRows({ + children, +}: { + children: + | ReactElement> + | ReactElement>[]; +}) { + children = Array.isArray(children) ? children : [children]; + return ( + + + {children.map((child, i) => ( + {child} + ))} + +
+ ); +} interface Props { label: ReactNode; @@ -8,17 +26,13 @@ interface Props { labelClassName?: string; } -export function KeyValueRows({ children }: { children: ReactNode }) { - return
{children}
; -} - export function KeyValueRow({ label, value, labelClassName }: Props) { return ( - -
+ <> + {label} -
-
{value}
-
+ + {value} + ); } diff --git a/src-web/hooks/useCancelHttpResponse.ts b/src-web/hooks/useCancelHttpResponse.ts new file mode 100644 index 00000000..073e752d --- /dev/null +++ b/src-web/hooks/useCancelHttpResponse.ts @@ -0,0 +1,10 @@ +import { useMutation } from '@tanstack/react-query'; +import { event } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; + +export function useCancelHttpResponse(id: string | null) { + return useMutation({ + mutationFn: () => event.emit(`cancel_http_response_${id}`), + onSettled: () => trackEvent('http_response', 'cancel'), + }); +} diff --git a/src-web/hooks/useCheckForUpdates.ts b/src-web/hooks/useCheckForUpdates.ts new file mode 100644 index 00000000..d754073b --- /dev/null +++ b/src-web/hooks/useCheckForUpdates.ts @@ -0,0 +1,20 @@ +import { useMutation } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { minPromiseMillis } from '../lib/minPromiseMillis'; +import { useAlert } from './useAlert'; + +export function useCheckForUpdates() { + const alert = useAlert(); + return useMutation({ + mutationFn: async () => { + const hasUpdate: boolean = await minPromiseMillis(invoke('cmd_check_for_updates'), 500); + if (!hasUpdate) { + alert({ + id: 'no-updates', + title: 'No Updates', + body: 'You are currently up to date', + }); + } + }, + }); +} diff --git a/src-web/hooks/useCreateCookieJar.ts b/src-web/hooks/useCreateCookieJar.ts index 846b9d93..63715351 100644 --- a/src-web/hooks/useCreateCookieJar.ts +++ b/src-web/hooks/useCreateCookieJar.ts @@ -26,7 +26,7 @@ export function useCreateCookieJar() { }); return invoke('cmd_create_cookie_jar', { workspaceId, name }); }, - onSettled: () => trackEvent('CookieJar', 'Create'), + onSettled: () => trackEvent('cookie_jar', 'create'), onSuccess: async (cookieJar) => { queryClient.setQueryData( cookieJarsQueryKey({ workspaceId: cookieJar.workspaceId }), diff --git a/src-web/hooks/useCreateEnvironment.ts b/src-web/hooks/useCreateEnvironment.ts index cf5102cb..2b435df2 100644 --- a/src-web/hooks/useCreateEnvironment.ts +++ b/src-web/hooks/useCreateEnvironment.ts @@ -26,7 +26,7 @@ export function useCreateEnvironment() { }); return invoke('cmd_create_environment', { name, variables: [], workspaceId }); }, - onSettled: () => trackEvent('Environment', 'Create'), + onSettled: () => trackEvent('environment', 'create'), onSuccess: async (environment) => { if (workspaceId == null) return; routes.setEnvironment(environment); diff --git a/src-web/hooks/useCreateFolder.ts b/src-web/hooks/useCreateFolder.ts index abe6eabb..041e226d 100644 --- a/src-web/hooks/useCreateFolder.ts +++ b/src-web/hooks/useCreateFolder.ts @@ -18,7 +18,7 @@ export function useCreateFolder() { patch.sortPriority = patch.sortPriority || -Date.now(); return invoke('cmd_create_folder', { workspaceId, ...patch }); }, - onSettled: () => trackEvent('Folder', 'Create'), + onSettled: () => trackEvent('folder', 'create'), onSuccess: async (request) => { await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId })); }, diff --git a/src-web/hooks/useCreateGrpcRequest.ts b/src-web/hooks/useCreateGrpcRequest.ts index 37c35ca6..f73bc983 100644 --- a/src-web/hooks/useCreateGrpcRequest.ts +++ b/src-web/hooks/useCreateGrpcRequest.ts @@ -33,7 +33,7 @@ export function useCreateGrpcRequest() { // patch.folderId = patch.folderId; // TODO: || activeRequest?.folderId; return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch }); }, - onSettled: () => trackEvent('GrpcRequest', 'Create'), + onSettled: () => trackEvent('grpc_request', 'create'), onSuccess: async (request) => { routes.navigate('request', { workspaceId: request.workspaceId, diff --git a/src-web/hooks/useCreateHttpRequest.ts b/src-web/hooks/useCreateHttpRequest.ts index 14313ffc..0d1debbd 100644 --- a/src-web/hooks/useCreateHttpRequest.ts +++ b/src-web/hooks/useCreateHttpRequest.ts @@ -34,7 +34,7 @@ export function useCreateHttpRequest() { patch.folderId = patch.folderId || activeRequest?.folderId; return invoke('cmd_create_http_request', { workspaceId, name: '', ...patch }); }, - onSettled: () => trackEvent('HttpRequest', 'Create'), + onSettled: () => trackEvent('http_request', 'create'), onSuccess: async (request) => { routes.navigate('request', { workspaceId: request.workspaceId, diff --git a/src-web/hooks/useCreateWorkspace.ts b/src-web/hooks/useCreateWorkspace.ts index b684f912..ade45178 100644 --- a/src-web/hooks/useCreateWorkspace.ts +++ b/src-web/hooks/useCreateWorkspace.ts @@ -10,7 +10,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean } mutationFn: (patch) => { return invoke('cmd_create_workspace', patch); }, - onSettled: () => trackEvent('Workspace', 'Create'), + onSettled: () => trackEvent('workspace', 'create'), onSuccess: async (workspace) => { if (navigateAfter) { routes.navigate('workspace', { workspaceId: workspace.id }); diff --git a/src-web/hooks/useDeleteAnyGrpcRequest.tsx b/src-web/hooks/useDeleteAnyGrpcRequest.tsx index d824605c..2a5d7213 100644 --- a/src-web/hooks/useDeleteAnyGrpcRequest.tsx +++ b/src-web/hooks/useDeleteAnyGrpcRequest.tsx @@ -30,7 +30,7 @@ export function useDeleteAnyGrpcRequest() { if (!confirmed) return null; return invoke('cmd_delete_grpc_request', { requestId: id }); }, - onSettled: () => trackEvent('GrpcRequest', 'Delete'), + onSettled: () => trackEvent('grpc_request', 'delete'), onSuccess: async (request) => { if (request === null) return; diff --git a/src-web/hooks/useDeleteAnyHttpRequest.tsx b/src-web/hooks/useDeleteAnyHttpRequest.tsx index de3af626..192d4ddf 100644 --- a/src-web/hooks/useDeleteAnyHttpRequest.tsx +++ b/src-web/hooks/useDeleteAnyHttpRequest.tsx @@ -31,7 +31,7 @@ export function useDeleteAnyHttpRequest() { if (!confirmed) return null; return invoke('cmd_delete_http_request', { requestId: id }); }, - onSettled: () => trackEvent('HttpRequest', 'Delete'), + onSettled: () => trackEvent('http_request', 'delete'), onSuccess: async (request) => { // Was it cancelled? if (request === null) return; diff --git a/src-web/hooks/useDeleteCookieJar.tsx b/src-web/hooks/useDeleteCookieJar.tsx index fc3bed00..77b14e67 100644 --- a/src-web/hooks/useDeleteCookieJar.tsx +++ b/src-web/hooks/useDeleteCookieJar.tsx @@ -25,7 +25,7 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) { if (!confirmed) return null; return invoke('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id }); }, - onSettled: () => trackEvent('CookieJar', 'Delete'), + onSettled: () => trackEvent('cookie_jar', 'delete'), onSuccess: async (cookieJar) => { if (cookieJar === null) return; diff --git a/src-web/hooks/useDeleteEnvironment.tsx b/src-web/hooks/useDeleteEnvironment.tsx index 982ed54b..992b3a8b 100644 --- a/src-web/hooks/useDeleteEnvironment.tsx +++ b/src-web/hooks/useDeleteEnvironment.tsx @@ -25,7 +25,7 @@ export function useDeleteEnvironment(environment: Environment | null) { if (!confirmed) return null; return invoke('cmd_delete_environment', { environmentId: environment?.id }); }, - onSettled: () => trackEvent('Environment', 'Delete'), + onSettled: () => trackEvent('environment', 'delete'), onSuccess: async (environment) => { if (environment === null) return; diff --git a/src-web/hooks/useDeleteFolder.tsx b/src-web/hooks/useDeleteFolder.tsx index 377c9baa..12c2db04 100644 --- a/src-web/hooks/useDeleteFolder.tsx +++ b/src-web/hooks/useDeleteFolder.tsx @@ -28,7 +28,7 @@ export function useDeleteFolder(id: string | null) { if (!confirmed) return null; return invoke('cmd_delete_folder', { folderId: id }); }, - onSettled: () => trackEvent('Folder', 'Delete'), + onSettled: () => trackEvent('folder', 'delete'), onSuccess: async (folder) => { // Was it cancelled? if (folder === null) return; diff --git a/src-web/hooks/useDeleteGrpcConnection.ts b/src-web/hooks/useDeleteGrpcConnection.ts index e01d8f23..3dddf976 100644 --- a/src-web/hooks/useDeleteGrpcConnection.ts +++ b/src-web/hooks/useDeleteGrpcConnection.ts @@ -10,7 +10,7 @@ export function useDeleteGrpcConnection(id: string | null) { mutationFn: async () => { return await invoke('cmd_delete_grpc_connection', { id: id }); }, - onSettled: () => trackEvent('GrpcConnection', 'Delete'), + onSettled: () => trackEvent('grpc_connection', 'delete'), onSuccess: ({ requestId, id: connectionId }) => { queryClient.setQueryData( grpcConnectionsQueryKey({ requestId }), diff --git a/src-web/hooks/useDeleteGrpcConnections.ts b/src-web/hooks/useDeleteGrpcConnections.ts index 5440ba2b..bf1ef1c7 100644 --- a/src-web/hooks/useDeleteGrpcConnections.ts +++ b/src-web/hooks/useDeleteGrpcConnections.ts @@ -10,7 +10,7 @@ export function useDeleteGrpcConnections(requestId?: string) { if (requestId === undefined) return; await invoke('cmd_delete_all_grpc_connections', { requestId }); }, - onSettled: () => trackEvent('GrpcConnection', 'DeleteMany'), + onSettled: () => trackEvent('grpc_connection', 'delete_many'), onSuccess: async () => { if (requestId === undefined) return; queryClient.setQueryData(grpcConnectionsQueryKey({ requestId }), []); diff --git a/src-web/hooks/useDeleteHttpResponse.ts b/src-web/hooks/useDeleteHttpResponse.ts index 3ee0bfee..0c7121eb 100644 --- a/src-web/hooks/useDeleteHttpResponse.ts +++ b/src-web/hooks/useDeleteHttpResponse.ts @@ -10,7 +10,7 @@ export function useDeleteHttpResponse(id: string | null) { mutationFn: async () => { return await invoke('cmd_delete_http_response', { id: id }); }, - onSettled: () => trackEvent('HttpResponse', 'Delete'), + onSettled: () => trackEvent('http_response', 'delete'), onSuccess: ({ requestId, id: responseId }) => { queryClient.setQueryData(httpResponsesQueryKey({ requestId }), (responses) => (responses ?? []).filter((response) => response.id !== responseId), diff --git a/src-web/hooks/useDeleteHttpResponses.ts b/src-web/hooks/useDeleteHttpResponses.ts index 264a87dd..49fbcc01 100644 --- a/src-web/hooks/useDeleteHttpResponses.ts +++ b/src-web/hooks/useDeleteHttpResponses.ts @@ -10,7 +10,7 @@ export function useDeleteHttpResponses(requestId?: string) { if (requestId === undefined) return; await invoke('cmd_delete_all_http_responses', { requestId }); }, - onSettled: () => trackEvent('HttpResponse', 'DeleteMany'), + onSettled: () => trackEvent('http_response', 'delete_many'), onSuccess: async () => { if (requestId === undefined) return; queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); diff --git a/src-web/hooks/useDeleteWorkspace.tsx b/src-web/hooks/useDeleteWorkspace.tsx index 36615228..9d20998a 100644 --- a/src-web/hooks/useDeleteWorkspace.tsx +++ b/src-web/hooks/useDeleteWorkspace.tsx @@ -30,7 +30,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) { if (!confirmed) return null; return invoke('cmd_delete_workspace', { workspaceId: workspace?.id }); }, - onSettled: () => trackEvent('Workspace', 'Delete'), + onSettled: () => trackEvent('workspace', 'delete'), onSuccess: async (workspace) => { if (workspace === null) return; diff --git a/src-web/hooks/useDuplicateGrpcRequest.ts b/src-web/hooks/useDuplicateGrpcRequest.ts index 04a98ccb..3ad45e0d 100644 --- a/src-web/hooks/useDuplicateGrpcRequest.ts +++ b/src-web/hooks/useDuplicateGrpcRequest.ts @@ -21,7 +21,7 @@ export function useDuplicateGrpcRequest({ if (id === null) throw new Error("Can't duplicate a null grpc request"); return invoke('cmd_duplicate_grpc_request', { id }); }, - onSettled: () => trackEvent('GrpcRequest', 'Duplicate'), + onSettled: () => trackEvent('grpc_request', 'duplicate'), onSuccess: async (request) => { if (navigateAfter && activeWorkspaceId !== null) { routes.navigate('request', { diff --git a/src-web/hooks/useDuplicateHttpRequest.ts b/src-web/hooks/useDuplicateHttpRequest.ts index a7077d0a..4a4442d5 100644 --- a/src-web/hooks/useDuplicateHttpRequest.ts +++ b/src-web/hooks/useDuplicateHttpRequest.ts @@ -21,7 +21,7 @@ export function useDuplicateHttpRequest({ if (id === null) throw new Error("Can't duplicate a null request"); return invoke('cmd_duplicate_http_request', { id }); }, - onSettled: () => trackEvent('HttpRequest', 'Duplicate'), + onSettled: () => trackEvent('http_request', 'duplicate'), onSuccess: async (request) => { if (navigateAfter && activeWorkspaceId !== null) { routes.navigate('request', { diff --git a/src-web/hooks/usePinnedHttpResponse.ts b/src-web/hooks/usePinnedHttpResponse.ts new file mode 100644 index 00000000..88cfbdc7 --- /dev/null +++ b/src-web/hooks/usePinnedHttpResponse.ts @@ -0,0 +1,28 @@ +import { useCallback, useEffect } from 'react'; +import { createGlobalState } from 'react-use'; +import type { HttpRequest, HttpResponse } from '../lib/models'; +import { useHttpResponses } from './useHttpResponses'; +import { useLatestHttpResponse } from './useLatestHttpResponse'; + +const usePinnedResponseIdState = createGlobalState(null); + +export function usePinnedHttpResponse(activeRequest: HttpRequest) { + const [pinnedResponseId, setPinnedResponseId] = usePinnedResponseIdState(); + const latestResponse = useLatestHttpResponse(activeRequest.id); + const responses = useHttpResponses(activeRequest.id); + const activeResponse: HttpResponse | null = pinnedResponseId + ? responses.find((r) => r.id === pinnedResponseId) ?? null + : latestResponse ?? null; + + // Unset pinned response when a new one comes in + useEffect(() => setPinnedResponseId(null), [responses.length, setPinnedResponseId]); + + const setPinnedResponse = useCallback( + (r: HttpResponse) => { + setPinnedResponseId(r.id); + }, + [setPinnedResponseId], + ); + + return { activeResponse, setPinnedResponse, pinnedResponseId, responses } as const; +} diff --git a/src-web/hooks/useSendAnyRequest.ts b/src-web/hooks/useSendAnyRequest.ts index e273ebf9..5c8c136d 100644 --- a/src-web/hooks/useSendAnyRequest.ts +++ b/src-web/hooks/useSendAnyRequest.ts @@ -38,7 +38,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) { cookieJarId: activeCookieJar?.id, }); }, - onSettled: () => trackEvent('HttpRequest', 'Send'), + onSettled: () => trackEvent('http_request', 'send'), onError: (err) => alert({ id: 'send-failed', title: 'Send Failed', body: err }), }); } diff --git a/src-web/lib/analytics.ts b/src-web/lib/analytics.ts index 91688280..e36f5501 100644 --- a/src-web/lib/analytics.ts +++ b/src-web/lib/analytics.ts @@ -2,30 +2,33 @@ import { invoke } from '@tauri-apps/api'; export function trackEvent( resource: - | 'App' - | 'Dialog' - | 'CookieJar' - | 'Sidebar' - | 'Workspace' - | 'Environment' - | 'Folder' - | 'GrpcEvent' - | 'GrpcConnection' - | 'GrpcRequest' - | 'HttpRequest' - | 'HttpResponse' - | 'KeyValue', + | 'app' + | 'cookie_jar' + | 'dialog' + | 'environment' + | 'folder' + | 'grpc_connection' + | 'grpc_event' + | 'grpc_request' + | 'http_request' + | 'http_response' + | 'key_value' + | 'setting' + | 'sidebar' + | 'workspace', action: - | 'Toggle' - | 'Show' - | 'Hide' - | 'Launch' - | 'Create' - | 'Update' - | 'Delete' - | 'DeleteMany' - | 'Send' - | 'Duplicate', + | 'cancel' + | 'create' + | 'delete' + | 'delete_many' + | 'duplicate' + | 'hide' + | 'launch' + | 'send' + | 'show' + | 'toggle' + | 'update', + attributes: Record = {}, ) { invoke('cmd_track_event', {