diff --git a/apps/yaak-proxy/components/ActionIconButton.tsx b/apps/yaak-proxy/components/ActionIconButton.tsx new file mode 100644 index 00000000..a993cc85 --- /dev/null +++ b/apps/yaak-proxy/components/ActionIconButton.tsx @@ -0,0 +1,29 @@ +import type { ActionInvocation } from '@yaakapp-internal/proxy-lib'; +import { IconButton, type IconButtonProps } from '@yaakapp-internal/ui'; +import { useCallback } from 'react'; +import { useRpcMutation } from '../hooks/useRpcMutation'; +import { useActionMetadata } from '../hooks/useActionMetadata'; + +type ActionIconButtonProps = Omit & { + action: ActionInvocation; + title?: string; +}; + +export function ActionIconButton({ action, ...props }: ActionIconButtonProps) { + const meta = useActionMetadata(action); + const { mutate, isPending } = useRpcMutation('execute_action'); + + const onClick = useCallback(() => { + mutate(action); + }, [action, mutate]); + + return ( + + ); +} diff --git a/apps/yaak-proxy/components/ExchangesTable.tsx b/apps/yaak-proxy/components/ExchangesTable.tsx new file mode 100644 index 00000000..4f8557ac --- /dev/null +++ b/apps/yaak-proxy/components/ExchangesTable.tsx @@ -0,0 +1,71 @@ +import type { HttpExchange, ProxyHeader } from '@yaakapp-internal/proxy-lib'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + TruncatedWideTableCell, +} from '@yaakapp-internal/ui'; +import classNames from 'classnames'; + +interface Props { + exchanges: HttpExchange[]; +} + +export function ExchangesTable({ exchanges }: Props) { + if (exchanges.length === 0) { + return

No traffic yet

; + } + + return ( + + + + 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/components/ProxyLayout.tsx b/apps/yaak-proxy/components/ProxyLayout.tsx index 3b77a176..4633b34e 100644 --- a/apps/yaak-proxy/components/ProxyLayout.tsx +++ b/apps/yaak-proxy/components/ProxyLayout.tsx @@ -1,24 +1,14 @@ -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 { HeaderSize } from '@yaakapp-internal/ui'; import classNames from 'classnames'; import { useAtomValue } from 'jotai'; import { useRpcQueryWithEvent } from '../hooks/useRpcQueryWithEvent'; -import { ActionButton } from './ActionButton'; +import { getOsType } from '../lib/tauri'; +import { ActionIconButton } from './ActionIconButton'; +import { ExchangesTable } from './ExchangesTable'; import { filteredExchangesAtom, Sidebar } from './Sidebar'; export function ProxyLayout() { - const osType = type(); + const os = getOsType(); const exchanges = useAtomValue(filteredExchangesAtom); const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed'); const isRunning = proxyState?.state === 'running'; @@ -27,12 +17,12 @@ export function ProxyLayout() {
Yaak Proxy
-
- +
+ + {isRunning ? 'Running on :9090' : 'Stopped'} + + {isRunning ? ( + + ) : ( + + )}
-
-
- - - - {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/index.html b/apps/yaak-proxy/index.html index aed2c7a9..c94dd2ab 100644 --- a/apps/yaak-proxy/index.html +++ b/apps/yaak-proxy/index.html @@ -21,7 +21,7 @@
- + diff --git a/apps/yaak-proxy/lib/rpc.ts b/apps/yaak-proxy/lib/rpc.ts index 3496895e..6b28e131 100644 --- a/apps/yaak-proxy/lib/rpc.ts +++ b/apps/yaak-proxy/lib/rpc.ts @@ -1,12 +1,11 @@ -import { invoke } from '@tauri-apps/api/core'; -import { listen as tauriListen } from '@tauri-apps/api/event'; import type { RpcEventSchema, RpcSchema } from '@yaakapp-internal/proxy-lib'; +import { command, subscribe } from './tauri'; export type Req = RpcSchema[K][0]; export type Res = RpcSchema[K][1]; export async function rpc(cmd: K, payload: Req): Promise> { - return invoke('rpc', { cmd, payload }) as Promise>; + return command>('rpc', { cmd, payload }); } /** Subscribe to a backend event. Returns an unsubscribe function. */ @@ -14,11 +13,5 @@ export function listen( event: K & string, callback: (payload: RpcEventSchema[K]) => void, ): () => void { - let unsub: (() => void) | null = null; - tauriListen(event, (e) => callback(e.payload)) - .then((fn) => { - unsub = fn; - }) - .catch(console.error); - return () => unsub?.(); + return subscribe(event, callback); } diff --git a/apps/yaak-proxy/lib/tauri.ts b/apps/yaak-proxy/lib/tauri.ts new file mode 100644 index 00000000..b5380843 --- /dev/null +++ b/apps/yaak-proxy/lib/tauri.ts @@ -0,0 +1,30 @@ +import { invoke } from '@tauri-apps/api/core'; +import { listen as tauriListen } from '@tauri-apps/api/event'; +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; +import { type as tauriOsType } from '@tauri-apps/plugin-os'; + +/** Call a Tauri command. */ +export function command(cmd: string, args?: Record): Promise { + return invoke(cmd, args) as Promise; +} + +/** Subscribe to a Tauri event. Returns an unsubscribe function. */ +export function subscribe(event: string, callback: (payload: T) => void): () => void { + let unsub: (() => void) | null = null; + tauriListen(event, (e) => callback(e.payload)) + .then((fn) => { + unsub = fn; + }) + .catch(console.error); + return () => unsub?.(); +} + +/** Show the current webview window. */ +export function showWindow(): Promise { + return getCurrentWebviewWindow().show(); +} + +/** Get the current OS type (e.g. "macos", "linux", "windows"). */ +export function getOsType() { + return tauriOsType(); +} diff --git a/apps/yaak-proxy/lib/theme.ts b/apps/yaak-proxy/lib/theme.ts index d4ae49c5..ef1e877b 100644 --- a/apps/yaak-proxy/lib/theme.ts +++ b/apps/yaak-proxy/lib/theme.ts @@ -1,4 +1,3 @@ -import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { setWindowTheme } from "@yaakapp-internal/mac-window"; import { applyThemeToDocument, @@ -10,23 +9,22 @@ import { subscribeToPreferredAppearance, type Appearance, } from "@yaakapp-internal/theme"; +import { showWindow } from "./tauri"; -export function initTheme() { - setPlatformOnDocument(platformFromUserAgent(navigator.userAgent)); +setPlatformOnDocument(platformFromUserAgent(navigator.userAgent)); - // Apply a quick initial theme based on CSS media query - let preferredAppearance: Appearance = getCSSAppearance(); +// Apply a quick initial theme based on CSS media query +let preferredAppearance: Appearance = getCSSAppearance(); +applyTheme(preferredAppearance); + +// Then subscribe to accurate OS appearance detection and changes +subscribeToPreferredAppearance((a) => { + preferredAppearance = a; applyTheme(preferredAppearance); +}); - // Then subscribe to accurate OS appearance detection and changes - subscribeToPreferredAppearance((a) => { - preferredAppearance = a; - applyTheme(preferredAppearance); - }); - - // Show window after initial theme is applied (window starts hidden to prevent flash) - getCurrentWebviewWindow().show().catch(console.error); -} +// Show window after initial theme is applied (window starts hidden to prevent flash) +showWindow().catch(console.error); function applyTheme(appearance: Appearance) { const theme = appearance === "dark" ? defaultDarkTheme : defaultLightTheme; diff --git a/apps/yaak-proxy/main.tsx b/apps/yaak-proxy/main.tsx index 5014f6fd..a5f2992d 100644 --- a/apps/yaak-proxy/main.tsx +++ b/apps/yaak-proxy/main.tsx @@ -6,11 +6,8 @@ import { ProxyLayout } from './components/ProxyLayout'; import { listen, rpc } from './lib/rpc'; import { initHotkeys } from './lib/hotkeys'; import { applyChange, dataAtom, replaceAll } from './lib/store'; -import { initTheme } from './lib/theme'; import './main.css'; -initTheme(); - const queryClient = new QueryClient(); const jotaiStore = createStore(); diff --git a/apps/yaak-proxy/tailwind.config.cjs b/apps/yaak-proxy/tailwind.config.cjs index 6d6eace8..05484d2f 100644 --- a/apps/yaak-proxy/tailwind.config.cjs +++ b/apps/yaak-proxy/tailwind.config.cjs @@ -3,5 +3,5 @@ const sharedConfig = require("@yaakapp-internal/tailwind-config"); /** @type {import('tailwindcss').Config} */ module.exports = { ...sharedConfig, - content: ["./*.{html,ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"], + content: ["./**/*.{html,ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"], }; diff --git a/biome.json b/biome.json index fbda2b07..257fc380 100644 --- a/biome.json +++ b/biome.json @@ -6,6 +6,19 @@ "recommended": true, "a11y": { "useKeyWithClickEvents": "off" + }, + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "paths": { + "@tauri-apps/api/core": "Use lib/tauri.ts instead of importing @tauri-apps directly", + "@tauri-apps/api/event": "Use lib/tauri.ts instead of importing @tauri-apps directly", + "@tauri-apps/api/webviewWindow": "Use lib/tauri.ts instead of importing @tauri-apps directly", + "@tauri-apps/plugin-os": "Use lib/tauri.ts instead of importing @tauri-apps directly" + } + } + } } } }, @@ -32,6 +45,18 @@ "semicolons": "always" } }, + "overrides": [ + { + "includes": ["apps/yaak-proxy/lib/tauri.ts"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": "off" + } + } + } + } + ], "files": { "includes": [ "**", diff --git a/packages/ui/src/components/Icon.tsx b/packages/ui/src/components/Icon.tsx index 61c1ca32..20bb048d 100644 --- a/packages/ui/src/components/Icon.tsx +++ b/packages/ui/src/components/Icon.tsx @@ -36,6 +36,9 @@ import { CircleFadingArrowUpIcon, CircleHelpIcon, CircleOffIcon, + CirclePauseIcon, + CirclePlayIcon, + CircleStopIcon, ClipboardPasteIcon, ClockIcon, CodeIcon, @@ -221,6 +224,9 @@ const icons = { globe: GlobeIcon, grip_vertical: GripVerticalIcon, circle_off: CircleOffIcon, + circle_pause: CirclePauseIcon, + circle_play: CirclePlayIcon, + circle_stop: CircleStopIcon, hand: HandIcon, hard_drive_download: HardDriveDownloadIcon, help: CircleHelpIcon,