From 5e3ef70d9383480e89b50b9172c16a805be7edcc Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 12 Mar 2026 08:31:05 -0700 Subject: [PATCH] Refactor proxy codebase --- apps/yaak-client/components/CopyButton.tsx | 2 +- .../yaak-client/components/CopyIconButton.tsx | 4 +- .../components/core/IconButton.tsx | 112 ++++---------- .../{ => components}/ActionButton.tsx | 4 +- apps/yaak-proxy/components/ProxyLayout.tsx | 133 ++++++++++++++++ apps/yaak-proxy/{ => components}/Sidebar.tsx | 46 +++--- .../{hooks.ts => hooks/useActionMetadata.ts} | 35 ++--- apps/yaak-proxy/hooks/useRpcEvent.ts | 15 ++ apps/yaak-proxy/hooks/useRpcMutation.ts | 18 +++ apps/yaak-proxy/hooks/useRpcQuery.ts | 20 +++ apps/yaak-proxy/hooks/useRpcQueryWithEvent.ts | 23 +++ apps/yaak-proxy/{ => lib}/hotkeys.ts | 36 +++-- apps/yaak-proxy/lib/rpc.ts | 24 +++ apps/yaak-proxy/{ => lib}/store.ts | 0 apps/yaak-proxy/{ => lib}/theme.ts | 24 +-- apps/yaak-proxy/main.tsx | 143 ++---------------- apps/yaak-proxy/rpc-hooks.ts | 78 ---------- apps/yaak-proxy/rpc.ts | 30 ---- packages/ui/src/components/IconButton.tsx | 95 ++++++++++++ .../ui/src}/hooks/useTimedBoolean.ts | 0 packages/ui/src/index.ts | 3 + 21 files changed, 437 insertions(+), 408 deletions(-) rename apps/yaak-proxy/{ => components}/ActionButton.tsx (87%) create mode 100644 apps/yaak-proxy/components/ProxyLayout.tsx rename apps/yaak-proxy/{ => components}/Sidebar.tsx (78%) rename apps/yaak-proxy/{hooks.ts => hooks/useActionMetadata.ts} (68%) create mode 100644 apps/yaak-proxy/hooks/useRpcEvent.ts create mode 100644 apps/yaak-proxy/hooks/useRpcMutation.ts create mode 100644 apps/yaak-proxy/hooks/useRpcQuery.ts create mode 100644 apps/yaak-proxy/hooks/useRpcQueryWithEvent.ts rename apps/yaak-proxy/{ => lib}/hotkeys.ts (55%) create mode 100644 apps/yaak-proxy/lib/rpc.ts rename apps/yaak-proxy/{ => lib}/store.ts (100%) rename apps/yaak-proxy/{ => lib}/theme.ts (54%) delete mode 100644 apps/yaak-proxy/rpc-hooks.ts delete mode 100644 apps/yaak-proxy/rpc.ts create mode 100644 packages/ui/src/components/IconButton.tsx rename {apps/yaak-client => packages/ui/src}/hooks/useTimedBoolean.ts (100%) diff --git a/apps/yaak-client/components/CopyButton.tsx b/apps/yaak-client/components/CopyButton.tsx index 2c262429..4a068b9d 100644 --- a/apps/yaak-client/components/CopyButton.tsx +++ b/apps/yaak-client/components/CopyButton.tsx @@ -1,4 +1,4 @@ -import { useTimedBoolean } from '../hooks/useTimedBoolean'; +import { useTimedBoolean } from '@yaakapp-internal/ui'; import { copyToClipboard } from '../lib/copy'; import { showToast } from '../lib/toast'; import type { ButtonProps } from './core/Button'; diff --git a/apps/yaak-client/components/CopyIconButton.tsx b/apps/yaak-client/components/CopyIconButton.tsx index 78c3c679..84e318c0 100644 --- a/apps/yaak-client/components/CopyIconButton.tsx +++ b/apps/yaak-client/components/CopyIconButton.tsx @@ -1,8 +1,6 @@ -import { useTimedBoolean } from '../hooks/useTimedBoolean'; +import { IconButton, type IconButtonProps, useTimedBoolean } from '@yaakapp-internal/ui'; import { copyToClipboard } from '../lib/copy'; import { showToast } from '../lib/toast'; -import type { IconButtonProps } from './core/IconButton'; -import { IconButton } from './core/IconButton'; interface Props extends Omit { text: string | (() => Promise); diff --git a/apps/yaak-client/components/core/IconButton.tsx b/apps/yaak-client/components/core/IconButton.tsx index 4b3b2df6..be36ded2 100644 --- a/apps/yaak-client/components/core/IconButton.tsx +++ b/apps/yaak-client/components/core/IconButton.tsx @@ -1,93 +1,37 @@ -import classNames from 'classnames'; -import type { MouseEvent } from 'react'; -import { forwardRef, useCallback } from 'react'; -import { useTimedBoolean } from '../../hooks/useTimedBoolean'; -import type { ButtonProps } from './Button'; -import { Button } from './Button'; -import { Icon, LoadingIcon, type IconProps } from '@yaakapp-internal/ui'; +import { + IconButton as BaseIconButton, + type IconButtonProps as BaseIconButtonProps, +} from '@yaakapp-internal/ui'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; +import type { HotkeyAction } from '../../hooks/useHotKey'; +import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey'; -export type IconButtonProps = IconProps & - ButtonProps & { - showConfirm?: boolean; - iconClassName?: string; - iconSize?: IconProps['size']; - iconColor?: IconProps['color']; - title: string; - showBadge?: boolean; - }; +export type IconButtonProps = BaseIconButtonProps & { + hotkeyAction?: HotkeyAction; + hotkeyLabelOnly?: boolean; + hotkeyPriority?: number; +}; export const IconButton = forwardRef(function IconButton( - { - showConfirm, - icon, - color = 'default', - spin, - onClick, - className, - iconClassName, - tabIndex, - size = 'md', - iconSize, - showBadge, - iconColor, - isLoading, - type = 'button', - ...props - }: IconButtonProps, + { hotkeyAction, hotkeyPriority, hotkeyLabelOnly, title, ...props }: IconButtonProps, ref, ) { - const [confirmed, setConfirmed] = useTimedBoolean(); - const handleClick = useCallback( - (e: MouseEvent) => { - if (showConfirm) setConfirmed(); - onClick?.(e); - }, - [onClick, setConfirmed, showConfirm], + const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join(''); + const fullTitle = hotkeyTrigger ? `${title ?? ''} ${hotkeyTrigger}`.trim() : title; + + const buttonRef = useRef(null); + useImperativeHandle( + ref, + () => buttonRef.current, ); - return ( - + useHotKey( + hotkeyAction ?? null, + () => { + buttonRef.current?.click(); + }, + { priority: hotkeyPriority, enable: !hotkeyLabelOnly }, ); + + return ; }); diff --git a/apps/yaak-proxy/ActionButton.tsx b/apps/yaak-proxy/components/ActionButton.tsx similarity index 87% rename from apps/yaak-proxy/ActionButton.tsx rename to apps/yaak-proxy/components/ActionButton.tsx index 6d345449..17bc876b 100644 --- a/apps/yaak-proxy/ActionButton.tsx +++ b/apps/yaak-proxy/components/ActionButton.tsx @@ -1,8 +1,8 @@ import type { ActionInvocation } from '@yaakapp-internal/proxy-lib'; import { Button, type ButtonProps } from '@yaakapp-internal/ui'; import { useCallback } from 'react'; -import { useActionMetadata } from './hooks'; -import { useRpcMutation } from './rpc-hooks'; +import { useRpcMutation } from '../hooks/useRpcMutation'; +import { useActionMetadata } from '../hooks/useActionMetadata'; type ActionButtonProps = Omit & { action: ActionInvocation; diff --git a/apps/yaak-proxy/components/ProxyLayout.tsx b/apps/yaak-proxy/components/ProxyLayout.tsx new file mode 100644 index 00000000..3b77a176 --- /dev/null +++ b/apps/yaak-proxy/components/ProxyLayout.tsx @@ -0,0 +1,133 @@ +import { type } from '@tauri-apps/plugin-os'; +import type { ProxyHeader } from '@yaakapp-internal/proxy-lib'; +import { + HeaderSize, + IconButton, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + TruncatedWideTableCell, +} from '@yaakapp-internal/ui'; +import classNames from 'classnames'; +import { useAtomValue } from 'jotai'; +import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent'; +import { ActionButton } from './ActionButton'; +import { filteredExchangesAtom, Sidebar } from './Sidebar'; + +export function ProxyLayout() { + const osType = type(); + const exchanges = useAtomValue(filteredExchangesAtom); + const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed'); + const isRunning = proxyState?.state === 'running'; + + return ( +
+ +
+
+ Yaak Proxy +
+
+ +
+
+
+
+ +
+
+ + + + {isRunning ? 'Running on :9090' : 'Stopped'} + +
+ + {exchanges.length === 0 ? ( +

No traffic yet

+ ) : ( + + + + Method + URL + Status + Type + + + + {exchanges.map((ex) => ( + + {ex.method} + + {ex.url} + + + + + + {getContentType(ex.resHeaders)} + + + ))} + +
+ )} +
+
+
+ ); +} + +function StatusBadge({ status, error }: { status: number | null; error: string | null }) { + if (error) return Error; + if (status == null) return ; + + const color = + status >= 500 + ? 'text-danger' + : status >= 400 + ? 'text-warning' + : status >= 300 + ? 'text-notice' + : 'text-success'; + + return {status}; +} + +function getContentType(headers: ProxyHeader[]): string { + const ct = headers.find((h) => h.name.toLowerCase() === 'content-type')?.value; + if (ct == null) return '—'; + // Strip parameters (e.g. "; charset=utf-8") + return ct.split(';')[0]?.trim() ?? ct; +} diff --git a/apps/yaak-proxy/Sidebar.tsx b/apps/yaak-proxy/components/Sidebar.tsx similarity index 78% rename from apps/yaak-proxy/Sidebar.tsx rename to apps/yaak-proxy/components/Sidebar.tsx index 59340684..0192b12b 100644 --- a/apps/yaak-proxy/Sidebar.tsx +++ b/apps/yaak-proxy/components/Sidebar.tsx @@ -1,10 +1,10 @@ -import type { HttpExchange } from "@yaakapp-internal/proxy-lib"; -import { selectedIdsFamily, Tree } from "@yaakapp-internal/ui"; -import type { TreeNode } from "@yaakapp-internal/ui"; -import { atom, useAtomValue } from "jotai"; -import { atomFamily } from "jotai/utils"; -import { useCallback } from "react"; -import { httpExchangesAtom } from "./store"; +import type { HttpExchange } from '@yaakapp-internal/proxy-lib'; +import type { TreeNode } from '@yaakapp-internal/ui'; +import { selectedIdsFamily, Tree } from '@yaakapp-internal/ui'; +import { atom, useAtomValue } from 'jotai'; +import { atomFamily } from 'jotai/utils'; +import { useCallback } from 'react'; +import { httpExchangesAtom } from '../lib/store'; /** A node in the sidebar tree — either a domain or a path segment. */ export type SidebarItem = { @@ -13,11 +13,9 @@ export type SidebarItem = { exchangeIds: string[]; }; -const collapsedAtom = atomFamily((treeId: string) => - atom>({}), -); +const collapsedAtom = atomFamily((_treeId: string) => atom>({})); -export const SIDEBAR_TREE_ID = "proxy-sidebar"; +export const SIDEBAR_TREE_ID = 'proxy-sidebar'; const sidebarTreeAtom = atom>((get) => { const exchanges = get(httpExchangesAtom); @@ -31,7 +29,7 @@ export const filteredExchangesAtom = atom((get) => { const selectedIds = get(selectedIdsFamily(SIDEBAR_TREE_ID)); // Nothing selected or root selected → show all - if (selectedIds.length === 0 || selectedIds.includes("root")) { + if (selectedIds.length === 0 || selectedIds.includes('root')) { return exchanges; } @@ -75,7 +73,7 @@ function collectNodes(node: TreeNode, map: Map * /orders */ function buildTree(exchanges: HttpExchange[]): TreeNode { - const root: SidebarItem = { id: "root", label: "All Traffic", exchangeIds: [] }; + const root: SidebarItem = { id: 'root', label: 'All Traffic', exchangeIds: [] }; const rootNode: TreeNode = { item: root, parent: null, @@ -100,7 +98,7 @@ function buildTree(exchanges: HttpExchange[]): TreeNode { try { const url = new URL(ex.url); hostname = url.host; - segments = url.pathname.split("/").filter(Boolean); + segments = url.pathname.split('/').filter(Boolean); } catch { hostname = ex.url; segments = []; @@ -127,7 +125,7 @@ function buildTree(exchanges: HttpExchange[]): TreeNode { let child = current.children.get(seg); if (!child) { child = { - id: `path:${hostname}/${pathSoFar.join("/")}`, + id: `path:${hostname}/${pathSoFar.join('/')}`, label: `/${seg}`, exchangeIds: [], children: new Map(), @@ -157,7 +155,7 @@ function buildTree(exchanges: HttpExchange[]): TreeNode { draggable: false, }; for (const child of trie.children.values()) { - node.children!.push(toTreeNode(child, node, depth + 1)); + node.children?.push(toTreeNode(child, node, depth + 1)); } return node; } @@ -165,21 +163,19 @@ function buildTree(exchanges: HttpExchange[]): TreeNode { // Add a "Domains" folder between root and domain nodes const allExchangeIds = exchanges.map((ex) => ex.id); const domainsFolder: TreeNode = { - item: { id: "domains", label: "Domains", exchangeIds: allExchangeIds }, + item: { id: 'domains', label: 'Domains', exchangeIds: allExchangeIds }, parent: rootNode, depth: 1, children: [], draggable: false, }; - const sortedDomains = [...domainMap.values()].sort((a, b) => - a.label.localeCompare(b.label), - ); + const sortedDomains = [...domainMap.values()].sort((a, b) => a.label.localeCompare(b.label)); for (const domain of sortedDomains) { - domainsFolder.children!.push(toTreeNode(domain, domainsFolder, 2)); + domainsFolder.children?.push(toTreeNode(domain, domainsFolder, 2)); } - rootNode.children!.push(domainsFolder); + rootNode.children?.push(domainsFolder); return rootNode; } @@ -189,9 +185,7 @@ function ItemInner({ item }: { item: SidebarItem }) { return (
{item.label} - {count > 0 && ( - {count} - )} + {count > 0 && {count}}
); } @@ -203,7 +197,7 @@ export function Sidebar() { const getItemKey = useCallback((item: SidebarItem) => item.id, []); return ( -