Variables under Environment, and render all props

This commit is contained in:
Gregory Schier
2023-10-28 11:29:29 -07:00
parent eb1cd1c14b
commit 15087f2d5a
17 changed files with 263 additions and 275 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE environments DROP COLUMN data;
ALTER TABLE environments ADD COLUMN variables DEFAULT '[]' NOT NULL;

View File

@@ -160,6 +160,16 @@
}, },
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n " "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n "
}, },
"3ec4710d28a7f38608c96798d971217ac97788bcb639089d0c5750c0d339bc9a": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 3
}
},
"query": "\n UPDATE environments\n SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
},
"448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": { "448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -282,6 +292,60 @@
}, },
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n " "query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n "
}, },
"689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417": {
"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": "created_at",
"ordinal": 3,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "name",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
"ordinal": 6,
"type_info": "Null"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM environments\n WHERE id = ?\n "
},
"6f0cb5a6d1e8dbc8cdfcc3c7e7944b2c83c22cb795b9d6b98fe067dabec9680b": { "6f0cb5a6d1e8dbc8cdfcc3c7e7944b2c83c22cb795b9d6b98fe067dabec9680b": {
"describe": { "describe": {
"columns": [ "columns": [
@@ -378,16 +442,6 @@
}, },
"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 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 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 headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n "
}, },
"6f12b56113b09966b472431b6cb95c354bea51b4dfb22a96517655c0fca0ab05": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 3
}
},
"query": "\n UPDATE environments\n SET (name, data, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
},
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": { "84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -398,6 +452,16 @@
}, },
"query": "\n DELETE FROM workspaces\n WHERE id = ?\n " "query": "\n DELETE FROM workspaces\n WHERE id = ?\n "
}, },
"86e32d6a6fadf35436f19b577a659c203a8d143cb3a8d6122951c5bf54a0888d": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"query": "\n INSERT INTO environments (id, workspace_id, name, variables)\n VALUES (?, ?, ?, ?)\n "
},
"8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c": { "8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c": {
"describe": { "describe": {
"columns": [], "columns": [],
@@ -408,7 +472,27 @@
}, },
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n " "query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n "
}, },
"986763e31599881f287ef378002fc35d8e983af10a30a9aa4ade606dacf83260": { "aeb0712785a9964d516dc8939bc54aa8206ad852e608b362d014b67a0f21b0ed": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 1
}
},
"query": "\n DELETE FROM environments\n WHERE id = ?\n "
},
"b19c275180909a39342b13c3cdcf993781636913ae590967f5508c46a56dc961": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 11
}
},
"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 )\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 "
},
"ba2b34a77723f24f86e4c3c45274dbfec6ca130e16e592f948844c037bdc0593": {
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -442,9 +526,9 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "data!: Json<HashMap<String, JsonValue>>", "name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Null"
} }
], ],
"nullable": [ "nullable": [
@@ -460,37 +544,7 @@
"Right": 1 "Right": 1
} }
}, },
"query": "\n SELECT id, workspace_id, model, created_at, updated_at, name,\n data AS \"data!: Json<HashMap<String, JsonValue>>\"\n FROM environments\n WHERE workspace_id = ?\n " "query": "\n SELECT id, workspace_id, model, created_at, updated_at, name,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM environments\n WHERE workspace_id = ?\n "
},
"ab7294b681f1202ef06aaa26885147ead2db6ac740023793cda1e1c92665d996": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"query": "\n INSERT INTO environments (\n id,\n workspace_id,\n name,\n data\n )\n VALUES (?, ?, ?, ?)\n "
},
"aeb0712785a9964d516dc8939bc54aa8206ad852e608b362d014b67a0f21b0ed": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 1
}
},
"query": "\n DELETE FROM environments\n WHERE id = ?\n "
},
"b19c275180909a39342b13c3cdcf993781636913ae590967f5508c46a56dc961": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 11
}
},
"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 )\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 "
}, },
"c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e": { "c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e": {
"describe": { "describe": {
@@ -815,59 +869,5 @@
} }
}, },
"query": "\n INSERT INTO workspaces (id, name, description)\n VALUES (?, ?, ?)\n " "query": "\n INSERT INTO workspaces (id, name, description)\n VALUES (?, ?, ?)\n "
},
"fb89f653780b3f3ab0dd0bb2af30c8d3945203819cb9df7bdd331df56a6ae690": {
"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": "created_at",
"ordinal": 3,
"type_info": "Datetime"
},
{
"name": "updated_at",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "name",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "data!: Json<HashMap<String, JsonValue>>",
"ordinal": 6,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n data AS \"data!: Json<HashMap<String, JsonValue>>\"\n FROM environments\n WHERE id = ?\n "
} }
} }

View File

@@ -16,7 +16,7 @@ use reqwest::redirect::Policy;
use serde::Serialize; use serde::Serialize;
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::{Json, JsonValue}; use sqlx::types::Json;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::collections::HashMap; use std::collections::HashMap;
use std::env::current_dir; use std::env::current_dir;
@@ -81,14 +81,10 @@ async fn actually_send_ephemeral_request(
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> { ) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok(); let environment = models::get_environment(environment_id, pool).await.ok();
let environment_ref = environment.as_ref();
// TODO: Use active environment let mut url_string = render::render(&request.url, environment.as_ref());
let mut url_string = match environment {
Some(e) => render::render(&request.url, e.clone()),
None => request.url.to_string(),
};
if !url_string.starts_with("http://") && !url_string.starts_with("https://") { if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string); url_string = format!("http://{}", url_string);
@@ -110,57 +106,54 @@ async fn actually_send_ephemeral_request(
if h.name.is_empty() && h.value.is_empty() { if h.name.is_empty() && h.value.is_empty() {
continue; continue;
} }
if !h.enabled { if !h.enabled {
continue; continue;
} }
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
let name = render::render(&h.name, environment_ref);
let value = render::render(&h.value, environment_ref);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n, Ok(n) => n,
Err(e) => { Err(e) => {
eprintln!("Failed to create header name: {}", e); eprintln!("Failed to create header name: {}", e);
continue; continue;
} }
}; };
let header_value = match HeaderValue::from_str(h.value.as_str()) { let header_value = match HeaderValue::from_str(value.as_str()) {
Ok(n) => n, Ok(n) => n,
Err(e) => { Err(e) => {
eprintln!("Failed to create header value: {}", e); eprintln!("Failed to create header value: {}", e);
continue; continue;
} }
}; };
headers.insert(header_name, header_value); headers.insert(header_name, header_value);
} }
if let Some(b) = &request.authentication_type { if let Some(b) = &request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap(); let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication.0;
if b == "basic" { if b == "basic" {
let a = request.authentication.0; let raw_username = a.get("username").unwrap_or(empty_value).as_str().unwrap_or("");
let auth = format!( let raw_password = a.get("password").unwrap_or(empty_value).as_str().unwrap_or("");
"{}:{}", let username = render::render(raw_username, environment_ref);
a.get("username") let password = render::render(raw_password, environment_ref);
.unwrap_or(empty_value)
.as_str() let auth = format!( "{username}:{password}");
.unwrap_or(""),
a.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or(""),
);
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth); let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
headers.insert( headers.insert(
"Authorization", "Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(), HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
); );
} else if b == "bearer" { } else if b == "bearer" {
let token = request let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
.authentication let token = render::render(raw_token, environment_ref);
.0
.get("token")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
headers.insert( headers.insert(
"Authorization", "Authorization",
HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
); );
} }
} }
@@ -170,7 +163,10 @@ async fn actually_send_ephemeral_request(
let builder = client.request(m, url_string.to_string()).headers(headers); let builder = client.request(m, url_string.to_string()).headers(headers);
let sendable_req_result = match (request.body, request.body_type) { let sendable_req_result = match (request.body, request.body_type) {
(Some(b), Some(_)) => builder.body(b).build(), (Some(raw_body), Some(_)) => {
let body = render::render(&raw_body, environment_ref);
builder.body(body).build()
},
_ => builder.build(), _ => builder.build(),
}; };
@@ -341,8 +337,8 @@ async fn create_environment(
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Environment, String> { ) -> Result<models::Environment, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let data: HashMap<String, JsonValue> = HashMap::new(); let variables = Vec::new();
let created_environment = models::create_environment(workspace_id, name, data, pool) let created_environment = models::create_environment(workspace_id, name, variables, pool)
.await .await
.expect("Failed to create environment"); .expect("Failed to create environment");
@@ -418,7 +414,7 @@ async fn update_environment(
let updated_environment = models::update_environment( let updated_environment = models::update_environment(
environment.id.as_str(), environment.id.as_str(),
environment.name.as_str(), environment.name.as_str(),
environment.data.0, environment.variables.0,
pool, pool,
) )
.await .await

View File

@@ -27,7 +27,30 @@ pub struct Environment {
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub name: String, pub name: String,
pub data: Json<HashMap<String, JsonValue>>, pub variables: Json<Vec<EnvironmentVariable>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvironmentVariable {
#[serde(default)]
pub enabled: bool,
pub name: String,
pub value: String,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Variable {
pub id: String,
pub workspace_id: String,
pub environment_id: String,
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub name: String,
pub value: String,
pub sort_priority: f64,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -213,7 +236,7 @@ pub async fn find_environments(
Environment, Environment,
r#" r#"
SELECT id, workspace_id, model, created_at, updated_at, name, SELECT id, workspace_id, model, created_at, updated_at, name,
data AS "data!: Json<HashMap<String, JsonValue>>" variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM environments FROM environments
WHERE workspace_id = ? WHERE workspace_id = ?
"#, "#,
@@ -226,26 +249,21 @@ pub async fn find_environments(
pub async fn create_environment( pub async fn create_environment(
workspace_id: &str, workspace_id: &str,
name: &str, name: &str,
data: HashMap<String, JsonValue>, variables: Vec<EnvironmentVariable>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<Environment, sqlx::Error> { ) -> Result<Environment, sqlx::Error> {
let id = generate_id(Some("en")); let id = generate_id(Some("en"));
let data_json = Json(data);
let trimmed_name = name.trim(); let trimmed_name = name.trim();
let variables_json = Json(variables);
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO environments ( INSERT INTO environments (id, workspace_id, name, variables)
id,
workspace_id,
name,
data
)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
"#, "#,
id, id,
workspace_id, workspace_id,
trimmed_name, trimmed_name,
data_json, variables_json,
) )
.execute(pool) .execute(pool)
.await?; .await?;
@@ -270,18 +288,18 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
pub async fn update_environment( pub async fn update_environment(
id: &str, id: &str,
name: &str, name: &str,
data: HashMap<String, JsonValue>, variables: Vec<EnvironmentVariable>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<Environment, sqlx::Error> { ) -> Result<Environment, sqlx::Error> {
let json_data = Json(data); let variables_json = Json(variables);
sqlx::query!( sqlx::query!(
r#" r#"
UPDATE environments UPDATE environments
SET (name, data, updated_at) = (?, ?, CURRENT_TIMESTAMP) SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)
WHERE id = ?; WHERE id = ?;
"#, "#,
name, name,
json_data, variables_json,
id, id,
) )
.execute(pool) .execute(pool)
@@ -300,7 +318,7 @@ pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environmen
created_at, created_at,
updated_at, updated_at,
name, name,
data AS "data!: Json<HashMap<String, JsonValue>>" variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM environments FROM environments
WHERE id = ? WHERE id = ?
"#, "#,

View File

@@ -1,23 +1,26 @@
use crate::models::Environment;
use std::collections::HashMap;
use tauri::regex::Regex; use tauri::regex::Regex;
use crate::models::Environment; pub fn render(template: &str, environment: Option<&Environment>) -> String {
match environment {
Some(environment) => render_with_environment(template, environment),
None => template.to_string(),
}
}
fn render_with_environment(template: &str, environment: &Environment) -> String {
let mut map = HashMap::new();
let variables = &environment.variables.0;
for variable in variables {
map.insert(variable.name.as_str(), variable.value.as_str());
}
pub fn render(template: &str, environment: Environment) -> String {
let variables = environment.data;
Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}") Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}")
.expect("Failed to create regex") .expect("Failed to create regex")
.replace(template, |caps: &tauri::regex::Captures| { .replace(template, |caps: &tauri::regex::Captures| {
let key = caps.get(1).unwrap().as_str(); let key = caps.get(1).unwrap().as_str();
match variables.get(key) { map.get(key).unwrap_or(&"")
Some(v) => {
if v.is_string() {
v.as_str().expect("Should be string").to_string()
} else {
v.to_string()
}
}
None => "".to_string(),
}
}) })
.to_string() .to_string()
} }

View File

@@ -14,6 +14,7 @@ export function BasicAuth({ requestId, authentication }: Props) {
return ( return (
<VStack className="my-2" space={2}> <VStack className="my-2" space={2}>
<Input <Input
useTemplating
label="Username" label="Username"
name="username" name="username"
size="sm" size="sm"
@@ -26,6 +27,7 @@ export function BasicAuth({ requestId, authentication }: Props) {
}} }}
/> />
<Input <Input
useTemplating
label="Password" label="Password"
name="password" name="password"
size="sm" size="sm"

View File

@@ -14,6 +14,7 @@ export function BearerAuth({ requestId, authentication }: Props) {
return ( return (
<VStack className="my-2" space={2}> <VStack className="my-2" space={2}>
<Input <Input
useTemplating
label="Token" label="Token"
name="token" name="token"
size="sm" size="sm"

View File

@@ -4,16 +4,11 @@ import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { useEnvironments } from '../hooks/useEnvironments'; import { useEnvironments } from '../hooks/useEnvironments';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { usePrompt } from '../hooks/usePrompt';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
import { EnvironmentEditDialog } from './EnvironmentEditDialog'; import { EnvironmentEditDialog } from './EnvironmentEditDialog';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
type Props = { type Props = {
className?: string; className?: string;
@@ -24,62 +19,23 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
}: Props) { }: Props) {
const environments = useEnvironments(); const environments = useEnvironments();
const activeEnvironment = useActiveEnvironment(); const activeEnvironment = useActiveEnvironment();
const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null);
const deleteEnvironment = useDeleteEnvironment(activeEnvironment);
const createEnvironment = useCreateEnvironment();
const prompt = usePrompt();
const dialog = useDialog(); const dialog = useDialog();
const routes = useAppRoutes(); const routes = useAppRoutes();
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(
const environmentItems: DropdownItem[] = environments.map( () => [
(e) => ({ ...environments.map(
key: e.id, (e) => ({
label: e.name, key: e.id,
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined, label: e.name,
onSelect: async () => { rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
routes.setEnvironment(e); onSelect: async () => {
}, routes.setEnvironment(e);
}), },
[activeEnvironment?.id], }),
); [activeEnvironment?.id],
),
return [ { type: 'separator', label: 'Environments' },
...environmentItems,
...((environmentItems.length > 0
? [{ type: 'separator', label: activeEnvironment?.name }]
: []) as DropdownItem[]),
...((activeEnvironment != null
? [
{
key: 'rename',
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
onSelect: async () => {
const name = await prompt({
title: 'Rename Environment',
description: (
<>
Enter a new name for <InlineCode>{activeEnvironment?.name}</InlineCode>
</>
),
name: 'name',
label: 'Name',
defaultValue: activeEnvironment?.name,
});
updateEnvironment.mutate({ name });
},
},
{
key: 'delete',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: deleteEnvironment.mutate,
variant: 'danger',
},
{ type: 'separator' },
]
: []) as DropdownItem[]),
...((environments.length > 0 ...((environments.length > 0
? [ ? [
{ {
@@ -95,32 +51,9 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
}, },
] ]
: []) as DropdownItem[]), : []) as DropdownItem[]),
{ ],
key: 'create-environment', [activeEnvironment, dialog, environments, routes],
label: 'Create Environment', );
leftSlot: <Icon icon="plus" />,
onSelect: async () => {
const name = await prompt({
name: 'name',
label: 'Name',
defaultValue: 'My Environment',
description: 'Enter a name for the new environment',
title: 'Create Environment',
});
createEnvironment.mutate({ name });
},
},
];
}, [
activeEnvironment,
createEnvironment,
deleteEnvironment,
dialog,
environments,
prompt,
routes,
updateEnvironment,
]);
return ( return (
<Dropdown items={items}> <Dropdown items={items}>

View File

@@ -1,16 +1,17 @@
import { useCreateEnvironment } from '../hooks/useCreateEnvironment'; import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useEnvironments } from '../hooks/useEnvironments'; import { useEnvironments } from '../hooks/useEnvironments';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import type { Environment } from '../lib/models'; import type { Environment } from '../lib/models';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { Editor } from './core/Editor';
import classNames from 'classnames'; import classNames from 'classnames';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { Link } from 'react-router-dom';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { PairEditor } from './core/PairEditor';
import type { PairEditorProps } from './core/PairEditor';
import { useCallback } from 'react';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
export const EnvironmentEditDialog = function() { export const EnvironmentEditDialog = function () {
const routes = useAppRoutes(); const routes = useAppRoutes();
const prompt = usePrompt(); const prompt = usePrompt();
const environments = useEnvironments(); const environments = useEnvironments();
@@ -58,21 +59,21 @@ export const EnvironmentEditDialog = function() {
); );
}; };
const EnvironmentEditor = function({ environment }: { environment: Environment }) { const EnvironmentEditor = function ({ environment }: { environment: Environment }) {
const updateEnvironment = useUpdateEnvironment(environment.id); const updateEnvironment = useUpdateEnvironment(environment.id);
const handleChange = useCallback<PairEditorProps['onChange']>(
(variables) => {
updateEnvironment.mutate({ variables });
},
[updateEnvironment],
);
return ( return (
<Editor <div>
contentType="application/json" <PairEditor
className="w-full min-h-[40px] !bg-gray-50" forceUpdateKey={environment.id}
defaultValue={JSON.stringify(environment.data, null, 2)} pairs={environment.variables}
forceUpdateKey={environment.id} onChange={handleChange}
onChange={(data) => { />
try { </div>
updateEnvironment.mutate({ data: JSON.parse(data) });
} catch (err) {
// That's okay
}
}}
/>
); );
}; };

View File

@@ -63,5 +63,7 @@ const validateHttpHeader = (v: string) => {
return true; return true;
} }
const hi = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, 'fo');
console.log('V', v, '-->', hi);
return v.match(/^[a-zA-Z0-9-_]+$/) !== null; return v.match(/^[a-zA-Z0-9-_]+$/) !== null;
}; };

View File

@@ -54,6 +54,8 @@
/* Bring above on hover */ /* Bring above on hover */
@apply hover:z-10 relative; @apply hover:z-10 relative;
-webkit-text-security: none;
} }
} }

View File

@@ -12,7 +12,6 @@ import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions'; import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import type { GenericCompletionConfig } from './genericCompletion'; import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExt } from './singleLine'; import { singleLineExt } from './singleLine';
import { useEnvironments } from '../../../hooks/useEnvironments';
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment'; import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
// Export some things so all the code-split parts are in this file // Export some things so all the code-split parts are in this file

View File

@@ -48,7 +48,7 @@ const placeholderMatcher = new BetterMatchDecorator({
if (groupMatch == null) { if (groupMatch == null) {
// Should never happen, but make TS happy // Should never happen, but make TS happy
console.warn('Group match was empty', match); console.warn('Group match was empty', match);
return Decoration.replace({});; return Decoration.replace({});
} }
return Decoration.replace({ return Decoration.replace({

View File

@@ -9,11 +9,13 @@ import { twigCompletion } from './completion';
import { parser as twigParser } from './twig'; import { parser as twigParser } from './twig';
import type { Environment } from '../../../../lib/models'; import type { Environment } from '../../../../lib/models';
export function twig(base: LanguageSupport, environment: Environment | null, autocomplete?: GenericCompletionConfig) { export function twig(
// TODO: fill variables here base: LanguageSupport,
const data = environment?.data ?? {}; environment: Environment | null,
const options = Object.keys(data).map(key => ({ name: key })); autocomplete?: GenericCompletionConfig,
const completions = twigCompletion({ options }); ) {
const variables = environment?.variables ?? [];
const completions = twigCompletion({ options: variables });
const language = mixLanguage(base); const language = mixLanguage(base);
const completion = language.data.of({ autocomplete: completions }); const completion = language.data.of({ autocomplete: completions });

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd'; import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@@ -24,7 +24,8 @@ export type PairEditorProps = {
valueValidate?: InputProps['validate']; valueValidate?: InputProps['validate'];
}; };
type Pair = { export type Pair = {
id?: string;
enabled?: boolean; enabled?: boolean;
name: string; name: string;
value: string; value: string;
@@ -342,6 +343,8 @@ const FormRow = memo(function FormRow({
); );
}); });
const newPairContainer = (pair?: Pair): PairContainer => { const newPairContainer = (initialPair?: Pair): PairContainer => {
return { pair: pair ?? { name: '', value: '', enabled: true }, id: uuid() }; const id = initialPair?.id ?? uuid();
const pair = initialPair ?? { name: '', value: '', enabled: true };
return { id, pair };
}; };

View File

@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { Variable } from '../lib/models';
export function variablesQueryKey({ environmentId }: { environmentId: string }) {
return ['variables', { environmentId }];
}
export function useVariables({ environmentId }: { environmentId: string }) {
return (
useQuery({
queryKey: variablesQueryKey({ environmentId }),
queryFn: async () => {
return (await invoke('list_variables', { environmentId })) as Variable[];
},
}).data ?? []
);
}

View File

@@ -22,7 +22,7 @@ export interface Workspace extends BaseModel {
description: string; description: string;
} }
export interface HttpHeader { export interface EnvironmentVariable {
name: string; name: string;
value: string; value: string;
enabled?: boolean; enabled?: boolean;
@@ -32,7 +32,13 @@ export interface Environment extends BaseModel {
readonly workspaceId: string; readonly workspaceId: string;
readonly model: 'environment'; readonly model: 'environment';
name: string; name: string;
data: Record<string, string | number | boolean | null | undefined>; variables: EnvironmentVariable[];
}
export interface HttpHeader {
name: string;
value: string;
enabled?: boolean;
} }
export interface HttpRequest extends BaseModel { export interface HttpRequest extends BaseModel {