mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-15 15:17:45 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219a6b78da | ||
|
|
fb11aff03f | ||
|
|
15714ae188 | ||
|
|
ce116d032d | ||
|
|
6f41df6e52 | ||
|
|
0853d2ca95 | ||
|
|
6798331ce5 | ||
|
|
5ffc75e0ad | ||
|
|
bf92371a49 | ||
|
|
bd3da86317 | ||
|
|
3db3d42246 | ||
|
|
de8bf3ca70 |
16
index.html
16
index.html
@@ -4,7 +4,21 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak App</title>
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
|
||||
<!-- Certain elements like webview (and maybe <select>?) will use background
|
||||
color depending on document background color-->
|
||||
<style>
|
||||
html, body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html, body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
|
||||
"query": "\n SELECT\n id, model, workspace_id, request_id, updated_at, created_at, url, status,\n status_reason, content_length, body_path, elapsed, elapsed_headers, error,\n version, remote_addr,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,13 +64,28 @@
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"name": "elapsed_headers",
|
||||
"ordinal": 12,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "remote_addr",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 13,
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -90,9 +105,12 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "679a519475adeb50abf046114d3c0d1e48e103f2bb11ef47637d7f0b00ed241f"
|
||||
"hash": "0fa6b56f8c996d14908a56928674b4b35af5fa36f63dc48b9b66ee6dfde78976"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "198bd086ccc87d2e6c24cb1c717f486d3ab58c0c958ede850c018fc266eade87"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078.json
generated
Normal file
12
src-tauri/.sqlx/query-587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5.json
generated
Normal file
12
src-tauri/.sqlx/query-a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
|
||||
"query": "\n SELECT\n id, model, workspace_id, request_id, updated_at, created_at, url, status,\n status_reason, content_length, body_path, elapsed, elapsed_headers, error,\n version, remote_addr,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,13 +64,28 @@
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"name": "elapsed_headers",
|
||||
"ordinal": 12,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "remote_addr",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 13,
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -90,9 +105,12 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "07b0c398efd1d5f8f479652de658716a9e7faef6aba6583dd209a4f290c5edd1"
|
||||
"hash": "ac38621cd947c3be9ca0d8ea73325fe35c3866d16f6482fc32c23762f112dc83"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json
generated
Normal file
12
src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json
generated
Normal file
12
src-tauri/.sqlx/query-b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM cookie_jars\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b98609f65dd3a6bbd1ea8dc8bed2840a6d5d13fec1bbc0aa61ca4f60de98a09c"
|
||||
}
|
||||
56
src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json
generated
Normal file
56
src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cookies!: sqlx::types::Json<Vec<JsonValue>>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"query": "\n SELECT\n id, model, workspace_id, request_id, updated_at, created_at, url, status,\n status_reason, content_length, body_path, elapsed, elapsed_headers, error,\n version, remote_addr,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -64,13 +64,28 @@
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"name": "elapsed_headers",
|
||||
"ordinal": 12,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "remote_addr",
|
||||
"ordinal": 15,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 13,
|
||||
"ordinal": 16,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -90,9 +105,12 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3d199d371be948211f4a50c869b307f5df60784293c52397d77a187633a406dd"
|
||||
"hash": "d5e087caa163a0c7bfbbadf07eccb80105501cf5baab706aa6792dfe90af8fc9"
|
||||
}
|
||||
56
src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json
generated
Normal file
56
src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"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 ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "cookies!: sqlx::types::Json<Vec<JsonValue>>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299"
|
||||
}
|
||||
129
src-tauri/Cargo.lock
generated
129
src-tauri/Cargo.lock
generated
@@ -83,9 +83,9 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"flate2",
|
||||
@@ -614,6 +614,72 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8"
|
||||
dependencies = [
|
||||
"time",
|
||||
"version_check 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa"
|
||||
dependencies = [
|
||||
"cookie 0.16.2",
|
||||
"idna 0.2.3",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6"
|
||||
dependencies = [
|
||||
"cookie 0.17.0",
|
||||
"idna 0.3.0",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
@@ -1955,6 +2021,27 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
@@ -3143,6 +3230,22 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||
|
||||
[[package]]
|
||||
name = "publicsuffix"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
|
||||
dependencies = [
|
||||
"idna 0.3.0",
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
@@ -3339,13 +3442,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.22"
|
||||
version = "0.11.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||
checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"cookie 0.16.2",
|
||||
"cookie_store 0.16.2",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -3379,6 +3484,18 @@ dependencies = [
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest_cookie_store"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cookie_store 0.20.0",
|
||||
"reqwest",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.10.0"
|
||||
@@ -4974,7 +5091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.4.0",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@@ -5805,6 +5922,7 @@ dependencies = [
|
||||
"boa_runtime",
|
||||
"chrono",
|
||||
"cocoa 0.25.0",
|
||||
"cookie 0.18.0",
|
||||
"datetime",
|
||||
"futures",
|
||||
"http",
|
||||
@@ -5813,6 +5931,7 @@ dependencies = [
|
||||
"openssl-sys",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"reqwest_cookie_store",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
||||
@@ -28,7 +28,8 @@ chrono = { version = "0.4.31", features = ["serde"] }
|
||||
futures = "0.3.26"
|
||||
http = "0.2.8"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart", "gzip", "brotli", "deflate"] }
|
||||
reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate"] }
|
||||
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"] }
|
||||
@@ -57,6 +58,7 @@ uuid = "1.3.0"
|
||||
log = "0.4.20"
|
||||
datetime = "0.5.2"
|
||||
window-shadows = "0.2.2"
|
||||
reqwest_cookie_store = "0.6.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
10
src-tauri/migrations/20240127013915_cookies.sql
Normal file
10
src-tauri/migrations/20240127013915_cookies.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE cookie_jars
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
model TEXT DEFAULT 'cookie_jar' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
cookies TEXT DEFAULT '[]' NOT NULL,
|
||||
workspace_id TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE http_responses ADD COLUMN elapsed_headers INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE http_responses ADD COLUMN remote_addr TEXT;
|
||||
ALTER TABLE http_responses ADD COLUMN version TEXT;
|
||||
@@ -12,26 +12,30 @@ use crate::{is_dev, models};
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsResource {
|
||||
App,
|
||||
Sidebar,
|
||||
Workspace,
|
||||
CookieJar,
|
||||
Dialog,
|
||||
Environment,
|
||||
Folder,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
KeyValue,
|
||||
Sidebar,
|
||||
Workspace,
|
||||
}
|
||||
|
||||
impl AnalyticsResource {
|
||||
pub fn from_str(s: &str) -> Option<AnalyticsResource> {
|
||||
match s {
|
||||
"App" => Some(AnalyticsResource::App),
|
||||
"Sidebar" => Some(AnalyticsResource::Sidebar),
|
||||
"Workspace" => Some(AnalyticsResource::Workspace),
|
||||
"Dialog" => Some(AnalyticsResource::Dialog),
|
||||
"CookieJar" => Some(AnalyticsResource::CookieJar),
|
||||
"Environment" => Some(AnalyticsResource::Environment),
|
||||
"Folder" => Some(AnalyticsResource::Folder),
|
||||
"HttpRequest" => Some(AnalyticsResource::HttpRequest),
|
||||
"HttpResponse" => Some(AnalyticsResource::HttpResponse),
|
||||
"KeyValue" => Some(AnalyticsResource::KeyValue),
|
||||
"Sidebar" => Some(AnalyticsResource::Sidebar),
|
||||
"Workspace" => Some(AnalyticsResource::Workspace),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -39,33 +43,41 @@ impl AnalyticsResource {
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsAction {
|
||||
Create,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
Duplicate,
|
||||
Export,
|
||||
Hide,
|
||||
Import,
|
||||
Launch,
|
||||
LaunchFirst,
|
||||
LaunchUpdate,
|
||||
Create,
|
||||
Send,
|
||||
Show,
|
||||
Toggle,
|
||||
Update,
|
||||
Upsert,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
Send,
|
||||
Toggle,
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
impl AnalyticsAction {
|
||||
pub fn from_str(s: &str) -> Option<AnalyticsAction> {
|
||||
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),
|
||||
"Create" => Some(AnalyticsAction::Create),
|
||||
"Send" => Some(AnalyticsAction::Send),
|
||||
"Show" => Some(AnalyticsAction::Show),
|
||||
"Toggle" => Some(AnalyticsAction::Toggle),
|
||||
"Update" => Some(AnalyticsAction::Update),
|
||||
"Upsert" => Some(AnalyticsAction::Upsert),
|
||||
"Delete" => Some(AnalyticsAction::Delete),
|
||||
"DeleteMany" => Some(AnalyticsAction::DeleteMany),
|
||||
"Send" => Some(AnalyticsAction::Send),
|
||||
"Duplicate" => Some(AnalyticsAction::Duplicate),
|
||||
"Toggle" => Some(AnalyticsAction::Toggle),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -74,29 +86,35 @@ impl AnalyticsAction {
|
||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
match resource {
|
||||
AnalyticsResource::App => "app",
|
||||
AnalyticsResource::Sidebar => "sidebar",
|
||||
AnalyticsResource::Workspace => "workspace",
|
||||
AnalyticsResource::CookieJar => "cookie_jar",
|
||||
AnalyticsResource::Dialog => "dialog",
|
||||
AnalyticsResource::Environment => "environment",
|
||||
AnalyticsResource::Folder => "folder",
|
||||
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::Create => "create",
|
||||
AnalyticsAction::Send => "send",
|
||||
AnalyticsAction::Show => "show",
|
||||
AnalyticsAction::Toggle => "toggle",
|
||||
AnalyticsAction::Update => "update",
|
||||
AnalyticsAction::Upsert => "upsert",
|
||||
AnalyticsAction::Delete => "delete",
|
||||
AnalyticsAction::DeleteMany => "delete_many",
|
||||
AnalyticsAction::Send => "send",
|
||||
AnalyticsAction::Duplicate => "duplicate",
|
||||
AnalyticsAction::Toggle => "toggle",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use log::{error, info, warn};
|
||||
use reqwest::multipart;
|
||||
use reqwest::{multipart, Url};
|
||||
use reqwest::redirect::Policy;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::Json;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use tauri::{AppHandle, Wry};
|
||||
|
||||
use crate::{emit_side_effect, models, render, response_err};
|
||||
@@ -19,13 +21,12 @@ use crate::{emit_side_effect, models, render, response_err};
|
||||
pub async fn send_http_request(
|
||||
request: models::HttpRequest,
|
||||
response: &models::HttpResponse,
|
||||
environment_id: &str,
|
||||
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 start = std::time::Instant::now();
|
||||
let environment = models::get_environment(environment_id, pool).await.ok();
|
||||
let environment_ref = environment.as_ref();
|
||||
let workspace = models::get_workspace(&request.workspace_id, pool)
|
||||
.await
|
||||
@@ -49,6 +50,32 @@ pub async fn send_http_request(
|
||||
.danger_accept_invalid_certs(!workspace.setting_validate_certificates)
|
||||
.tls_info(true);
|
||||
|
||||
// Add cookie store if specified
|
||||
let maybe_cookie_manager = match cookie_jar.clone() {
|
||||
Some(cj) => {
|
||||
// HACK: Can't construct Cookie without serde, so we have to do this
|
||||
let cookies = cj
|
||||
.cookies
|
||||
.0
|
||||
.iter()
|
||||
.map(|json_cookie| {
|
||||
serde_json::from_value(json_cookie.clone())
|
||||
.expect("Failed to deserialize cookie")
|
||||
})
|
||||
.map(|c| Ok(c))
|
||||
.collect::<Vec<Result<_, ()>>>();
|
||||
|
||||
let store = reqwest_cookie_store::CookieStore::from_cookies(cookies, true)
|
||||
.expect("Failed to create cookie store");
|
||||
let cookie_store = reqwest_cookie_store::CookieStoreMutex::new(store);
|
||||
let cookie_store = Arc::new(cookie_store);
|
||||
client_builder = client_builder.cookie_provider(Arc::clone(&cookie_store));
|
||||
|
||||
Some((cookie_store, cj))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
if workspace.setting_request_timeout > 0 {
|
||||
client_builder = client_builder.timeout(Duration::from_millis(
|
||||
workspace.setting_request_timeout.unsigned_abs(),
|
||||
@@ -58,14 +85,37 @@ pub async fn send_http_request(
|
||||
// .use_rustls_tls() // TODO: Make this configurable (maybe)
|
||||
let client = client_builder.build().expect("Failed to build client");
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
|
||||
.expect("Failed to create method");
|
||||
let mut request_builder = client.request(m, url_string.to_string());
|
||||
let mut request_builder = client.request(m, url.clone());
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
|
||||
|
||||
// TODO: Set cookie header ourselves once we also handle redirects. We need to do this
|
||||
// because reqwest doesn't give us a way to inspect the headers it sent (we have to do
|
||||
// everything manually to know that).
|
||||
// if let Some(cookie_store) = maybe_cookie_store.clone() {
|
||||
// let values1 = cookie_store.get_request_values(&url);
|
||||
// println!("COOKIE VLUAES: {:?}", values1.collect::<Vec<_>>());
|
||||
// let raw_value = cookie_store.get_request_values(&url)
|
||||
// .map(|(name, value)| format!("{}={}", name, value))
|
||||
// .collect::<Vec<_>>()
|
||||
// .join("; ");
|
||||
// headers.insert(
|
||||
// COOKIE,
|
||||
// HeaderValue::from_str(&raw_value).expect("Failed to create cookie header"),
|
||||
// );
|
||||
// }
|
||||
|
||||
for h in request.headers.0 {
|
||||
if h.name.is_empty() && h.value.is_empty() {
|
||||
continue;
|
||||
@@ -247,15 +297,18 @@ pub async fn send_http_request(
|
||||
}
|
||||
};
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let raw_response = client.execute(sendable_req).await;
|
||||
|
||||
match raw_response {
|
||||
Ok(v) => {
|
||||
let mut response = response.clone();
|
||||
response.elapsed_headers = start.elapsed().as_millis() as i64;
|
||||
let response_headers = v.headers().clone();
|
||||
response.status = v.status().as_u16() as i64;
|
||||
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||
response.headers = Json(
|
||||
v.headers()
|
||||
response_headers
|
||||
.iter()
|
||||
.map(|(k, v)| models::HttpResponseHeader {
|
||||
name: k.as_str().to_string(),
|
||||
@@ -264,8 +317,25 @@ pub async fn send_http_request(
|
||||
.collect(),
|
||||
);
|
||||
response.url = v.url().to_string();
|
||||
response.remote_addr = v.remote_addr().map(|a| a.to_string());
|
||||
response.version = match v.version() {
|
||||
http::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
|
||||
http::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
|
||||
http::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
|
||||
http::Version::HTTP_2 => Some("HTTP/2".to_string()),
|
||||
http::Version::HTTP_3 => Some("HTTP/3".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let content_length = v.content_length();
|
||||
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
|
||||
response.content_length = Some(body_bytes.len() as i64);
|
||||
response.elapsed = start.elapsed().as_millis() as i64;
|
||||
|
||||
// Use content length if available, otherwise use body length
|
||||
response.content_length = match content_length {
|
||||
Some(l) => Some(l as i64),
|
||||
None => Some(body_bytes.len() as i64),
|
||||
};
|
||||
|
||||
{
|
||||
// Write body to FS
|
||||
@@ -292,7 +362,6 @@ pub async fn send_http_request(
|
||||
);
|
||||
}
|
||||
|
||||
response.elapsed = start.elapsed().as_millis() as i64;
|
||||
response = models::update_response_if_id(&response, pool)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
@@ -304,15 +373,42 @@ pub async fn send_http_request(
|
||||
match (download_path, response.body_path.clone()) {
|
||||
(Some(dl_path), Some(body_path)) => {
|
||||
info!("Downloading response body to {}", dl_path.display());
|
||||
fs::copy(body_path, dl_path).expect("Failed to copy file for response download");
|
||||
fs::copy(body_path, dl_path)
|
||||
.expect("Failed to copy file for response download");
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
// Add cookie store if specified
|
||||
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
|
||||
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
|
||||
// println!("RESPONSE COOKIE: {}", h.to_str().unwrap());
|
||||
// cookie_store::RawCookie::from_str(h.to_str().unwrap())
|
||||
// .expect("Failed to parse cookie")
|
||||
// });
|
||||
// store.store_response_cookies(cookies, &url);
|
||||
|
||||
let json_cookies: Json<Vec<JsonValue>> = Json(
|
||||
cookie_store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter_any()
|
||||
.map(|c| serde_json::to_value(&c).expect("Failed to serialize cookie"))
|
||||
.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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => {
|
||||
response_err(response, e.to_string(), app_handle, pool).await
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
extern crate core;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
@@ -16,7 +17,7 @@ use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, error, info, warn};
|
||||
use rand::random;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::types::Json;
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
@@ -78,17 +79,36 @@ async fn migrate_db(
|
||||
async fn send_ephemeral_request(
|
||||
mut request: models::HttpRequest,
|
||||
environment_id: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
app_handle: AppHandle<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let response = models::HttpResponse::new();
|
||||
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
|
||||
request.id = "".to_string();
|
||||
let environment = match environment_id {
|
||||
Some(id) => Some(
|
||||
models::get_environment(id, pool)
|
||||
.await
|
||||
.expect("Failed to get environment"),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let cookie_jar = match cookie_jar_id {
|
||||
Some(id) => Some(
|
||||
models::get_cookie_jar(id, pool)
|
||||
.await
|
||||
.expect("Failed to get cookie jar"),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// let cookie_jar_id2 = cookie_jar_id.unwrap_or("").to_string();
|
||||
send_http_request(
|
||||
request,
|
||||
&response,
|
||||
&environment_id2,
|
||||
environment,
|
||||
cookie_jar,
|
||||
&app_handle,
|
||||
pool,
|
||||
None,
|
||||
@@ -151,6 +171,13 @@ async fn import_data(
|
||||
)
|
||||
.await
|
||||
{
|
||||
analytics::track_event(
|
||||
&window.app_handle(),
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Import,
|
||||
Some(json!({ "plugin": plugin_name })),
|
||||
)
|
||||
.await;
|
||||
result = Some(r);
|
||||
break;
|
||||
}
|
||||
@@ -217,8 +244,17 @@ async fn export_data(
|
||||
serde_json::to_writer_pretty(&f, &export_data)
|
||||
.map_err(|e| e.to_string())
|
||||
.expect("Failed to write");
|
||||
|
||||
f.sync_all().expect("Failed to sync");
|
||||
info!("Exported Yaak workspace to {:?}", export_path);
|
||||
|
||||
analytics::track_event(
|
||||
&app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Export,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -228,22 +264,50 @@ async fn send_request(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
request_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
download_dir: Option<&str>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let app_handle = window.app_handle();
|
||||
|
||||
let req = models::get_request(request_id, pool)
|
||||
let request = models::get_request(request_id, pool)
|
||||
.await
|
||||
.expect("Failed to get request");
|
||||
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, None, None, vec![], pool)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
let environment = match environment_id {
|
||||
Some(id) => Some(
|
||||
models::get_environment(id, pool)
|
||||
.await
|
||||
.expect("Failed to get environment"),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let response2 = response.clone();
|
||||
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
|
||||
let app_handle2 = window.app_handle().clone();
|
||||
let pool2 = pool.clone();
|
||||
let cookie_jar = match cookie_jar_id {
|
||||
Some(id) => Some(
|
||||
models::get_cookie_jar(id, pool)
|
||||
.await
|
||||
.expect("Failed to get cookie jar"),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let response = models::create_response(
|
||||
&request.id,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
|
||||
let download_path = if let Some(p) = download_dir {
|
||||
Some(std::path::Path::new(p).to_path_buf())
|
||||
@@ -251,24 +315,18 @@ async fn send_request(
|
||||
None
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = send_http_request(
|
||||
req,
|
||||
&response2,
|
||||
&environment_id2,
|
||||
&app_handle2,
|
||||
&pool2,
|
||||
download_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
response_err(&response2, e, &app_handle2, &pool2)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
}
|
||||
});
|
||||
emit_side_effect(&app_handle, "created_model", response.clone());
|
||||
|
||||
emit_and_return(&window, "created_model", response)
|
||||
send_http_request(
|
||||
request.clone(),
|
||||
&response,
|
||||
environment,
|
||||
cookie_jar,
|
||||
&app_handle,
|
||||
&pool,
|
||||
download_path,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn response_err(
|
||||
@@ -362,6 +420,57 @@ async fn create_workspace(
|
||||
emit_and_return(&window, "created_model", created_workspace)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_cookie_jar(
|
||||
cookie_jar: models::CookieJar,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::CookieJar, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
println!("Updating cookie jar {}", cookie_jar.cookies.len());
|
||||
|
||||
let updated = models::upsert_cookie_jar(pool, &cookie_jar)
|
||||
.await
|
||||
.expect("Failed to update cookie jar");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_cookie_jar(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
cookie_jar_id: &str,
|
||||
) -> Result<models::CookieJar, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let req = models::delete_cookie_jar(cookie_jar_id, pool)
|
||||
.await
|
||||
.expect("Failed to delete cookie jar");
|
||||
emit_and_return(&window, "deleted_model", req)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_cookie_jar(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::CookieJar, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_cookie_jar = models::upsert_cookie_jar(
|
||||
pool,
|
||||
&models::CookieJar {
|
||||
name: name.to_string(),
|
||||
workspace_id: workspace_id.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create cookie jar");
|
||||
|
||||
emit_and_return(&window, "created_model", created_cookie_jar)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_environment(
|
||||
workspace_id: &str,
|
||||
@@ -627,6 +736,44 @@ async fn get_request(
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_cookie_jar(
|
||||
id: &str,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::CookieJar, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::get_cookie_jar(id, pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_cookie_jars(
|
||||
workspace_id: &str,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<Vec<models::CookieJar>, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let cookie_jars = models::find_cookie_jars(workspace_id, pool)
|
||||
.await
|
||||
.expect("Failed to find cookie jars");
|
||||
|
||||
if cookie_jars.is_empty() {
|
||||
let cookie_jar = models::upsert_cookie_jar(
|
||||
pool,
|
||||
&models::CookieJar {
|
||||
name: "Default".to_string(),
|
||||
workspace_id: workspace_id.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create CookieJar");
|
||||
Ok(vec![cookie_jar])
|
||||
} else {
|
||||
Ok(cookie_jars)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_environment(
|
||||
id: &str,
|
||||
@@ -733,7 +880,7 @@ async fn check_for_updates(
|
||||
app_handle: AppHandle<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
yaak_updater: State<'_, Mutex<YaakUpdater>>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<bool, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let update_mode = get_update_mode(pool).await;
|
||||
yaak_updater
|
||||
@@ -755,6 +902,7 @@ fn main() {
|
||||
.level_for("tracing", log::LevelFilter::Info)
|
||||
.level_for("reqwest", log::LevelFilter::Info)
|
||||
.level_for("tokio_util", log::LevelFilter::Info)
|
||||
.level_for("cookie_store", log::LevelFilter::Info)
|
||||
.with_colors(ColoredLevelConfig::default())
|
||||
.level(log::LevelFilter::Trace)
|
||||
.build(),
|
||||
@@ -807,11 +955,13 @@ fn main() {
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_for_updates,
|
||||
create_cookie_jar,
|
||||
create_environment,
|
||||
create_folder,
|
||||
create_request,
|
||||
create_workspace,
|
||||
delete_all_responses,
|
||||
delete_cookie_jar,
|
||||
delete_environment,
|
||||
delete_folder,
|
||||
delete_request,
|
||||
@@ -820,13 +970,15 @@ fn main() {
|
||||
duplicate_request,
|
||||
export_data,
|
||||
filter_response,
|
||||
get_key_value,
|
||||
get_cookie_jar,
|
||||
get_environment,
|
||||
get_folder,
|
||||
get_key_value,
|
||||
get_request,
|
||||
get_settings,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_cookie_jars,
|
||||
list_environments,
|
||||
list_folders,
|
||||
list_requests,
|
||||
@@ -838,6 +990,7 @@ fn main() {
|
||||
set_key_value,
|
||||
set_update_mode,
|
||||
track_event,
|
||||
update_cookie_jar,
|
||||
update_environment,
|
||||
update_folder,
|
||||
update_request,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use log::error;
|
||||
|
||||
use log::error;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
@@ -57,6 +57,23 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CookieX {
|
||||
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct CookieJar {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub workspace_id: String,
|
||||
pub name: String,
|
||||
pub cookies: Json<Vec<JsonValue>>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Environment {
|
||||
@@ -154,7 +171,10 @@ pub struct HttpResponse {
|
||||
pub error: Option<String>,
|
||||
pub url: String,
|
||||
pub content_length: Option<i64>,
|
||||
pub version: Option<String>,
|
||||
pub elapsed: i64,
|
||||
pub elapsed_headers: i64,
|
||||
pub remote_addr: Option<String>,
|
||||
pub status: i64,
|
||||
pub status_reason: Option<String>,
|
||||
pub body_path: Option<String>,
|
||||
@@ -351,6 +371,96 @@ pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
pub async fn get_cookie_jar(id: &str, pool: &Pool<Sqlite>) -> Result<CookieJar, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
CookieJar,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
workspace_id,
|
||||
name,
|
||||
cookies AS "cookies!: sqlx::types::Json<Vec<JsonValue>>"
|
||||
FROM cookie_jars WHERE id = ?
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_cookie_jars(workspace_id: &str, pool: &Pool<Sqlite>) -> Result<Vec<CookieJar>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
CookieJar,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
workspace_id,
|
||||
name,
|
||||
cookies AS "cookies!: sqlx::types::Json<Vec<JsonValue>>"
|
||||
FROM cookie_jars WHERE workspace_id = ?
|
||||
"#,
|
||||
workspace_id,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_cookie_jar(id: &str, pool: &Pool<Sqlite>) -> Result<CookieJar, sqlx::Error> {
|
||||
let cookie_jar = get_cookie_jar(id, pool).await?;
|
||||
|
||||
let _ = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM cookie_jars
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
Ok(cookie_jar)
|
||||
}
|
||||
|
||||
pub async fn upsert_cookie_jar(
|
||||
pool: &Pool<Sqlite>,
|
||||
cookie_jar: &CookieJar,
|
||||
) -> Result<CookieJar, sqlx::Error> {
|
||||
let id = match cookie_jar.id.as_str() {
|
||||
"" => generate_id(Some("cj")),
|
||||
_ => cookie_jar.id.to_string(),
|
||||
};
|
||||
let trimmed_name = cookie_jar.name.trim();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO cookie_jars (
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
cookies
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
cookies = excluded.cookies
|
||||
"#,
|
||||
id,
|
||||
cookie_jar.workspace_id,
|
||||
trimmed_name,
|
||||
cookie_jar.cookies,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
get_cookie_jar(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn find_environments(
|
||||
workspace_id: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -744,15 +854,19 @@ pub async fn delete_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest
|
||||
Ok(req)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_response(
|
||||
request_id: &str,
|
||||
elapsed: i64,
|
||||
elapsed_headers: i64,
|
||||
url: &str,
|
||||
status: i64,
|
||||
status_reason: Option<&str>,
|
||||
content_length: Option<i64>,
|
||||
body_path: Option<&str>,
|
||||
headers: Vec<HttpResponseHeader>,
|
||||
version: Option<&str>,
|
||||
remote_addr: Option<&str>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<HttpResponse, sqlx::Error> {
|
||||
let req = get_request(request_id, pool).await?;
|
||||
@@ -765,25 +879,31 @@ pub async fn create_response(
|
||||
request_id,
|
||||
workspace_id,
|
||||
elapsed,
|
||||
elapsed_headers,
|
||||
url,
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body_path,
|
||||
headers
|
||||
headers,
|
||||
version,
|
||||
remote_addr
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
id,
|
||||
request_id,
|
||||
req.workspace_id,
|
||||
elapsed,
|
||||
elapsed_headers,
|
||||
url,
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body_path,
|
||||
headers_json,
|
||||
version,
|
||||
remote_addr,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
@@ -868,6 +988,7 @@ pub async fn update_response(
|
||||
r#"
|
||||
UPDATE http_responses SET (
|
||||
elapsed,
|
||||
elapsed_headers,
|
||||
url,
|
||||
status,
|
||||
status_reason,
|
||||
@@ -875,10 +996,13 @@ pub async fn update_response(
|
||||
body_path,
|
||||
error,
|
||||
headers,
|
||||
version,
|
||||
remote_addr,
|
||||
updated_at
|
||||
) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
"#,
|
||||
response.elapsed,
|
||||
response.elapsed_headers,
|
||||
response.url,
|
||||
response.status,
|
||||
response.status_reason,
|
||||
@@ -886,6 +1010,8 @@ pub async fn update_response(
|
||||
response.body_path,
|
||||
response.error,
|
||||
headers_json,
|
||||
response.version,
|
||||
response.remote_addr,
|
||||
response.id,
|
||||
)
|
||||
.execute(pool)
|
||||
@@ -897,8 +1023,10 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
|
||||
sqlx::query_as!(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
SELECT
|
||||
id, model, workspace_id, request_id, updated_at, created_at, url, status,
|
||||
status_reason, content_length, body_path, elapsed, elapsed_headers, error,
|
||||
version, remote_addr,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE id = ?
|
||||
@@ -914,15 +1042,14 @@ pub async fn find_responses(
|
||||
limit: Option<i64>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Vec<HttpResponse>, sqlx::Error> {
|
||||
let limit_unwrapped = match limit {
|
||||
Some(l) => l,
|
||||
None => i64::MAX,
|
||||
};
|
||||
let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX);
|
||||
sqlx::query_as!(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
SELECT
|
||||
id, model, workspace_id, request_id, updated_at, created_at, url, status,
|
||||
status_reason, content_length, body_path, elapsed, elapsed_headers, error,
|
||||
version, remote_addr,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE request_id = ?
|
||||
@@ -943,8 +1070,10 @@ pub async fn find_responses_by_workspace_id(
|
||||
sqlx::query_as!(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
SELECT
|
||||
id, model, workspace_id, request_id, updated_at, created_at, url, status,
|
||||
status_reason, content_length, body_path, elapsed, elapsed_headers, error,
|
||||
version, remote_addr,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE workspace_id = ?
|
||||
|
||||
@@ -29,7 +29,7 @@ impl YaakUpdater {
|
||||
&mut self,
|
||||
app_handle: &AppHandle<Wry>,
|
||||
mode: UpdateMode,
|
||||
) -> Result<(), updater::Error> {
|
||||
) -> Result<bool, updater::Error> {
|
||||
self.last_update_check = SystemTime::now();
|
||||
|
||||
let update_mode = get_update_mode_str(mode);
|
||||
@@ -37,7 +37,7 @@ impl YaakUpdater {
|
||||
info!("Checking for updates mode={} enabled={}", update_mode, enabled);
|
||||
|
||||
if !enabled {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match app_handle
|
||||
@@ -81,9 +81,9 @@ impl YaakUpdater {
|
||||
});
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
Err(updater::Error::UpToDate) => Ok(()),
|
||||
Err(updater::Error::UpToDate) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
@@ -91,9 +91,10 @@ impl YaakUpdater {
|
||||
&mut self,
|
||||
app_handle: &AppHandle<Wry>,
|
||||
mode: UpdateMode,
|
||||
) -> Result<(), updater::Error> {
|
||||
if self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS {
|
||||
return Ok(());
|
||||
) -> Result<bool, updater::Error> {
|
||||
let ignore_check = self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
|
||||
if ignore_check {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.force_check(app_handle, mode).await
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2024.1.0"
|
||||
"version": "2024.2.0"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
|
||||
74
src-web/components/CookieDialog.tsx
Normal file
74
src-web/components/CookieDialog.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useCookieJars } from '../hooks/useCookieJars';
|
||||
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
|
||||
import { cookieDomain } from '../lib/models';
|
||||
import { Banner } from './core/Banner';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
|
||||
interface Props {
|
||||
cookieJarId: string | null;
|
||||
}
|
||||
|
||||
export const CookieDialog = function ({ cookieJarId }: Props) {
|
||||
const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null);
|
||||
const cookieJars = useCookieJars();
|
||||
const cookieJar = cookieJars.find((c) => c.id === cookieJarId);
|
||||
|
||||
if (cookieJar == null) {
|
||||
return <div>No cookie jar selected</div>;
|
||||
}
|
||||
|
||||
if (cookieJar.cookies.length === 0) {
|
||||
return (
|
||||
<Banner>
|
||||
Cookies will appear when a response contains the <InlineCode>Set-Cookie</InlineCode> header
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-2">
|
||||
<table className="w-full text-xs mb-auto min-w-full max-w-full divide-y">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 text-left">Domain</th>
|
||||
<th className="py-2 text-left pl-4">Cookie</th>
|
||||
<th className="py-2 pl-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{cookieJar?.cookies.map((c) => (
|
||||
<tr key={c.domain + c.raw_cookie}>
|
||||
<td className="py-2 select-text cursor-text font-mono font-semibold max-w-0">
|
||||
{cookieDomain(c)}
|
||||
</td>
|
||||
<td className="py-2 pl-4 select-text cursor-text font-mono text-gray-700 whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars">
|
||||
{c.raw_cookie}
|
||||
</td>
|
||||
<td className="max-w-0 w-10">
|
||||
<IconButton
|
||||
icon="trash"
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
title="Delete"
|
||||
className="ml-auto"
|
||||
onClick={async () => {
|
||||
console.log(
|
||||
'DELETE COOKIE',
|
||||
c,
|
||||
cookieJar.cookies.filter((c2) => c2 !== c).length,
|
||||
);
|
||||
await updateCookieJar.mutateAsync({
|
||||
...cookieJar,
|
||||
cookies: cookieJar.cookies.filter((c2) => c2 !== c),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
94
src-web/components/CookieDropdown.tsx
Normal file
94
src-web/components/CookieDropdown.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
import { useCookieJars } from '../hooks/useCookieJars';
|
||||
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
|
||||
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
|
||||
import { CookieDialog } from './CookieDialog';
|
||||
import { Dropdown, type DropdownItem } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { useDialog } from './DialogContext';
|
||||
|
||||
export function CookieDropdown() {
|
||||
const cookieJars = useCookieJars();
|
||||
const { activeCookieJar, setActiveCookieJarId } = useActiveCookieJar();
|
||||
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
|
||||
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
|
||||
const createCookieJar = useCreateCookieJar();
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={[
|
||||
...cookieJars.map((j) => ({
|
||||
key: j.id,
|
||||
label: j.name,
|
||||
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
|
||||
onSelect: () => setActiveCookieJarId(j.id),
|
||||
})),
|
||||
...((cookieJars.length > 0 && activeCookieJar != null
|
||||
? [
|
||||
{ type: 'separator', label: activeCookieJar.name },
|
||||
{
|
||||
key: 'manage',
|
||||
label: 'Manage Cookies',
|
||||
leftSlot: <Icon icon="cookie" />,
|
||||
onSelect: () => {
|
||||
if (activeCookieJar == null) return;
|
||||
dialog.show({
|
||||
id: 'cookies',
|
||||
title: 'Manage Cookies',
|
||||
size: 'full',
|
||||
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'rename-cookie-jar',
|
||||
title: 'Rename Cookie Jar',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: activeCookieJar?.name,
|
||||
});
|
||||
updateCookieJar.mutate({ name });
|
||||
},
|
||||
},
|
||||
...((cookieJars.length > 1 // Never delete the last one
|
||||
? [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
variant: 'danger',
|
||||
onSelect: () => deleteCookieJar.mutateAsync(),
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'create-cookie-jar',
|
||||
label: 'New Cookie Jar',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createCookieJar.mutate(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton size="sm" icon="cookie" title="Cookie Jar" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { DialogProps } from './core/Dialog';
|
||||
import { Dialog } from './core/Dialog';
|
||||
|
||||
@@ -7,15 +8,13 @@ type DialogEntry = {
|
||||
render: ({ hide }: { hide: () => void }) => React.ReactNode;
|
||||
} & Pick<DialogProps, 'title' | 'description' | 'hideX' | 'className' | 'size'>;
|
||||
|
||||
type DialogEntryOptionalId = Omit<DialogEntry, 'id'> & { id?: string };
|
||||
|
||||
interface State {
|
||||
dialogs: DialogEntry[];
|
||||
actions: Actions;
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
show: (d: DialogEntryOptionalId) => void;
|
||||
show: (d: DialogEntry) => void;
|
||||
toggle: (d: DialogEntry) => void;
|
||||
hide: (id: string) => void;
|
||||
}
|
||||
@@ -27,12 +26,11 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
|
||||
const actions = useMemo<Actions>(
|
||||
() => ({
|
||||
show({ id: oid, ...props }: DialogEntryOptionalId) {
|
||||
const id = oid ?? Math.random().toString(36).slice(2);
|
||||
show({ id, ...props }: DialogEntry) {
|
||||
trackEvent('Dialog', 'Show', { id });
|
||||
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
|
||||
},
|
||||
toggle({ id: oid, ...props }: DialogEntryOptionalId) {
|
||||
const id = oid ?? Math.random().toString(36).slice(2);
|
||||
toggle({ id, ...props }: DialogEntry) {
|
||||
if (dialogs.some((d) => d.id === id)) this.hide(id);
|
||||
else this.show({ id, ...props });
|
||||
},
|
||||
|
||||
@@ -54,23 +54,26 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
...((environments.length > 0
|
||||
? [{ type: 'separator', label: 'Environments' }]
|
||||
: []) as DropdownItem[]),
|
||||
environments.length
|
||||
? {
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
hotKeyAction: 'environmentEditor.toggle',
|
||||
leftSlot: <Icon icon="box" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
}
|
||||
: {
|
||||
key: 'new',
|
||||
label: 'New Environment',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: async () => {
|
||||
await createEnvironment.mutateAsync();
|
||||
showEnvironmentDialog();
|
||||
...((environments.length > 0
|
||||
? [
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
hotKeyAction: 'environmentEditor.toggle',
|
||||
leftSlot: <Icon icon="box" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
key: 'new',
|
||||
label: 'New Environment',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: async () => {
|
||||
await createEnvironment.mutateAsync();
|
||||
showEnvironmentDialog();
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeEnvironment?.id, createEnvironment, environments, routes, showEnvironmentDialog],
|
||||
);
|
||||
|
||||
@@ -51,12 +51,12 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full grid gap-x-8 grid-rows-[minmax(0,1fr)]',
|
||||
'h-full pt-1 grid gap-x-8 grid-rows-[minmax(0,1fr)]',
|
||||
showSidebar ? 'grid-cols-[auto_minmax(0,1fr)]' : 'grid-cols-[minmax(0,1fr)]',
|
||||
)}
|
||||
>
|
||||
{showSidebar && (
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2">
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2 pb-4">
|
||||
<div className="min-w-0 h-full w-full overflow-y-scroll">
|
||||
{environments.map((e) => (
|
||||
<SidebarButton
|
||||
@@ -152,6 +152,7 @@ const EnvironmentEditor = function ({
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'rename-environment',
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
@@ -55,6 +56,8 @@ export function GlobalHooks() {
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: payload.model === 'cookie_jar'
|
||||
? cookieJarsQueryKey(payload)
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -80,6 +83,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'cookie_jar'
|
||||
? cookieJarsQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
@@ -115,6 +120,8 @@ export function GlobalHooks() {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||
} else if (payload.model === 'cookie_jar') {
|
||||
queryClient.setQueryData(cookieJarsQueryKey(payload), undefined);
|
||||
} else if (payload.model === 'settings') {
|
||||
queryClient.setQueryData(settingsQueryKey(), undefined);
|
||||
}
|
||||
|
||||
@@ -84,39 +84,41 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
actions={
|
||||
error || isLoading
|
||||
? [
|
||||
<Button
|
||||
key="introspection"
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full mt-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>,
|
||||
<div key="introspection" className="!opacity-100">
|
||||
<Button
|
||||
key="introspection"
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full my-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>
|
||||
</div>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
export const KeyboardShortcutsDialog = () => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="h-full w-full pb-2">
|
||||
<HotKeyList hotkeys={hotkeyActions} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -40,6 +40,7 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
|
||||
leftSlot: <Icon icon="sparkles" />,
|
||||
onSelect: async () => {
|
||||
const newMethod = await prompt({
|
||||
id: 'custom-method',
|
||||
label: 'Http Method',
|
||||
name: 'httpMethod',
|
||||
defaultValue: '',
|
||||
|
||||
@@ -4,9 +4,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
|
||||
import {
|
||||
@@ -22,13 +20,10 @@ import {
|
||||
} from '../lib/models';
|
||||
import { BasicAuth } from './BasicAuth';
|
||||
import { BearerAuth } from './BearerAuth';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
|
||||
@@ -1,26 +1,66 @@
|
||||
import { shell } from '@tauri-apps/api';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { Separator } from './core/Separator';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
headers: HttpResponse['headers'];
|
||||
response: HttpResponse;
|
||||
}
|
||||
|
||||
export function ResponseHeaders({ headers }: Props) {
|
||||
export function ResponseHeaders({ response }: Props) {
|
||||
return (
|
||||
<dl className="text-xs w-full h-full font-mono overflow-auto">
|
||||
{headers.map((h, i) => {
|
||||
return (
|
||||
<HStack
|
||||
space={3}
|
||||
key={i}
|
||||
className={classNames(i > 0 ? 'border-t border-highlightSecondary py-1' : 'pb-1')}
|
||||
>
|
||||
<dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd>
|
||||
<dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
<div className="overflow-auto h-full pb-4">
|
||||
<dl className="text-xs w-full font-mono flex flex-col">
|
||||
{response.headers.map((h, i) => (
|
||||
<Row key={i} label={h.name} value={h.value} labelClassName="!text-violet-600" />
|
||||
))}
|
||||
</dl>
|
||||
<Separator className="my-4">Other Info</Separator>
|
||||
<dl className="text-xs w-full font-mono divide-highlightSecondary">
|
||||
<Row label="Version" value={response.version} />
|
||||
<Row label="Remote Address" value={response.remoteAddr} />
|
||||
<Row
|
||||
label={
|
||||
<div className="flex items-center">
|
||||
URL
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
className="inline-block w-auto ml-1 !h-auto opacity-50 hover:opacity-100"
|
||||
icon="externalLink"
|
||||
onClick={() => shell.open(response.url)}
|
||||
title="Open in browser"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
value={
|
||||
<div className="flex">
|
||||
<span className="select-text cursor-text">{response.url}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
label,
|
||||
value,
|
||||
labelClassName,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
labelClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<HStack space={3} className="py-0.5">
|
||||
<dd className={classNames(labelClassName, 'w-1/3 text-gray-700 select-text cursor-text')}>
|
||||
{label}
|
||||
</dd>
|
||||
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||
)}
|
||||
>
|
||||
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
|
||||
{activeResponse?.error && (
|
||||
<Banner color="danger" className="m-2">
|
||||
{activeResponse.error}
|
||||
</Banner>
|
||||
)}
|
||||
{!activeResponse && (
|
||||
<>
|
||||
<span />
|
||||
@@ -126,7 +130,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
{activeResponse.elapsed > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<DurationTag millis={activeResponse.elapsed} />
|
||||
<DurationTag
|
||||
headers={activeResponse.elapsedHeaders}
|
||||
total={activeResponse.elapsed}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!!activeResponse.contentLength && (
|
||||
@@ -156,7 +163,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
tabListClassName="mt-1.5"
|
||||
>
|
||||
<TabContent value="headers">
|
||||
<ResponseHeaders headers={activeResponse?.headers ?? []} />
|
||||
<ResponseHeaders response={activeResponse} />
|
||||
</TabContent>
|
||||
<TabContent value="body">
|
||||
{!activeResponse.contentLength ? (
|
||||
|
||||
@@ -20,7 +20,7 @@ export const SettingsDialog = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-2">
|
||||
<VStack space={2} className="mb-4">
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { invoke, shell } from '@tauri-apps/api';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useAlert } from '../hooks/useAlert';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
@@ -20,6 +21,7 @@ export function SettingsDropdown() {
|
||||
const appVersion = useAppVersion();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const dialog = useDialog();
|
||||
const alert = useAlert();
|
||||
const [showChangelog, setShowChangelog] = useState<boolean>(false);
|
||||
|
||||
useListenToTauriEvent('show_changelog', () => {
|
||||
@@ -52,7 +54,7 @@ export function SettingsDropdown() {
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
id: 'hotkey',
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'sm',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
@@ -65,11 +67,12 @@ export function SettingsDropdown() {
|
||||
leftSlot: <Icon icon="folderInput" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'import',
|
||||
title: 'Import Data',
|
||||
size: 'sm',
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<VStack space={3} className="pb-4">
|
||||
<p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -98,7 +101,17 @@ export function SettingsDropdown() {
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
leftSlot: <Icon icon="update" />,
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
onSelect: async () => {
|
||||
const hasUpdate: boolean = await invoke('check_for_updates');
|
||||
if (!hasUpdate) {
|
||||
alert({
|
||||
id: 'no-updates',
|
||||
title: 'No Updates',
|
||||
body: 'You are currently up to date',
|
||||
});
|
||||
}
|
||||
console.log('HAS UPDATE', hasUpdate);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'feedback',
|
||||
|
||||
@@ -613,6 +613,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'rename-folder',
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { FormEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
@@ -49,11 +48,12 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classNames('url-bar', className)}>
|
||||
<form onSubmit={handleSubmit} className={className}>
|
||||
<Input
|
||||
autocompleteVariables
|
||||
ref={inputRef}
|
||||
size={isFocused ? 'auto' : 'sm'}
|
||||
size="sm"
|
||||
wrapLines={isFocused}
|
||||
hideLabel
|
||||
useTemplating
|
||||
contentType="url"
|
||||
|
||||
@@ -51,7 +51,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
),
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<HStack space={2} justifyContent="end" alignItems="center" className="mt-6">
|
||||
<HStack space={2} justifyContent="end" alignItems="center" className="mt-4 mb-6">
|
||||
<Button
|
||||
className="focus"
|
||||
color="gray"
|
||||
@@ -104,6 +104,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'rename-workspace',
|
||||
title: 'Rename Workspace',
|
||||
description: (
|
||||
<>
|
||||
@@ -131,10 +132,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'new-workspace',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: 'My Workspace',
|
||||
title: 'New Workspace',
|
||||
confirmLabel: 'Create',
|
||||
});
|
||||
createWorkspace.mutate({ name });
|
||||
},
|
||||
@@ -143,6 +146,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
}, [
|
||||
activeWorkspace?.name,
|
||||
activeWorkspaceId,
|
||||
createWorkspace,
|
||||
deleteWorkspace.mutate,
|
||||
dialog,
|
||||
prompt,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { CookieDropdown } from './CookieDropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
@@ -27,6 +28,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
>
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarActions />
|
||||
<CookieDropdown />
|
||||
<HStack alignItems="center">
|
||||
<WorkspaceActionsDropdown />
|
||||
<Icon icon="chevronRight" className="text-gray-900 text-opacity-disabled" />
|
||||
|
||||
@@ -4,14 +4,18 @@ import type { ReactNode } from 'react';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'danger' | 'success' | 'gray';
|
||||
}
|
||||
export function Banner({ children, className }: Props) {
|
||||
export function Banner({ children, className, color = 'gray' }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'border border-red-500 bg-red-300/10 text-red-800 px-3 py-2 rounded select-auto cursor-text',
|
||||
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
|
||||
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800',
|
||||
color === 'danger' && 'border-red-500/60 bg-red-300/10 text-red-800',
|
||||
color === 'success' && 'border-green-500/60 bg-green-300/10 text-green-800',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -5,20 +5,9 @@ import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const colorStyles = {
|
||||
custom: 'ring-blue-500/50',
|
||||
default:
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
gray: 'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50',
|
||||
primary: 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50',
|
||||
secondary: 'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50',
|
||||
warning: 'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50',
|
||||
danger: 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50',
|
||||
};
|
||||
|
||||
export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
||||
innerClassName?: string;
|
||||
color?: keyof typeof colorStyles;
|
||||
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
|
||||
isLoading?: boolean;
|
||||
size?: 'sm' | 'md' | 'xs';
|
||||
justify?: 'start' | 'center';
|
||||
@@ -64,7 +53,17 @@ 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',
|
||||
colorStyles[color || 'default'],
|
||||
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',
|
||||
|
||||
@@ -54,10 +54,10 @@ export function Dialog({
|
||||
className={classNames(
|
||||
className,
|
||||
'gap-2 grid grid-rows-[auto_minmax(0,1fr)]',
|
||||
'relative bg-gray-50 pointer-events-auto',
|
||||
'p-5 rounded-lg overflow-auto',
|
||||
'pt-4 relative bg-gray-50 pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'dark:border border-highlight shadow shadow-black/10',
|
||||
'max-w-[90vw] max-h-[calc(100vh-8em)]',
|
||||
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
|
||||
size === 'sm' && 'w-[25rem] max-h-[80vh]',
|
||||
size === 'md' && 'w-[45rem] max-h-[80vh]',
|
||||
size === 'full' && 'w-[100vw] h-[100vh]',
|
||||
@@ -65,22 +65,28 @@ export function Dialog({
|
||||
)}
|
||||
>
|
||||
{title ? (
|
||||
<Heading size={1} id={titleId}>
|
||||
{' '}
|
||||
{title}{' '}
|
||||
<Heading className="px-6 pt-4" size={1} id={titleId}>
|
||||
{title}
|
||||
</Heading>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{description && <p id={descriptionId}>{description}</p>}
|
||||
<div className="h-full w-full grid grid-cols-[minmax(0,1fr)]">{children}</div>
|
||||
{description && (
|
||||
<p className="px-6" id={descriptionId}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className="h-full w-full grid grid-cols-[minmax(0,1fr)] overflow-y-auto px-6 py-2">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
||||
{!hideX && (
|
||||
<div className="ml-auto absolute right-1 top-1">
|
||||
<IconButton
|
||||
className="opacity-70 hover:opacity-100"
|
||||
onClick={onClose}
|
||||
title="Close dialog"
|
||||
title="Close dialog (Esc)"
|
||||
aria-label="Close"
|
||||
size="sm"
|
||||
icon="x"
|
||||
|
||||
@@ -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="my-1.5">
|
||||
<Separator key={i} className="ml-2 my-1.5">
|
||||
{item.label}
|
||||
</Separator>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
interface Props {
|
||||
millis: number;
|
||||
total: number;
|
||||
headers: number;
|
||||
}
|
||||
|
||||
export function DurationTag({ millis }: Props) {
|
||||
export function DurationTag({ total, headers }: Props) {
|
||||
return (
|
||||
<span title={`HEADER: ${formatMillis(headers)}\nTOTAL: ${formatMillis(total)}`}>
|
||||
{formatMillis(total)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function formatMillis(millis: number) {
|
||||
let num;
|
||||
let unit;
|
||||
|
||||
@@ -17,9 +26,5 @@ export function DurationTag({ millis }: Props) {
|
||||
unit = 'ms';
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={`${millis} milliseconds`}>
|
||||
{Math.round(num * 10) / 10} {unit}
|
||||
</span>
|
||||
);
|
||||
return `${Math.round(num * 10) / 10} ${unit}`;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function FormattedError({ children }: Props) {
|
||||
<pre
|
||||
className={classNames(
|
||||
'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
|
||||
'whitespace-pre border border-red-500 border-dashed overflow-x-auto',
|
||||
'whitespace-pre-wrap border border-red-500 border-dashed overflow-x-auto',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLHeadingElement> {
|
||||
size?: 1 | 2 | 3;
|
||||
|
||||
@@ -11,6 +11,7 @@ const icons = {
|
||||
check: lucide.CheckIcon,
|
||||
chevronDown: lucide.ChevronDownIcon,
|
||||
chevronRight: lucide.ChevronRightIcon,
|
||||
cookie: lucide.CookieIcon,
|
||||
code: lucide.CodeIcon,
|
||||
copy: lucide.CopyIcon,
|
||||
download: lucide.DownloadIcon,
|
||||
|
||||
@@ -40,6 +40,7 @@ export type InputProps = Omit<
|
||||
placeholder?: string;
|
||||
validate?: (v: string) => boolean;
|
||||
require?: boolean;
|
||||
wrapLines?: boolean;
|
||||
};
|
||||
|
||||
export const Input = forwardRef<EditorView | undefined, InputProps>(function Input(
|
||||
@@ -60,6 +61,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
placeholder,
|
||||
require,
|
||||
rightSlot,
|
||||
wrapLines,
|
||||
size = 'md',
|
||||
type = 'text',
|
||||
validate,
|
||||
@@ -82,7 +84,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
}, [onBlur]);
|
||||
|
||||
const id = `input-${name}`;
|
||||
const inputClassName = classNames(
|
||||
const editorClassName = classNames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder',
|
||||
);
|
||||
@@ -144,10 +146,9 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
'border',
|
||||
focused ? 'border-focus' : 'border-highlight',
|
||||
!isValid && '!border-invalid',
|
||||
size === 'md' && 'h-md',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'auto' && 'min-h-sm',
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
)}
|
||||
>
|
||||
{leftSlot}
|
||||
@@ -163,14 +164,14 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
ref={ref}
|
||||
id={id}
|
||||
singleLine
|
||||
wrapLines={size === 'auto'}
|
||||
wrapLines={wrapLines}
|
||||
onKeyDown={handleKeyDown}
|
||||
type={type === 'password' && !obscured ? 'text' : type}
|
||||
defaultValue={defaultValue}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
className={inputClassName}
|
||||
className={editorClassName}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
{...props}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Separator({
|
||||
}: Props) {
|
||||
return (
|
||||
<div role="separator" className={classNames(className, 'flex items-center')}>
|
||||
{children && <div className="text-xs text-gray-500 mx-2 whitespace-nowrap">{children}</div>}
|
||||
{children && <div className="text-xs text-gray-500 mr-2 whitespace-nowrap">{children}</div>}
|
||||
<div
|
||||
className={classNames(
|
||||
variant === 'primary' && 'bg-highlight',
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface AlertProps {
|
||||
|
||||
export function Alert({ onHide, body }: AlertProps) {
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<VStack space={3} className="pb-4">
|
||||
<div>{body}</div>
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button className="focus" color="primary" onClick={onHide}>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps)
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack space={2} justifyContent="end" className="mt-6">
|
||||
<HStack space={2} justifyContent="end" className="mt-2 mb-4">
|
||||
<Button className="focus" color="gray" onClick={handleHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -12,9 +12,18 @@ export interface PromptProps {
|
||||
name: InputProps['name'];
|
||||
defaultValue?: InputProps['defaultValue'];
|
||||
placeholder?: InputProps['placeholder'];
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
export function Prompt({ onHide, label, name, defaultValue, placeholder, onResult }: PromptProps) {
|
||||
export function Prompt({
|
||||
onHide,
|
||||
label,
|
||||
name,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
onResult,
|
||||
confirmLabel = 'Save',
|
||||
}: PromptProps) {
|
||||
const [value, setValue] = useState<string>(defaultValue ?? '');
|
||||
const handleSubmit = useCallback(
|
||||
(e: FormEvent<HTMLFormElement>) => {
|
||||
@@ -27,7 +36,7 @@ export function Prompt({ onHide, label, name, defaultValue, placeholder, onResul
|
||||
|
||||
return (
|
||||
<form
|
||||
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-6"
|
||||
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Input
|
||||
@@ -45,7 +54,7 @@ export function Prompt({ onHide, label, name, defaultValue, placeholder, onResul
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" className="focus" color="primary">
|
||||
Save
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</HStack>
|
||||
</form>
|
||||
|
||||
29
src-web/hooks/useActiveCookieJar.ts
Normal file
29
src-web/hooks/useActiveCookieJar.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useCookieJars } from './useCookieJars';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
|
||||
export function useActiveCookieJar() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const cookieJars = useCookieJars();
|
||||
|
||||
const kv = useKeyValue<string | null>({
|
||||
namespace: NAMESPACE_GLOBAL,
|
||||
key: ['activeCookieJar', workspaceId ?? 'n/a'],
|
||||
defaultValue: null,
|
||||
});
|
||||
|
||||
const activeCookieJar = cookieJars.find((cookieJar) => cookieJar.id === kv.value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kv.isLoading && activeCookieJar == null && cookieJars.length > 0) {
|
||||
kv.set(cookieJars[0]?.id ?? null);
|
||||
}
|
||||
}, [activeCookieJar, cookieJars, kv]);
|
||||
|
||||
return {
|
||||
activeCookieJar: activeCookieJar ?? null,
|
||||
setActiveCookieJarId: kv.set,
|
||||
};
|
||||
}
|
||||
@@ -5,8 +5,17 @@ import { Alert } from './Alert';
|
||||
|
||||
export function useAlert() {
|
||||
const dialog = useDialog();
|
||||
return ({ title, body }: { title: DialogProps['title']; body: AlertProps['body'] }) =>
|
||||
return ({
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
}: {
|
||||
id: string;
|
||||
title: DialogProps['title'];
|
||||
body: AlertProps['body'];
|
||||
}) =>
|
||||
dialog.show({
|
||||
id,
|
||||
title,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
|
||||
@@ -6,16 +6,19 @@ import { Confirm } from './Confirm';
|
||||
export function useConfirm() {
|
||||
const dialog = useDialog();
|
||||
return ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
variant,
|
||||
}: {
|
||||
id: string;
|
||||
title: DialogProps['title'];
|
||||
description?: DialogProps['description'];
|
||||
variant: ConfirmProps['variant'];
|
||||
}) =>
|
||||
new Promise((onResult: ConfirmProps['onResult']) => {
|
||||
dialog.show({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
hideX: true,
|
||||
|
||||
22
src-web/hooks/useCookieJars.ts
Normal file
22
src-web/hooks/useCookieJars.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { CookieJar } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
|
||||
export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) {
|
||||
return ['cookie_jars', { workspaceId }];
|
||||
}
|
||||
|
||||
export function useCookieJars() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
return (
|
||||
useQuery({
|
||||
enabled: workspaceId != null,
|
||||
queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
|
||||
queryFn: async () => {
|
||||
if (workspaceId == null) return [];
|
||||
return (await invoke('list_cookie_jars', { workspaceId })) as CookieJar[];
|
||||
},
|
||||
}).data ?? []
|
||||
);
|
||||
}
|
||||
36
src-web/hooks/useCreateCookieJar.ts
Normal file
36
src-web/hooks/useCreateCookieJar.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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 { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { usePrompt } from './usePrompt';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
|
||||
export function useCreateCookieJar() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
const prompt = usePrompt();
|
||||
|
||||
return useMutation<HttpRequest>({
|
||||
mutationFn: async () => {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create cookie jar when there's no active workspace");
|
||||
}
|
||||
const name = await prompt({
|
||||
id: 'new-cookie-jar',
|
||||
name: 'name',
|
||||
title: 'New CookieJar',
|
||||
label: 'Name',
|
||||
defaultValue: 'My Jar',
|
||||
});
|
||||
return invoke('create_cookie_jar', { workspaceId, name });
|
||||
},
|
||||
onSettled: () => trackEvent('CookieJar', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
(requests) => [...(requests ?? []), request],
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,9 +4,8 @@ import { trackEvent } from '../lib/analytics';
|
||||
import type { Environment } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { environmentsQueryKey, useEnvironments } from './useEnvironments';
|
||||
import { environmentsQueryKey } from './useEnvironments';
|
||||
import { usePrompt } from './usePrompt';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
export function useCreateEnvironment() {
|
||||
const routes = useAppRoutes();
|
||||
@@ -17,6 +16,7 @@ export function useCreateEnvironment() {
|
||||
return useMutation<Environment, unknown, void>({
|
||||
mutationFn: async () => {
|
||||
const name = await prompt({
|
||||
id: 'new-environment',
|
||||
name: 'name',
|
||||
title: 'New Environment',
|
||||
label: 'Name',
|
||||
|
||||
@@ -17,6 +17,7 @@ export function useDeleteAnyRequest() {
|
||||
mutationFn: async (id) => {
|
||||
const request = await getRequest(id);
|
||||
const confirmed = await confirm({
|
||||
id: 'delete-request',
|
||||
title: 'Delete Request',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
|
||||
38
src-web/hooks/useDeleteCookieJar.tsx
Normal file
38
src-web/hooks/useDeleteCookieJar.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { CookieJar } from '../lib/models';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { cookieJarsQueryKey } from './useCookieJars';
|
||||
|
||||
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
|
||||
return useMutation<CookieJar | null, string>({
|
||||
mutationFn: async () => {
|
||||
const confirmed = await confirm({
|
||||
id: 'delete-cookie-jar',
|
||||
title: 'Delete CookieJar',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Permanently delete <InlineCode>{cookieJar?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_cookie_jar', { cookieJarId: cookieJar?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('CookieJar', 'Delete'),
|
||||
onSuccess: async (cookieJar) => {
|
||||
if (cookieJar === null) return;
|
||||
|
||||
const { id: cookieJarId, workspaceId } = cookieJar;
|
||||
queryClient.setQueryData<CookieJar[]>(cookieJarsQueryKey({ workspaceId }), (cookieJars) =>
|
||||
cookieJars?.filter((e) => e.id !== cookieJarId),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
return useMutation<Environment | null, string>({
|
||||
mutationFn: async () => {
|
||||
const confirmed = await confirm({
|
||||
id: 'delete-environment',
|
||||
title: 'Delete Environment',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
|
||||
@@ -16,6 +16,7 @@ export function useDeleteFolder(id: string | null) {
|
||||
mutationFn: async () => {
|
||||
const folder = await getFolder(id);
|
||||
const confirmed = await confirm({
|
||||
id: 'delete-folder',
|
||||
title: 'Delete Folder',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
|
||||
@@ -18,6 +18,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
return useMutation<Workspace | null, string>({
|
||||
mutationFn: async () => {
|
||||
const confirmed = await confirm({
|
||||
id: 'delete-workspace',
|
||||
title: 'Delete Workspace',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
|
||||
@@ -11,7 +11,7 @@ export function useExportData() {
|
||||
|
||||
return useMutation({
|
||||
onError: (err: string) => {
|
||||
alert({ title: 'Export Failed', body: err });
|
||||
alert({ id: 'export-failed', title: 'Export Failed', body: err });
|
||||
},
|
||||
mutationFn: async () => {
|
||||
if (workspace == null) return;
|
||||
|
||||
@@ -24,8 +24,8 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'sidebar.toggle': ['CmdCtrl+b'],
|
||||
'sidebar.focus': ['CmdCtrl+1'],
|
||||
'urlBar.focus': ['CmdCtrl+l'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+/'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+Shift+/'],
|
||||
'settings.show': ['CmdCtrl+,'],
|
||||
'requestSwitcher.prev': ['Control+Tab'],
|
||||
'requestSwitcher.next': ['Control+Shift+Tab'],
|
||||
|
||||
@@ -22,7 +22,7 @@ export function useImportData() {
|
||||
|
||||
return useMutation({
|
||||
onError: (err: string) => {
|
||||
alert({ title: 'Import Failed', body: err });
|
||||
alert({ id: 'import-failed', title: 'Import Failed', body: err });
|
||||
},
|
||||
mutationFn: async () => {
|
||||
const selected = await open(openArgs);
|
||||
@@ -41,13 +41,14 @@ export function useImportData() {
|
||||
const importedWorkspace = imported.workspaces[0];
|
||||
|
||||
dialog.show({
|
||||
id: 'import-complete',
|
||||
title: 'Import Complete',
|
||||
size: 'sm',
|
||||
hideX: true,
|
||||
render: ({ hide }) => {
|
||||
const { workspaces, environments, folders, requests } = imported;
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<VStack space={3} className="pb-4">
|
||||
<ul className="list-disc pl-6">
|
||||
<li>{count('Workspace', workspaces.length)}</li>
|
||||
<li>{count('Environment', environments.length)}</li>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { dialog } from '@tauri-apps/api';
|
||||
import type { DialogProps } from '../components/core/Dialog';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import type { PromptProps } from './Prompt';
|
||||
@@ -7,22 +6,25 @@ import { Prompt } from './Prompt';
|
||||
export function usePrompt() {
|
||||
const dialog = useDialog();
|
||||
return ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
name,
|
||||
label,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
confirmLabel,
|
||||
}: Pick<DialogProps, 'title' | 'description'> &
|
||||
Pick<PromptProps, 'name' | 'label' | 'defaultValue' | 'placeholder'>) =>
|
||||
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
|
||||
new Promise((onResult: PromptProps['onResult']) => {
|
||||
dialog.show({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
render: ({ hide }) =>
|
||||
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder }),
|
||||
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import slugify from 'slugify';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { useActiveCookieJar } from './useActiveCookieJar';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useAlert } from './useAlert';
|
||||
|
||||
export function useSendAnyRequest(options: { download?: boolean } = {}) {
|
||||
const environmentId = useActiveEnvironmentId();
|
||||
const alert = useAlert();
|
||||
const { activeCookieJar } = useActiveCookieJar();
|
||||
return useMutation<HttpResponse | null, string, string | null>({
|
||||
mutationFn: async (id) => {
|
||||
const request = await getRequest(id);
|
||||
@@ -33,9 +35,10 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) {
|
||||
requestId: id,
|
||||
environmentId,
|
||||
downloadDir: downloadDir,
|
||||
cookieJarId: activeCookieJar?.id,
|
||||
});
|
||||
},
|
||||
onSettled: () => trackEvent('HttpRequest', 'Send'),
|
||||
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
||||
onError: (err) => alert({ id: 'send-failed', title: 'Send Failed', body: err }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { useSendAnyRequest } from './useSendAnyRequest';
|
||||
|
||||
export function useSendManyRequests() {
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { useEffect } from 'react';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
// import { useEffect } from 'react';
|
||||
// import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
// import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
// import { useActiveRequest } from './useActiveRequest';
|
||||
// import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
|
||||
export function useSyncWindowTitle() {
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
useEffect(() => {
|
||||
let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
|
||||
if (activeEnvironment) {
|
||||
newTitle += ` [${activeEnvironment.name}]`;
|
||||
}
|
||||
if (activeRequest) {
|
||||
newTitle += ` – ${fallbackRequestName(activeRequest)}`;
|
||||
}
|
||||
|
||||
console.log('Skipping setting window title to ', newTitle);
|
||||
// TODO: This resets the stoplight position so we can't use it yet
|
||||
// appWindow.setTitle(newTitle).catch(console.error);
|
||||
}, [activeEnvironment, activeRequest, activeWorkspace]);
|
||||
// const activeRequest = useActiveRequest();
|
||||
// const activeWorkspace = useActiveWorkspace();
|
||||
// const activeEnvironment = useActiveEnvironment();
|
||||
// useEffect(() => {
|
||||
// let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
|
||||
// if (activeEnvironment) {
|
||||
// newTitle += ` [${activeEnvironment.name}]`;
|
||||
// }
|
||||
// if (activeRequest) {
|
||||
// newTitle += ` – ${fallbackRequestName(activeRequest)}`;
|
||||
// }
|
||||
//
|
||||
// // TODO: This resets the stoplight position so we can't use it yet
|
||||
// // appWindow.setTitle(newTitle).catch(console.error);
|
||||
// }, [activeEnvironment, activeRequest, activeWorkspace]);
|
||||
}
|
||||
|
||||
30
src-web/hooks/useUpdateCookieJar.ts
Normal file
30
src-web/hooks/useUpdateCookieJar.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { CookieJar } from '../lib/models';
|
||||
import { getCookieJar } from '../lib/store';
|
||||
import { cookieJarsQueryKey } from './useCookieJars';
|
||||
|
||||
export function useUpdateCookieJar(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, unknown, Partial<CookieJar> | ((j: CookieJar) => CookieJar)>({
|
||||
mutationFn: async (v) => {
|
||||
const cookieJar = await getCookieJar(id);
|
||||
if (cookieJar == null) {
|
||||
throw new Error("Can't update a null workspace");
|
||||
}
|
||||
|
||||
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
|
||||
console.log('NEW COOKIE JAR', newCookieJar.cookies.length);
|
||||
await invoke('update_cookie_jar', { cookieJar: newCookieJar });
|
||||
},
|
||||
onMutate: async (v) => {
|
||||
const cookieJar = await getCookieJar(id);
|
||||
if (cookieJar === null) return;
|
||||
|
||||
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
|
||||
queryClient.setQueryData<CookieJar[]>(cookieJarsQueryKey(cookieJar), (cookieJars) =>
|
||||
(cookieJars ?? []).map((j) => (j.id === newCookieJar.id ? newCookieJar : j)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest, Settings } from '../lib/models';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import type { Settings } from '../lib/models';
|
||||
import { settingsQueryKey } from './useSettings';
|
||||
|
||||
export function useUpdateSettings() {
|
||||
|
||||
@@ -21,7 +21,6 @@ export function useUpdateWorkspace(id: string | null) {
|
||||
if (workspace === null) return;
|
||||
|
||||
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
|
||||
console.log('NEW WORKSPACE', newWorkspace);
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(workspace), (workspaces) =>
|
||||
(workspaces ?? []).map((w) => (w.id === newWorkspace.id ? newWorkspace : w)),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { invoke } from '@tauri-apps/api';
|
||||
export function trackEvent(
|
||||
resource:
|
||||
| 'App'
|
||||
| 'Dialog'
|
||||
| 'CookieJar'
|
||||
| 'Sidebar'
|
||||
| 'Workspace'
|
||||
| 'Environment'
|
||||
|
||||
@@ -9,7 +9,14 @@ export const AUTH_TYPE_NONE = null;
|
||||
export const AUTH_TYPE_BASIC = 'basic';
|
||||
export const AUTH_TYPE_BEARER = 'bearer';
|
||||
|
||||
export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
export type Model =
|
||||
| Settings
|
||||
| Workspace
|
||||
| HttpRequest
|
||||
| HttpResponse
|
||||
| KeyValue
|
||||
| Environment
|
||||
| CookieJar;
|
||||
|
||||
export interface BaseModel {
|
||||
readonly id: string;
|
||||
@@ -34,6 +41,33 @@ export interface Workspace extends BaseModel {
|
||||
settingRequestTimeout: number;
|
||||
}
|
||||
|
||||
export interface CookieJar extends BaseModel {
|
||||
readonly model: 'cookie_jar';
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
cookies: Cookie[];
|
||||
}
|
||||
|
||||
export interface Cookie {
|
||||
raw_cookie: string;
|
||||
domain: { HostOnly: string } | { Suffix: string } | 'NotPresent' | 'Empty';
|
||||
expires: { AtUtc: string } | 'SessionEnd';
|
||||
path: [string, boolean];
|
||||
}
|
||||
|
||||
export function cookieDomain(cookie: Cookie): string {
|
||||
if (cookie.domain === 'NotPresent' || cookie.domain === 'Empty') {
|
||||
return 'n/a';
|
||||
}
|
||||
if ('HostOnly' in cookie.domain) {
|
||||
return cookie.domain.HostOnly;
|
||||
}
|
||||
if ('Suffix' in cookie.domain) {
|
||||
return cookie.domain.Suffix;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export interface EnvironmentVariable {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -99,7 +133,10 @@ export interface HttpResponse extends BaseModel {
|
||||
readonly error: string;
|
||||
readonly status: number;
|
||||
readonly elapsed: number;
|
||||
readonly elapsedHeaders: number;
|
||||
readonly statusReason: string;
|
||||
readonly version: string;
|
||||
readonly remoteAddr: string;
|
||||
readonly url: string;
|
||||
readonly headers: HttpHeader[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Environment, Folder, HttpRequest, Settings, Workspace } from './models';
|
||||
import type { CookieJar, Environment, Folder, HttpRequest, Settings, Workspace } from './models';
|
||||
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
return invoke('get_settings', {});
|
||||
@@ -40,3 +40,12 @@ export async function getWorkspace(id: string | null): Promise<Workspace | null>
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
|
||||
if (id === null) return null;
|
||||
const cookieJar: CookieJar = (await invoke('get_cookie_jar', { id })) ?? null;
|
||||
if (cookieJar == null) {
|
||||
return null;
|
||||
}
|
||||
return cookieJar;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
}
|
||||
|
||||
/* Disable user selection to make it more "app-like" */
|
||||
:not(a),
|
||||
:not(input):not(textarea),
|
||||
:not(input):not(textarea)::after,
|
||||
:not(input):not(textarea)::before {
|
||||
|
||||
Reference in New Issue
Block a user