Download response, and some fixes

This commit is contained in:
Gregory Schier
2024-01-16 17:02:55 -08:00
parent e196f1d98e
commit e459674338
12 changed files with 2650 additions and 4804 deletions

View File

@@ -24,7 +24,7 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa
base64 = "0.21.0"
boa_engine = { version = "0.17.3", features = ["annex-b"] }
boa_runtime = { version = "0.17.3" }
chrono = { version = "0.4.23", features = ["serde"] }
chrono = { version = "0.4.31", features = ["serde"] }
futures = "0.3.26"
http = "0.2.8"
rand = "0.8.5"

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
use std::fs;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::PathBuf;
use std::time::Duration;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use log::warn;
use http::header::{ACCEPT, USER_AGENT};
use log::{info, warn};
use reqwest::multipart;
use reqwest::redirect::Policy;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite};
use sqlx::types::Json;
use tauri::{AppHandle, Wry};
use crate::{emit_side_effect, models, render, response_err};
@@ -21,6 +22,7 @@ pub async fn send_http_request(
environment_id: &str,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
download_path: Option<PathBuf>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok();
@@ -299,6 +301,16 @@ pub async fn send_http_request(
if !request.id.is_empty() {
emit_side_effect(app_handle, "updated_model", &response);
}
// Copy response to download path, if specified
match (download_path, response.body_path.clone()) {
(Some(dl_path), Some(body_path)) => {
info!("Downloading response body to {}", dl_path.display());
fs::copy(body_path, dl_path).expect("Failed to copy file for response download");
}
_ => {}
};
Ok(response)
}
Err(e) => {

View File

@@ -9,7 +9,7 @@ extern crate objc;
use std::collections::HashMap;
use std::env::current_dir;
use std::fs::{create_dir_all, read_to_string, File};
use std::fs::{create_dir_all, File, read_to_string};
use std::process::exit;
use fern::colors::ColoredLevelConfig;
@@ -17,13 +17,13 @@ use log::{debug, info, warn};
use rand::random;
use serde::Serialize;
use serde_json::Value;
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
use tauri::{Manager, WindowEvent};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri_plugin_log::{fern, LogTarget};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use tokio::sync::Mutex;
@@ -84,7 +84,7 @@ async fn send_ephemeral_request(
let response = models::HttpResponse::new();
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
request.id = "".to_string();
send_http_request(request, &response, &environment_id2, &app_handle, pool).await
send_http_request(request, &response, &environment_id2, &app_handle, pool, None).await
}
#[tauri::command]
@@ -119,10 +119,9 @@ async fn filter_response(
};
let body = read_to_string(response.body_path.unwrap()).unwrap();
let filter_result =
plugin::run_plugin_filter(&window.app_handle(), plugin_name, filter, &body)
.await
.expect("Failed to run filter");
let filter_result = plugin::run_plugin_filter(&window.app_handle(), plugin_name, filter, &body)
.await
.expect("Failed to run filter");
Ok(filter_result.filtered)
}
@@ -220,6 +219,7 @@ async fn send_request(
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str,
environment_id: Option<&str>,
download_dir: Option<&str>,
) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await;
@@ -236,9 +236,15 @@ async fn send_request(
let app_handle2 = window.app_handle().clone();
let pool2 = pool.clone();
let download_path = if let Some(p) = download_dir {
Some(std::path::Path::new(p).to_path_buf())
} else {
None
};
tokio::spawn(async move {
if let Err(e) =
send_http_request(req, &response2, &environment_id2, &app_handle2, &pool2).await
send_http_request(req, &response2, &environment_id2, &app_handle2, &pool2, download_path).await
{
response_err(&response2, e, &app_handle2, &pool2)
.await

View File

@@ -58,7 +58,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
? {
key: 'edit',
label: 'Manage Environments',
hotkeyAction: 'environmentEditor.toggle',
hotKeyAction: 'environmentEditor.toggle',
leftSlot: <Icon icon="box" />,
onSelect: showEnvironmentDialog,
}

View File

@@ -63,7 +63,7 @@ export function SettingsDropdown() {
{
key: 'hotkeys',
label: 'Keyboard shortcuts',
hotkeyAction: 'hotkeys.showHelp',
hotKeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />,
onSelect: () => {
dialog.show({
@@ -77,7 +77,7 @@ export function SettingsDropdown() {
{
key: 'settings',
label: 'Settings',
hotkeyAction: 'settings.show',
hotKeyAction: 'settings.show',
leftSlot: <Icon icon="settings" />,
onSelect: () => {
dialog.show({

View File

@@ -23,6 +23,7 @@ import { useLatestResponse } from '../hooks/useLatestResponse';
import { usePrompt } from '../hooks/usePrompt';
import { useRequests } from '../hooks/useRequests';
import { useSendManyRequests } from '../hooks/useSendFolder';
import { useSendRequest } from '../hooks/useSendRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
@@ -61,6 +62,7 @@ export function Sidebar({ className }: Props) {
const folders = useFolders();
const deleteAnyRequest = useDeleteAnyRequest();
const activeWorkspace = useActiveWorkspace();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const routes = useAppRoutes();
const [hasFocus, setHasFocus] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -76,6 +78,10 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC,
});
useHotKey('request.duplicate', () => {
duplicateRequest.mutate();
});
const isCollapsed = useCallback(
(id: string) => collapsed.value?.[id] ?? false,
[collapsed.value],
@@ -517,18 +523,20 @@ const SidebarItem = forwardRef(function SidebarItem(
}: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>,
) {
const activeRequest = useActiveRequest();
const createRequest = useCreateRequest();
const createFolder = useCreateFolder();
const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(itemId);
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
const sendRequest = useSendRequest(itemId);
const sendAndDownloadRequest = useSendRequest(itemId, { download: true });
const sendManyRequests = useSendManyRequests();
const latestResponse = useLatestResponse(itemId);
const updateRequest = useUpdateRequest(itemId);
const updateAnyFolder = useUpdateAnyFolder();
const prompt = usePrompt();
const [editing, setEditing] = useState<boolean>(false);
const activeRequest = useActiveRequest();
const isActive = activeRequest?.id === itemId;
const handleSubmitNameEdit = useCallback(
@@ -599,7 +607,6 @@ const SidebarItem = forwardRef(function SidebarItem(
leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
},
{ type: 'separator', label: itemName },
{
key: 'rename',
label: 'Rename',
@@ -641,15 +648,29 @@ const SidebarItem = forwardRef(function SidebarItem(
},
]
: [
{
key: 'sendRequest',
label: 'Send',
hotKeyAction: 'request.send',
hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="sendHorizontal" />,
onSelect: () => sendRequest.mutate(),
},
{
key: 'sendAndDownloadRequest',
label: 'Send and Download',
leftSlot: <Icon icon="download" />,
onSelect: () => sendAndDownloadRequest.mutate(),
},
{ type: 'separator' },
{
key: 'duplicateRequest',
label: 'Duplicate',
hotkeyAction: 'request.duplicate',
hotKeyAction: 'request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => {
if (activeRequest?.id === itemId) {
duplicateRequest.mutate();
}
duplicateRequest.mutate();
},
},
{

View File

@@ -26,7 +26,7 @@ export const SidebarActions = memo(function SidebarActions() {
{
key: 'create-request',
label: 'New Request',
hotkeyAction: 'request.create',
hotKeyAction: 'request.create',
onSelect: () => createRequest.mutate({}),
},
{

View File

@@ -37,7 +37,8 @@ export type DropdownItemDefault = {
key: string;
type?: 'default';
label: ReactNode;
hotkeyAction?: HotkeyAction;
hotKeyAction?: HotkeyAction;
hotKeyLabelOnly?: boolean;
variant?: 'danger';
disabled?: boolean;
hidden?: boolean;
@@ -338,12 +339,13 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
<>
{items.map(
(item) =>
item.type !== 'separator' && (
item.type !== 'separator' &&
!item.hotKeyLabelOnly && (
<MenuItemHotKey
key={item.key}
onSelect={handleSelect}
item={item}
action={item.hotkeyAction}
action={item.hotKeyAction}
/>
),
)}
@@ -440,7 +442,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
[focused],
);
const rightSlot = item.rightSlot ?? <HotKey action={item.hotkeyAction ?? null} />;
const rightSlot = item.rightSlot ?? <HotKey action={item.hotKeyAction ?? null} />;
return (
<Button

View File

@@ -121,6 +121,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
ref={wrapperRef}
className={classNames(
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'flex items-center gap-2',
labelPosition === 'top' && 'flex-row gap-0.5',
)}

View File

@@ -1,15 +1,40 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { save } from '@tauri-apps/api/dialog';
import slugify from 'slugify';
import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '../lib/models';
import { getRequest } from '../lib/store';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useAlert } from './useAlert';
export function useSendAnyRequest() {
export function useSendAnyRequest(options: { download?: boolean } = {}) {
const environmentId = useActiveEnvironmentId();
const alert = useAlert();
return useMutation<HttpResponse, string, string | null>({
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
return useMutation<HttpResponse | null, string, string | null>({
mutationFn: async (id) => {
const request = await getRequest(id);
if (request == null) {
return null;
}
let downloadDir: string | null = null;
if (options.download) {
downloadDir = await save({
title: 'Select Download Destination',
defaultPath: slugify(request.name, { lower: true, trim: true, strict: true }),
});
if (downloadDir == null) {
return null;
}
}
return invoke('send_request', {
requestId: id,
environmentId,
downloadDir: downloadDir,
});
},
onSettled: () => trackEvent('HttpRequest', 'Send'),
onError: (err) => alert({ title: 'Export Failed', body: err }),
});

View File

@@ -2,9 +2,9 @@ import { useMutation } from '@tanstack/react-query';
import type { HttpResponse } from '../lib/models';
import { useSendAnyRequest } from './useSendAnyRequest';
export function useSendRequest(id: string | null) {
const sendAnyRequest = useSendAnyRequest();
return useMutation<HttpResponse, string>({
export function useSendRequest(id: string | null, options: { download?: boolean } = {}) {
const sendAnyRequest = useSendAnyRequest(options);
return useMutation<HttpResponse | null, string>({
mutationFn: () => sendAnyRequest.mutateAsync(id),
});
}