mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-08 13:55:14 +02:00
Response streaming
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE http_responses ADD COLUMN error TEXT NULL;
|
||||||
+190
-168
@@ -20,90 +20,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n DELETE FROM http_responses\n WHERE request_id = ?\n "
|
"query": "\n DELETE FROM http_responses\n WHERE request_id = ?\n "
|
||||||
},
|
},
|
||||||
"28675cd7ad73860417a667050694675e132b5e92cf6d3195a6eec218834e3a1d": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "workspace_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "request_id",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updated_at",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Datetime"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "deleted_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": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n "
|
|
||||||
},
|
|
||||||
"3d2a542964d946ff9854d053b1adf04985d97a6de27b713188505c1f99c77707": {
|
"3d2a542964d946ff9854d053b1adf04985d97a6de27b713188505c1f99c77707": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@@ -196,90 +112,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n DELETE FROM http_requests\n WHERE id = ?\n "
|
"query": "\n DELETE FROM http_requests\n WHERE id = ?\n "
|
||||||
},
|
},
|
||||||
"55eae4b20a2c313134579b0ea43bad4dc2dd313db6cd1654f783bac12602db8a": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "workspace_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "request_id",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updated_at",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Datetime"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "deleted_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": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n "
|
|
||||||
},
|
|
||||||
"7ec60cbc3c9f26e8af86a21ef6b66e564f4fa518925c92308b04f882237a244e": {
|
"7ec60cbc3c9f26e8af86a21ef6b66e564f4fa518925c92308b04f882237a244e": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@@ -352,6 +184,96 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT id, workspace_id, created_at, updated_at, deleted_at, name, url, method, body,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ORDER BY created_at DESC\n "
|
"query": "\n SELECT id, workspace_id, created_at, updated_at, deleted_at, name, url, method, body,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ORDER BY created_at DESC\n "
|
||||||
},
|
},
|
||||||
|
"7f623d0e8f1ddad33d356e2d159b776a2bef1a238cb9200d74eb0c5e3983df85": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "workspace_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "request_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted_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,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_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 "
|
||||||
|
},
|
||||||
"8069c0bd326f659faca7b45b03e5317d7339a168f4cd7776d9f84304bb7ae7ac": {
|
"8069c0bd326f659faca7b45b03e5317d7339a168f4cd7776d9f84304bb7ae7ac": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@@ -400,6 +322,16 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT id, created_at, updated_at, deleted_at, name, description\n FROM workspaces\n "
|
"query": "\n SELECT id, created_at, updated_at, deleted_at, name, description\n FROM workspaces\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 "
|
||||||
|
},
|
||||||
"e767522f92c8c49cd2e563e58737a05092daf9b1dc763bacc82a5c14d696d78e": {
|
"e767522f92c8c49cd2e563e58737a05092daf9b1dc763bacc82a5c14d696d78e": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@@ -467,5 +399,95 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"query": "\n SELECT id, created_at, updated_at, deleted_at, name, description\n FROM workspaces\n WHERE id = ?\n "
|
"query": "\n SELECT id, created_at, updated_at, deleted_at, name, description\n FROM workspaces\n WHERE id = ?\n "
|
||||||
|
},
|
||||||
|
"fb2c2328678bbdcb64b79ced26f3d7a1b08d315ef6dedfe4d5ae4231c861b079": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "workspace_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "request_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted_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,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT id, workspace_id, request_id, updated_at, deleted_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 "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+46
-34
@@ -16,12 +16,14 @@ use http::{HeaderMap, HeaderValue, Method};
|
|||||||
use reqwest::redirect::Policy;
|
use reqwest::redirect::Policy;
|
||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use sqlx::sqlite::SqlitePoolOptions;
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
use sqlx::types::Json;
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
use tauri::regex::Regex;
|
use tauri::regex::Regex;
|
||||||
use tauri::{AppHandle, State, Wry};
|
use tauri::{AppHandle, State, Wry};
|
||||||
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::models::{update_response, HttpResponse};
|
||||||
use window_ext::WindowExt;
|
use window_ext::WindowExt;
|
||||||
|
|
||||||
mod models;
|
mod models;
|
||||||
@@ -55,11 +57,18 @@ async fn send_request(
|
|||||||
app_handle: AppHandle<Wry>,
|
app_handle: AppHandle<Wry>,
|
||||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
) -> Result<models::HttpResponse, String> {
|
) -> Result<String, String> {
|
||||||
let pool = &*db_instance.lock().await;
|
let pool = &*db_instance.lock().await;
|
||||||
|
|
||||||
let req = models::get_request(request_id, pool)
|
let req = models::get_request(request_id, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get request");
|
.expect("Failed to get request");
|
||||||
|
|
||||||
|
let mut response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create response");
|
||||||
|
app_handle.emit_all("updated_response", &response).unwrap();
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
let mut url_string = req.url.to_string();
|
let mut url_string = req.url.to_string();
|
||||||
@@ -110,7 +119,7 @@ async fn send_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 Err(e.to_string());
|
return response_err(response, e.to_string(), app_handle, pool).await;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,41 +134,44 @@ async fn send_request(
|
|||||||
|
|
||||||
match resp {
|
match resp {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
let status = v.status().as_u16() as i64;
|
response.status = v.status().as_u16() as i64;
|
||||||
let status_reason = v.status().canonical_reason();
|
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||||
let headers = v
|
response.headers = Json(
|
||||||
.headers()
|
v.headers()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| models::HttpResponseHeader {
|
.map(|(k, v)| models::HttpResponseHeader {
|
||||||
name: k.as_str().to_string(),
|
name: k.as_str().to_string(),
|
||||||
value: v.to_str().unwrap().to_string(),
|
value: v.to_str().unwrap().to_string(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect(),
|
||||||
let url = v.url().clone();
|
);
|
||||||
let body = v.text().await.expect("Failed to get body");
|
response.url = v.url().to_string();
|
||||||
let elapsed = start.elapsed().as_millis() as i64;
|
response.body = v.text().await.expect("Failed to get body");
|
||||||
let response = models::create_response(
|
response.elapsed = start.elapsed().as_millis() as i64;
|
||||||
&req.id,
|
response = update_response(response, pool)
|
||||||
elapsed,
|
.await
|
||||||
url.as_str(),
|
.expect("Failed to update response");
|
||||||
status,
|
app_handle.emit_all("updated_response", &response).unwrap();
|
||||||
status_reason,
|
Ok(response.id)
|
||||||
body.as_str(),
|
|
||||||
headers,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to create response");
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {}", e);
|
|
||||||
Err(e.to_string())
|
|
||||||
}
|
}
|
||||||
|
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn response_err(
|
||||||
|
mut response: HttpResponse,
|
||||||
|
error: String,
|
||||||
|
app_handle: AppHandle<Wry>,
|
||||||
|
pool: &Pool<Sqlite>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
response.error = Some(error.clone());
|
||||||
|
response = update_response(response, pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update response");
|
||||||
|
app_handle.emit_all("updated_response", &response).unwrap();
|
||||||
|
Ok(response.id)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn create_request(
|
async fn create_request(
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
@@ -175,7 +187,7 @@ async fn create_request(
|
|||||||
.expect("Failed to create request");
|
.expect("Failed to create request");
|
||||||
|
|
||||||
app_handle
|
app_handle
|
||||||
.emit_all("created_request", &created_request)
|
.emit_all("updated_request", &created_request)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Ok(created_request.id)
|
Ok(created_request.id)
|
||||||
|
|||||||
+34
-6
@@ -53,6 +53,7 @@ pub struct HttpResponse {
|
|||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
pub deleted_at: Option<NaiveDateTime>,
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
|
pub error: Option<String>,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub elapsed: i64,
|
pub elapsed: i64,
|
||||||
pub status: i64,
|
pub status: i64,
|
||||||
@@ -241,11 +242,37 @@ pub async fn create_response(
|
|||||||
get_response(&id, pool).await
|
get_response(&id, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_response(
|
||||||
|
response: HttpResponse,
|
||||||
|
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 = ?;
|
||||||
|
"#,
|
||||||
|
response.elapsed,
|
||||||
|
response.url,
|
||||||
|
response.status,
|
||||||
|
response.status_reason,
|
||||||
|
response.body,
|
||||||
|
response.error,
|
||||||
|
headers_json,
|
||||||
|
response.id,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update response");
|
||||||
|
get_response(&response.id, pool).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse, sqlx::Error> {
|
pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse, sqlx::Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as_unchecked!(
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
r#"
|
r#"
|
||||||
SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url,
|
SELECT id, workspace_id, request_id, updated_at, deleted_at, 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
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -263,16 +290,17 @@ pub async fn find_responses(
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
r#"
|
r#"
|
||||||
SELECT id, workspace_id, request_id, updated_at, deleted_at, created_at, status, status_reason, body, elapsed, url,
|
SELECT id, workspace_id, request_id, updated_at, deleted_at,
|
||||||
|
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
|
||||||
WHERE request_id = ?
|
WHERE request_id = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at ASC
|
||||||
"#,
|
"#,
|
||||||
request_id,
|
request_id,
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<(), sqlx::Error> {
|
pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<(), sqlx::Error> {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { autocompletion } from '@codemirror/autocomplete';
|
|
||||||
import type { Transaction, TransactionSpec } from '@codemirror/state';
|
import type { Transaction, TransactionSpec } from '@codemirror/state';
|
||||||
import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state';
|
import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const variables = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function myCompletions(context: CompletionContext) {
|
export function myCompletions(context: CompletionContext) {
|
||||||
// console.log('COMPLETE', context);
|
|
||||||
const toStartOfName = context.matchBefore(/\w*/);
|
const toStartOfName = context.matchBefore(/\w*/);
|
||||||
const toStartOfVariable = context.matchBefore(/\$\{.*/);
|
const toStartOfVariable = context.matchBefore(/\$\{.*/);
|
||||||
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import { html } from '@codemirror/lang-html';
|
|||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
import { xml } from '@codemirror/lang-xml';
|
import { xml } from '@codemirror/lang-xml';
|
||||||
|
import type { LanguageSupport } from '@codemirror/language';
|
||||||
import {
|
import {
|
||||||
bracketMatching,
|
bracketMatching,
|
||||||
foldGutter,
|
foldGutter,
|
||||||
foldKeymap,
|
foldKeymap,
|
||||||
HighlightStyle,
|
HighlightStyle,
|
||||||
indentOnInput,
|
indentOnInput,
|
||||||
LanguageSupport,
|
|
||||||
LRLanguage,
|
|
||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
} from '@codemirror/language';
|
} from '@codemirror/language';
|
||||||
import { lintKeymap } from '@codemirror/lint';
|
import { lintKeymap } from '@codemirror/lint';
|
||||||
@@ -33,12 +32,9 @@ import {
|
|||||||
lineNumbers,
|
lineNumbers,
|
||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { parseMixed } from '@lezer/common';
|
|
||||||
import { tags as t } from '@lezer/highlight';
|
import { tags as t } from '@lezer/highlight';
|
||||||
import { myCompletions } from './completion/completion';
|
import { twig } from './twig/extension';
|
||||||
import { parser as twigParser } from './twig/twig';
|
|
||||||
import { url } from './url/extension';
|
import { url } from './url/extension';
|
||||||
import { placeholders } from './widgets';
|
|
||||||
|
|
||||||
export const myHighlightStyle = HighlightStyle.define([
|
export const myHighlightStyle = HighlightStyle.define([
|
||||||
{
|
{
|
||||||
@@ -102,35 +98,7 @@ export function getLanguageExtension({
|
|||||||
return [base];
|
return [base];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mixedTwigParser = twigParser.configure({
|
return twig(base);
|
||||||
props: [
|
|
||||||
// Add basic folding/indent metadata
|
|
||||||
// foldNodeProp.add({ Conditional: foldInside }),
|
|
||||||
// indentNodeProp.add({
|
|
||||||
// Conditional: (cx) => {
|
|
||||||
// const closed = /^\s*\{% endif/.test(cx.textAfter);
|
|
||||||
// return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit);
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
],
|
|
||||||
wrap: parseMixed((node) => {
|
|
||||||
return node.type.isTop
|
|
||||||
? {
|
|
||||||
parser: base.language.parser,
|
|
||||||
overlay: (node) => node.type.name === 'Text',
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const twigLanguage = LRLanguage.define({ parser: mixedTwigParser, languageData: {} });
|
|
||||||
const completion = twigLanguage.data.of({
|
|
||||||
autocomplete: myCompletions,
|
|
||||||
});
|
|
||||||
const languageSupport = new LanguageSupport(twigLanguage, [completion]);
|
|
||||||
const completion2 = base.language.data.of({ autocomplete: myCompletions });
|
|
||||||
const languageSupport2 = new LanguageSupport(base.language, [completion2]);
|
|
||||||
return [languageSupport, languageSupport2, placeholders, base.support];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseExtensions = [
|
export const baseExtensions = [
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { LRLanguage, LanguageSupport } from '@codemirror/language';
|
||||||
|
import { parseMixed } from '@lezer/common';
|
||||||
|
import { myCompletions } from '../completion/completion';
|
||||||
|
import { placeholders } from '../widgets';
|
||||||
|
import { parser as twigParser } from './twig';
|
||||||
|
|
||||||
|
export function twig(base?: LanguageSupport) {
|
||||||
|
const parser = mixedOrPlainParser(base);
|
||||||
|
const twigLanguage = LRLanguage.define({ name: 'twig', parser, languageData: {} });
|
||||||
|
const completion = twigLanguage.data.of({
|
||||||
|
autocomplete: myCompletions,
|
||||||
|
});
|
||||||
|
const languageSupport = new LanguageSupport(twigLanguage, [completion]);
|
||||||
|
|
||||||
|
if (base) {
|
||||||
|
const completion2 = base.language.data.of({ autocomplete: myCompletions });
|
||||||
|
const languageSupport2 = new LanguageSupport(base.language, [completion2]);
|
||||||
|
return [languageSupport, languageSupport2, placeholders, base.support];
|
||||||
|
} else {
|
||||||
|
return [languageSupport, placeholders];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixedOrPlainParser(base?: LanguageSupport) {
|
||||||
|
if (base === undefined) {
|
||||||
|
return twigParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mixedParser = twigParser.configure({
|
||||||
|
props: [
|
||||||
|
// Add basic folding/indent metadata
|
||||||
|
// foldNodeProp.add({ Conditional: foldInside }),
|
||||||
|
// indentNodeProp.add({
|
||||||
|
// Conditional: (cx) => {
|
||||||
|
// const closed = /^\s*\{% endif/.test(cx.textAfter);
|
||||||
|
// return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit);
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
],
|
||||||
|
wrap: parseMixed((node) => {
|
||||||
|
return node.type.isTop
|
||||||
|
? {
|
||||||
|
parser: base.language.parser,
|
||||||
|
overlay: (node) => node.type.name === 'Text',
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mixedParser;
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { HStack, VStack } from './Stacks';
|
|
||||||
import Editor from './Editor/Editor';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { WindowDragRegion } from './WindowDragRegion';
|
import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses';
|
||||||
import { Dropdown } from './Dropdown';
|
import { Dropdown } from './Dropdown';
|
||||||
import { IconButton } from './IconButton';
|
import Editor from './Editor/Editor';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
|
import { HStack, VStack } from './Stacks';
|
||||||
|
import { WindowDragRegion } from './WindowDragRegion';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@@ -19,9 +19,10 @@ export function ResponsePane({ requestId, error }: Props) {
|
|||||||
const responses = useResponses(requestId);
|
const responses = useResponses(requestId);
|
||||||
const response = activeResponseId
|
const response = activeResponseId
|
||||||
? responses.data.find((r) => r.id === activeResponseId)
|
? responses.data.find((r) => r.id === activeResponseId)
|
||||||
: responses.data[0];
|
: responses.data[responses.data.length - 1];
|
||||||
const deleteResponse = useDeleteResponse(response);
|
const deleteResponse = useDeleteResponse(response);
|
||||||
const deleteAllResponses = useDeleteAllResponses(response?.requestId);
|
const deleteAllResponses = useDeleteAllResponses(response?.requestId);
|
||||||
|
error = response?.error ?? error;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveResponseId(null);
|
setActiveResponseId(null);
|
||||||
@@ -76,7 +77,9 @@ export function ResponsePane({ requestId, error }: Props) {
|
|||||||
items="center"
|
items="center"
|
||||||
className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0"
|
className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0"
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="whitespace-nowrap">
|
||||||
|
{response.updatedAt.toISOString()}
|
||||||
|
•
|
||||||
{response.status}
|
{response.status}
|
||||||
{response.statusReason && ` ${response.statusReason}`}
|
{response.statusReason && ` ${response.statusReason}`}
|
||||||
•
|
•
|
||||||
@@ -84,7 +87,6 @@ export function ResponsePane({ requestId, error }: Props) {
|
|||||||
{Math.round(response.body.length / 1000)} KB
|
{Math.round(response.body.length / 1000)} KB
|
||||||
</div>
|
</div>
|
||||||
<HStack items="center" className="ml-auto">
|
<HStack items="center" className="ml-auto">
|
||||||
<div className="font-mono">{response.url}</div>
|
|
||||||
{contentType.includes('html') && (
|
{contentType.includes('html') && (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={viewMode === 'pretty' ? 'eye' : 'code'}
|
icon={viewMode === 'pretty' ? 'eye' : 'code'}
|
||||||
@@ -104,7 +106,7 @@ export function ResponsePane({ requestId, error }: Props) {
|
|||||||
/>
|
/>
|
||||||
) : response?.body ? (
|
) : response?.body ? (
|
||||||
<Editor
|
<Editor
|
||||||
valueKey={response.id}
|
valueKey={`${contentType}:${response.body}`}
|
||||||
defaultValue={response?.body}
|
defaultValue={response?.body}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface HttpResponse extends BaseModel {
|
|||||||
id: string;
|
id: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
error: string;
|
||||||
status: string;
|
status: string;
|
||||||
elapsed: number;
|
elapsed: number;
|
||||||
statusReason: string;
|
statusReason: string;
|
||||||
|
|||||||
+41
-8
@@ -5,13 +5,15 @@ import App from './App';
|
|||||||
import { HelmetProvider } from 'react-helmet-async';
|
import { HelmetProvider } from 'react-helmet-async';
|
||||||
import { MotionConfig } from 'framer-motion';
|
import { MotionConfig } from 'framer-motion';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { responsesQueryKey } from './hooks/useResponses';
|
||||||
import { setTheme } from './lib/theme';
|
import { setTheme } from './lib/theme';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
import { Workspaces } from './pages/Workspaces';
|
import { Workspaces } from './pages/Workspaces';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import { convertDates, HttpRequest } from './lib/models';
|
import type { HttpRequest, HttpResponse } from './lib/models';
|
||||||
|
import { convertDates } from './lib/models';
|
||||||
import { requestsQueryKey } from './hooks/useRequest';
|
import { requestsQueryKey } from './hooks/useRequest';
|
||||||
|
|
||||||
setTheme();
|
setTheme();
|
||||||
@@ -23,15 +25,24 @@ greet();
|
|||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
|
await listen('updated_request', ({ payload: request }: { payload: HttpRequest }) => {
|
||||||
queryClient.setQueryData(requestsQueryKey(request.workspaceId), (requests: HttpRequest[] = []) =>
|
|
||||||
requests.map((r) => (r.id === request.id ? convertDates(request) : r)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await listen('created_request', ({ payload: request }: { payload: HttpRequest }) => {
|
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
requestsQueryKey(request.workspaceId),
|
requestsQueryKey(request.workspaceId),
|
||||||
(requests: HttpRequest[] = []) => [...requests, convertDates(request)],
|
(requests: HttpRequest[] = []) => {
|
||||||
|
const newRequests = [];
|
||||||
|
let found = false;
|
||||||
|
for (const r of requests) {
|
||||||
|
if (r.id === request.id) {
|
||||||
|
found = true;
|
||||||
|
newRequests.push(convertDates(request));
|
||||||
|
} else {
|
||||||
|
newRequests.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
newRequests.push(convertDates(request));
|
||||||
|
}
|
||||||
|
return newRequests;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,6 +52,28 @@ await listen('deleted_request', ({ payload: request }: { payload: HttpRequest })
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
responsesQueryKey(response.requestId),
|
||||||
|
(responses: HttpResponse[] = []) => {
|
||||||
|
const newResponses = [];
|
||||||
|
let found = false;
|
||||||
|
for (const r of responses) {
|
||||||
|
if (r.id === response.id) {
|
||||||
|
found = true;
|
||||||
|
newResponses.push(convertDates(response));
|
||||||
|
} else {
|
||||||
|
newResponses.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
newResponses.push(convertDates(response));
|
||||||
|
}
|
||||||
|
return newResponses;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
Reference in New Issue
Block a user