mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
Keep Clear in the header with Cmd+Shift+K in its tooltip, and leave the port to the menu's address line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4061d5dee
commit
d12beda8a9
5 files changed
+65
-70
No files matched your search
+30
-21
@@ -1,28 +1,19 @@
|
||||
import { Dropdown, type DropdownItem, type DropdownRef } from "@yaakapp-internal/ui";
|
||||
import { Dropdown, type DropdownItem, IconButton, useFormattedHotkey } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import type { Ref } from "react";
|
||||
import { useState } from "react";
|
||||
import { clearFlowsAtom, flowTotalAtom, lockedAtom } from "../lib/store";
|
||||
|
||||
/** Whether there are flows to clear: some, and readable, as they aren't while traffic is locked */
|
||||
export function useCanClearFlows(): boolean {
|
||||
const total = useAtomValue(flowTotalAtom);
|
||||
const locked = useAtomValue(lockedAtom);
|
||||
return locked == null && total > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears every flow, once confirmed in its menu, which its hotkey or the status menu's Clear
|
||||
* Traffic opens. It has no button of its own: it covers the element it is placed over, without
|
||||
* taking the pointer, so the menu opens from there. The list empties at once; the flows are
|
||||
* deleted in the background. Neither menu nor hotkey with nothing to clear, or while captured
|
||||
* Clears every flow, once confirmed in its menu, which its hotkey opens too. The list empties at
|
||||
* once; the flows are deleted in the background. Disabled with none to clear, or while captured
|
||||
* traffic is locked.
|
||||
*/
|
||||
export function ClearFlowsMenu({ ref }: { ref?: Ref<DropdownRef> }) {
|
||||
export function ClearFlowsButton() {
|
||||
const total = useAtomValue(flowTotalAtom);
|
||||
const canClear = useCanClearFlows();
|
||||
const locked = useAtomValue(lockedAtom);
|
||||
const clear = useSetAtom(clearFlowsAtom);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const hotkey = useFormattedHotkey("flows_clear")?.join("");
|
||||
|
||||
const items: DropdownItem[] = [
|
||||
{
|
||||
@@ -57,13 +48,31 @@ export function ClearFlowsMenu({ ref }: { ref?: Ref<DropdownRef> }) {
|
||||
},
|
||||
];
|
||||
|
||||
// Nothing to clear, or nothing that can be read while traffic is locked: no menu, nor hotkey.
|
||||
if (locked != null || total === 0) {
|
||||
return (
|
||||
<IconButton
|
||||
className="pointer-events-auto"
|
||||
icon="trash"
|
||||
title={locked != null ? "Clear flows (traffic is locked)" : "Clear flows (none yet)"}
|
||||
tooltip
|
||||
size="sm"
|
||||
iconColor="secondary"
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={ref}
|
||||
items={canClear ? items : []}
|
||||
hotKeyAction={canClear ? "flows_clear" : undefined}
|
||||
>
|
||||
<span aria-hidden className="absolute inset-0 pointer-events-none" />
|
||||
<Dropdown items={items} hotKeyAction="flows_clear">
|
||||
<IconButton
|
||||
className="pointer-events-auto"
|
||||
icon="trash"
|
||||
title={hotkey == null ? "Clear flows" : `Clear flows ${hotkey}`}
|
||||
tooltip
|
||||
size="sm"
|
||||
iconColor="secondary"
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function FlowDetail({ style, sideBySide }: { style?: CSSProperties; sideB
|
||||
<Keys keys={["↑", "↓"]} />
|
||||
<span>Pause or resume recording</span>
|
||||
<Keys keys={["⌘", "R"]} />
|
||||
<span>Clear traffic</span>
|
||||
<span>Clear flows</span>
|
||||
<Keys keys={["⌘", "⇧", "K"]} />
|
||||
</div>
|
||||
</EmptyState>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { proxyStatusOf, statusTintColor } from "../lib/proxyStatus";
|
||||
import { lockedAtom, noFlowsAtom, proxyStateAtom, systemProxyEverOnAtom } from "../lib/store";
|
||||
import { getOsType } from "../lib/tauri";
|
||||
import { AddColumnDialog } from "./AddColumnDialog";
|
||||
import { ClearFlowsButton } from "./ClearFlowsButton";
|
||||
import { FirstRun } from "./FirstRun";
|
||||
import { FlowDetail } from "./FlowDetail";
|
||||
import { FlowList } from "./FlowList";
|
||||
@@ -95,6 +96,7 @@ export function ProxyLayout() {
|
||||
</div>
|
||||
</div>
|
||||
<div data-tauri-drag-region className="flex items-center justify-end gap-1 pr-1">
|
||||
<ClearFlowsButton />
|
||||
<IconButton
|
||||
className="pointer-events-auto"
|
||||
icon={flowLayout === "horizontal" ? "columns_2" : "rows_2"}
|
||||
|
||||
@@ -24,18 +24,15 @@ import { proxyStateAtom, systemProxyEverOnAtom } from "../lib/store";
|
||||
import { helperPollInterval, recordingLabel, systemProxySwitch } from "../lib/systemProxy";
|
||||
import { onWindowFocus } from "../lib/tauri";
|
||||
import { useShowToast } from "../lib/toast";
|
||||
import { ClearFlowsMenu, useCanClearFlows } from "./ClearFlowsMenu";
|
||||
|
||||
/**
|
||||
* The header's state pill and its menu: the listening address, the state, Pause or Resume
|
||||
* Recording, Clear Traffic, Override system proxy, and the certificate. The engine runs for as long as the app does, so the
|
||||
* pill shows whether flows are being stored. A certificate that is not trusted steps out of the
|
||||
* The header's state pill and its menu: the listening address, which copies itself, the state,
|
||||
* Pause or Resume Recording, Override system proxy, and the certificate. The engine runs for as
|
||||
* long as the app does, so the pill shows whether flows are being stored. A certificate that is not trusted steps out of the
|
||||
* menu beside the pill until it is. Everything set once and left alone lives in Settings.
|
||||
*/
|
||||
export function ProxyStatus() {
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const clearRef = useRef<DropdownRef>(null);
|
||||
const canClear = useCanClearFlows();
|
||||
const [copied, setCopied] = useCopied();
|
||||
const proxyState = useAtomValue(proxyStateAtom);
|
||||
const setup = useProxySetup();
|
||||
@@ -134,17 +131,6 @@ export function ProxyStatus() {
|
||||
hotKeyLabelOnly: true,
|
||||
onSelect: () => execute.mutate({ scope: "global", action: "recording_toggle" }),
|
||||
},
|
||||
{
|
||||
label: "Clear Traffic",
|
||||
icon: "trash",
|
||||
disabled: !canClear,
|
||||
hotKeyAction: "flows_clear",
|
||||
hotKeyLabelOnly: true,
|
||||
// Its confirmation opens from the pill once this menu has closed.
|
||||
onSelect: () => {
|
||||
setTimeout(() => clearRef.current?.open());
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "content",
|
||||
@@ -222,36 +208,33 @@ export function ProxyStatus() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="relative flex">
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
<Pill color={status === "stopped" ? "warning" : undefined}>
|
||||
{status === "encrypting" ? (
|
||||
<>
|
||||
<LoadingIcon size="xs" />
|
||||
<span className="text-text">Encrypting</span>
|
||||
<span className="font-mono text-text-subtle">{encrypting}</span>
|
||||
</>
|
||||
) : status === "cli" && other != null ? (
|
||||
<>
|
||||
<StateDot state="cli" />
|
||||
<span className="text-text">CLI</span>
|
||||
</>
|
||||
) : status === "recording" || status === "paused" ? (
|
||||
<>
|
||||
<StateDot state={status} />
|
||||
<span className="text-text">{recording ? "Recording" : "Paused"}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon icon="alert_triangle" size="xs" />
|
||||
Not running
|
||||
</>
|
||||
)}
|
||||
<Icon icon="chevron_down" size="xs" className="text-text-subtlest" />
|
||||
</Pill>
|
||||
</Dropdown>
|
||||
<ClearFlowsMenu ref={clearRef} />
|
||||
</span>
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
<Pill color={status === "stopped" ? "warning" : undefined}>
|
||||
{status === "encrypting" ? (
|
||||
<>
|
||||
<LoadingIcon size="xs" />
|
||||
<span className="text-text">Encrypting</span>
|
||||
<span className="font-mono text-text-subtle">{encrypting}</span>
|
||||
</>
|
||||
) : status === "cli" && other != null ? (
|
||||
<>
|
||||
<StateDot state="cli" />
|
||||
<span className="text-text">CLI</span>
|
||||
</>
|
||||
) : status === "recording" || status === "paused" ? (
|
||||
<>
|
||||
<StateDot state={status} />
|
||||
<span className="text-text">{recording ? "Recording" : "Paused"}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon icon="alert_triangle" size="xs" />
|
||||
Not running
|
||||
</>
|
||||
)}
|
||||
<Icon icon="chevron_down" size="xs" className="text-text-subtlest" />
|
||||
</Pill>
|
||||
</Dropdown>
|
||||
{untrusted && (
|
||||
<Pill color="warning" onClick={() => dropdownRef.current?.open()}>
|
||||
<Icon icon="shield_off" size="xs" />
|
||||
@@ -337,6 +320,7 @@ export function useCopyEnvironment() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether something was just copied, for a moment, and the call that says it was */
|
||||
function useCopied(): [boolean, () => void] {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn all_global_actions() -> Vec<(ActionInvocation, ActionMetadata)> {
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::FlowsClear },
|
||||
ActionMetadata {
|
||||
label: "Clear Traffic".into(),
|
||||
label: "Clear Flows".into(),
|
||||
// Not Cmd+K, which is kept for a command palette.
|
||||
default_hotkey: default_hotkey("Meta+Shift+K", "Ctrl+Shift+K"),
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user