This commit is contained in:
Julian Freeman
2026-04-19 08:55:44 -04:00
parent eb1d802f5b
commit e256e596c5
7 changed files with 161 additions and 80 deletions

View File

@@ -1,6 +1,7 @@
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
/target/ /target/
/target*/
# Generated by Tauri # Generated by Tauri
# will have schema files for capabilities auto-completion # will have schema files for capabilities auto-completion

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Instant; use std::time::Instant;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tauri::State;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct HttpResponse { struct HttpResponse {
@@ -39,8 +40,13 @@ struct AuthConfig {
api_key: ApiKeyAuth, api_key: ApiKeyAuth,
} }
struct AppState {
client: reqwest::Client,
}
#[tauri::command] #[tauri::command]
async fn execute_request( async fn execute_request(
state: State<'_, AppState>,
method: String, method: String,
url: String, url: String,
headers: HashMap<String, String>, headers: HashMap<String, String>,
@@ -48,14 +54,8 @@ async fn execute_request(
query_params: Option<HashMap<String, String>>, query_params: Option<HashMap<String, String>>,
auth: Option<AuthConfig>, auth: Option<AuthConfig>,
) -> Result<HttpResponse, String> { ) -> Result<HttpResponse, String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
let req_method = method.parse::<reqwest::Method>().map_err(|e| e.to_string())?; let req_method = method.parse::<reqwest::Method>().map_err(|e| e.to_string())?;
let mut request_builder = state.client.request(req_method, &url);
let mut request_builder = client.request(req_method, &url);
// Add Query Params // Add Query Params
if let Some(params) = query_params { if let Some(params) = query_params {
@@ -126,9 +126,15 @@ async fn execute_request(
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("failed to create HTTP client");
tauri::Builder::default() tauri::Builder::default()
.manage(AppState { client })
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![execute_request]) .invoke_handler(tauri::generate_handler![execute_request])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -9,6 +9,14 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const updateRow = (index: number, key: keyof KeyValue, value: string | boolean) => {
const newData = props.modelValue.map((item, itemIndex) => {
if (itemIndex !== index) return item;
return { ...item, [key]: value };
});
emit('update:modelValue', newData);
};
const addRow = () => { const addRow = () => {
const newData = [...props.modelValue, { const newData = [...props.modelValue, {
id: crypto.randomUUID(), id: crypto.randomUUID(),
@@ -47,14 +55,16 @@ const removeRow = (index: number) => {
<td class="py-1 pl-2 align-middle"> <td class="py-1 pl-2 align-middle">
<input <input
type="checkbox" type="checkbox"
v-model="item.enabled" :checked="item.enabled"
@change="updateRow(index, 'enabled', ($event.target as HTMLInputElement).checked)"
class="rounded bg-slate-800 border-slate-700 text-indigo-500 focus:ring-indigo-500/50 focus:ring-offset-0" class="rounded bg-slate-800 border-slate-700 text-indigo-500 focus:ring-indigo-500/50 focus:ring-offset-0"
> >
</td> </td>
<td class="py-1 pr-2 align-middle"> <td class="py-1 pr-2 align-middle">
<input <input
type="text" type="text"
v-model="item.key" :value="item.key"
@input="updateRow(index, 'key', ($event.target as HTMLInputElement).value)"
placeholder="Key" placeholder="Key"
class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors" class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors"
> >
@@ -62,7 +72,8 @@ const removeRow = (index: number) => {
<td class="py-1 pr-2 align-middle"> <td class="py-1 pr-2 align-middle">
<input <input
type="text" type="text"
v-model="item.value" :value="item.value"
@input="updateRow(index, 'value', ($event.target as HTMLInputElement).value)"
placeholder="Value" placeholder="Value"
class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors" class="w-full bg-transparent border-b border-transparent hover:border-slate-700 focus:border-indigo-500 focus:outline-none py-1 px-1 text-sm text-slate-300 transition-colors"
> >

View File

@@ -37,7 +37,7 @@ const extensions = computed(() => {
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
const executeRequest = async () => { const executeRequest = async () => {
if (!store.activeRequest.url) return; if (isLoading.value || !store.activeRequest.url) return;
isLoading.value = true; isLoading.value = true;
@@ -73,7 +73,6 @@ const executeRequest = async () => {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
// Simple alert for now, or could use a toast
alert('Request Failed: ' + error); alert('Request Failed: ' + error);
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View File

@@ -12,6 +12,23 @@ const store = useRequestStore();
const settings = useSettingsStore(); const settings = useSettingsStore();
const isCopied = ref(false); const isCopied = ref(false);
const isJsonResponse = computed(() => {
const response = store.activeRequest.response;
if (!response) return false;
const contentType = response.headers['content-type'] || response.headers['Content-Type'] || '';
if (contentType.toLowerCase().includes('json')) {
return true;
}
try {
JSON.parse(response.body);
return true;
} catch {
return false;
}
});
const extensions = computed(() => { const extensions = computed(() => {
const theme = EditorView.theme({ const theme = EditorView.theme({
"&": { "&": {
@@ -26,15 +43,20 @@ const extensions = computed(() => {
} }
}); });
return [json(), oneDark, theme, EditorView.editable.of(false)]; return [
oneDark,
theme,
EditorView.editable.of(false),
...(isJsonResponse.value ? [json()] : []),
];
}); });
const formattedBody = computed(() => { const formattedBody = computed(() => {
if (!store.activeRequest.response) return ''; if (!store.activeRequest.response) return '';
if (!isJsonResponse.value) return store.activeRequest.response.body;
try { try {
// Auto pretty print
return JSON.stringify(JSON.parse(store.activeRequest.response.body), null, 2); return JSON.stringify(JSON.parse(store.activeRequest.response.body), null, 2);
} catch (e) { } catch {
return store.activeRequest.response.body; return store.activeRequest.response.body;
} }
}); });

View File

@@ -68,7 +68,6 @@ const displayFontOptions = computed(() => {
placeholder="Select or enter font family" placeholder="Select or enter font family"
/> />
<input <input
v-if="!fontOptions.includes(settings.editorFontFamily)"
type="text" type="text"
v-model="settings.editorFontFamily" v-model="settings.editorFontFamily"
placeholder="e.g. 'JetBrains Mono', monospace" placeholder="e.g. 'JetBrains Mono', monospace"

View File

@@ -33,34 +33,110 @@ export interface RequestData {
timestamp: number; timestamp: number;
} }
const createEmptyKeyValue = (): KeyValue => ({
id: crypto.randomUUID(),
key: '',
value: '',
enabled: true,
});
const createDefaultAuthState = (): AuthState => ({
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' },
});
const createDefaultRequest = (): RequestData => ({
id: crypto.randomUUID(),
method: 'GET',
url: '',
params: [createEmptyKeyValue()],
headers: [createEmptyKeyValue()],
body: '',
auth: createDefaultAuthState(),
timestamp: Date.now(),
});
const cloneKeyValueList = (items: KeyValue[] | undefined): KeyValue[] => {
if (!Array.isArray(items) || items.length === 0) return [];
return items.map((item) => ({
id: item.id || crypto.randomUUID(),
key: item.key || '',
value: item.value || '',
enabled: item.enabled ?? true,
}));
};
const cloneAuthState = (auth: Partial<AuthState> | undefined): AuthState => ({
type: auth?.type || 'none',
basic: {
username: auth?.basic?.username || '',
password: auth?.basic?.password || '',
},
bearer: {
token: auth?.bearer?.token || '',
},
apiKey: {
key: auth?.apiKey?.key || '',
value: auth?.apiKey?.value || '',
addTo: auth?.apiKey?.addTo || 'header',
},
});
const cloneRequest = (req: Partial<RequestData>): RequestData => ({
id: typeof req.id === 'string' && req.id ? req.id : crypto.randomUUID(),
method: req.method || 'GET',
url: req.url || '',
params: cloneKeyValueList(req.params),
headers: cloneKeyValueList(req.headers),
body: req.body || '',
auth: cloneAuthState(req.auth),
response: req.response
? {
status: req.response.status,
headers: { ...req.response.headers },
body: req.response.body,
time: req.response.time,
size: req.response.size,
}
: undefined,
timestamp: typeof req.timestamp === 'number' ? req.timestamp : Date.now(),
});
const normalizeHistoryItem = (item: Partial<RequestData>): RequestData => {
const cloned = cloneRequest(item);
return {
id: cloned.id,
method: cloned.method,
url: cloned.url,
params: cloned.params.length > 0
? cloned.params
: [createEmptyKeyValue()],
headers: cloned.headers.length > 0
? cloned.headers
: [createEmptyKeyValue()],
body: cloned.body,
auth: cloned.auth,
response: cloned.response,
timestamp: cloned.timestamp,
};
};
export const useRequestStore = defineStore('request', () => { export const useRequestStore = defineStore('request', () => {
// State // State
const history = ref<RequestData[]>([]); const history = ref<RequestData[]>([]);
const activeRequest = ref<RequestData>({ const activeRequest = ref<RequestData>(createDefaultRequest());
id: crypto.randomUUID(),
method: 'GET',
url: '',
params: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
headers: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
body: '',
auth: {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
},
timestamp: Date.now(),
});
// Load history from local storage // Load history from local storage
const savedHistory = localStorage.getItem('request_history'); const savedHistory = localStorage.getItem('request_history');
if (savedHistory) { if (savedHistory) {
try { try {
history.value = JSON.parse(savedHistory); const parsed = JSON.parse(savedHistory);
history.value = Array.isArray(parsed)
? parsed.map((item) => normalizeHistoryItem(item))
: [];
} catch (e) { } catch (e) {
console.error('Failed to parse history', e); console.error('Failed to parse history', e);
} }
@@ -68,13 +144,12 @@ export const useRequestStore = defineStore('request', () => {
// Actions // Actions
const addToHistory = (req: RequestData) => { const addToHistory = (req: RequestData) => {
// Add to beginning, limit to 50 const newEntry = cloneRequest(req);
const newEntry = { ...req, timestamp: Date.now() }; newEntry.id = crypto.randomUUID();
// Don't store the empty trailing rows in history to save space? newEntry.timestamp = Date.now();
// Actually keep them for consistency or filter them. Let's filter empty keys. newEntry.params = newEntry.params.filter((p) => p.key.trim() !== '');
newEntry.params = newEntry.params.filter(p => p.key.trim() !== ''); newEntry.headers = newEntry.headers.filter((h) => h.key.trim() !== '');
newEntry.headers = newEntry.headers.filter(h => h.key.trim() !== '');
history.value.unshift(newEntry); history.value.unshift(newEntry);
if (history.value.length > 50) { if (history.value.length > 50) {
history.value.pop(); history.value.pop();
@@ -93,46 +168,14 @@ export const useRequestStore = defineStore('request', () => {
}; };
const resetActiveRequest = () => { const resetActiveRequest = () => {
activeRequest.value = { activeRequest.value = createDefaultRequest();
id: crypto.randomUUID(),
method: 'GET',
url: '',
params: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
headers: [
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
body: '',
auth: {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
},
timestamp: Date.now(),
};
}; };
const loadRequest = (req: RequestData) => { const loadRequest = (req: RequestData) => {
// Deep copy const loaded = normalizeHistoryItem(cloneRequest(req));
const loaded = JSON.parse(JSON.stringify(req));
loaded.id = crypto.randomUUID(); loaded.id = crypto.randomUUID();
// loaded.response = undefined; // Keep response for history viewing if (loaded.params.length === 0) loaded.params.push(createEmptyKeyValue());
if (loaded.headers.length === 0) loaded.headers.push(createEmptyKeyValue());
// Ensure at least one empty row for editing
if (loaded.params.length === 0) loaded.params.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
if (loaded.headers.length === 0) loaded.headers.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
// Ensure auth object exists (migration for old history)
if (!loaded.auth) {
loaded.auth = {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
};
}
activeRequest.value = loaded; activeRequest.value = loaded;
}; };