feat(agent-graph): add activity visibility toggle
This commit is contained in:
parent
aefd2e93ac
commit
a76404fec7
7 changed files with 150 additions and 4 deletions
|
|
@ -32,6 +32,7 @@ export interface GraphConfigPort {
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Filters (show/hide node kinds) ────────────────────────────────────
|
// ─── Filters (show/hide node kinds) ────────────────────────────────────
|
||||||
|
showActivity?: boolean;
|
||||||
showTasks?: boolean;
|
showTasks?: boolean;
|
||||||
showProcesses?: boolean;
|
showProcesses?: boolean;
|
||||||
showCompletedTasks?: boolean;
|
showCompletedTasks?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
import {
|
import {
|
||||||
|
Activity,
|
||||||
Columns3,
|
Columns3,
|
||||||
Expand,
|
Expand,
|
||||||
Settings2,
|
Settings2,
|
||||||
|
|
@ -26,6 +27,7 @@ import {
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export interface GraphFilterState {
|
export interface GraphFilterState {
|
||||||
|
showActivity: boolean;
|
||||||
showTasks: boolean;
|
showTasks: boolean;
|
||||||
showProcesses: boolean;
|
showProcesses: boolean;
|
||||||
showEdges: boolean;
|
showEdges: boolean;
|
||||||
|
|
@ -219,6 +221,13 @@ export function GraphControls({
|
||||||
border: '1px solid rgba(100, 200, 255, 0.12)',
|
border: '1px solid rgba(100, 200, 255, 0.12)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<ToolbarToggle
|
||||||
|
active={filters.showActivity}
|
||||||
|
onClick={() => toggle('showActivity')}
|
||||||
|
icon={<Activity size={13} />}
|
||||||
|
label="Activity"
|
||||||
|
block
|
||||||
|
/>
|
||||||
<ToolbarToggle
|
<ToolbarToggle
|
||||||
active={filters.showTasks}
|
active={filters.showTasks}
|
||||||
onClick={() => toggle('showTasks')}
|
onClick={() => toggle('showTasks')}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export interface GraphViewProps {
|
||||||
onSelectNode: (nodeId: string) => void;
|
onSelectNode: (nodeId: string) => void;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
renderHud?: (props: {
|
renderHud?: (props: {
|
||||||
|
filters: GraphFilterState;
|
||||||
getLaunchAnchorScreenPlacement: (
|
getLaunchAnchorScreenPlacement: (
|
||||||
leadNodeId: string,
|
leadNodeId: string,
|
||||||
) => { x: number; y: number; scale: number; visible: boolean } | null;
|
) => { x: number; y: number; scale: number; visible: boolean } | null;
|
||||||
|
|
@ -112,6 +113,7 @@ export function GraphView({
|
||||||
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
||||||
const [interactionLocked, setInteractionLocked] = useState(false);
|
const [interactionLocked, setInteractionLocked] = useState(false);
|
||||||
const [filters, setFilters] = useState<GraphFilterState>({
|
const [filters, setFilters] = useState<GraphFilterState>({
|
||||||
|
showActivity: config?.showActivity ?? true,
|
||||||
showTasks: config?.showTasks ?? true,
|
showTasks: config?.showTasks ?? true,
|
||||||
showProcesses: config?.showProcesses ?? true,
|
showProcesses: config?.showProcesses ?? true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -1016,6 +1018,7 @@ export function GraphView({
|
||||||
{renderHud ? (
|
{renderHud ? (
|
||||||
<div className="pointer-events-none absolute inset-0 z-[5] overflow-hidden">
|
<div className="pointer-events-none absolute inset-0 z-[5] overflow-hidden">
|
||||||
{renderHud({
|
{renderHud({
|
||||||
|
filters,
|
||||||
getLaunchAnchorScreenPlacement,
|
getLaunchAnchorScreenPlacement,
|
||||||
getActivityWorldRect,
|
getActivityWorldRect,
|
||||||
getTransientHandoffSnapshot,
|
getTransientHandoffSnapshot,
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ export const TeamGraphOverlay = ({
|
||||||
getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null;
|
getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null;
|
||||||
focusEdgeIds?: ReadonlySet<string> | null;
|
focusEdgeIds?: ReadonlySet<string> | null;
|
||||||
};
|
};
|
||||||
const { getViewportSize, focusNodeIds } = extraHudProps;
|
const { getViewportSize, focusNodeIds, filters } = extraHudProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -174,6 +174,7 @@ export const TeamGraphOverlay = ({
|
||||||
getNodeWorldPosition={extraHudProps.getNodeWorldPosition}
|
getNodeWorldPosition={extraHudProps.getNodeWorldPosition}
|
||||||
getViewportSize={getViewportSize}
|
getViewportSize={getViewportSize}
|
||||||
focusNodeIds={focusNodeIds}
|
focusNodeIds={focusNodeIds}
|
||||||
|
enabled={filters?.showActivity ?? true}
|
||||||
onOpenTaskDetail={onOpenTaskDetail}
|
onOpenTaskDetail={onOpenTaskDetail}
|
||||||
onOpenMemberProfile={onOpenMemberProfile}
|
onOpenMemberProfile={onOpenMemberProfile}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ export const TeamGraphTab = ({
|
||||||
getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null;
|
getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null;
|
||||||
focusEdgeIds?: ReadonlySet<string> | null;
|
focusEdgeIds?: ReadonlySet<string> | null;
|
||||||
};
|
};
|
||||||
const { getViewportSize, focusNodeIds } = extraHudProps;
|
const { getViewportSize, focusNodeIds, filters } = extraHudProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -199,7 +199,7 @@ export const TeamGraphTab = ({
|
||||||
getNodeWorldPosition={extraHudProps.getNodeWorldPosition}
|
getNodeWorldPosition={extraHudProps.getNodeWorldPosition}
|
||||||
getViewportSize={getViewportSize}
|
getViewportSize={getViewportSize}
|
||||||
focusNodeIds={focusNodeIds}
|
focusNodeIds={focusNodeIds}
|
||||||
enabled={isActive}
|
enabled={isActive && (filters?.showActivity ?? true)}
|
||||||
onOpenTaskDetail={dispatchOpenTask}
|
onOpenTaskDetail={dispatchOpenTask}
|
||||||
onOpenMemberProfile={dispatchOpenProfile}
|
onOpenMemberProfile={dispatchOpenProfile}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ describe('GraphControls', () => {
|
||||||
root.render(
|
root.render(
|
||||||
React.createElement(GraphControls, {
|
React.createElement(GraphControls, {
|
||||||
filters: {
|
filters: {
|
||||||
|
showActivity: true,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -88,6 +89,7 @@ describe('GraphControls', () => {
|
||||||
root.render(
|
root.render(
|
||||||
React.createElement(GraphControls, {
|
React.createElement(GraphControls, {
|
||||||
filters: {
|
filters: {
|
||||||
|
showActivity: true,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -112,4 +114,62 @@ describe('GraphControls', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('toggles activity visibility from graph settings', async () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const onFiltersChange = vi.fn();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(GraphControls, {
|
||||||
|
filters: {
|
||||||
|
showActivity: true,
|
||||||
|
showTasks: true,
|
||||||
|
showProcesses: true,
|
||||||
|
showEdges: true,
|
||||||
|
paused: false,
|
||||||
|
},
|
||||||
|
onFiltersChange,
|
||||||
|
onZoomIn: vi.fn(),
|
||||||
|
onZoomOut: vi.fn(),
|
||||||
|
onZoomToFit: vi.fn(),
|
||||||
|
teamName: 'demo-team',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsButton = host.querySelector('button[aria-label="Graph settings"]');
|
||||||
|
expect(settingsButton).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
settingsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const activityButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
button.textContent?.includes('Activity')
|
||||||
|
);
|
||||||
|
expect(activityButton).not.toBeUndefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
activityButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onFiltersChange).toHaveBeenCalledWith({
|
||||||
|
showActivity: false,
|
||||||
|
showTasks: true,
|
||||||
|
showProcesses: true,
|
||||||
|
showEdges: true,
|
||||||
|
paused: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const hoisted = vi.hoisted(() => ({
|
||||||
time: 0,
|
time: 0,
|
||||||
},
|
},
|
||||||
clearTransientOwnerPositions: vi.fn(),
|
clearTransientOwnerPositions: vi.fn(),
|
||||||
|
graphControlsProps: null as null | Record<string, unknown>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../../packages/agent-graph/src/hooks/useGraphCamera', () => ({
|
vi.mock('../../../../packages/agent-graph/src/hooks/useGraphCamera', () => ({
|
||||||
|
|
@ -69,7 +70,10 @@ vi.mock('../../../../packages/agent-graph/src/hooks/useGraphSimulation', () => (
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../../packages/agent-graph/src/ui/GraphControls', () => ({
|
vi.mock('../../../../packages/agent-graph/src/ui/GraphControls', () => ({
|
||||||
GraphControls: () => null,
|
GraphControls: (props: Record<string, unknown>) => {
|
||||||
|
hoisted.graphControlsProps = props;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../../packages/agent-graph/src/ui/GraphOverlay', () => ({
|
vi.mock('../../../../packages/agent-graph/src/ui/GraphOverlay', () => ({
|
||||||
|
|
@ -95,6 +99,7 @@ describe('GraphView pan interactions', () => {
|
||||||
hoisted.interaction.isDragging.current = false;
|
hoisted.interaction.isDragging.current = false;
|
||||||
hoisted.simulationState.nodes = [];
|
hoisted.simulationState.nodes = [];
|
||||||
hoisted.simulationState.edges = [];
|
hoisted.simulationState.edges = [];
|
||||||
|
hoisted.graphControlsProps = null;
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'ResizeObserver',
|
'ResizeObserver',
|
||||||
class {
|
class {
|
||||||
|
|
@ -397,4 +402,71 @@ describe('GraphView pan interactions', () => {
|
||||||
expect(hoisted.interaction.handleMouseUp).toHaveBeenCalledTimes(1);
|
expect(hoisted.interaction.handleMouseUp).toHaveBeenCalledTimes(1);
|
||||||
expect(hoisted.clearTransientOwnerPositions).toHaveBeenCalledTimes(1);
|
expect(hoisted.clearTransientOwnerPositions).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes activity filter state to renderHud and updates it through graph controls', async () => {
|
||||||
|
const renderHud = vi.fn(() => null);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(GraphView, {
|
||||||
|
data: {
|
||||||
|
teamName: 'demo-team',
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
particles: [],
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
animationEnabled: false,
|
||||||
|
showActivity: false,
|
||||||
|
},
|
||||||
|
renderHud,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(renderHud).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
filters: expect.objectContaining({
|
||||||
|
showActivity: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const controlsProps = hoisted.graphControlsProps as
|
||||||
|
| {
|
||||||
|
filters: {
|
||||||
|
showActivity: boolean;
|
||||||
|
showTasks: boolean;
|
||||||
|
showProcesses: boolean;
|
||||||
|
showEdges: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
};
|
||||||
|
onFiltersChange: (filters: {
|
||||||
|
showActivity: boolean;
|
||||||
|
showTasks: boolean;
|
||||||
|
showProcesses: boolean;
|
||||||
|
showEdges: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
expect(controlsProps).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
controlsProps?.onFiltersChange({
|
||||||
|
...controlsProps!.filters,
|
||||||
|
showActivity: true,
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(renderHud).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
filters: expect.objectContaining({
|
||||||
|
showActivity: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue