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 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>
<style>
body {
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",
"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": {
"describe": {
"columns": [],
@@ -68,6 +20,70 @@
},
"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": {
"describe": {
"columns": [],
@@ -78,7 +94,7 @@
},
"query": "\n DELETE FROM http_requests\n WHERE id = ?\n "
},
"539bb11d635c0295f969d32c6bf1e3d78f2686521a5ef2a4af661b7e645f58c1": {
"51ee4652a889417dce585e4da457a629dd9e064b8866c3a916bc3bd2c933e14f": {
"describe": {
"columns": [
{
@@ -97,9 +113,9 @@
"type_info": "Text"
},
{
"name": "created_at",
"name": "request_id",
"ordinal": 3,
"type_info": "Datetime"
"type_info": "Text"
},
{
"name": "updated_at",
@@ -107,39 +123,49 @@
"type_info": "Datetime"
},
{
"name": "name",
"name": "updated_by",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "url",
"name": "created_at",
"ordinal": 6,
"type_info": "Text"
"type_info": "Datetime"
},
{
"name": "method",
"name": "status",
"ordinal": 7,
"type_info": "Text"
"type_info": "Int64"
},
{
"name": "body",
"name": "status_reason",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "body_type",
"name": "body",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "sort_priority",
"name": "elapsed",
"ordinal": 10,
"type_info": "Float"
"type_info": "Int64"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"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": [
@@ -152,15 +178,17 @@
false,
false,
true,
true,
false,
false,
false,
true,
false
],
"parameters": {
"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": {
"describe": {
@@ -172,48 +200,43 @@
},
"query": "\n DELETE FROM workspaces\n WHERE id = ?\n "
},
"a83698dcf9a815b881097133edb31a34ba25e7c6c114d463c495342a85371639": {
"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": {
"8d71216aa3902af45acc36bb4671e36bea6da2d30b42cfe8b70cff00cd00f256": {
"describe": {
"columns": [
{
"name": "id",
"name": "model",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "model",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 2,
"ordinal": 1,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 3,
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "name",
"name": "updated_by",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "namespace",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "description",
"name": "key",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "value",
"ordinal": 6,
"type_info": "Text"
}
],
"nullable": [
@@ -222,15 +245,16 @@
false,
false,
false,
false,
false
],
"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": {
"columns": [
{
@@ -254,14 +278,19 @@
"type_info": "Datetime"
},
{
"name": "name",
"name": "updated_by",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "description",
"name": "name",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 6,
"type_info": "Text"
}
],
"nullable": [
@@ -270,205 +299,26 @@
false,
false,
false,
false,
false
],
"parameters": {
"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": {
"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": {
"afe2d3a2b2198582d2a4360a0947785d435528eb77f67c420e830a997b5ad101": {
"describe": {
"columns": [],
"nullable": [],
"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": {
"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": {
"bc1b3220c104567176ff741b5648a14054485603ebb04222a7b9f60fc54f0970": {
"describe": {
"columns": [
{
@@ -522,13 +372,28 @@
"type_info": "Text"
},
{
"name": "sort_priority",
"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": 11,
"ordinal": 14,
"type_info": "Text"
}
],
@@ -543,6 +408,9 @@
false,
true,
true,
true,
true,
false,
false,
false
],
@@ -550,19 +418,125 @@
"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": {
"columns": [],
"nullable": [],
"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": {
"columns": [],
"nullable": [],
@@ -570,16 +544,108 @@
"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": {
"columns": [],
"nullable": [],
"columns": [
{
"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": {
"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::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::Json;
use tauri::{AppHandle, Menu, MenuItem, State, Submenu, Wry};
use sqlx::types::{Json};
use tauri::{AppHandle, Menu, MenuItem, State, Submenu, TitleBarStyle, Window, Wry};
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
use tauri::regex::Regex;
use tokio::sync::Mutex;
use window_ext::WindowExt;
use crate::models::generate_id;
mod models;
mod runtime;
mod window_ext;
@@ -60,18 +62,18 @@ async fn migrate_db(
#[tauri::command]
async fn send_ephemeral_request(
request: models::HttpRequest,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await;
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(
request: models::HttpRequest,
mut response: models::HttpResponse,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
@@ -131,8 +133,8 @@ async fn actually_send_ephemeral_request(
headers.insert(header_name, header_value);
}
let m =
Method::from_bytes(request.method.to_uppercase().as_bytes()).expect("Failed to create method");
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let builder = client.request(m, url_string.to_string()).headers(headers);
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 {
Ok(r) => r,
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 p = app_handle
let p = window.app_handle()
.path_resolver()
.resolve_resource("plugins/plugin.ts")
.expect("failed to resolve resource");
@@ -172,19 +174,19 @@ async fn actually_send_ephemeral_request(
response.url = v.url().to_string();
response.body = v.text().await.expect("Failed to get body");
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
.expect("Failed to update response");
app_handle.emit_all("updated_response", &response).unwrap();
window.app_handle().emit_all("updated_response", &response).unwrap();
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]
async fn send_request(
app_handle: AppHandle<Wry>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str,
) -> Result<(), String> {
@@ -194,26 +196,26 @@ async fn send_request(
.await
.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
.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(())
}
async fn response_err(
mut response: models::HttpResponse,
error: String,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
response.error = Some(error.clone());
response = models::update_response_if_id(response, pool)
response = models::update_response_if_id(response, window.label(), pool)
.await
.expect("Failed to update response");
app_handle.emit_all("updated_response", &response).unwrap();
window.app_handle().emit_all("updated_response", &response).unwrap();
Ok(response)
}
@@ -237,10 +239,9 @@ async fn set_key_value(
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<(), String> {
let pool = &*db_instance.lock().await;
let created_key_value =
models::set_key_value(namespace, key, value, pool)
.await
.expect("Failed to create key value");
let created_key_value = models::set_key_value(namespace, key, value, pool)
.await
.expect("Failed to create key value");
app_handle
.emit_all("updated_key_value", &created_key_value)
@@ -252,14 +253,16 @@ async fn set_key_value(
#[tauri::command]
async fn create_workspace(
name: &str,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<String, String> {
let pool = &*db_instance.lock().await;
let created_workspace =
models::create_workspace(name, "", pool).await.expect("Failed to create workspace");
let created_workspace = models::create_workspace(name, "", window.label(), pool)
.await
.expect("Failed to create workspace");
app_handle
window
.app_handle()
.emit_all("updated_workspace", &created_workspace)
.unwrap();
@@ -271,17 +274,30 @@ async fn create_request(
workspace_id: &str,
name: &str,
sort_priority: f64,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<String, String> {
let pool = &*db_instance.lock().await;
let headers = Vec::new();
let created_request =
models::upsert_request(None, workspace_id, name, "GET", None, None, "", headers, sort_priority, pool)
.await
.expect("Failed to create request");
let created_request = models::upsert_request(
None,
workspace_id,
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)
.unwrap();
@@ -291,21 +307,21 @@ async fn create_request(
#[tauri::command]
async fn duplicate_request(
id: &str,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<String, String> {
let pool = &*db_instance.lock().await;
let request = models::duplicate_request(id, pool).await.expect("Failed to duplicate request");
app_handle
.emit_all("updated_request", &request)
.unwrap();
let request = models::duplicate_request(id, window.label(), pool)
.await
.expect("Failed to duplicate request");
window.app_handle().emit_all("updated_request", &request).unwrap();
Ok(request.id)
}
#[tauri::command]
async fn update_request(
request: models::HttpRequest,
app_handle: AppHandle<Wry>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<(), String> {
let pool = &*db_instance.lock().await;
@@ -320,6 +336,7 @@ async fn update_request(
None => None,
};
// TODO: Figure out how to make this better
let updated_request = models::upsert_request(
Some(request.id.as_str()),
request.workspace_id.as_str(),
@@ -327,15 +344,18 @@ async fn update_request(
request.method.as_str(),
body,
request.body_type,
request.authentication.0,
request.authentication_type,
request.url.as_str(),
request.headers.0,
request.sort_priority,
window.label(),
pool,
)
.await
.expect("Failed to update request");
app_handle
window.app_handle()
.emit_all("updated_request", updated_request)
.unwrap();
@@ -373,7 +393,9 @@ async fn get_request(
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> {
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]
@@ -415,15 +437,17 @@ async fn delete_all_responses(
#[tauri::command]
async fn workspaces(
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
window: Window<Wry>,
) -> Result<Vec<models::Workspace>, String> {
let pool = &*db_instance.lock().await;
let workspaces = models::find_workspaces(pool)
.await
.expect("Failed to find workspaces");
if workspaces.is_empty() {
let workspace = models::create_workspace("My Project", "This is the default workspace", pool)
.await
.expect("Failed to create workspace");
let workspace =
models::create_workspace("My Project", "This is the default workspace", window.label(), pool)
.await
.expect("Failed to create workspace");
Ok(vec![workspace])
} else {
Ok(workspaces)
@@ -454,36 +478,16 @@ fn main() {
let tray_menu = SystemTrayMenu::new().add_item(quit);
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()
.menu(menu)
.system_tray(system_tray)
.setup(|app| {
let win = app.get_window("main").unwrap();
#[cfg(target_os = "macos")]
win.position_traffic_lights();
Ok(())
})
.setup(|app| {
let handle = app.handle();
std::thread::spawn(move || {
let win = create_window(handle);
if let Err(e) = win.show() {
println!("Failed to show window {}", e)
}
});
let dir = match is_dev() {
true => current_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![
greet,
workspaces,
@@ -582,3 +552,102 @@ fn is_dev() -> bool {
let env = option_env!("YAAK_ENV");
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 serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::Json;
use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite};
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
@@ -11,6 +12,7 @@ pub struct Workspace {
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub updated_by: String,
pub name: String,
pub description: String,
}
@@ -31,6 +33,7 @@ pub struct HttpRequest {
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub updated_by: String,
pub sort_priority: f64,
pub workspace_id: String,
pub name: String,
@@ -38,6 +41,8 @@ pub struct HttpRequest {
pub method: String,
pub body: Option<String>,
pub body_type: Option<String>,
pub authentication: Json<HashMap<String, JsonValue>>,
pub authentication_type: Option<String>,
pub headers: Json<Vec<HttpRequestHeader>>,
}
@@ -57,6 +62,7 @@ pub struct HttpResponse {
pub request_id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub updated_by: String,
pub error: Option<String>,
pub url: String,
pub elapsed: i64,
@@ -72,6 +78,7 @@ pub struct KeyValue {
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub updated_by: String,
pub namespace: String,
pub key: String,
pub value: String,
@@ -94,9 +101,9 @@ pub async fn set_key_value(
key,
value,
)
.execute(pool)
.await
.expect("Failed to insert key value");
.execute(pool)
.await
.expect("Failed to insert key value");
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!(
KeyValue,
r#"
SELECT model, created_at, updated_at, namespace, key, value
SELECT model, created_at, updated_at, updated_by, namespace, key, value
FROM key_values
WHERE namespace = ? AND key = ?
"#,
namespace,
key,
)
.fetch_one(pool)
.await
.ok()
.fetch_one(pool)
.await
.ok()
}
pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx::Error> {
sqlx::query_as!(
Workspace,
r#"
SELECT id, model, created_at, updated_at, name, description
SELECT id, model, created_at, updated_at, updated_by, name, description
FROM workspaces
"#,
)
@@ -133,7 +140,7 @@ pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, s
sqlx::query_as!(
Workspace,
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 = ?
"#,
id,
@@ -153,25 +160,27 @@ pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace
"#,
id,
)
.execute(pool)
.await;
.execute(pool)
.await;
Ok(workspace)
}
pub async fn create_workspace(
name: &str,
description: &str,
updated_by: &str,
pool: &Pool<Sqlite>,
) -> Result<Workspace, sqlx::Error> {
let id = generate_id("wk");
sqlx::query!(
r#"
INSERT INTO workspaces (id, name, description)
VALUES (?, ?, ?)
INSERT INTO workspaces (id, updated_by, name, description)
VALUES (?, ?, ?, ?)
"#,
id,
name,
description,
updated_by,
)
.execute(pool)
.await
@@ -180,11 +189,10 @@ pub async fn create_workspace(
get_workspace(&id, pool).await
}
pub async fn duplicate_request(
id: &str,
pool: &Pool<Sqlite>,
) -> Result<HttpRequest, sqlx::Error> {
let existing = get_request(id, pool).await.expect("Failed to get request to duplicate");
pub async fn duplicate_request(id: &str, updated_by: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
let existing = get_request(id, pool)
.await
.expect("Failed to get request to duplicate");
// TODO: Figure out how to make this better
let b2;
let body = match existing.body {
@@ -194,6 +202,7 @@ pub async fn duplicate_request(
}
None => None,
};
upsert_request(
None,
existing.workspace_id.as_str(),
@@ -201,14 +210,17 @@ pub async fn duplicate_request(
existing.method.as_str(),
body,
existing.body_type,
existing.authentication.0,
existing.authentication_type,
existing.url.as_str(),
existing.headers.0,
existing.sort_priority,
updated_by,
pool,
).await
)
.await
}
pub async fn upsert_request(
id: Option<&str>,
workspace_id: &str,
@@ -216,9 +228,12 @@ pub async fn upsert_request(
method: &str,
body: Option<&str>,
body_type: Option<String>,
authentication: HashMap<String, JsonValue>,
authentication_type: Option<String>,
url: &str,
headers: Vec<HttpRequestHeader>,
sort_priority: f64,
updated_by: &str,
pool: &Pool<Sqlite>,
) -> Result<HttpRequest, sqlx::Error> {
let generated_id;
@@ -230,6 +245,7 @@ pub async fn upsert_request(
}
};
let headers_json = Json(headers);
let auth_json = Json(authentication);
sqlx::query!(
r#"
INSERT INTO http_requests (
@@ -240,10 +256,13 @@ pub async fn upsert_request(
method,
body,
body_type,
authentication,
authentication_type,
headers,
sort_priority
sort_priority,
updated_by
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
name = excluded.name,
@@ -251,8 +270,11 @@ pub async fn upsert_request(
headers = excluded.headers,
body = excluded.body,
body_type = excluded.body_type,
authentication = excluded.authentication,
authentication_type = excluded.authentication_type,
url = excluded.url,
sort_priority = excluded.sort_priority
sort_priority = excluded.sort_priority,
updated_by = excluded.updated_by
"#,
id,
workspace_id,
@@ -261,8 +283,11 @@ pub async fn upsert_request(
method,
body,
body_type,
auth_json,
authentication_type,
headers_json,
sort_priority,
updated_by,
)
.execute(pool)
.await
@@ -288,7 +313,10 @@ pub async fn find_requests(
method,
body,
body_type,
authentication AS "authentication!: Json<HashMap<String, JsonValue>>",
authentication_type,
sort_priority,
updated_by,
headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>"
FROM http_requests
WHERE workspace_id = ?
@@ -314,7 +342,10 @@ pub async fn get_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, s
method,
body,
body_type,
authentication AS "authentication!: Json<HashMap<String, JsonValue>>",
authentication_type,
sort_priority,
updated_by,
headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>"
FROM http_requests
WHERE id = ?
@@ -350,6 +381,7 @@ pub async fn create_response(
status_reason: Option<&str>,
body: &str,
headers: Vec<HttpResponseHeader>,
updated_by: &str,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
let req = get_request(request_id, pool)
@@ -359,8 +391,19 @@ pub async fn create_response(
let headers_json = Json(headers);
sqlx::query!(
r#"
INSERT INTO http_responses (id, request_id, workspace_id, elapsed, url, status, status_reason, body, headers)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
INSERT INTO http_responses (
id,
request_id,
workspace_id,
elapsed,
url,
status,
status_reason,
body,
headers,
updated_by
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
id,
request_id,
@@ -371,6 +414,7 @@ pub async fn create_response(
status_reason,
body,
headers_json,
updated_by,
)
.execute(pool)
.await
@@ -381,23 +425,25 @@ pub async fn create_response(
pub async fn update_response_if_id(
response: HttpResponse,
updated_by: &str,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
if response.id == "" {
return Ok(response);
}
return update_response(response, pool).await;
return update_response(response, updated_by, pool).await;
}
pub async fn update_response(
response: HttpResponse,
updated_by: &str,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
let headers_json = Json(response.headers);
sqlx::query!(
r#"
UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) =
(?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_by, updated_at) =
(?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
"#,
response.elapsed,
response.url,
@@ -406,6 +452,7 @@ pub async fn update_response(
response.body,
response.error,
headers_json,
updated_by,
response.id,
)
.execute(pool)
@@ -418,7 +465,7 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
sqlx::query_as_unchecked!(
HttpResponse,
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,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
@@ -437,7 +484,7 @@ pub async fn find_responses(
sqlx::query_as!(
HttpResponse,
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,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,8 @@ export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Pr
onClick={onClose}
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}
</motion.div>
</FocusTrap>

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
className,
'outline-none whitespace-nowrap',
// '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',
colorStyles[color || 'default'],
justify === 'start' && 'justify-start',

View File

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

View File

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

View File

@@ -7,23 +7,27 @@ import type { RadioDropdownProps } from '../RadioDropdown';
import { RadioDropdown } from '../RadioDropdown';
import { HStack } from '../Stacks';
export type TabItem<T = string> = {
value: string;
label: string;
options?: Omit<RadioDropdownProps<T>, 'children'>;
};
export type TabItem =
| {
value: string;
label: string;
}
| {
value: string;
options: Omit<RadioDropdownProps, 'children'>;
};
interface Props<T = unknown> {
interface Props {
label: string;
value?: string;
onChangeValue: (value: string) => void;
tabs: TabItem<T>[];
tabs: TabItem[];
tabListClassName?: string;
className?: string;
children: ReactNode;
}
export function Tabs<T>({
export function Tabs({
value,
onChangeValue,
label,
@@ -31,7 +35,7 @@ export function Tabs<T>({
tabs,
className,
tabListClassName,
}: Props<T>) {
}: Props) {
const ref = useRef<HTMLDivElement | null>(null);
const handleTabChange = (value: string) => {
@@ -77,7 +81,8 @@ export function Tabs<T>({
const btnClassName = classnames(
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 (
<RadioDropdown
key={t.value}
@@ -91,7 +96,7 @@ export function Tabs<T>({
onClick={isActive ? undefined : () => handleTabChange(t.value)}
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" />
</Button>
</RadioDropdown>

View File

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

View File

@@ -1,5 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { act } from 'react-dom/test-utils';
import { useMemo } from 'react';
import type { Workspace } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
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 type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store';
import { requestsQueryKey } from './useRequests';
export function useUpdateRequest(id: string | null) {
const queryClient = useQueryClient();
return useMutation<void, unknown, Partial<HttpRequest>>({
mutationFn: async (patch) => {
const request = await getRequest(id);
@@ -13,9 +15,19 @@ export function useUpdateRequest(id: string | null) {
const updatedRequest = { ...request, ...patch };
console.log('UPDATING REQUEST', patch);
await invoke('update_request', {
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 {
readonly id: string;
readonly createdAt: Date;
readonly updatedAt: Date;
readonly createdAt: string;
readonly updatedAt: string;
readonly updatedBy: string;
}
export interface Workspace extends BaseModel {
@@ -16,11 +17,13 @@ export interface HttpHeader {
enabled?: boolean;
}
export enum HttpRequestBodyType {
GraphQL = 'graphql',
JSON = 'application/json',
XML = 'text/xml',
}
export const BODY_TYPE_NONE = null;
export const BODY_TYPE_GRAPHQL = 'graphql';
export const BODY_TYPE_JSON = 'application/json';
export const BODY_TYPE_XML = 'text/xml';
export const AUTH_TYPE_NONE = null;
export const AUTH_TYPE_BASIC = 'basic';
export interface HttpRequest extends BaseModel {
readonly workspaceId: string;
@@ -29,7 +32,11 @@ export interface HttpRequest extends BaseModel {
name: string;
url: string;
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;
headers: HttpHeader[];
}

View File

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