Url parameters done

This commit is contained in:
Gregory Schier
2023-11-13 10:52:11 -08:00
parent d289f1fd13
commit df83a61d6f
13 changed files with 308 additions and 238 deletions

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n url_parameters,\n method,\n body,\n body_type,\n authentication,\n authentication_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 folder_id = excluded.folder_id,\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 url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_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\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -44,38 +44,43 @@
"type_info": "Text"
},
{
"name": "method",
"name": "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "body!: Json<HashMap<String, JsonValue>>",
"name": "method",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "body_type",
"name": "body!: Json<HashMap<String, JsonValue>>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"name": "body_type",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "authentication_type",
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "sort_priority",
"name": "authentication_type",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 14,
"type_info": "Float"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"ordinal": 14,
"ordinal": 15,
"type_info": "Text"
}
],
@@ -93,6 +98,7 @@
false,
false,
false,
false,
true,
false,
true,
@@ -100,5 +106,5 @@
false
]
},
"hash": "55e4e8b66c18f85d17ada00b302720e5dcd35ec4288006e4a556448d59b63952"
"hash": "6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_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 folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>\",\n method,\n body AS \"body!: Json<HashMap<String, JsonValue>>\",\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n ",
"describe": {
"columns": [
{
@@ -44,38 +44,43 @@
"type_info": "Text"
},
{
"name": "method",
"name": "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "body!: Json<HashMap<String, JsonValue>>",
"name": "method",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "body_type",
"name": "body!: Json<HashMap<String, JsonValue>>",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"name": "body_type",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "authentication_type",
"name": "authentication!: Json<HashMap<String, JsonValue>>",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "sort_priority",
"name": "authentication_type",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "sort_priority",
"ordinal": 14,
"type_info": "Float"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
"ordinal": 14,
"ordinal": 15,
"type_info": "Text"
}
],
@@ -93,6 +98,7 @@
false,
false,
false,
false,
true,
false,
true,
@@ -100,5 +106,5 @@
false
]
},
"hash": "ae31827b9576ffba83a9de05e30688df3c83e145860f8dd608410a9a9254659d"
"hash": "7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n method,\n body,\n body_type,\n authentication,\n authentication_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 folder_id = excluded.folder_id,\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 ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759"
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE http_requests
ADD COLUMN url_parameters TEXT NOT NULL DEFAULT '[]';

View File

@@ -10,16 +10,11 @@ extern crate objc;
use std::collections::HashMap;
use std::env::current_dir;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::process::exit;
use base64::Engine;
use fern::colors::ColoredLevelConfig;
use http::{HeaderMap, HeaderValue, Method};
use http::header::{ACCEPT, HeaderName, USER_AGENT};
use log::{debug, error, info, warn};
use log::{debug, error, info};
use rand::random;
use reqwest::redirect::Policy;
use serde::Serialize;
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator;
@@ -36,6 +31,7 @@ use window_ext::TrafficLightWindowExt;
use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
use crate::plugin::{ImportResources, ImportResult};
use crate::send::actually_send_request;
use crate::updates::YaakUpdater;
mod analytics;
@@ -45,6 +41,7 @@ mod render;
mod window_ext;
mod window_menu;
mod updates;
mod send;
#[derive(serde::Serialize)]
pub struct CustomResponse {
@@ -88,188 +85,6 @@ async fn send_ephemeral_request(
actually_send_request(request, &response, &environment_id2, &app_handle, pool).await
}
async fn actually_send_request(
request: models::HttpRequest,
response: &models::HttpResponse,
environment_id: &str,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok();
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
.await
.expect("Failed to get Workspace");
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
let client = reqwest::Client::builder()
.redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build()
.expect("Failed to build client");
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
for h in request.headers.0 {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header value: {}", e);
continue;
}
};
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication.0;
if b == "basic" {
let raw_username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url_string.to_string()).headers(headers);
if let Some(t) = &request.body_type {
let empty_value = &serde_json::to_value("").unwrap();
let b = request.body.0;
if b.contains_key("text") {
let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or("");
let body = render::render(raw_text, &workspace, environment_ref);
request_builder = request_builder.body(body);
} else {
warn!("Unsupported body type: {}", t);
}
}
let sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await;
}
};
let raw_response = client.execute(sendable_req).await;
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.status = v.status().as_u16() as i64;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
response.headers = Json(
v.headers()
.iter()
.map(|(k, v)| models::HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap().to_string(),
})
.collect(),
);
response.url = v.url().to_string();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
{
// Write body to FS
let dir = app_handle.path_resolver().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id.is_empty() {
false => base_dir.join(response.id.clone()),
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
};
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.expect("Failed to open file");
f.write_all(body_bytes.as_slice())
.expect("Failed to write to file");
response.body_path = Some(
body_path
.to_str()
.expect("Failed to get body path")
.to_string(),
);
}
// Also store body directly on the model, if small enough
if body_bytes.len() < 100_000 {
response.body = Some(body_bytes);
}
response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");
if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response);
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
}
}
#[tauri::command]
async fn import_data(
window: Window<Wry>,
@@ -925,8 +740,9 @@ fn main() {
None,
);
}
RunEvent::WindowEvent { label, event: WindowEvent::Focused(true), .. } => {
RunEvent::WindowEvent { label: _label, event: WindowEvent::Focused(true), .. } => {
let h = app_handle.clone();
// Run update check whenever window is focused
tauri::async_runtime::spawn(async move {
let val: State<'_, Mutex<YaakUpdater>> = h.state();
_ = val.lock().await.check(&h).await;

View File

@@ -3,9 +3,9 @@ use std::fs;
use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite};
use sqlx::types::{Json, JsonValue};
use sqlx::types::chrono::NaiveDateTime;
use tauri::AppHandle;
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -54,6 +54,15 @@ pub struct HttpRequestHeader {
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct HttpUrlParameter {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub name: String,
pub value: String,
}
fn default_http_request_method() -> String {
"GET".to_string()
}
@@ -70,6 +79,7 @@ pub struct HttpRequest {
pub sort_priority: f64,
pub name: String,
pub url: String,
pub url_parameters: Json<Vec<HttpUrlParameter>>,
#[serde(default = "default_http_request_method")]
pub method: String,
pub body: Json<HashMap<String, JsonValue>>,
@@ -439,6 +449,7 @@ pub async fn upsert_request(
folder_id,
name,
url,
url_parameters,
method,
body,
body_type,
@@ -447,7 +458,7 @@ pub async fn upsert_request(
headers,
sort_priority
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
name = excluded.name,
@@ -459,6 +470,7 @@ pub async fn upsert_request(
authentication = excluded.authentication,
authentication_type = excluded.authentication_type,
url = excluded.url,
url_parameters = excluded.url_parameters,
sort_priority = excluded.sort_priority
"#,
id,
@@ -466,6 +478,7 @@ pub async fn upsert_request(
r.folder_id,
trimmed_name,
r.url,
r.url_parameters,
r.method,
r.body,
r.body_type,
@@ -496,6 +509,7 @@ pub async fn find_requests(
updated_at,
name,
url,
url_parameters AS "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
method,
body AS "body!: Json<HashMap<String, JsonValue>>",
body_type,
@@ -525,6 +539,7 @@ pub async fn get_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, s
updated_at,
name,
url,
url_parameters AS "url_parameters!: sqlx::types::Json<Vec<HttpUrlParameter>>",
method,
body AS "body!: Json<HashMap<String, JsonValue>>",
body_type,

205
src-tauri/src/send.rs Normal file
View File

@@ -0,0 +1,205 @@
use std::fs::{create_dir_all, File};
use std::io::Write;
use base64::Engine;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::header::{ACCEPT, USER_AGENT};
use log::warn;
use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite};
use sqlx::types::Json;
use tauri::{AppHandle, Wry};
use crate::{emit_side_effect, models, render, response_err};
pub async fn actually_send_request(
request: models::HttpRequest,
response: &models::HttpResponse,
environment_id: &str,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok();
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
.await
.expect("Failed to get Workspace");
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
let client = reqwest::Client::builder()
.redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build()
.expect("Failed to build client");
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
for h in request.headers.0 {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header value: {}", e);
continue;
}
};
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication.0;
if b == "basic" {
let raw_username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url_string.to_string()).headers(headers);
let mut query_params = Vec::new();
for p in request.url_parameters.0 {
if !p.enabled || p.name.is_empty() { continue; }
query_params.push((
render::render(&p.name, &workspace, environment_ref),
render::render(&p.value, &workspace, environment_ref),
));
}
request_builder = request_builder.query(&query_params);
if let Some(t) = &request.body_type {
let empty_value = &serde_json::to_value("").unwrap();
let b = request.body.0;
if b.contains_key("text") {
let raw_text = b.get("text").unwrap_or(empty_value).as_str().unwrap_or("");
let body = render::render(raw_text, &workspace, environment_ref);
request_builder = request_builder.body(body);
} else {
warn!("Unsupported body type: {}", t);
}
}
let sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), app_handle, pool).await;
}
};
let raw_response = client.execute(sendable_req).await;
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.status = v.status().as_u16() as i64;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
response.headers = Json(
v.headers()
.iter()
.map(|(k, v)| models::HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap().to_string(),
})
.collect(),
);
response.url = v.url().to_string();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
{
// Write body to FS
let dir = app_handle.path_resolver().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id.is_empty() {
false => base_dir.join(response.id.clone()),
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
};
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.expect("Failed to open file");
f.write_all(body_bytes.as_slice())
.expect("Failed to write to file");
response.body_path = Some(
body_path
.to_str()
.expect("Failed to get body path")
.to_string(),
);
}
// Also store body directly on the model, if small enough
if body_bytes.len() < 100_000 {
response.body = Some(body_bytes);
}
response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");
if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response);
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
}
}

View File

@@ -4,7 +4,7 @@ use tauri::{AppHandle, updater, Window, Wry};
use tauri::api::dialog;
// Check for updates every 3 hours
const MAX_UPDATE_CHECK_SECONDS: u64 = 3600 * 3;
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60 * 3;
// Create updater struct
pub struct YaakUpdater {

View File

@@ -14,7 +14,7 @@ type Props = {
onChange: (headers: HttpRequest['headers']) => void;
};
export function HeaderEditor({ headers, onChange, forceUpdateKey }: Props) {
export function HeadersEditor({ headers, onChange, forceUpdateKey }: Props) {
return (
<PairEditor
valueAutocompleteVariables

View File

@@ -3,17 +3,18 @@ import { PairEditor } from './core/PairEditor';
type Props = {
forceUpdateKey: string;
parameters: { name: string; value: string }[];
onChange: (headers: HttpRequest['headers']) => void;
urlParameters: HttpRequest['headers'];
onChange: (headers: HttpRequest['urlParameters']) => void;
};
export function ParametersEditor({ parameters, forceUpdateKey, onChange }: Props) {
export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) {
return (
<PairEditor
forceUpdateKey={forceUpdateKey}
pairs={parameters}
valueAutocompleteVariables
nameAutocompleteVariables
pairs={urlParameters}
onChange={onChange}
namePlaceholder="name"
forceUpdateKey={forceUpdateKey}
/>
);
}

View File

@@ -10,7 +10,7 @@ import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters';
import type { HttpHeader, HttpRequest } from '../lib/models';
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
import {
AUTH_TYPE_BASIC,
AUTH_TYPE_BEARER,
@@ -28,8 +28,8 @@ import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { GraphQLEditor } from './GraphQLEditor';
import { HeaderEditor } from './HeaderEditor';
import { ParametersEditor } from './ParameterEditor';
import { HeadersEditor } from './HeadersEditor';
import { UrlParametersEditor } from './ParameterEditor';
import { UrlBar } from './UrlBar';
interface Props {
@@ -92,7 +92,15 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
},
},
},
// { value: 'params', label: 'URL Params' },
{
value: 'params',
label: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
</div>
),
},
{
value: 'headers',
label: (
@@ -141,6 +149,10 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
(headers: HttpHeader[]) => updateRequest.mutate({ headers }),
[updateRequest],
);
const handleUrlParametersChange = useCallback(
(urlParameters: HttpUrlParameter[]) => updateRequest.mutate({ urlParameters }),
[updateRequest],
);
useListenToTauriEvent(
'send_request',
@@ -189,17 +201,17 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
)}
</TabContent>
<TabContent value="headers">
<HeaderEditor
<HeadersEditor
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers}
onChange={handleHeadersChange}
/>
</TabContent>
<TabContent value="params">
<ParametersEditor
<UrlParametersEditor
forceUpdateKey={forceUpdateKey}
parameters={[]}
onChange={() => null}
urlParameters={activeRequest.urlParameters}
onChange={handleUrlParametersChange}
/>
</TabContent>
<TabContent value="body">

View File

@@ -49,6 +49,12 @@ export interface HttpHeader {
enabled?: boolean;
}
export interface HttpUrlParameter {
name: string;
value: string;
enabled?: boolean;
}
export interface HttpRequest extends BaseModel {
readonly workspaceId: string;
readonly model: 'http_request';
@@ -56,6 +62,7 @@ export interface HttpRequest extends BaseModel {
sortPriority: number;
name: string;
url: string;
urlParameters: HttpUrlParameter[];
body: Record<string, string | number | boolean | null | undefined>;
bodyType: string | null;
authentication: Record<string, string | number | boolean | null | undefined>;