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 "
},
"3ec4710d28a7f38608c96798d971217ac97788bcb639089d0c5750c0d339bc9a": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 3
}
},
"query": "\n UPDATE environments\n SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
},
"448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": {
"describe": {
"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 "
},
"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": {
"describe": {
"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 "
},
"6f12b56113b09966b472431b6cb95c354bea51b4dfb22a96517655c0fca0ab05": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 3
}
},
"query": "\n UPDATE environments\n SET (name, data, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
},
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
"describe": {
"columns": [],
@@ -398,6 +452,16 @@
},
"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": {
"describe": {
"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 "
},
"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": {
"columns": [
{
@@ -442,9 +526,9 @@
"type_info": "Text"
},
{
"name": "data!: Json<HashMap<String, JsonValue>>",
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
"ordinal": 6,
"type_info": "Text"
"type_info": "Null"
}
],
"nullable": [
@@ -460,37 +544,7 @@
"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 "
},
"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 "
"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 "
},
"c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e": {
"describe": {
@@ -815,59 +869,5 @@
}
},
"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 sqlx::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::{Json, JsonValue};
use sqlx::types::Json;
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
use std::env::current_dir;
@@ -81,14 +81,10 @@ async fn actually_send_ephemeral_request(
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();
// TODO: Use active environment
let mut url_string = match environment {
Some(e) => render::render(&request.url, e.clone()),
None => request.url.to_string(),
};
let mut url_string = render::render(&request.url, environment.as_ref());
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
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() {
continue;
}
if !h.enabled {
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,
Err(e) => {
eprintln!("Failed to create header name: {}", e);
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,
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 a = request.authentication.0;
let auth = format!(
"{}:{}",
a.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or(""),
a.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or(""),
);
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, environment_ref);
let password = render::render(raw_password, 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 token = request
.authentication
.0
.get("token")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, environment_ref);
headers.insert(
"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 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(),
};
@@ -341,8 +337,8 @@ async fn create_environment(
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Environment, String> {
let pool = &*db_instance.lock().await;
let data: HashMap<String, JsonValue> = HashMap::new();
let created_environment = models::create_environment(workspace_id, name, data, pool)
let variables = Vec::new();
let created_environment = models::create_environment(workspace_id, name, variables, pool)
.await
.expect("Failed to create environment");
@@ -418,7 +414,7 @@ async fn update_environment(
let updated_environment = models::update_environment(
environment.id.as_str(),
environment.name.as_str(),
environment.data.0,
environment.variables.0,
pool,
)
.await

View File

@@ -27,7 +27,30 @@ pub struct Environment {
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
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)]
@@ -213,7 +236,7 @@ pub async fn find_environments(
Environment,
r#"
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
WHERE workspace_id = ?
"#,
@@ -226,26 +249,21 @@ pub async fn find_environments(
pub async fn create_environment(
workspace_id: &str,
name: &str,
data: HashMap<String, JsonValue>,
variables: Vec<EnvironmentVariable>,
pool: &Pool<Sqlite>,
) -> Result<Environment, sqlx::Error> {
let id = generate_id(Some("en"));
let data_json = Json(data);
let trimmed_name = name.trim();
let variables_json = Json(variables);
sqlx::query!(
r#"
INSERT INTO environments (
id,
workspace_id,
name,
data
)
INSERT INTO environments (id, workspace_id, name, variables)
VALUES (?, ?, ?, ?)
"#,
id,
workspace_id,
trimmed_name,
data_json,
variables_json,
)
.execute(pool)
.await?;
@@ -270,18 +288,18 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
pub async fn update_environment(
id: &str,
name: &str,
data: HashMap<String, JsonValue>,
variables: Vec<EnvironmentVariable>,
pool: &Pool<Sqlite>,
) -> Result<Environment, sqlx::Error> {
let json_data = Json(data);
let variables_json = Json(variables);
sqlx::query!(
r#"
UPDATE environments
SET (name, data, updated_at) = (?, ?, CURRENT_TIMESTAMP)
SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)
WHERE id = ?;
"#,
name,
json_data,
variables_json,
id,
)
.execute(pool)
@@ -300,7 +318,7 @@ pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environmen
created_at,
updated_at,
name,
data AS "data!: Json<HashMap<String, JsonValue>>"
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM environments
WHERE id = ?
"#,

View File

@@ -1,23 +1,26 @@
use crate::models::Environment;
use std::collections::HashMap;
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*]}")
.expect("Failed to create regex")
.replace(template, |caps: &tauri::regex::Captures| {
let key = caps.get(1).unwrap().as_str();
match variables.get(key) {
Some(v) => {
if v.is_string() {
v.as_str().expect("Should be string").to_string()
} else {
v.to_string()
}
}
None => "".to_string(),
}
map.get(key).unwrap_or(&"")
})
.to_string()
}

View File

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

View File

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

View File

@@ -4,16 +4,11 @@ import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { useEnvironments } from '../hooks/useEnvironments';
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 { EnvironmentEditDialog } from './EnvironmentEditDialog';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
type Props = {
className?: string;
@@ -24,62 +19,23 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
}: Props) {
const environments = useEnvironments();
const activeEnvironment = useActiveEnvironment();
const updateEnvironment = useUpdateEnvironment(activeEnvironment?.id ?? null);
const deleteEnvironment = useDeleteEnvironment(activeEnvironment);
const createEnvironment = useCreateEnvironment();
const prompt = usePrompt();
const dialog = useDialog();
const routes = useAppRoutes();
const items: DropdownItem[] = useMemo(() => {
const environmentItems: DropdownItem[] = environments.map(
(e) => ({
key: e.id,
label: e.name,
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
onSelect: async () => {
routes.setEnvironment(e);
},
}),
[activeEnvironment?.id],
);
return [
...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[]),
const items: DropdownItem[] = useMemo(
() => [
...environments.map(
(e) => ({
key: e.id,
label: e.name,
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
onSelect: async () => {
routes.setEnvironment(e);
},
}),
[activeEnvironment?.id],
),
{ type: 'separator', label: 'Environments' },
...((environments.length > 0
? [
{
@@ -95,32 +51,9 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
},
]
: []) as DropdownItem[]),
{
key: 'create-environment',
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,
]);
],
[activeEnvironment, dialog, environments, routes],
);
return (
<Dropdown items={items}>

View File

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

View File

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

View File

@@ -54,6 +54,8 @@
/* Bring above on hover */
@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 type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExt } from './singleLine';
import { useEnvironments } from '../../../hooks/useEnvironments';
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
// 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) {
// Should never happen, but make TS happy
console.warn('Group match was empty', match);
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 type { Environment } from '../../../../lib/models';
export function twig(base: LanguageSupport, environment: Environment | null, autocomplete?: GenericCompletionConfig) {
// TODO: fill variables here
const data = environment?.data ?? {};
const options = Object.keys(data).map(key => ({ name: key }));
const completions = twigCompletion({ options });
export function twig(
base: LanguageSupport,
environment: Environment | null,
autocomplete?: GenericCompletionConfig,
) {
const variables = environment?.variables ?? [];
const completions = twigCompletion({ options: variables });
const language = mixLanguage(base);
const completion = language.data.of({ autocomplete: completions });

View File

@@ -1,5 +1,5 @@
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 { useDrag, useDrop } from 'react-dnd';
import { v4 as uuid } from 'uuid';
@@ -24,7 +24,8 @@ export type PairEditorProps = {
valueValidate?: InputProps['validate'];
};
type Pair = {
export type Pair = {
id?: string;
enabled?: boolean;
name: string;
value: string;
@@ -342,6 +343,8 @@ const FormRow = memo(function FormRow({
);
});
const newPairContainer = (pair?: Pair): PairContainer => {
return { pair: pair ?? { name: '', value: '', enabled: true }, id: uuid() };
const newPairContainer = (initialPair?: Pair): PairContainer => {
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;
}
export interface HttpHeader {
export interface EnvironmentVariable {
name: string;
value: string;
enabled?: boolean;
@@ -32,7 +32,13 @@ export interface Environment extends BaseModel {
readonly workspaceId: string;
readonly model: 'environment';
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 {