Sidebar filtering

This commit is contained in:
Gregory Schier
2026-03-11 15:55:25 -07:00
parent 0c52fd03e2
commit d4a6735881
2 changed files with 63 additions and 10 deletions

View File

@@ -1,9 +1,9 @@
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 { atom, useAtomValue } from "jotai";
import { atomFamily } from "jotai/utils";
import { useCallback, useMemo } from "react";
import { useCallback } from "react";
import { httpExchangesAtom } from "./store";
/** 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>>({}),
);
export const SIDEBAR_TREE_ID = "proxy-sidebar";
const sidebarTreeAtom = atom<TreeNode<SidebarItem>>((get) => {
const exchanges = get(httpExchangesAtom);
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.
*
@@ -123,14 +160,24 @@ function buildTree(exchanges: HttpExchange[]): TreeNode<SidebarItem> {
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) =>
a.label.localeCompare(b.label),
);
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;
}
@@ -148,7 +195,7 @@ function ItemInner({ item }: { item: SidebarItem }) {
export function Sidebar() {
const tree = useAtomValue(sidebarTreeAtom);
const treeId = "proxy-sidebar";
const treeId = SIDEBAR_TREE_ID;
const getItemKey = useCallback((item: SidebarItem) => item.id, []);

View File

@@ -15,13 +15,13 @@ import { createStore, Provider, useAtomValue } from 'jotai';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ActionButton } from './ActionButton';
import { Sidebar } from './Sidebar';
import { filteredExchangesAtom, Sidebar } from './Sidebar';
import './main.css';
import type { ProxyHeader } from '@yaakapp-internal/proxy-lib';
import { initHotkeys } from './hotkeys';
import { listen, rpc } from './rpc';
import { useRpcQueryWithEvent } from './rpc-hooks';
import type { ProxyHeader } from '@yaakapp-internal/proxy-lib';
import { applyChange, dataAtom, httpExchangesAtom, replaceAll } from './store';
import { applyChange, dataAtom, replaceAll } from './store';
const queryClient = new QueryClient();
const jotaiStore = createStore();
@@ -43,7 +43,7 @@ listen('model_write', (payload) => {
function App() {
const osType = type();
const exchanges = useAtomValue(httpExchangesAtom);
const exchanges = useAtomValue(filteredExchangesAtom);
const { data: proxyState } = useRpcQueryWithEvent('get_proxy_state', {}, 'proxy_state_changed');
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>;
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>;
}