mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:38:26 +02:00
Sidebar filtering
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
|
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
|
||||||
import { Tree } from "@yaakapp-internal/ui";
|
import { selectedIdsFamily, Tree } from "@yaakapp-internal/ui";
|
||||||
import type { TreeNode } from "@yaakapp-internal/ui";
|
import type { TreeNode } from "@yaakapp-internal/ui";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import { atomFamily } from "jotai/utils";
|
import { atomFamily } from "jotai/utils";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback } from "react";
|
||||||
import { httpExchangesAtom } from "./store";
|
import { httpExchangesAtom } from "./store";
|
||||||
|
|
||||||
/** A node in the sidebar tree — either a domain or a path segment. */
|
/** A node in the sidebar tree — either a domain or a path segment. */
|
||||||
@@ -17,11 +17,48 @@ const collapsedAtom = atomFamily((treeId: string) =>
|
|||||||
atom<Record<string, boolean>>({}),
|
atom<Record<string, boolean>>({}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SIDEBAR_TREE_ID = "proxy-sidebar";
|
||||||
|
|
||||||
const sidebarTreeAtom = atom<TreeNode<SidebarItem>>((get) => {
|
const sidebarTreeAtom = atom<TreeNode<SidebarItem>>((get) => {
|
||||||
const exchanges = get(httpExchangesAtom);
|
const exchanges = get(httpExchangesAtom);
|
||||||
return buildTree(exchanges);
|
return buildTree(exchanges);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Exchanges filtered by the currently selected sidebar node(s). */
|
||||||
|
export const filteredExchangesAtom = atom((get) => {
|
||||||
|
const exchanges = get(httpExchangesAtom);
|
||||||
|
const tree = get(sidebarTreeAtom);
|
||||||
|
const selectedIds = get(selectedIdsFamily(SIDEBAR_TREE_ID));
|
||||||
|
|
||||||
|
// Nothing selected or root selected → show all
|
||||||
|
if (selectedIds.length === 0 || selectedIds.includes("root")) {
|
||||||
|
return exchanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect exchange IDs from all selected nodes
|
||||||
|
const allowedIds = new Set<string>();
|
||||||
|
const nodeMap = new Map<string, SidebarItem>();
|
||||||
|
collectNodes(tree, nodeMap);
|
||||||
|
|
||||||
|
for (const selectedId of selectedIds) {
|
||||||
|
const node = nodeMap.get(selectedId);
|
||||||
|
if (node) {
|
||||||
|
for (const id of node.exchangeIds) {
|
||||||
|
allowedIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exchanges.filter((ex) => allowedIds.has(ex.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
function collectNodes(node: TreeNode<SidebarItem>, map: Map<string, SidebarItem>) {
|
||||||
|
map.set(node.item.id, node.item);
|
||||||
|
for (const child of node.children ?? []) {
|
||||||
|
collectNodes(child, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a domain → path-segment trie from a flat list of exchanges.
|
* Build a domain → path-segment trie from a flat list of exchanges.
|
||||||
*
|
*
|
||||||
@@ -123,14 +160,24 @@ function buildTree(exchanges: HttpExchange[]): TreeNode<SidebarItem> {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort domains alphabetically, add to root
|
// Add a "Domains" folder between root and domain nodes
|
||||||
|
const allExchangeIds = exchanges.map((ex) => ex.id);
|
||||||
|
const domainsFolder: TreeNode<SidebarItem> = {
|
||||||
|
item: { id: "domains", label: "Domains", exchangeIds: allExchangeIds },
|
||||||
|
parent: rootNode,
|
||||||
|
depth: 1,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
const sortedDomains = [...domainMap.values()].sort((a, b) =>
|
const sortedDomains = [...domainMap.values()].sort((a, b) =>
|
||||||
a.label.localeCompare(b.label),
|
a.label.localeCompare(b.label),
|
||||||
);
|
);
|
||||||
for (const domain of sortedDomains) {
|
for (const domain of sortedDomains) {
|
||||||
rootNode.children!.push(toTreeNode(domain, rootNode, 1));
|
domainsFolder.children!.push(toTreeNode(domain, domainsFolder, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootNode.children!.push(domainsFolder);
|
||||||
|
|
||||||
return rootNode;
|
return rootNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +195,7 @@ function ItemInner({ item }: { item: SidebarItem }) {
|
|||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const tree = useAtomValue(sidebarTreeAtom);
|
const tree = useAtomValue(sidebarTreeAtom);
|
||||||
const treeId = "proxy-sidebar";
|
const treeId = SIDEBAR_TREE_ID;
|
||||||
|
|
||||||
const getItemKey = useCallback((item: SidebarItem) => item.id, []);
|
const getItemKey = useCallback((item: SidebarItem) => item.id, []);
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import { createStore, Provider, useAtomValue } from 'jotai';
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { ActionButton } from './ActionButton';
|
import { ActionButton } from './ActionButton';
|
||||||
import { Sidebar } from './Sidebar';
|
import { filteredExchangesAtom, Sidebar } from './Sidebar';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
import type { ProxyHeader } from '@yaakapp-internal/proxy-lib';
|
||||||
import { initHotkeys } from './hotkeys';
|
import { initHotkeys } from './hotkeys';
|
||||||
import { listen, rpc } from './rpc';
|
import { listen, rpc } from './rpc';
|
||||||
import { useRpcQueryWithEvent } from './rpc-hooks';
|
import { useRpcQueryWithEvent } from './rpc-hooks';
|
||||||
import type { ProxyHeader } from '@yaakapp-internal/proxy-lib';
|
import { applyChange, dataAtom, replaceAll } from './store';
|
||||||
import { applyChange, dataAtom, httpExchangesAtom, replaceAll } from './store';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
const jotaiStore = createStore();
|
const jotaiStore = createStore();
|
||||||
@@ -43,7 +43,7 @@ listen('model_write', (payload) => {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const osType = type();
|
const osType = type();
|
||||||
const exchanges = useAtomValue(httpExchangesAtom);
|
const exchanges = useAtomValue(filteredExchangesAtom);
|
||||||
const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed');
|
const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed');
|
||||||
const isRunning = proxyState?.state === 'running';
|
const isRunning = proxyState?.state === 'running';
|
||||||
|
|
||||||
@@ -136,7 +136,13 @@ function StatusBadge({ status, error }: { status: number | null; error: string |
|
|||||||
if (status == null) return <span className="text-xs text-text-subtlest">—</span>;
|
if (status == null) return <span className="text-xs text-text-subtlest">—</span>;
|
||||||
|
|
||||||
const color =
|
const color =
|
||||||
status >= 500 ? 'text-danger' : status >= 400 ? 'text-warning' : status >= 300 ? 'text-notice' : 'text-success';
|
status >= 500
|
||||||
|
? 'text-danger'
|
||||||
|
: status >= 400
|
||||||
|
? 'text-warning'
|
||||||
|
: status >= 300
|
||||||
|
? 'text-notice'
|
||||||
|
: 'text-success';
|
||||||
|
|
||||||
return <span className={classNames('text-xs font-mono', color)}>{status}</span>;
|
return <span className={classNames('text-xs font-mono', color)}>{status}</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user