Good start to multi-window

This commit is contained in:
Gregory Schier
2023-03-28 18:29:40 -07:00
parent 4f501abb72
commit 4c22215ca5
20 changed files with 771 additions and 517 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Yaak App</title> <title>Yaak App</title>
<!-- <script src="http://localhost:8097"></script>--> <script src="http://localhost:8097"></script>
<style> <style>
body { body {
background-color: white; background-color: white;

View File

@@ -0,0 +1,7 @@
ALTER TABLE http_requests ADD COLUMN updated_by TEXT NOT NULL DEFAULT '';
ALTER TABLE http_responses ADD COLUMN updated_by TEXT NOT NULL DEFAULT '';
ALTER TABLE workspaces ADD COLUMN updated_by TEXT NOT NULL DEFAULT '';
ALTER TABLE key_values ADD COLUMN updated_by TEXT NOT NULL DEFAULT '';
ALTER TABLE http_requests ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE http_requests ADD COLUMN authentication_type TEXT;

View File

@@ -1,53 +1,5 @@
{ {
"db": "SQLite", "db": "SQLite",
"06aaf8f4a17566f1d25da2a60f0baf4b5fc28c3cf0c001a84e25edf9eab3c7e3": {
"describe": {
"columns": [
{
"name": "model",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 1,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "namespace",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "key",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "value",
"ordinal": 5,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false
],
"parameters": {
"Right": 2
}
},
"query": "\n SELECT model, created_at, updated_at, namespace, key, value\n FROM key_values\n WHERE namespace = ? AND key = ?\n "
},
"07d1a1c7b4f3d9625a766e60fd57bb779b71dae30e5bbce34885a911a5a42428": { "07d1a1c7b4f3d9625a766e60fd57bb779b71dae30e5bbce34885a911a5a42428": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -68,6 +20,70 @@
}, },
"query": "\n DELETE FROM http_responses\n WHERE request_id = ?\n " "query": "\n DELETE FROM http_responses\n WHERE request_id = ?\n "
}, },
"19e0076c3cd13b73a46619b5c0ee5bf304fe27245db3d19a648f625bf5231cb0": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"query": "\n INSERT INTO workspaces (id, updated_by, name, description)\n VALUES (?, ?, ?, ?)\n "
},
"2f93d7bd211af59c7cc15765a746216632fbe4f02301acf8382f1cf3f8d24c8d": {
"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": "updated_by",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "name",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 6,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "\n SELECT id, model, created_at, updated_at, updated_by, name, description\n FROM workspaces WHERE id = ?\n "
},
"448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": { "448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -78,7 +94,7 @@
}, },
"query": "\n DELETE FROM http_requests\n WHERE id = ?\n " "query": "\n DELETE FROM http_requests\n WHERE id = ?\n "
}, },
"539bb11d635c0295f969d32c6bf1e3d78f2686521a5ef2a4af661b7e645f58c1": { "51ee4652a889417dce585e4da457a629dd9e064b8866c3a916bc3bd2c933e14f": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -97,9 +113,9 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "created_at", "name": "request_id",
"ordinal": 3, "ordinal": 3,
"type_info": "Datetime" "type_info": "Text"
}, },
{ {
"name": "updated_at", "name": "updated_at",
@@ -107,39 +123,49 @@
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "name", "name": "updated_by",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "url", "name": "created_at",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Datetime"
}, },
{ {
"name": "method", "name": "status",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Int64"
}, },
{ {
"name": "body", "name": "status_reason",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "body_type", "name": "body",
"ordinal": 9, "ordinal": 9,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "sort_priority", "name": "elapsed",
"ordinal": 10, "ordinal": 10,
"type_info": "Float" "type_info": "Int64"
}, },
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>", "name": "url",
"ordinal": 11, "ordinal": 11,
"type_info": "Text" "type_info": "Text"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 13,
"type_info": "Text"
} }
], ],
"nullable": [ "nullable": [
@@ -152,15 +178,17 @@
false, false,
false, false,
true, true,
true,
false, false,
false,
false,
true,
false false
], ],
"parameters": { "parameters": {
"Right": 1 "Right": 1
} }
}, },
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n " "query": "\n SELECT id, model, workspace_id, request_id, updated_at, updated_by,\n created_at, status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at ASC\n "
}, },
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": { "84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
"describe": { "describe": {
@@ -172,48 +200,43 @@
}, },
"query": "\n DELETE FROM workspaces\n WHERE id = ?\n " "query": "\n DELETE FROM workspaces\n WHERE id = ?\n "
}, },
"a83698dcf9a815b881097133edb31a34ba25e7c6c114d463c495342a85371639": { "8d71216aa3902af45acc36bb4671e36bea6da2d30b42cfe8b70cff00cd00f256": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 8
}
},
"query": "\n UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) =\n (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n "
},
"caf3f21bf291dfbd36446592066e96c1f83abe96f6ea9211a3e049eb9c58a8c8": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
"name": "id", "name": "model",
"ordinal": 0, "ordinal": 0,
"type_info": "Text" "type_info": "Text"
}, },
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{ {
"name": "created_at", "name": "created_at",
"ordinal": 2, "ordinal": 1,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "updated_at", "name": "updated_at",
"ordinal": 3, "ordinal": 2,
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "name", "name": "updated_by",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "namespace",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "description", "name": "key",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
},
{
"name": "value",
"ordinal": 6,
"type_info": "Text"
} }
], ],
"nullable": [ "nullable": [
@@ -222,15 +245,16 @@
false, false,
false, false,
false, false,
false,
false false
], ],
"parameters": { "parameters": {
"Right": 1 "Right": 2
} }
}, },
"query": "\n SELECT id, model, created_at, updated_at, name, description\n FROM workspaces WHERE id = ?\n " "query": "\n SELECT model, created_at, updated_at, updated_by, namespace, key, value\n FROM key_values\n WHERE namespace = ? AND key = ?\n "
}, },
"cea4cae52f16ec78aca9a47b17117422d4f165e5a3b308c70fd1a180382475ea": { "9f09f300e04d9b77d408bea52069b7c812dcad163d144df4b7b02a9ba7969345": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -254,14 +278,19 @@
"type_info": "Datetime" "type_info": "Datetime"
}, },
{ {
"name": "name", "name": "updated_by",
"ordinal": 4, "ordinal": 4,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "description", "name": "name",
"ordinal": 5, "ordinal": 5,
"type_info": "Text" "type_info": "Text"
},
{
"name": "description",
"ordinal": 6,
"type_info": "Text"
} }
], ],
"nullable": [ "nullable": [
@@ -270,205 +299,26 @@
false, false,
false, false,
false, false,
false,
false false
], ],
"parameters": { "parameters": {
"Right": 0 "Right": 0
} }
}, },
"query": "\n SELECT id, model, created_at, updated_at, name, description\n FROM workspaces\n " "query": "\n SELECT id, model, created_at, updated_at, updated_by, name, description\n FROM workspaces\n "
}, },
"d5ad6d5f82fe837fa9215bd4619ec18a7c95b3088d4fbf9825f2d1d28069d1ce": { "afe2d3a2b2198582d2a4360a0947785d435528eb77f67c420e830a997b5ad101": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "updated_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "status",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "status_reason",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "body",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "url",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "error",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Right": 1
}
},
"query": "\n SELECT id, model, workspace_id, request_id, updated_at,\n created_at, status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at ASC\n "
},
"d80c09497771e3641022e73ec6c6a87e73a551f88a948a5445d754922b82b50b": {
"describe": { "describe": {
"columns": [], "columns": [],
"nullable": [], "nullable": [],
"parameters": { "parameters": {
"Right": 3 "Right": 9
} }
}, },
"query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n " "query": "\n UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_by, updated_at) =\n (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n "
}, },
"e3ade0a69348d512e47e964bded9d7d890b92fdc1e01c6c22fa5e91f943639f2": { "bc1b3220c104567176ff741b5648a14054485603ebb04222a7b9f60fc54f0970": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "updated_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "status",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "status_reason",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "body",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "url",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "error",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Right": 1
}
},
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at,\n status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n "
},
"e523dc91256b4409a734850eae59ac73b951177ce88d35e2ab708871f3067ace": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -522,13 +372,28 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "sort_priority", "name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 10, "ordinal": 10,
"type_info": "Text"
},
{
"name": "authentication_type",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 12,
"type_info": "Float" "type_info": "Float"
}, },
{
"name": "updated_by",
"ordinal": 13,
"type_info": "Text"
},
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>", "name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"ordinal": 11, "ordinal": 14,
"type_info": "Text" "type_info": "Text"
} }
], ],
@@ -543,6 +408,9 @@
false, false,
true, true,
true, true,
true,
true,
false,
false, false,
false false
], ],
@@ -550,19 +418,125 @@
"Right": 1 "Right": 1
} }
}, },
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n " "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n updated_by,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n "
}, },
"e767522f92c8c49cd2e563e58737a05092daf9b1dc763bacc82a5c14d696d78e": { "ce62a799babc731dc53e43144751006c3905367c47c511b8abee832c58f8111d": {
"describe": { "describe": {
"columns": [], "columns": [],
"nullable": [], "nullable": [],
"parameters": { "parameters": {
"Right": 9 "Right": 12
} }
}, },
"query": "\n INSERT INTO http_responses (id, request_id, workspace_id, elapsed, url, status, status_reason, body, headers)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);\n " "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n name,\n url,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority,\n updated_by\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority,\n updated_by = excluded.updated_by\n "
}, },
"f116d8cf9aad828135bb8c3a4c8b8e6b857ae13303989e9133a33b2d1cf20e96": { "d56b00aeaca0edd9a9fea4c7fdce0229a6d6500c8294854974dd4fc30af8bda8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 10
}
},
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n body,\n headers,\n updated_by\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n "
},
"d68f12980ff00de36f02a82a626f99e86abf5a26ebdf74c95832d3be396206da": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "request_id",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "updated_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "updated_by",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 6,
"type_info": "Datetime"
},
{
"name": "status",
"ordinal": 7,
"type_info": "Int64"
},
{
"name": "status_reason",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "body",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "url",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 13,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Right": 1
}
},
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, updated_by, created_at,\n status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n "
},
"d80c09497771e3641022e73ec6c6a87e73a551f88a948a5445d754922b82b50b": {
"describe": { "describe": {
"columns": [], "columns": [],
"nullable": [], "nullable": [],
@@ -570,16 +544,108 @@
"Right": 3 "Right": 3
} }
}, },
"query": "\n INSERT INTO workspaces (id, name, description)\n VALUES (?, ?, ?)\n " "query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n "
}, },
"f506d6b1451d95489cf41fea2d1cd3fae4f0773e16ae11ded6fd5923f015c8d5": { "dc749b8ed41ac55776e4f9dae36a82b3b880eb781eaa6fb53382e6db10f5a741": {
"describe": { "describe": {
"columns": [], "columns": [
"nullable": [], {
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "workspace_id",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 3,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "name",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "url",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "method",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "body",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "body_type",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "authentication_type",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 12,
"type_info": "Float"
},
{
"name": "updated_by",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"ordinal": 14,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true,
false,
false,
false
],
"parameters": { "parameters": {
"Right": 9 "Right": 1
} }
}, },
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n name,\n url,\n method,\n body,\n body_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority\n " "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n updated_by,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n "
} }
} }

View File

@@ -18,14 +18,16 @@ use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::Json; use sqlx::types::{Json};
use tauri::{AppHandle, Menu, MenuItem, State, Submenu, Wry}; use tauri::{AppHandle, Menu, MenuItem, State, Submenu, TitleBarStyle, Window, Wry};
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent}; use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
use tauri::regex::Regex; use tauri::regex::Regex;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use window_ext::WindowExt; use window_ext::WindowExt;
use crate::models::generate_id;
mod models; mod models;
mod runtime; mod runtime;
mod window_ext; mod window_ext;
@@ -60,18 +62,18 @@ async fn migrate_db(
#[tauri::command] #[tauri::command]
async fn send_ephemeral_request( async fn send_ephemeral_request(
request: models::HttpRequest, request: models::HttpRequest,
app_handle: AppHandle<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let response = models::HttpResponse::default(); let response = models::HttpResponse::default();
return actually_send_ephemeral_request(request, response, app_handle, pool).await; return actually_send_ephemeral_request(request, response, window, pool).await;
} }
async fn actually_send_ephemeral_request( async fn actually_send_ephemeral_request(
request: models::HttpRequest, request: models::HttpRequest,
mut response: models::HttpResponse, mut response: models::HttpResponse,
app_handle: AppHandle<Wry>, window: Window<Wry>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
@@ -131,8 +133,8 @@ async fn actually_send_ephemeral_request(
headers.insert(header_name, header_value); headers.insert(header_name, header_value);
} }
let m = let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
Method::from_bytes(request.method.to_uppercase().as_bytes()).expect("Failed to create method"); .expect("Failed to create method");
let builder = client.request(m, url_string.to_string()).headers(headers); let builder = client.request(m, url_string.to_string()).headers(headers);
let sendable_req_result = match (request.body, request.body_type) { let sendable_req_result = match (request.body, request.body_type) {
@@ -143,13 +145,13 @@ async fn actually_send_ephemeral_request(
let sendable_req = match sendable_req_result { let sendable_req = match sendable_req_result {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await; return response_err(response, e.to_string(), window, pool).await;
} }
}; };
let resp = client.execute(sendable_req).await; let resp = client.execute(sendable_req).await;
let p = app_handle let p = window.app_handle()
.path_resolver() .path_resolver()
.resolve_resource("plugins/plugin.ts") .resolve_resource("plugins/plugin.ts")
.expect("failed to resolve resource"); .expect("failed to resolve resource");
@@ -172,19 +174,19 @@ async fn actually_send_ephemeral_request(
response.url = v.url().to_string(); response.url = v.url().to_string();
response.body = v.text().await.expect("Failed to get body"); response.body = v.text().await.expect("Failed to get body");
response.elapsed = start.elapsed().as_millis() as i64; response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(response, pool) response = models::update_response_if_id(response, window.label(), pool)
.await .await
.expect("Failed to update response"); .expect("Failed to update response");
app_handle.emit_all("updated_response", &response).unwrap(); window.app_handle().emit_all("updated_response", &response).unwrap();
Ok(response) Ok(response)
} }
Err(e) => response_err(response, e.to_string(), app_handle, pool).await, Err(e) => response_err(response, e.to_string(), window, pool).await,
} }
} }
#[tauri::command] #[tauri::command]
async fn send_request( async fn send_request(
app_handle: AppHandle<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str, request_id: &str,
) -> Result<(), String> { ) -> Result<(), String> {
@@ -194,26 +196,26 @@ async fn send_request(
.await .await
.expect("Failed to get request"); .expect("Failed to get request");
let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool) let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool)
.await .await
.expect("Failed to create response"); .expect("Failed to create response");
app_handle.emit_all("updated_response", &response).unwrap(); window.app_handle().emit_all("updated_response", &response).unwrap();
actually_send_ephemeral_request(req, response, app_handle, pool).await?; actually_send_ephemeral_request(req, response, window, pool).await?;
Ok(()) Ok(())
} }
async fn response_err( async fn response_err(
mut response: models::HttpResponse, mut response: models::HttpResponse,
error: String, error: String,
app_handle: AppHandle<Wry>, window: Window<Wry>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
response.error = Some(error.clone()); response.error = Some(error.clone());
response = models::update_response_if_id(response, pool) response = models::update_response_if_id(response, window.label(), pool)
.await .await
.expect("Failed to update response"); .expect("Failed to update response");
app_handle.emit_all("updated_response", &response).unwrap(); window.app_handle().emit_all("updated_response", &response).unwrap();
Ok(response) Ok(response)
} }
@@ -237,10 +239,9 @@ async fn set_key_value(
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<(), String> { ) -> Result<(), String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let created_key_value = let created_key_value = models::set_key_value(namespace, key, value, pool)
models::set_key_value(namespace, key, value, pool) .await
.await .expect("Failed to create key value");
.expect("Failed to create key value");
app_handle app_handle
.emit_all("updated_key_value", &created_key_value) .emit_all("updated_key_value", &created_key_value)
@@ -252,14 +253,16 @@ async fn set_key_value(
#[tauri::command] #[tauri::command]
async fn create_workspace( async fn create_workspace(
name: &str, name: &str,
app_handle: AppHandle<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<String, String> { ) -> Result<String, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let created_workspace = let created_workspace = models::create_workspace(name, "", window.label(), pool)
models::create_workspace(name, "", pool).await.expect("Failed to create workspace"); .await
.expect("Failed to create workspace");
app_handle window
.app_handle()
.emit_all("updated_workspace", &created_workspace) .emit_all("updated_workspace", &created_workspace)
.unwrap(); .unwrap();
@@ -271,17 +274,30 @@ async fn create_request(
workspace_id: &str, workspace_id: &str,
name: &str, name: &str,
sort_priority: f64, sort_priority: f64,
app_handle: AppHandle<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<String, String> { ) -> Result<String, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let headers = Vec::new(); let headers = Vec::new();
let created_request = let created_request = models::upsert_request(
models::upsert_request(None, workspace_id, name, "GET", None, None, "", headers, sort_priority, pool) None,
.await workspace_id,
.expect("Failed to create request"); name,
"GET",
None,
None,
HashMap::new(),
None,
"",
headers,
sort_priority,
window.label(),
pool,
)
.await
.expect("Failed to create request");
app_handle window.app_handle()
.emit_all("updated_request", &created_request) .emit_all("updated_request", &created_request)
.unwrap(); .unwrap();
@@ -291,21 +307,21 @@ async fn create_request(
#[tauri::command] #[tauri::command]
async fn duplicate_request( async fn duplicate_request(
id: &str, id: &str,
app_handle: AppHandle<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<String, String> { ) -> Result<String, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let request = models::duplicate_request(id, pool).await.expect("Failed to duplicate request"); let request = models::duplicate_request(id, window.label(), pool)
app_handle .await
.emit_all("updated_request", &request) .expect("Failed to duplicate request");
.unwrap(); window.app_handle().emit_all("updated_request", &request).unwrap();
Ok(request.id) Ok(request.id)
} }
#[tauri::command] #[tauri::command]
async fn update_request( async fn update_request(
request: models::HttpRequest, request: models::HttpRequest,
app_handle: AppHandle<Wry>, window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<(), String> { ) -> Result<(), String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
@@ -320,6 +336,7 @@ async fn update_request(
None => None, None => None,
}; };
// TODO: Figure out how to make this better
let updated_request = models::upsert_request( let updated_request = models::upsert_request(
Some(request.id.as_str()), Some(request.id.as_str()),
request.workspace_id.as_str(), request.workspace_id.as_str(),
@@ -327,15 +344,18 @@ async fn update_request(
request.method.as_str(), request.method.as_str(),
body, body,
request.body_type, request.body_type,
request.authentication.0,
request.authentication_type,
request.url.as_str(), request.url.as_str(),
request.headers.0, request.headers.0,
request.sort_priority, request.sort_priority,
window.label(),
pool, pool,
) )
.await .await
.expect("Failed to update request"); .expect("Failed to update request");
app_handle window.app_handle()
.emit_all("updated_request", updated_request) .emit_all("updated_request", updated_request)
.unwrap(); .unwrap();
@@ -373,7 +393,9 @@ async fn get_request(
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> { ) -> Result<models::HttpRequest, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
models::get_request(id, pool).await.map_err(|e| e.to_string()) models::get_request(id, pool)
.await
.map_err(|e| e.to_string())
} }
#[tauri::command] #[tauri::command]
@@ -415,15 +437,17 @@ async fn delete_all_responses(
#[tauri::command] #[tauri::command]
async fn workspaces( async fn workspaces(
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
window: Window<Wry>,
) -> Result<Vec<models::Workspace>, String> { ) -> Result<Vec<models::Workspace>, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let workspaces = models::find_workspaces(pool) let workspaces = models::find_workspaces(pool)
.await .await
.expect("Failed to find workspaces"); .expect("Failed to find workspaces");
if workspaces.is_empty() { if workspaces.is_empty() {
let workspace = models::create_workspace("My Project", "This is the default workspace", pool) let workspace =
.await models::create_workspace("My Project", "This is the default workspace", window.label(), pool)
.expect("Failed to create workspace"); .await
.expect("Failed to create workspace");
Ok(vec![workspace]) Ok(vec![workspace])
} else { } else {
Ok(workspaces) Ok(workspaces)
@@ -454,36 +478,16 @@ fn main() {
let tray_menu = SystemTrayMenu::new().add_item(quit); let tray_menu = SystemTrayMenu::new().add_item(quit);
let system_tray = SystemTray::new().with_menu(tray_menu); let system_tray = SystemTray::new().with_menu(tray_menu);
let default_menu = Menu::os_default("Yaak".to_string().as_str());
let mut test_menu = Menu::new()
.add_item(CustomMenuItem::new("send_request".to_string(), "Send Request").accelerator("CmdOrCtrl+r"))
.add_item(CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size").accelerator("CmdOrCtrl+0"))
.add_item(CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"))
.add_item(CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"))
.add_item(CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar").accelerator("CmdOrCtrl+b"));
if is_dev() {
test_menu = test_menu
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("refresh".to_string(), "Refresh").accelerator("CmdOrCtrl + Shift + r"))
.add_item(CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools").accelerator("CmdOrCtrl + Option + i"));
}
let submenu = Submenu::new("Test Menu", test_menu);
let menu = default_menu.add_submenu(submenu);
tauri::Builder::default() tauri::Builder::default()
.menu(menu)
.system_tray(system_tray) .system_tray(system_tray)
.setup(|app| { .setup(|app| {
let win = app.get_window("main").unwrap(); let handle = app.handle();
std::thread::spawn(move || {
#[cfg(target_os = "macos")] let win = create_window(handle);
win.position_traffic_lights(); if let Err(e) = win.show() {
println!("Failed to show window {}", e)
Ok(()) }
}) });
.setup(|app| {
let dir = match is_dev() { let dir = match is_dev() {
true => current_dir().unwrap(), true => current_dir().unwrap(),
false => app.path_resolver().app_data_dir().unwrap(), false => app.path_resolver().app_data_dir().unwrap(),
@@ -521,40 +525,6 @@ fn main() {
}; };
} }
}) })
.on_menu_event(|event| {
match event.menu_item_id() {
"quit" => std::process::exit(0),
"close" => event.window().close().unwrap(),
"zoom_reset" => event.window().emit("zoom", 0).unwrap(),
"zoom_in" => event.window().emit("zoom", 1).unwrap(),
"zoom_out" => event.window().emit("zoom", -1).unwrap(),
"toggle_sidebar" => event.window().emit("toggle_sidebar", true).unwrap(),
"refresh" => event.window().emit("refresh", true).unwrap(),
"send_request" => event.window().emit("send_request", true).unwrap(),
"toggle_devtools" => {
if event.window().is_devtools_open() {
event.window().close_devtools();
} else {
event.window().open_devtools();
}
}
_ => {}
};
})
.on_window_event(|e| {
let apply_offset = || {
let win = e.window();
#[cfg(target_os = "macos")]
win.position_traffic_lights();
};
match e.event() {
WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
_ => {}
}
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
greet, greet,
workspaces, workspaces,
@@ -582,3 +552,102 @@ fn is_dev() -> bool {
let env = option_env!("YAAK_ENV"); let env = option_env!("YAAK_ENV");
env.unwrap_or("production") != "production" env.unwrap_or("production") != "production"
} }
fn create_window(handle: AppHandle<Wry>) -> Window<Wry> {
let default_menu = Menu::os_default("Yaak".to_string().as_str());
let mut test_menu = Menu::new()
.add_item(
CustomMenuItem::new("send_request".to_string(), "Send Request")
.accelerator("CmdOrCtrl+r"),
)
.add_item(
CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size")
.accelerator("CmdOrCtrl+0"),
)
.add_item(
CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"),
)
.add_item(
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
)
.add_item(
CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
.accelerator("CmdOrCtrl+b"),
);
if is_dev() {
test_menu = test_menu
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("refresh".to_string(), "Refresh")
.accelerator("CmdOrCtrl + Shift + r"),
)
.add_item(
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
.accelerator("CmdOrCtrl + Option + i"),
)
.add_item(CustomMenuItem::new("new_window".to_string(), "New Window"));
}
let submenu = Submenu::new("Test Menu", test_menu);
let menu = default_menu.add_submenu(submenu);
let window_id = generate_id("win");
let win = tauri::WindowBuilder::new(
&handle,
window_id,
tauri::WindowUrl::App("workspaces".into()),
)
.menu(menu)
.fullscreen(false)
.resizable(true)
.inner_size(1100.0, 600.0)
.hidden_title(true)
.title("Yaak")
.title_bar_style(TitleBarStyle::Overlay)
.build()
.expect("failed to build window");
let win2 = win.clone();
win.on_menu_event(move |event| {
match event.menu_item_id() {
"quit" => std::process::exit(0),
"close" => win2.close().unwrap(),
"zoom_reset" => win2.emit("zoom", 0).unwrap(),
"zoom_in" => win2.emit("zoom", 1).unwrap(),
"zoom_out" => win2.emit("zoom", -1).unwrap(),
"toggle_sidebar" => win2.emit("toggle_sidebar", true).unwrap(),
"refresh" => win2.emit("refresh", true).unwrap(),
"send_request" => win2.emit("send_request", true).unwrap(),
"new_window" => {
create_window(handle.clone()).show().unwrap();
}
"toggle_devtools" => {
if win2.is_devtools_open() {
win2.close_devtools();
} else {
win2.open_devtools();
}
}
_ => {}
};
});
let win3 = win.clone();
win.on_window_event(move |e| {
let apply_offset = || {
#[cfg(target_os = "macos")]
win3.position_traffic_lights();
};
match e {
WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
_ => {}
}
});
#[cfg(target_os = "macos")]
win.position_traffic_lights();
win
}

View File

@@ -1,7 +1,8 @@
use std::collections::HashMap;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::Json; use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)] #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
@@ -11,6 +12,7 @@ pub struct Workspace {
pub model: String, pub model: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub updated_by: String,
pub name: String, pub name: String,
pub description: String, pub description: String,
} }
@@ -31,6 +33,7 @@ pub struct HttpRequest {
pub model: String, pub model: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub updated_by: String,
pub sort_priority: f64, pub sort_priority: f64,
pub workspace_id: String, pub workspace_id: String,
pub name: String, pub name: String,
@@ -38,6 +41,8 @@ pub struct HttpRequest {
pub method: String, pub method: String,
pub body: Option<String>, pub body: Option<String>,
pub body_type: Option<String>, pub body_type: Option<String>,
pub authentication: Json<HashMap<String, JsonValue>>,
pub authentication_type: Option<String>,
pub headers: Json<Vec<HttpRequestHeader>>, pub headers: Json<Vec<HttpRequestHeader>>,
} }
@@ -57,6 +62,7 @@ pub struct HttpResponse {
pub request_id: String, pub request_id: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub updated_by: String,
pub error: Option<String>, pub error: Option<String>,
pub url: String, pub url: String,
pub elapsed: i64, pub elapsed: i64,
@@ -72,6 +78,7 @@ pub struct KeyValue {
pub model: String, pub model: String,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub updated_by: String,
pub namespace: String, pub namespace: String,
pub key: String, pub key: String,
pub value: String, pub value: String,
@@ -94,9 +101,9 @@ pub async fn set_key_value(
key, key,
value, value,
) )
.execute(pool) .execute(pool)
.await .await
.expect("Failed to insert key value"); .expect("Failed to insert key value");
get_key_value(namespace, key, pool).await get_key_value(namespace, key, pool).await
} }
@@ -105,23 +112,23 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> O
sqlx::query_as!( sqlx::query_as!(
KeyValue, KeyValue,
r#" r#"
SELECT model, created_at, updated_at, namespace, key, value SELECT model, created_at, updated_at, updated_by, namespace, key, value
FROM key_values FROM key_values
WHERE namespace = ? AND key = ? WHERE namespace = ? AND key = ?
"#, "#,
namespace, namespace,
key, key,
) )
.fetch_one(pool) .fetch_one(pool)
.await .await
.ok() .ok()
} }
pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx::Error> { pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
Workspace, Workspace,
r#" r#"
SELECT id, model, created_at, updated_at, name, description SELECT id, model, created_at, updated_at, updated_by, name, description
FROM workspaces FROM workspaces
"#, "#,
) )
@@ -133,7 +140,7 @@ pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, s
sqlx::query_as!( sqlx::query_as!(
Workspace, Workspace,
r#" r#"
SELECT id, model, created_at, updated_at, name, description SELECT id, model, created_at, updated_at, updated_by, name, description
FROM workspaces WHERE id = ? FROM workspaces WHERE id = ?
"#, "#,
id, id,
@@ -153,25 +160,27 @@ pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace
"#, "#,
id, id,
) )
.execute(pool) .execute(pool)
.await; .await;
Ok(workspace) Ok(workspace)
} }
pub async fn create_workspace( pub async fn create_workspace(
name: &str, name: &str,
description: &str, description: &str,
updated_by: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<Workspace, sqlx::Error> { ) -> Result<Workspace, sqlx::Error> {
let id = generate_id("wk"); let id = generate_id("wk");
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO workspaces (id, name, description) INSERT INTO workspaces (id, updated_by, name, description)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
"#, "#,
id, id,
name, name,
description, description,
updated_by,
) )
.execute(pool) .execute(pool)
.await .await
@@ -180,11 +189,10 @@ pub async fn create_workspace(
get_workspace(&id, pool).await get_workspace(&id, pool).await
} }
pub async fn duplicate_request( pub async fn duplicate_request(id: &str, updated_by: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
id: &str, let existing = get_request(id, pool)
pool: &Pool<Sqlite>, .await
) -> Result<HttpRequest, sqlx::Error> { .expect("Failed to get request to duplicate");
let existing = get_request(id, pool).await.expect("Failed to get request to duplicate");
// TODO: Figure out how to make this better // TODO: Figure out how to make this better
let b2; let b2;
let body = match existing.body { let body = match existing.body {
@@ -194,6 +202,7 @@ pub async fn duplicate_request(
} }
None => None, None => None,
}; };
upsert_request( upsert_request(
None, None,
existing.workspace_id.as_str(), existing.workspace_id.as_str(),
@@ -201,14 +210,17 @@ pub async fn duplicate_request(
existing.method.as_str(), existing.method.as_str(),
body, body,
existing.body_type, existing.body_type,
existing.authentication.0,
existing.authentication_type,
existing.url.as_str(), existing.url.as_str(),
existing.headers.0, existing.headers.0,
existing.sort_priority, existing.sort_priority,
updated_by,
pool, pool,
).await )
.await
} }
pub async fn upsert_request( pub async fn upsert_request(
id: Option<&str>, id: Option<&str>,
workspace_id: &str, workspace_id: &str,
@@ -216,9 +228,12 @@ pub async fn upsert_request(
method: &str, method: &str,
body: Option<&str>, body: Option<&str>,
body_type: Option<String>, body_type: Option<String>,
authentication: HashMap<String, JsonValue>,
authentication_type: Option<String>,
url: &str, url: &str,
headers: Vec<HttpRequestHeader>, headers: Vec<HttpRequestHeader>,
sort_priority: f64, sort_priority: f64,
updated_by: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<HttpRequest, sqlx::Error> { ) -> Result<HttpRequest, sqlx::Error> {
let generated_id; let generated_id;
@@ -230,6 +245,7 @@ pub async fn upsert_request(
} }
}; };
let headers_json = Json(headers); let headers_json = Json(headers);
let auth_json = Json(authentication);
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO http_requests ( INSERT INTO http_requests (
@@ -240,10 +256,13 @@ pub async fn upsert_request(
method, method,
body, body,
body_type, body_type,
authentication,
authentication_type,
headers, headers,
sort_priority sort_priority,
updated_by
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP,
name = excluded.name, name = excluded.name,
@@ -251,8 +270,11 @@ pub async fn upsert_request(
headers = excluded.headers, headers = excluded.headers,
body = excluded.body, body = excluded.body,
body_type = excluded.body_type, body_type = excluded.body_type,
authentication = excluded.authentication,
authentication_type = excluded.authentication_type,
url = excluded.url, url = excluded.url,
sort_priority = excluded.sort_priority sort_priority = excluded.sort_priority,
updated_by = excluded.updated_by
"#, "#,
id, id,
workspace_id, workspace_id,
@@ -261,8 +283,11 @@ pub async fn upsert_request(
method, method,
body, body,
body_type, body_type,
auth_json,
authentication_type,
headers_json, headers_json,
sort_priority, sort_priority,
updated_by,
) )
.execute(pool) .execute(pool)
.await .await
@@ -288,7 +313,10 @@ pub async fn find_requests(
method, method,
body, body,
body_type, body_type,
authentication AS "authentication!: Json<HashMap<String, JsonValue>>",
authentication_type,
sort_priority, sort_priority,
updated_by,
headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>"
FROM http_requests FROM http_requests
WHERE workspace_id = ? WHERE workspace_id = ?
@@ -314,7 +342,10 @@ pub async fn get_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, s
method, method,
body, body,
body_type, body_type,
authentication AS "authentication!: Json<HashMap<String, JsonValue>>",
authentication_type,
sort_priority, sort_priority,
updated_by,
headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>"
FROM http_requests FROM http_requests
WHERE id = ? WHERE id = ?
@@ -350,6 +381,7 @@ pub async fn create_response(
status_reason: Option<&str>, status_reason: Option<&str>,
body: &str, body: &str,
headers: Vec<HttpResponseHeader>, headers: Vec<HttpResponseHeader>,
updated_by: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> { ) -> Result<HttpResponse, sqlx::Error> {
let req = get_request(request_id, pool) let req = get_request(request_id, pool)
@@ -359,8 +391,19 @@ pub async fn create_response(
let headers_json = Json(headers); let headers_json = Json(headers);
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO http_responses (id, request_id, workspace_id, elapsed, url, status, status_reason, body, headers) INSERT INTO http_responses (
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); id,
request_id,
workspace_id,
elapsed,
url,
status,
status_reason,
body,
headers,
updated_by
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
id, id,
request_id, request_id,
@@ -371,6 +414,7 @@ pub async fn create_response(
status_reason, status_reason,
body, body,
headers_json, headers_json,
updated_by,
) )
.execute(pool) .execute(pool)
.await .await
@@ -381,23 +425,25 @@ pub async fn create_response(
pub async fn update_response_if_id( pub async fn update_response_if_id(
response: HttpResponse, response: HttpResponse,
updated_by: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> { ) -> Result<HttpResponse, sqlx::Error> {
if response.id == "" { if response.id == "" {
return Ok(response); return Ok(response);
} }
return update_response(response, pool).await; return update_response(response, updated_by, pool).await;
} }
pub async fn update_response( pub async fn update_response(
response: HttpResponse, response: HttpResponse,
updated_by: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> { ) -> Result<HttpResponse, sqlx::Error> {
let headers_json = Json(response.headers); let headers_json = Json(response.headers);
sqlx::query!( sqlx::query!(
r#" r#"
UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) = UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_by, updated_at) =
(?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
"#, "#,
response.elapsed, response.elapsed,
response.url, response.url,
@@ -406,6 +452,7 @@ pub async fn update_response(
response.body, response.body,
response.error, response.error,
headers_json, headers_json,
updated_by,
response.id, response.id,
) )
.execute(pool) .execute(pool)
@@ -418,7 +465,7 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
sqlx::query_as_unchecked!( sqlx::query_as_unchecked!(
HttpResponse, HttpResponse,
r#" r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, SELECT id, model, workspace_id, request_id, updated_at, updated_by, created_at,
status, status_reason, body, elapsed, url, error, status, status_reason, body, elapsed, url, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses FROM http_responses
@@ -437,7 +484,7 @@ pub async fn find_responses(
sqlx::query_as!( sqlx::query_as!(
HttpResponse, HttpResponse,
r#" r#"
SELECT id, model, workspace_id, request_id, updated_at, SELECT id, model, workspace_id, request_id, updated_at, updated_by,
created_at, status, status_reason, body, elapsed, url, error, created_at, status, status_reason, body, elapsed, url, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses FROM http_responses

View File

@@ -11,19 +11,7 @@
"version": "0.0.2" "version": "0.0.2"
}, },
"tauri": { "tauri": {
"windows": [ "windows": [],
{
"fullscreen": false,
"hiddenTitle": true,
"resizable": true,
"title": "Yaak",
"titleBarStyle": "Overlay",
"height": 600,
"width": 1100,
"minWidth": 400,
"minHeight": 400
}
],
"allowlist": { "allowlist": {
"all": false, "all": false,
"fs": { "fs": {

View File

@@ -1,6 +1,5 @@
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'; import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { persistQueryClient } from '@tanstack/react-query-persist-client'; import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
@@ -21,6 +20,7 @@ import { extractKeyValue, getKeyValue, setKeyValue } from '../lib/keyValueStore'
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models'; import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
import { AppRouter } from './AppRouter'; import { AppRouter } from './AppRouter';
import { DialogProvider } from './DialogContext'; import { DialogProvider } from './DialogContext';
import { appWindow, WebviewWindow } from '@tauri-apps/api/window';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -43,10 +43,13 @@ persistQueryClient({
}); });
await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => { await listen('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
if (keyValue.updatedBy === appWindow.label) return;
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue)); queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
}); });
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => { await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
if (request.updatedBy === appWindow.label) return;
queryClient.setQueryData( queryClient.setQueryData(
requestsQueryKey(request.workspaceId), requestsQueryKey(request.workspaceId),
(requests: HttpRequest[] = []) => { (requests: HttpRequest[] = []) => {
@@ -72,6 +75,8 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
queryClient.setQueryData( queryClient.setQueryData(
responsesQueryKey(response.requestId), responsesQueryKey(response.requestId),
(responses: HttpResponse[] = []) => { (responses: HttpResponse[] = []) => {
if (response.updatedBy === appWindow.label) return;
const newResponses = []; const newResponses = [];
let found = false; let found = false;
for (const r of responses) { for (const r of responses) {
@@ -92,6 +97,8 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse
await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace }) => { await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace }) => {
queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => { queryClient.setQueryData(workspacesQueryKey(), (workspaces: Workspace[] = []) => {
if (workspace.updatedBy === appWindow.label) return;
const newWorkspaces = []; const newWorkspaces = [];
let found = false; let found = false;
for (const w of workspaces) { for (const w of workspaces) {
@@ -175,7 +182,7 @@ export function App() {
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<DialogProvider> <DialogProvider>
<AppRouter /> <AppRouter />
<ReactQueryDevtools initialIsOpen={false} /> {/*<ReactQueryDevtools initialIsOpen={false} />*/}
</DialogProvider> </DialogProvider>
</DndProvider> </DndProvider>
</HelmetProvider> </HelmetProvider>

View File

@@ -62,9 +62,14 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
}); });
const req: HttpRequest = { ...baseRequest, body, id: '' }; const req: HttpRequest = { ...baseRequest, body, id: '' };
sendEphemeralRequest(req).then((response) => { sendEphemeralRequest(req).then((response) => {
const { data } = JSON.parse(response.body); try {
const schema = buildClientSchema(data); const { data } = JSON.parse(response.body);
setGraphqlExtension(graphql(schema, {})); const schema = buildClientSchema(data);
setGraphqlExtension(graphql(schema, {}));
} catch (err) {
console.log('Failed to parse introspection query', err);
return;
}
}); });
}, [baseRequest.url]); }, [baseRequest.url]);

View File

@@ -35,6 +35,8 @@ export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Pr
onClick={onClose} onClick={onClose}
className="absolute inset-0 bg-gray-600/60 dark:bg-black/50" className="absolute inset-0 bg-gray-600/60 dark:bg-black/50"
/> />
{/* Add region to still be able to drag the window */}
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
{children} {children}
</motion.div> </motion.div>
</FocusTrap> </FocusTrap>

View File

@@ -1,3 +1,4 @@
import { appWindow } from '@tauri-apps/api/window';
import classnames from 'classnames'; import classnames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
@@ -6,7 +7,7 @@ import { useKeyValue } from '../hooks/useKeyValue';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest } from '../lib/models'; import type { HttpHeader, HttpRequest } from '../lib/models';
import { HttpRequestBodyType } from '../lib/models'; import { BODY_TYPE_GRAPHQL, BODY_TYPE_JSON, BODY_TYPE_NONE, BODY_TYPE_XML } from '../lib/models';
import { Editor } from './core/Editor'; import { Editor } from './core/Editor';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
@@ -32,45 +33,64 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
defaultValue: 'body', defaultValue: 'body',
}); });
const tabs: TabItem<HttpRequest['bodyType']>[] = useMemo( const tabs: TabItem[] = useMemo(
() => [ () =>
{ activeRequest === null
value: 'body', ? []
label: activeRequest?.bodyType ?? 'No Body', : [
options: { {
onChange: async (bodyType: HttpRequest['bodyType']) => { value: 'body',
const patch: Partial<HttpRequest> = { bodyType }; options: {
if (bodyType == HttpRequestBodyType.GraphQL) { value: activeRequest.bodyType,
patch.method = 'POST'; items: [
patch.headers = [ { label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
...(activeRequest?.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ?? { label: 'JSON', value: BODY_TYPE_JSON },
[]), { label: 'XML', value: BODY_TYPE_XML },
{ { label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
name: 'Content-Type', ],
value: 'application/json', onChange: async (bodyType) => {
enabled: true, const patch: Partial<HttpRequest> = { bodyType };
if (bodyType == BODY_TYPE_GRAPHQL) {
patch.method = 'POST';
patch.headers = [
...(activeRequest?.headers.filter(
(h) => h.name.toLowerCase() !== 'content-type',
) ?? []),
{
name: 'Content-Type',
value: 'application/json',
enabled: true,
},
];
setTimeout(() => {
setForceUpdateHeaderEditorKey((u) => u + 1);
}, 100);
}
await updateRequest.mutate(patch);
}, },
]; },
setTimeout(() => { },
setForceUpdateHeaderEditorKey((u) => u + 1); {
}, 100); value: 'auth',
} label: 'Auth',
await updateRequest.mutate(patch); options: {
}, value: activeRequest.authenticationType,
value: activeRequest?.bodyType ?? null, items: [
items: [ { label: 'No Auth', shortLabel: 'Auth', value: null },
{ label: 'No Body', value: null }, { label: 'Basic', value: 'basic' },
{ label: 'JSON', value: HttpRequestBodyType.JSON }, ],
{ label: 'XML', value: HttpRequestBodyType.XML }, onChange: async (a) => {
{ label: 'GraphQL', value: HttpRequestBodyType.GraphQL }, await updateRequest.mutate({
authenticationType: a,
authentication: { username: '', password: '' },
});
},
},
},
{ value: 'params', label: 'URL Params' },
{ value: 'headers', label: 'Headers' },
], ],
}, [activeRequest?.bodyType, activeRequest?.headers, activeRequest?.authenticationType],
},
{ value: 'params', label: 'URL Params' },
{ value: 'headers', label: 'Headers' },
{ value: 'auth', label: 'Auth' },
],
[activeRequest?.bodyType, activeRequest?.headers],
); );
const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []); const handleBodyChange = useCallback((body: string) => updateRequest.mutate({ body }), []);
@@ -79,6 +99,9 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
[], [],
); );
const forceUpdateKey =
activeRequest?.updatedBy === appWindow.label ? undefined : activeRequest?.updatedAt;
return ( return (
<div <div
style={style} style={style}
@@ -86,7 +109,12 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
> >
{activeRequest && ( {activeRequest && (
<> <>
<UrlBar id={activeRequest.id} url={activeRequest.url} method={activeRequest.method} /> <UrlBar
key={forceUpdateKey}
id={activeRequest.id}
url={activeRequest.url}
method={activeRequest.method}
/>
<Tabs <Tabs
value={activeTab.value} value={activeTab.value}
onChangeValue={activeTab.set} onChangeValue={activeTab.set}
@@ -110,7 +138,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
<ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} /> <ParametersEditor key={activeRequestId} parameters={[]} onChange={() => null} />
</TabContent> </TabContent>
<TabContent value="body" className="pl-3 mt-1"> <TabContent value="body" className="pl-3 mt-1">
{activeRequest.bodyType === HttpRequestBodyType.JSON ? ( {activeRequest.bodyType === BODY_TYPE_JSON ? (
<Editor <Editor
key={activeRequest.id} key={activeRequest.id}
useTemplating useTemplating
@@ -122,7 +150,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
onChange={handleBodyChange} onChange={handleBodyChange}
format={(v) => tryFormatJson(v)} format={(v) => tryFormatJson(v)}
/> />
) : activeRequest.bodyType === HttpRequestBodyType.XML ? ( ) : activeRequest.bodyType === BODY_TYPE_XML ? (
<Editor <Editor
key={activeRequest.id} key={activeRequest.id}
useTemplating useTemplating
@@ -133,7 +161,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
contentType="text/xml" contentType="text/xml"
onChange={handleBodyChange} onChange={handleBodyChange}
/> />
) : activeRequest.bodyType === HttpRequestBodyType.GraphQL ? ( ) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
<GraphQLEditor <GraphQLEditor
key={activeRequest.id} key={activeRequest.id}
baseRequest={activeRequest} baseRequest={activeRequest}

View File

@@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useWindowSize } from 'react-use'; import { useWindowSize } from 'react-use';
import { useSidebarDisplay } from '../hooks/useSidebarDisplay'; import { useSidebarDisplay } from '../hooks/useSidebarDisplay';
import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants'; import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants';
import { Button } from './core/Button';
import { Overlay } from './Overlay'; import { Overlay } from './Overlay';
import { RequestResponse } from './RequestResponse'; import { RequestResponse } from './RequestResponse';
import { ResizeHandle } from './ResizeHandle'; import { ResizeHandle } from './ResizeHandle';
@@ -90,6 +91,14 @@ export default function Workspace() {
[sideWidth, floating], [sideWidth, floating],
); );
if (windowSize.width <= 100) {
return (
<div>
<Button>Send</Button>
</div>
);
}
return ( return (
<div <div
style={styles} style={styles}

View File

@@ -45,7 +45,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
className, className,
'outline-none whitespace-nowrap', 'outline-none whitespace-nowrap',
// 'border border-transparent focus-visible:border-focus', // 'border border-transparent focus-visible:border-focus',
'focus-visible:ring ring-blue-300', 'focus-visible-or-class:ring ring-blue-300',
'rounded-md flex items-center', 'rounded-md flex items-center',
colorStyles[color || 'default'], colorStyles[color || 'default'],
justify === 'start' && 'justify-start', justify === 'start' && 'justify-start',

View File

@@ -3,7 +3,7 @@ import FocusTrap from 'focus-trap-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react'; import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Children, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useKeyPressEvent, useMount } from 'react-use'; import { useKeyPressEvent } from 'react-use';
import { Portal } from '../Portal'; import { Portal } from '../Portal';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { VStack } from './Stacks'; import { VStack } from './Stacks';
@@ -83,10 +83,6 @@ interface MenuProps {
function Menu({ className, items, onClose, triggerRect }: MenuProps) { function Menu({ className, items, onClose, triggerRect }: MenuProps) {
if (triggerRect === undefined) return null; if (triggerRect === undefined) return null;
useMount(() => {
console.log(document.activeElement);
});
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [menuStyles, setMenuStyles] = useState<CSSProperties>({}); const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
@@ -98,6 +94,10 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 }); setMenuStyles({ maxHeight: windowBox.height - menuBox.top - 5 });
}, []); }, []);
useKeyPressEvent('Escape', () => {
onClose();
});
useKeyPressEvent('ArrowUp', () => { useKeyPressEvent('ArrowUp', () => {
setSelectedIndex((currIndex) => { setSelectedIndex((currIndex) => {
let nextIndex = (currIndex ?? 0) - 1; let nextIndex = (currIndex ?? 0) - 1;

View File

@@ -5,10 +5,11 @@ import { Icon } from './Icon';
export interface RadioDropdownItem<T> { export interface RadioDropdownItem<T> {
label: string; label: string;
shortLabel?: string;
value: T; value: T;
} }
export interface RadioDropdownProps<T> { export interface RadioDropdownProps<T = string | null> {
value: T; value: T;
onChange: (value: T) => void; onChange: (value: T) => void;
items: RadioDropdownItem<T>[]; items: RadioDropdownItem<T>[];
@@ -18,8 +19,9 @@ export interface RadioDropdownProps<T> {
export function RadioDropdown<T>({ value, items, onChange, children }: RadioDropdownProps<T>) { export function RadioDropdown<T>({ value, items, onChange, children }: RadioDropdownProps<T>) {
const dropdownItems = useMemo( const dropdownItems = useMemo(
() => () =>
items.map(({ label, value: v }) => ({ items.map(({ label, shortLabel, value: v }) => ({
label, label,
shortLabel,
onSelect: () => onChange(v), onSelect: () => onChange(v),
leftSlot: <Icon icon={value === v ? 'check' : 'empty'} />, leftSlot: <Icon icon={value === v ? 'check' : 'empty'} />,
})), })),

View File

@@ -7,23 +7,27 @@ import type { RadioDropdownProps } from '../RadioDropdown';
import { RadioDropdown } from '../RadioDropdown'; import { RadioDropdown } from '../RadioDropdown';
import { HStack } from '../Stacks'; import { HStack } from '../Stacks';
export type TabItem<T = string> = { export type TabItem =
value: string; | {
label: string; value: string;
options?: Omit<RadioDropdownProps<T>, 'children'>; label: string;
}; }
| {
value: string;
options: Omit<RadioDropdownProps, 'children'>;
};
interface Props<T = unknown> { interface Props {
label: string; label: string;
value?: string; value?: string;
onChangeValue: (value: string) => void; onChangeValue: (value: string) => void;
tabs: TabItem<T>[]; tabs: TabItem[];
tabListClassName?: string; tabListClassName?: string;
className?: string; className?: string;
children: ReactNode; children: ReactNode;
} }
export function Tabs<T>({ export function Tabs({
value, value,
onChangeValue, onChangeValue,
label, label,
@@ -31,7 +35,7 @@ export function Tabs<T>({
tabs, tabs,
className, className,
tabListClassName, tabListClassName,
}: Props<T>) { }: Props) {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const handleTabChange = (value: string) => { const handleTabChange = (value: string) => {
@@ -77,7 +81,8 @@ export function Tabs<T>({
const btnClassName = classnames( const btnClassName = classnames(
isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900', isActive ? 'bg-gray-100 text-gray-800' : 'text-gray-600 hover:text-gray-900',
); );
if (t.options) { if ('options' in t) {
const option = t.options.items.find((i) => i.value === t.options?.value);
return ( return (
<RadioDropdown <RadioDropdown
key={t.value} key={t.value}
@@ -91,7 +96,7 @@ export function Tabs<T>({
onClick={isActive ? undefined : () => handleTabChange(t.value)} onClick={isActive ? undefined : () => handleTabChange(t.value)}
className={btnClassName} className={btnClassName}
> >
{t.options.items.find((i) => i.value === t.options?.value)?.label ?? ''} {option?.shortLabel ?? option?.label ?? 'Unknown'}
<Icon icon="triangleDown" className="-mr-1.5" /> <Icon icon="triangleDown" className="-mr-1.5" />
</Button> </Button>
</RadioDropdown> </RadioDropdown>

View File

@@ -13,10 +13,10 @@ export function Confirm({ hide }: Props) {
return ( return (
<HStack space={2} justifyContent="end"> <HStack space={2} justifyContent="end">
<Button color="gray" onClick={hide}> <Button className="focus" color="gray" onClick={hide}>
Cancel Cancel
</Button> </Button>
<Button ref={focusRef} color="primary"> <Button className="focus" ref={focusRef} color="primary">
Confirm Confirm
</Button> </Button>
</HStack> </HStack>

View File

@@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useMemo } from 'react';
import { act } from 'react-dom/test-utils';
import type { Workspace } from '../lib/models'; import type { Workspace } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useWorkspaces } from './useWorkspaces'; import { useWorkspaces } from './useWorkspaces';

View File

@@ -1,9 +1,11 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store'; import { getRequest } from '../lib/store';
import { requestsQueryKey } from './useRequests';
export function useUpdateRequest(id: string | null) { export function useUpdateRequest(id: string | null) {
const queryClient = useQueryClient();
return useMutation<void, unknown, Partial<HttpRequest>>({ return useMutation<void, unknown, Partial<HttpRequest>>({
mutationFn: async (patch) => { mutationFn: async (patch) => {
const request = await getRequest(id); const request = await getRequest(id);
@@ -13,9 +15,19 @@ export function useUpdateRequest(id: string | null) {
const updatedRequest = { ...request, ...patch }; const updatedRequest = { ...request, ...patch };
console.log('UPDATING REQUEST', patch);
await invoke('update_request', { await invoke('update_request', {
request: updatedRequest, request: updatedRequest,
}); });
}, },
onMutate: async (patch) => {
const request = await getRequest(id);
if (request === null) return;
queryClient.setQueryData(
requestsQueryKey(request?.workspaceId),
(requests: HttpRequest[] | undefined) =>
requests?.map((r) => (r.id === request.id ? { ...r, ...patch } : r)),
);
},
}); });
} }

View File

@@ -1,7 +1,8 @@
export interface BaseModel { export interface BaseModel {
readonly id: string; readonly id: string;
readonly createdAt: Date; readonly createdAt: string;
readonly updatedAt: Date; readonly updatedAt: string;
readonly updatedBy: string;
} }
export interface Workspace extends BaseModel { export interface Workspace extends BaseModel {
@@ -16,11 +17,13 @@ export interface HttpHeader {
enabled?: boolean; enabled?: boolean;
} }
export enum HttpRequestBodyType { export const BODY_TYPE_NONE = null;
GraphQL = 'graphql', export const BODY_TYPE_GRAPHQL = 'graphql';
JSON = 'application/json', export const BODY_TYPE_JSON = 'application/json';
XML = 'text/xml', export const BODY_TYPE_XML = 'text/xml';
}
export const AUTH_TYPE_NONE = null;
export const AUTH_TYPE_BASIC = 'basic';
export interface HttpRequest extends BaseModel { export interface HttpRequest extends BaseModel {
readonly workspaceId: string; readonly workspaceId: string;
@@ -29,7 +32,11 @@ export interface HttpRequest extends BaseModel {
name: string; name: string;
url: string; url: string;
body: string | null; body: string | null;
bodyType: HttpRequestBodyType | null; bodyType: string | null;
authentication: any | null;
authenticationType: string | null;
auth: Record<string, string | number | null>;
authType: string | null;
method: string; method: string;
headers: HttpHeader[]; headers: HttpHeader[];
} }

View File

@@ -63,7 +63,8 @@ module.exports = {
plugins: [ plugins: [
require("@tailwindcss/container-queries"), require("@tailwindcss/container-queries"),
plugin(function({ addVariant }) { plugin(function({ addVariant }) {
addVariant('hocus', ['&:hover', '&:focus']) addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus'])
addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus'])
}) })
] ]
}; };