Fix performance related to having 100s of requests (#123)

This commit is contained in:
Gregory Schier
2024-10-08 15:16:57 -06:00
committed by GitHub
parent 4b7712df80
commit c7eccddac9
34 changed files with 456 additions and 423 deletions
+22 -9
View File
@@ -46,12 +46,25 @@ use yaak_models::models::{
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace, GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace,
}; };
use yaak_models::queries::{cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_plugin, delete_workspace, duplicate_grpc_request, duplicate_http_request, generate_id, generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace}; use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
delete_all_grpc_connections, delete_all_http_responses_for_request, delete_cookie_jar,
delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request,
delete_http_request, delete_http_response, delete_plugin, delete_workspace,
duplicate_grpc_request, duplicate_http_request, generate_id, generate_model_id, get_cookie_jar,
get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request,
get_http_response, get_key_value_raw, get_or_create_settings, get_plugin, get_workspace,
list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events,
list_grpc_requests, list_http_requests, list_http_responses, list_http_responses_for_request,
list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
};
use yaak_plugin_runtime::events::{ use yaak_plugin_runtime::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse, BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, Icon, GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, Icon,
InternalEvent, InternalEventPayload, RenderHttpRequestResponse, RenderPurpose, InternalEvent, InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse,
SendHttpRequestResponse, PromptTextResponse, ShowToastRequest, TemplateRenderResponse, RenderPurpose, SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse,
WindowContext, WindowContext,
}; };
use yaak_plugin_runtime::plugin_handle::PluginHandle; use yaak_plugin_runtime::plugin_handle::PluginHandle;
@@ -1454,10 +1467,10 @@ async fn cmd_delete_environment(
#[tauri::command] #[tauri::command]
async fn cmd_list_grpc_connections( async fn cmd_list_grpc_connections(
request_id: &str, workspace_id: &str,
w: WebviewWindow, w: WebviewWindow,
) -> Result<Vec<GrpcConnection>, String> { ) -> Result<Vec<GrpcConnection>, String> {
list_grpc_connections(&w, request_id) list_grpc_connections(&w, workspace_id)
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@@ -1604,11 +1617,11 @@ async fn cmd_get_workspace(id: &str, w: WebviewWindow) -> Result<Workspace, Stri
#[tauri::command] #[tauri::command]
async fn cmd_list_http_responses( async fn cmd_list_http_responses(
request_id: &str, workspace_id: &str,
limit: Option<i64>, limit: Option<i64>,
w: WebviewWindow, w: WebviewWindow,
) -> Result<Vec<HttpResponse>, String> { ) -> Result<Vec<HttpResponse>, String> {
list_http_responses(&w, request_id, limit) list_http_responses(&w, workspace_id, limit)
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@@ -1636,7 +1649,7 @@ async fn cmd_delete_all_grpc_connections(request_id: &str, w: WebviewWindow) ->
#[tauri::command] #[tauri::command]
async fn cmd_delete_all_http_responses(request_id: &str, w: WebviewWindow) -> Result<(), String> { async fn cmd_delete_all_http_responses(request_id: &str, w: WebviewWindow) -> Result<(), String> {
delete_all_http_responses(&w, request_id) delete_all_http_responses_for_request(&w, request_id)
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@@ -2215,7 +2228,7 @@ async fn handle_plugin_event<R: Runtime>(
Some(InternalEventPayload::PromptTextResponse(resp)) Some(InternalEventPayload::PromptTextResponse(resp))
} }
InternalEventPayload::FindHttpResponsesRequest(req) => { InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses( let http_responses = list_http_responses_for_request(
app_handle, app_handle,
req.request_id.as_str(), req.request_id.as_str(),
req.limit.map(|l| l as i64), req.limit.map(|l| l as i64),
+67 -16
View File
@@ -1,7 +1,12 @@
use std::fs; use std::fs;
use crate::error::Result; use crate::error::Result;
use crate::models::{CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden}; use crate::models::{
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest,
HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden,
ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden,
};
use crate::plugin::SqliteConnection; use crate::plugin::SqliteConnection;
use log::{debug, error}; use log::{debug, error};
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
@@ -12,6 +17,9 @@ use sea_query_rusqlite::RusqliteBinder;
use serde::Serialize; use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow}; use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
const MAX_GRPC_CONNECTIONS_PER_REQUEST: usize = 20;
const MAX_HTTP_RESPONSES_PER_REQUEST: usize = MAX_GRPC_CONNECTIONS_PER_REQUEST;
pub async fn set_key_value_string<R: Runtime>( pub async fn set_key_value_string<R: Runtime>(
mgr: &WebviewWindow<R>, mgr: &WebviewWindow<R>,
namespace: &str, namespace: &str,
@@ -423,6 +431,13 @@ pub async fn upsert_grpc_connection<R: Runtime>(
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
connection: &GrpcConnection, connection: &GrpcConnection,
) -> Result<GrpcConnection> { ) -> Result<GrpcConnection> {
let connections =
list_http_responses_for_request(window, connection.request_id.as_str(), None).await?;
for c in connections.iter().skip(MAX_GRPC_CONNECTIONS_PER_REQUEST - 1) {
debug!("Deleting old grpc connection {}", c.id);
delete_grpc_connection(window, c.id.as_str()).await?;
}
let id = match connection.id.as_str() { let id = match connection.id.as_str() {
"" => generate_model_id(ModelType::TypeGrpcConnection), "" => generate_model_id(ModelType::TypeGrpcConnection),
_ => connection.id.to_string(), _ => connection.id.to_string(),
@@ -497,6 +512,24 @@ pub async fn get_grpc_connection<R: Runtime>(
} }
pub async fn list_grpc_connections<R: Runtime>( pub async fn list_grpc_connections<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<GrpcConnection>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(GrpcConnectionIden::Table)
.cond_where(Expr::col(GrpcConnectionIden::WorkspaceId).eq(workspace_id))
.column(Asterisk)
.order_by(GrpcConnectionIden::CreatedAt, Order::Desc)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn list_grpc_connections_for_request<R: Runtime>(
mgr: &impl Manager<R>, mgr: &impl Manager<R>,
request_id: &str, request_id: &str,
) -> Result<Vec<GrpcConnection>> { ) -> Result<Vec<GrpcConnection>> {
@@ -536,7 +569,7 @@ pub async fn delete_all_grpc_connections<R: Runtime>(
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
request_id: &str, request_id: &str,
) -> Result<()> { ) -> Result<()> {
for r in list_grpc_connections(window, request_id).await? { for r in list_grpc_connections_for_request(window, request_id).await? {
delete_grpc_connection(window, &r.id).await?; delete_grpc_connection(window, &r.id).await?;
} }
Ok(()) Ok(())
@@ -794,10 +827,7 @@ pub async fn update_settings<R: Runtime>(
SettingsIden::EditorSoftWrap, SettingsIden::EditorSoftWrap,
settings.editor_soft_wrap.into(), settings.editor_soft_wrap.into(),
), ),
( (SettingsIden::Telemetry, settings.telemetry.into()),
SettingsIden::Telemetry,
settings.telemetry.into(),
),
( (
SettingsIden::OpenWorkspaceNewWindow, SettingsIden::OpenWorkspaceNewWindow,
settings.open_workspace_new_window.into(), settings.open_workspace_new_window.into(),
@@ -872,10 +902,7 @@ pub async fn get_environment<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Res
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?) Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
} }
pub async fn get_plugin<R: Runtime>( pub async fn get_plugin<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Plugin> {
mgr: &impl Manager<R>,
id: &str
) -> Result<Plugin> {
let dbm = &*mgr.state::<SqliteConnection>(); let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap(); let db = dbm.0.lock().await.get().unwrap();
@@ -888,9 +915,7 @@ pub async fn get_plugin<R: Runtime>(
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?) Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
} }
pub async fn list_plugins<R: Runtime>( pub async fn list_plugins<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Plugin>> {
mgr: &impl Manager<R>,
) -> Result<Vec<Plugin>> {
let dbm = &*mgr.state::<SqliteConnection>(); let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap(); let db = dbm.0.lock().await.get().unwrap();
@@ -1185,7 +1210,7 @@ pub async fn delete_http_request<R: Runtime>(
let req = get_http_request(window, id).await?; let req = get_http_request(window, id).await?;
// DB deletes will cascade but this will delete the files // DB deletes will cascade but this will delete the files
delete_all_http_responses(window, id).await?; delete_all_http_responses_for_request(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>(); let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap(); let db = dbm.0.lock().await.get().unwrap();
@@ -1234,6 +1259,12 @@ pub async fn create_http_response<R: Runtime>(
version: Option<&str>, version: Option<&str>,
remote_addr: Option<&str>, remote_addr: Option<&str>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let responses = list_http_responses_for_request(window, request_id, None).await?;
for response in responses.iter().skip(MAX_HTTP_RESPONSES_PER_REQUEST - 1) {
debug!("Deleting old response {}", response.id);
delete_http_response(window, response.id.as_str()).await?;
}
let req = get_http_request(window, request_id).await?; let req = get_http_request(window, request_id).await?;
let id = generate_model_id(ModelType::TypeHttpResponse); let id = generate_model_id(ModelType::TypeHttpResponse);
let dbm = &*window.app_handle().state::<SqliteConnection>(); let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -1418,17 +1449,37 @@ pub async fn delete_http_response<R: Runtime>(
emit_deleted_model(window, resp) emit_deleted_model(window, resp)
} }
pub async fn delete_all_http_responses<R: Runtime>( pub async fn delete_all_http_responses_for_request<R: Runtime>(
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
request_id: &str, request_id: &str,
) -> Result<()> { ) -> Result<()> {
for r in list_http_responses(window, request_id, None).await? { for r in list_http_responses_for_request(window, request_id, None).await? {
delete_http_response(window, &r.id).await?; delete_http_response(window, &r.id).await?;
} }
Ok(()) Ok(())
} }
pub async fn list_http_responses<R: Runtime>( pub async fn list_http_responses<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
limit: Option<i64>,
) -> Result<Vec<HttpResponse>> {
let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX);
let dbm = mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(HttpResponseIden::Table)
.cond_where(Expr::col(HttpResponseIden::WorkspaceId).eq(workspace_id))
.column(Asterisk)
.order_by(HttpResponseIden::CreatedAt, Order::Desc)
.limit(limit_unwrapped as u64)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn list_http_responses_for_request<R: Runtime>(
mgr: &impl Manager<R>, mgr: &impl Manager<R>,
request_id: &str, request_id: &str,
limit: Option<i64>, limit: Option<i64>,
+3 -1
View File
@@ -13,8 +13,10 @@ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
retry: false, retry: false,
refetchOnWindowFocus: true,
networkMode: 'offlineFirst', networkMode: 'offlineFirst',
refetchOnWindowFocus: true,
refetchOnReconnect: false,
refetchOnMount: false, // Don't refetch when a hook mounts
}, },
}, },
}); });
+10
View File
@@ -1,6 +1,10 @@
import { lazy } from 'react'; import { lazy } from 'react';
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom'; import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes'; import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { DefaultLayout } from './DefaultLayout'; import { DefaultLayout } from './DefaultLayout';
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace'; import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
import RouteError from './RouteError'; import RouteError from './RouteError';
@@ -50,6 +54,12 @@ const router = createBrowserRouter([
]); ]);
export function AppRouter() { export function AppRouter() {
// Add some global hooks that should remain persistent
useSyncModelStores();
useSyncZoomSetting();
useSyncFontSizeSetting();
useGenerateThemeCss();
return <RouterProvider router={router} />; return <RouterProvider router={router} />;
} }
+1 -1
View File
@@ -12,7 +12,7 @@ interface Props {
export const CookieDialog = function ({ cookieJarId }: Props) { export const CookieDialog = function ({ cookieJarId }: Props) {
const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null); const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null);
const cookieJars = useCookieJars().data ?? []; const cookieJars = useCookieJars();
const cookieJar = cookieJars.find((c) => c.id === cookieJarId); const cookieJar = cookieJars.find((c) => c.id === cookieJarId);
if (cookieJar == null) { if (cookieJar == null) {
+1 -1
View File
@@ -12,7 +12,7 @@ import { InlineCode } from './core/InlineCode';
import { useDialog } from './DialogContext'; import { useDialog } from './DialogContext';
export function CookieDropdown() { export function CookieDropdown() {
const cookieJars = useCookieJars().data ?? []; const cookieJars = useCookieJars();
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar(); const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null); const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null); const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
+1 -1
View File
@@ -2,8 +2,8 @@ import classNames from 'classnames';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { useOsInfo } from '../hooks/useOsInfo'; import { useOsInfo } from '../hooks/useOsInfo';
import { DialogProvider, Dialogs } from './DialogContext'; import { DialogProvider, Dialogs } from './DialogContext';
import { GlobalHooks } from './GlobalHooks';
import { ToastProvider, Toasts } from './ToastContext'; import { ToastProvider, Toasts } from './ToastContext';
import { GlobalHooks } from './GlobalHooks';
export function DefaultLayout() { export function DefaultLayout() {
const osInfo = useOsInfo(); const osInfo = useOsInfo();
+3 -197
View File
@@ -1,50 +1,17 @@
import { useQueryClient } from '@tanstack/react-query';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { AnyModel } from '@yaakapp-internal/models';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin'; import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useSetAtom } from 'jotai'; import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useEffect } from 'react';
import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { useCopy } from '../hooks/useCopy';
import { environmentsAtom } from '../hooks/useEnvironments';
import { foldersQueryKey } from '../hooks/useFolders';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast'; import { useNotificationToast } from '../hooks/useNotificationToast';
import { pluginsAtom } from '../hooks/usePlugins';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars'; import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { settingsAtom, useSettings } from '../hooks/useSettings';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { workspacesAtom } from '../hooks/useWorkspaces';
import { useZoom } from '../hooks/useZoom';
import { extractKeyValue } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util';
import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin';
import { githubLight } from '../lib/theme/themes/github';
import { hotdogStandDefault } from '../lib/theme/themes/hotdog-stand';
import { monokaiProDefault } from '../lib/theme/themes/monokai-pro';
import { rosePineDefault } from '../lib/theme/themes/rose-pine';
import { yaakDark } from '../lib/theme/themes/yaak';
import { getThemeCSS } from '../lib/theme/window';
export interface ModelPayload {
model: AnyModel;
windowLabel: string;
}
export function GlobalHooks() { export function GlobalHooks() {
// Include here so they always update, even if no component references them // Include here so they always update, even if no component references them
@@ -52,136 +19,16 @@ export function GlobalHooks() {
useRecentEnvironments(); useRecentEnvironments();
useRecentCookieJars(); useRecentCookieJars();
useRecentRequests(); useRecentRequests();
useSyncWorkspaceChildModels();
// Other useful things // Other useful things
useNotificationToast(); useNotificationToast();
useActiveWorkspaceChangedToast(); useActiveWorkspaceChangedToast();
useEnsureActiveCookieJar(); useEnsureActiveCookieJar();
// TODO: Remove in future version
useMigrateActiveCookieJarId();
const toggleCommandPalette = useToggleCommandPalette(); const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette); useHotKey('command_palette.toggle', toggleCommandPalette);
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
const setSettings = useSetAtom(settingsAtom);
const setWorkspaces = useSetAtom(workspacesAtom);
const setPlugins = useSetAtom(pluginsAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
console.log('Upserted model', payload.model);
const { model, windowLabel } = payload;
const queryKey =
model.model === 'http_response'
? httpResponsesQueryKey(model)
: model.model === 'folder'
? foldersQueryKey(model)
: model.model === 'grpc_connection'
? grpcConnectionsQueryKey(model)
: model.model === 'grpc_event'
? grpcEventsQueryKey(model)
: model.model === 'key_value'
? keyValueQueryKey(model)
: model.model === 'cookie_jar'
? cookieJarsQueryKey(model)
: null;
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
wasUpdatedExternally(model.id);
}
const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes(
model.model,
);
if (shouldIgnoreModel(model, windowLabel)) return;
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_request') {
setGrpcRequests(updateModelList(model, pushToFront));
} else if (model.model === 'environment') {
setEnvironments(updateModelList(model, pushToFront));
} else if (model.model === 'settings') {
setSettings(model);
} else if (queryKey != null) {
// TODO: Convert all models to use Jotai
queryClient.setQueryData(queryKey, (current: unknown) => {
if (model.model === 'key_value') {
// Special-case for KeyValue
return extractKeyValue(model);
}
if (Array.isArray(current)) {
return updateModelList(model, pushToFront)(current);
}
});
}
});
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
const { model, windowLabel } = payload;
if (shouldIgnoreModel(model, windowLabel)) return;
console.log('Delete model', payload.model);
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
} else if (model.model === 'http_response') {
queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model));
} else if (model.model === 'folder') {
queryClient.setQueryData(foldersQueryKey(model), removeById(model));
} else if (model.model === 'environment') {
setEnvironments(removeById(model));
} else if (model.model === 'grpc_request') {
setGrpcRequests(removeById(model));
} else if (model.model === 'grpc_connection') {
queryClient.setQueryData(grpcConnectionsQueryKey(model), removeById(model));
} else if (model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model));
} else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), undefined);
} else if (model.model === 'cookie_jar') {
queryClient.setQueryData(cookieJarsQueryKey(model), undefined);
}
});
const settings = useSettings();
useEffect(() => {
if (settings == null) {
return;
}
const { interfaceScale, editorFontSize } = settings;
getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error);
document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`);
}, [settings]);
// Handle Zoom.
// Note, Mac handles it in the app menu, so need to also handle keyboard
// shortcuts for Windows/Linux
const zoom = useZoom();
useHotKey('app.zoom_in', zoom.zoomIn);
useListenToTauriEvent('zoom_in', zoom.zoomIn);
useHotKey('app.zoom_out', zoom.zoomOut);
useListenToTauriEvent('zoom_out', zoom.zoomOut);
useHotKey('app.zoom_reset', zoom.zoomReset);
useListenToTauriEvent('zoom_reset', zoom.zoomReset);
const prompt = usePrompt(); const prompt = usePrompt();
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>( useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
'show_prompt', 'show_prompt',
@@ -192,46 +39,5 @@ export function GlobalHooks() {
}, },
); );
const copy = useCopy();
useListenToTauriEvent('generate_theme_css', () => {
const themesCss = [
yaakDark,
monokaiProDefault,
rosePineDefault,
catppuccinMacchiato,
githubLight,
hotdogStandDefault,
]
.map(getThemeCSS)
.join('\n\n');
copy(themesCss);
});
return null; return null;
} }
function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
return (current: T[]): T[] => {
const index = current.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
return [...current.slice(0, index), model, ...current.slice(index + 1)];
} else {
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
}
};
}
function removeById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
}
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
if (windowLabel === getCurrentWebviewWindow().label) {
// Never ignore same-window updates
return false;
}
if (payload.model === 'key_value') {
return payload.namespace === 'no_sync';
}
return false;
};
+1 -1
View File
@@ -22,7 +22,7 @@ const emptyArray: string[] = [];
export function GrpcConnectionLayout({ style }: Props) { export function GrpcConnectionLayout({ style }: Props) {
const activeRequest = useActiveRequest('grpc_request'); const activeRequest = useActiveRequest('grpc_request');
const updateRequest = useUpdateAnyGrpcRequest(); const updateRequest = useUpdateAnyGrpcRequest();
const connections = useGrpcConnections(activeRequest?.id ?? null); const connections = useGrpcConnections().filter((c) => c.requestId === activeRequest?.id);
const activeConnection = connections[0] ?? null; const activeConnection = connections[0] ?? null;
const messages = useGrpcEvents(activeConnection?.id ?? null); const messages = useGrpcEvents(activeConnection?.id ?? null);
const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null); const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null);
+43 -20
View File
@@ -1,8 +1,10 @@
import type { import type {
AnyModel, AnyModel,
Folder, Folder,
GrpcConnection,
GrpcRequest, GrpcRequest,
HttpRequest, HttpRequest,
HttpResponse,
Workspace, Workspace,
} from '@yaakapp-internal/models'; } from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -22,18 +24,19 @@ import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest'; import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest'; import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
import { useFolders } from '../hooks/useFolders'; import { useFolders } from '../hooks/useFolders';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import type { CallableHttpRequestAction } from '../hooks/useHttpRequestActions';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useHttpResponses } from '../hooks/useHttpResponses';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace'; import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useRenameRequest } from '../hooks/useRenameRequest'; import { useRenameRequest } from '../hooks/useRenameRequest';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useScrollIntoView } from '../hooks/useScrollIntoView'; import { useScrollIntoView } from '../hooks/useScrollIntoView';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSendManyRequests } from '../hooks/useSendFolder'; import { useSendManyRequests } from '../hooks/useSendManyRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder'; import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
@@ -73,6 +76,9 @@ export function Sidebar({ className }: Props) {
const folders = useFolders(); const folders = useFolders();
const requests = useRequests(); const requests = useRequests();
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const httpRequestActions = useHttpRequestActions();
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
const duplicateHttpRequest = useDuplicateHttpRequest({ const duplicateHttpRequest = useDuplicateHttpRequest({
id: activeRequest?.id ?? null, id: activeRequest?.id ?? null,
navigateAfter: true, navigateAfter: true,
@@ -453,6 +459,9 @@ export function Sidebar({ className }: Props) {
selectedId={selectedId} selectedId={selectedId}
selectedTree={selectedTree} selectedTree={selectedTree}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
httpRequestActions={httpRequestActions}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
tree={tree} tree={tree}
focused={hasFocus} focused={hasFocus}
draggingId={draggingId} draggingId={draggingId}
@@ -483,6 +492,9 @@ interface SidebarItemsProps {
handleDragStart: (id: string) => void; handleDragStart: (id: string) => void;
onSelect: (requestId: string) => void; onSelect: (requestId: string) => void;
isCollapsed: (id: string) => boolean; isCollapsed: (id: string) => boolean;
httpRequestActions: CallableHttpRequestAction[];
httpResponses: HttpResponse[];
grpcConnections: GrpcConnection[];
} }
function SidebarItems({ function SidebarItems({
@@ -500,6 +512,9 @@ function SidebarItems({
handleEnd, handleEnd,
handleMove, handleMove,
handleDragStart, handleDragStart,
httpRequestActions,
httpResponses,
grpcConnections,
}: SidebarItemsProps) { }: SidebarItemsProps) {
return ( return (
<VStack <VStack
@@ -537,6 +552,11 @@ function SidebarItems({
/> />
) )
} }
httpRequestActions={httpRequestActions}
latestHttpResponse={httpResponses.find((r) => r.requestId === child.item.id) ?? null}
latestGrpcConnection={
grpcConnections.find((c) => c.requestId === child.item.id) ?? null
}
onMove={handleMove} onMove={handleMove}
onEnd={handleEnd} onEnd={handleEnd}
onSelect={onSelect} onSelect={onSelect}
@@ -549,20 +569,23 @@ function SidebarItems({
!isCollapsed(child.item.id) && !isCollapsed(child.item.id) &&
draggingId !== child.item.id && ( draggingId !== child.item.id && (
<SidebarItems <SidebarItems
treeParentMap={treeParentMap}
tree={child}
isCollapsed={isCollapsed}
draggingId={draggingId}
hoveredTree={hoveredTree}
hoveredIndex={hoveredIndex}
focused={focused}
activeId={activeId} activeId={activeId}
draggingId={draggingId}
focused={focused}
handleDragStart={handleDragStart}
handleEnd={handleEnd}
handleMove={handleMove}
hoveredIndex={hoveredIndex}
hoveredTree={hoveredTree}
httpRequestActions={httpRequestActions}
httpResponses={httpResponses}
grpcConnections={grpcConnections}
isCollapsed={isCollapsed}
onSelect={onSelect}
selectedId={selectedId} selectedId={selectedId}
selectedTree={selectedTree} selectedTree={selectedTree}
onSelect={onSelect} tree={child}
handleMove={handleMove} treeParentMap={treeParentMap}
handleEnd={handleEnd}
handleDragStart={handleDragStart}
/> />
)} )}
</SidebarItem> </SidebarItem>
@@ -590,7 +613,9 @@ type SidebarItemProps = {
onDragStart: (id: string) => void; onDragStart: (id: string) => void;
children?: ReactNode; children?: ReactNode;
child: TreeNode; child: TreeNode;
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect'>; latestHttpResponse: HttpResponse | null;
latestGrpcConnection: GrpcConnection | null;
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect' | 'httpRequestActions'>;
type DragItem = { type DragItem = {
id: string; id: string;
@@ -612,6 +637,9 @@ function SidebarItem({
selected, selected,
itemFallbackName, itemFallbackName,
useProminentStyles, useProminentStyles,
latestHttpResponse,
latestGrpcConnection,
httpRequestActions,
children, children,
}: SidebarItemProps) { }: SidebarItemProps) {
const ref = useRef<HTMLLIElement>(null); const ref = useRef<HTMLLIElement>(null);
@@ -659,14 +687,9 @@ function SidebarItem({
const renameRequest = useRenameRequest(itemId); const renameRequest = useRenameRequest(itemId);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true }); const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true }); const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const httpRequestActions = useHttpRequestActions();
const sendRequest = useSendAnyHttpRequest(); const sendRequest = useSendAnyHttpRequest();
const moveToWorkspace = useMoveToWorkspace(itemId); const moveToWorkspace = useMoveToWorkspace(itemId);
const sendManyRequests = useSendManyRequests(); const sendManyRequests = useSendManyRequests();
const latestHttpResponse = useLatestHttpResponse(itemModel === 'http_request' ? itemId : null);
const latestGrpcConnection = useLatestGrpcConnection(
itemModel === 'grpc_request' ? itemId : null,
);
const updateHttpRequest = useUpdateAnyHttpRequest(); const updateHttpRequest = useUpdateAnyHttpRequest();
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
const updateGrpcRequest = useUpdateAnyGrpcRequest(); const updateGrpcRequest = useUpdateAnyGrpcRequest();
+1 -1
View File
@@ -1,6 +1,6 @@
// Listen for settings changes, the re-compute theme // Listen for settings changes, the re-compute theme
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import type { ModelPayload } from './components/GlobalHooks'; import type { ModelPayload } from './hooks/useSyncModelStores';
import { getSettings } from './lib/store'; import { getSettings } from './lib/store';
function setFontSizeOnDocument(fontSize: number) { function setFontSizeOnDocument(fontSize: number) {
+5 -38
View File
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useCookieJars } from './useCookieJars'; import { useCookieJars } from './useCookieJars';
import { useKeyValue } from './useKeyValue';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id'; export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
@@ -11,9 +9,8 @@ export function useActiveCookieJar() {
const cookieJars = useCookieJars(); const cookieJars = useCookieJars();
const activeCookieJar = useMemo(() => { const activeCookieJar = useMemo(() => {
if (cookieJars.data == null) return undefined; return cookieJars.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null;
return cookieJars.data.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null; }, [activeCookieJarId, cookieJars]);
}, [activeCookieJarId, cookieJars.data]);
return [activeCookieJar ?? null, setActiveCookieJarId] as const; return [activeCookieJar ?? null, setActiveCookieJarId] as const;
} }
@@ -22,13 +19,11 @@ export function useEnsureActiveCookieJar() {
const cookieJars = useCookieJars(); const cookieJars = useCookieJars();
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId(); const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
useEffect(() => { useEffect(() => {
if (cookieJars.data == null) return; if (cookieJars.find((j) => j.id === activeCookieJarId)) {
if (cookieJars.data.find((j) => j.id === activeCookieJarId)) {
return; // There's an active jar return; // There's an active jar
} }
const firstJar = cookieJars.data[0]; const firstJar = cookieJars[0];
if (firstJar == null) { if (firstJar == null) {
console.log("Workspace doesn't have any cookie jars to activate"); console.log("Workspace doesn't have any cookie jars to activate");
return; return;
@@ -37,35 +32,7 @@ export function useEnsureActiveCookieJar() {
// There's no active jar, so set it to the first one // There's no active jar, so set it to the first one
console.log('Setting active cookie jar to', firstJar.id); console.log('Setting active cookie jar to', firstJar.id);
setActiveCookieJarId(firstJar.id); setActiveCookieJarId(firstJar.id);
}, [activeCookieJarId, cookieJars, cookieJars.data, setActiveCookieJarId]); }, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
}
export function useMigrateActiveCookieJarId() {
const workspace = useActiveWorkspace();
const [, setActiveCookieJarId] = useActiveCookieJarId();
const {
set: setLegacyActiveCookieJarId,
value: legacyActiveCookieJarId,
isLoading: isLoadingLegacyActiveCookieJarId,
} = useKeyValue<string | null>({
namespace: 'global',
key: ['activeCookieJar', workspace?.id ?? 'n/a'],
fallback: null,
});
useEffect(() => {
if (legacyActiveCookieJarId == null || isLoadingLegacyActiveCookieJarId) return;
console.log('Migrating active cookie jar ID to query param', legacyActiveCookieJarId);
setActiveCookieJarId(legacyActiveCookieJarId);
setLegacyActiveCookieJarId(null).catch(console.error);
}, [
workspace,
isLoadingLegacyActiveCookieJarId,
legacyActiveCookieJarId,
setActiveCookieJarId,
setLegacyActiveCookieJarId,
]);
} }
function useActiveCookieJarId() { function useActiveCookieJarId() {
+3 -17
View File
@@ -1,22 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp-internal/models'; import type { CookieJar } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri'; import { atom, useAtomValue } from 'jotai';
import { useActiveWorkspace } from './useActiveWorkspace';
export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) { export const cookieJarsAtom = atom<CookieJar[]>([]);
return ['cookie_jars', { workspaceId }];
}
export function useCookieJars() { export function useCookieJars() {
const workspace = useActiveWorkspace(); return useAtomValue(cookieJarsAtom);
return useQuery({
enabled: workspace != null,
queryKey: cookieJarsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspace == null) return [];
return (await invokeCmd('cmd_list_cookie_jars', {
workspaceId: workspace.id,
})) as CookieJar[];
},
});
} }
+3 -14
View File
@@ -1,20 +1,9 @@
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from '@yaakapp-internal/models';
import { atom, useAtom } from 'jotai/index'; import { useAtomValue } from 'jotai';
import { useEffect } from 'react'; import { atom } from 'jotai/index';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
export const environmentsAtom = atom<Environment[]>([]); export const environmentsAtom = atom<Environment[]>([]);
export function useEnvironments() { export function useEnvironments() {
const [items, setItems] = useAtom(environmentsAtom); return useAtomValue(environmentsAtom);
const workspace = useActiveWorkspace();
// Fetch new requests when workspace changes
useEffect(() => {
if (workspace == null) return;
invokeCmd<Environment[]>('cmd_list_environments', { workspaceId: workspace.id }).then(setItems);
}, [setItems, workspace]);
return items;
} }
+4 -17
View File
@@ -1,22 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import type { Folder } from '@yaakapp-internal/models'; import type { Folder } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri'; import { useAtomValue } from 'jotai';
import { useActiveWorkspace } from './useActiveWorkspace'; import { atom } from 'jotai/index';
export function foldersQueryKey({ workspaceId }: { workspaceId: string }) { export const foldersAtom = atom<Folder[]>([]);
return ['folders', { workspaceId }];
}
export function useFolders() { export function useFolders() {
const workspace = useActiveWorkspace(); return useAtomValue(foldersAtom);
return (
useQuery({
enabled: workspace != null,
queryKey: foldersQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspace == null) return [];
return (await invokeCmd('cmd_list_folders', { workspaceId: workspace.id })) as Folder[];
},
}).data ?? []
);
} }
+26
View File
@@ -0,0 +1,26 @@
import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin';
import { githubLight } from '../lib/theme/themes/github';
import { hotdogStandDefault } from '../lib/theme/themes/hotdog-stand';
import { monokaiProDefault } from '../lib/theme/themes/monokai-pro';
import { rosePineDefault } from '../lib/theme/themes/rose-pine';
import { yaakDark } from '../lib/theme/themes/yaak';
import { getThemeCSS } from '../lib/theme/window';
import { useCopy } from './useCopy';
import { useListenToTauriEvent } from './useListenToTauriEvent';
export function useGenerateThemeCss() {
const copy = useCopy();
useListenToTauriEvent('generate_theme_css', () => {
const themesCss = [
yaakDark,
monokaiProDefault,
rosePineDefault,
catppuccinMacchiato,
githubLight,
hotdogStandDefault,
]
.map(getThemeCSS)
.join('\n\n');
copy(themesCss);
});
}
+4 -20
View File
@@ -1,24 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcConnection } from '@yaakapp-internal/models'; import type { GrpcConnection } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri'; import { atom, useAtomValue } from 'jotai/index';
export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) { export const grpcConnectionsAtom = atom<GrpcConnection[]>([]);
return ['grpc_connections', { requestId }];
}
export function useGrpcConnections(requestId: string | null) { export function useGrpcConnections() {
return ( return useAtomValue(grpcConnectionsAtom);
useQuery<GrpcConnection[]>({
enabled: requestId !== null,
initialData: [],
queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
if (requestId == null) return [];
return (await invokeCmd('cmd_list_grpc_connections', {
requestId,
limit: 200,
})) as GrpcConnection[];
},
}).data ?? []
);
} }
+2 -16
View File
@@ -1,22 +1,8 @@
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from '@yaakapp-internal/models';
import { atom, useAtom } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
export const grpcRequestsAtom = atom<GrpcRequest[]>([]); export const grpcRequestsAtom = atom<GrpcRequest[]>([]);
export function useGrpcRequests() { export function useGrpcRequests() {
const [items, setItems] = useAtom(grpcRequestsAtom); return useAtomValue(grpcRequestsAtom);
const workspace = useActiveWorkspace();
// Fetch new requests when workspace changes
useEffect(() => {
if (workspace == null) return;
invokeCmd<GrpcRequest[]>('cmd_list_grpc_requests', { workspaceId: workspace.id }).then(
setItems,
);
}, [setItems, workspace]);
return items;
} }
+9 -3
View File
@@ -3,10 +3,15 @@ import type { HttpRequest } from '@yaakapp-internal/models';
import type { import type {
CallHttpRequestActionRequest, CallHttpRequestActionRequest,
GetHttpRequestActionsResponse, GetHttpRequestActionsResponse,
HttpRequestAction,
} from '@yaakapp-internal/plugin'; } from '@yaakapp-internal/plugin';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins'; import { usePluginsKey } from './usePlugins';
export type CallableHttpRequestAction = Pick<HttpRequestAction, 'key' | 'label' | 'icon'> & {
call: (httpRequest: HttpRequest) => Promise<void>;
};
export function useHttpRequestActions() { export function useHttpRequestActions() {
const pluginsKey = usePluginsKey(); const pluginsKey = usePluginsKey();
@@ -20,7 +25,7 @@ export function useHttpRequestActions() {
}, },
}); });
return ( const actions: CallableHttpRequestAction[] =
httpRequestActions.data?.flatMap((r) => httpRequestActions.data?.flatMap((r) =>
r.actions.map((a) => ({ r.actions.map((a) => ({
key: a.key, key: a.key,
@@ -35,6 +40,7 @@ export function useHttpRequestActions() {
await invokeCmd('cmd_call_http_request_action', { req: payload }); await invokeCmd('cmd_call_http_request_action', { req: payload });
}, },
})), })),
) ?? [] ) ?? [];
);
return actions;
} }
+2 -15
View File
@@ -1,21 +1,8 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { atom, useAtom } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
export const httpRequestsAtom = atom<HttpRequest[]>([]); export const httpRequestsAtom = atom<HttpRequest[]>([]);
export function useHttpRequests() { export function useHttpRequests() {
const [items, setItems] = useAtom(httpRequestsAtom); return useAtomValue(httpRequestsAtom);
const workspace = useActiveWorkspace();
useEffect(() => {
if (workspace == null) return;
invokeCmd<HttpRequest[]>('cmd_list_http_requests', { workspaceId: workspace.id }).then(
setItems,
);
}, [setItems, workspace]);
return items;
} }
+5 -20
View File
@@ -1,24 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import { invokeCmd } from '../lib/tauri'; import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
export function httpResponsesQueryKey({ requestId }: { requestId: string }) { export const httpResponsesAtom = atom<HttpResponse[]>([]);
return ['http_responses', { requestId }];
}
export function useHttpResponses(requestId: string | null) { export function useHttpResponses() {
return ( return useAtomValue(httpResponsesAtom);
useQuery<HttpResponse[]>({
enabled: requestId !== null,
initialData: [],
queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
if (requestId == null) return [];
return (await invokeCmd('cmd_list_http_responses', {
requestId,
limit: 200,
})) as HttpResponse[];
},
}).data ?? []
);
} }
+1 -2
View File
@@ -2,6 +2,5 @@ import type { GrpcConnection } from '@yaakapp-internal/models';
import { useGrpcConnections } from './useGrpcConnections'; import { useGrpcConnections } from './useGrpcConnections';
export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null { export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null {
const connections = useGrpcConnections(requestId); return useGrpcConnections().find((c) => c.requestId === requestId) ?? null;
return connections[0] ?? null;
} }
+1 -2
View File
@@ -2,6 +2,5 @@ import type { HttpResponse } from '@yaakapp-internal/models';
import { useHttpResponses } from './useHttpResponses'; import { useHttpResponses } from './useHttpResponses';
export function useLatestHttpResponse(requestId: string | null): HttpResponse | null { export function useLatestHttpResponse(requestId: string | null): HttpResponse | null {
const responses = useHttpResponses(requestId); return useHttpResponses().find((r) => r.requestId === requestId) ?? null;
return responses[0] ?? null;
} }
+1 -1
View File
@@ -11,7 +11,7 @@ export function usePinnedGrpcConnection(activeRequest: GrpcRequest) {
fallback: null, fallback: null,
namespace: 'global', namespace: 'global',
}); });
const connections = useGrpcConnections(activeRequest.id); const connections = useGrpcConnections().filter((c) => c.requestId === activeRequest.id);
const activeConnection: GrpcConnection | null = const activeConnection: GrpcConnection | null =
connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection; connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection;
+3 -2
View File
@@ -6,12 +6,13 @@ import { useLatestHttpResponse } from './useLatestHttpResponse';
export function usePinnedHttpResponse(activeRequest: HttpRequest) { export function usePinnedHttpResponse(activeRequest: HttpRequest) {
const latestResponse = useLatestHttpResponse(activeRequest.id); const latestResponse = useLatestHttpResponse(activeRequest.id);
const { set, value: pinnedResponseId } = useKeyValue<string | null>({ const { set, value: pinnedResponseId } = useKeyValue<string | null>({
// Key on latest response instead of activeRequest because responses change out of band of active request // Key on the latest response instead of activeRequest because responses change out of band of active request
key: ['pinned_http_response_id', latestResponse?.id ?? 'n/a'], key: ['pinned_http_response_id', latestResponse?.id ?? 'n/a'],
fallback: null, fallback: null,
namespace: 'global', namespace: 'global',
}); });
const responses = useHttpResponses(activeRequest.id); const allResponses = useHttpResponses();
const responses = allResponses.filter((r) => r.requestId === activeRequest.id);
const activeResponse: HttpResponse | null = const activeResponse: HttpResponse | null =
responses.find((r) => r.id === pinnedResponseId) ?? latestResponse; responses.find((r) => r.id === pinnedResponseId) ?? latestResponse;
+1 -1
View File
@@ -31,7 +31,7 @@ export function useRecentCookieJars() {
}, [activeCookieJarId]); }, [activeCookieJarId]);
const onlyValidIds = useMemo( const onlyValidIds = useMemo(
() => kv.value?.filter((id) => cookieJars.data?.some((e) => e.id === id)) ?? [], () => kv.value?.filter((id) => cookieJars.some((e) => e.id === id)) ?? [],
[kv.value, cookieJars], [kv.value, cookieJars],
); );
+3 -4
View File
@@ -1,21 +1,20 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics'; import { trackEvent } from '../lib/analytics';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { useActiveCookieJar } from './useActiveCookieJar'; import { useActiveCookieJar } from './useActiveCookieJar';
import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveEnvironment } from './useActiveEnvironment';
import { useAlert } from './useAlert'; import { useAlert } from './useAlert';
import { useHttpRequests } from './useHttpRequests';
export function useSendAnyHttpRequest() { export function useSendAnyHttpRequest() {
const [environment] = useActiveEnvironment();
const alert = useAlert(); const alert = useAlert();
const [environment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar(); const [activeCookieJar] = useActiveCookieJar();
const requests = useHttpRequests();
return useMutation<HttpResponse | null, string, string | null>({ return useMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'], mutationKey: ['send_any_request'],
mutationFn: async (id) => { mutationFn: async (id) => {
const request = requests.find((r) => r.id === id) ?? null; const request = await getHttpRequest(id);
if (request == null) { if (request == null) {
return null; return null;
} }
+16
View File
@@ -0,0 +1,16 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { useSettings } from './useSettings';
export function useSyncFontSizeSetting() {
const settings = useSettings();
useEffect(() => {
if (settings == null) {
return;
}
const { interfaceScale, editorFontSize } = settings;
getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error);
document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`);
}, [settings]);
}
+155
View File
@@ -0,0 +1,155 @@
import { useQueryClient } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { AnyModel } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { extractKeyValue } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util';
import { cookieJarsAtom } from './useCookieJars';
import { environmentsAtom } from './useEnvironments';
import { foldersAtom } from './useFolders';
import { grpcConnectionsAtom } from './useGrpcConnections';
import { grpcEventsQueryKey } from './useGrpcEvents';
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
import { httpResponsesAtom } from './useHttpResponses';
import { keyValueQueryKey } from './useKeyValue';
import { useListenToTauriEvent } from './useListenToTauriEvent';
import { pluginsAtom } from './usePlugins';
import { useRequestUpdateKey } from './useRequestUpdateKey';
import { settingsAtom } from './useSettings';
import { workspacesAtom } from './useWorkspaces';
export interface ModelPayload {
model: AnyModel;
windowLabel: string;
}
export function useSyncModelStores() {
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
const setSettings = useSetAtom(settingsAtom);
const setWorkspaces = useSetAtom(workspacesAtom);
const setCookieJars = useSetAtom(cookieJarsAtom);
const setFolders = useSetAtom(foldersAtom);
const setPlugins = useSetAtom(pluginsAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setHttpResponses = useSetAtom(httpResponsesAtom);
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
useListenToTauriEvent<ModelPayload>('upserted_model', ({ payload }) => {
if (payload.model.model !== 'key_value') {
console.log('Upserted model', payload.model);
}
const { model, windowLabel } = payload;
const queryKey =
model.model === 'grpc_event'
? grpcEventsQueryKey(model)
: model.model === 'key_value'
? keyValueQueryKey(model)
: null;
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
wasUpdatedExternally(model.id);
}
const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes(
model.model,
);
if (shouldIgnoreModel(model, windowLabel)) return;
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
} else if (model.model === 'folder') {
setFolders(updateModelList(model, pushToFront));
} else if (model.model === 'http_response') {
setHttpResponses(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_request') {
setGrpcRequests(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_connection') {
setGrpcConnections(updateModelList(model, pushToFront));
} else if (model.model === 'environment') {
setEnvironments(updateModelList(model, pushToFront));
} else if (model.model === 'cookie_jar') {
setCookieJars(updateModelList(model, pushToFront));
} else if (model.model === 'settings') {
setSettings(model);
} else if (queryKey != null) {
// TODO: Convert all models to use Jotai
queryClient.setQueryData(queryKey, (current: unknown) => {
if (model.model === 'key_value') {
// Special-case for KeyValue
return extractKeyValue(model);
}
if (Array.isArray(current)) {
return updateModelList(model, pushToFront)(current);
}
});
}
});
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
const { model, windowLabel } = payload;
if (shouldIgnoreModel(model, windowLabel)) return;
console.log('Delete model', payload.model);
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
} else if (model.model === 'http_response') {
setHttpResponses(removeById(model));
} else if (model.model === 'folder') {
setFolders(removeById(model));
} else if (model.model === 'environment') {
setEnvironments(removeById(model));
} else if (model.model === 'grpc_request') {
setGrpcRequests(removeById(model));
} else if (model.model === 'grpc_connection') {
setGrpcConnections(removeById(model));
} else if (model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model));
} else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), undefined);
} else if (model.model === 'cookie_jar') {
setCookieJars(removeById(model));
}
});
}
function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
return (current: T[]): T[] => {
const index = current.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
return [...current.slice(0, index), model, ...current.slice(index + 1)];
} else {
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
}
};
}
function removeById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
}
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
if (windowLabel === getCurrentWebviewWindow().label) {
// Never ignore same-window updates
return false;
}
if (payload.model === 'key_value') {
return payload.namespace === 'no_sync';
}
return false;
};
@@ -0,0 +1,40 @@
import { useSetAtom } from 'jotai/index';
import { useEffect } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { cookieJarsAtom } from './useCookieJars';
import { environmentsAtom } from './useEnvironments';
import { foldersAtom } from './useFolders';
import { grpcConnectionsAtom } from './useGrpcConnections';
import { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
import { httpResponsesAtom } from './useHttpResponses';
export function useSyncWorkspaceChildModels() {
const setCookieJars = useSetAtom(cookieJarsAtom);
const setFolders = useSetAtom(foldersAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setHttpResponses = useSetAtom(httpResponsesAtom);
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
const workspace = useActiveWorkspace();
const workspaceId = workspace?.id ?? 'n/a';
useEffect(() => {
(async function () {
// Set the things we need first, first
setHttpRequests(await invokeCmd('cmd_list_http_requests', { workspaceId }));
setGrpcRequests(await invokeCmd('cmd_list_grpc_requests', { workspaceId }));
setFolders(await invokeCmd('cmd_list_folders', { workspaceId }));
// Then, set the rest
setCookieJars(await invokeCmd('cmd_list_cookie_jars', { workspaceId }));
setHttpResponses(await invokeCmd('cmd_list_http_responses', { workspaceId }));
setGrpcConnections(await invokeCmd('cmd_list_grpc_connections', { workspaceId }));
setEnvironments(await invokeCmd('cmd_list_environments', { workspaceId }));
})().catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workspaceId]);
}
+16
View File
@@ -0,0 +1,16 @@
import { useHotKey } from './useHotKey';
import { useListenToTauriEvent } from './useListenToTauriEvent';
import { useZoom } from './useZoom';
export function useSyncZoomSetting() {
// Handle Zoom.
// Note, Mac handles it in the app menu, so need to also handle keyboard
// shortcuts for Windows/Linux
const zoom = useZoom();
useHotKey('app.zoom_in', zoom.zoomIn);
useListenToTauriEvent('zoom_in', zoom.zoomIn);
useHotKey('app.zoom_out', zoom.zoomOut);
useListenToTauriEvent('zoom_out', zoom.zoomOut);
useHotKey('app.zoom_reset', zoom.zoomReset);
useListenToTauriEvent('zoom_reset', zoom.zoomReset);
}
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev --force",
"build": "vite build", "build": "vite build",
"lint": "tsc && eslint . --ext .ts,.tsx" "lint": "tsc && eslint . --ext .ts,.tsx"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
import { emit, listen } from '@tauri-apps/api/event'; import { emit, listen } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { ModelPayload } from './components/GlobalHooks'; import type { ModelPayload } from './hooks/useSyncModelStores';
import { getSettings } from './lib/store'; import { getSettings } from './lib/store';
import type { Appearance } from './lib/theme/appearance'; import type { Appearance } from './lib/theme/appearance';
import { getCSSAppearance, subscribeToPreferredAppearance } from './lib/theme/appearance'; import { getCSSAppearance, subscribeToPreferredAppearance } from './lib/theme/appearance';