Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree View for Flow items #5996

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions web/src/js/__tests__/ducks/tutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const testState: RootState = {
flow: {
contentViewFor: {},
tab: "request",
isTreeView: false,
},
modal: {
activeModal: undefined,
Expand Down
124 changes: 124 additions & 0 deletions web/src/js/components/FlowTreeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as React from "react";
import { useDispatch, useSelector } from "react-redux";
import { useAppSelector } from "../ducks";
import { RequestUtils } from "../flow/utils";
import classnames from "classnames";
import { select } from "../ducks/flows";
import Filt from "../filt/filt";
import { Flow } from "../flow";

interface TreeViewFlowWrapper {
path: string;
child: Map<string, TreeViewFlowWrapper>;
flow: Flow | null;
highlight: boolean | undefined;
}

function FlowTreeView() {
const flows = useAppSelector((state) => state.flows.view);
const newFlows: Map<string, TreeViewFlowWrapper> = new Map();
const highlightFilter = useAppSelector((state) => state.flows.highlight);
const isHighlightedFn = highlightFilter
? Filt.parse(highlightFilter)
: () => false;

flows.map((flow) => {
if (flow.server_conn?.address) {
if (flow.type === "http") {
try {
const url = new URL(RequestUtils.pretty_url(flow.request));
if (!newFlows.has(url.host)) {
newFlows.set(url.host, {
path: url.href,
child: new Map(),
flow: null,
highlight: false,
});
}
const isHighlighted = flow && isHighlightedFn(flow);
newFlows.get(url.host)?.child.set(url.pathname, {
path: url.href,
child: new Map(),
flow: flow,
highlight: isHighlighted,
});
if (isHighlighted) newFlows.get(url.host)!.highlight = true;
} catch (error) {
console.error(error);
}
}
}
});

return (
<div
className="flow-table"
style={{
width: "100%",
maxHeight: "90vh",
}}
>
<ul
className="list-group w-100 overflow-auto"
style={{ width: "100%", height: "100%" }}
>
{Array.from(newFlows).map((el) => (
<FlowRow flow={el[1]} text={el[0]} />
))}
</ul>
</div>
);
}

function FlowRow({
active,
flow,
text,
}: {
active?: boolean;
flow: TreeViewFlowWrapper;
text: string;
}) {
const [show, setShow] = React.useState(false);
const childs = Array.from(flow.child ?? []);
const dispatch = useDispatch();
const selected = useAppSelector((state) => state.flows.selected);

return (
<>
<li
onClick={() => {
if (childs.length !== 0) setShow(!show);
if (flow.flow) dispatch(select(flow.flow.id));
}}
style={{
backgroundColor: active
? "#7bbefc"
: flow.highlight
? "#ffeb99"
: "",
}}
className={classnames([
"list-group-item",
active ? "active" : "",
])}
>
<span style={{}}></span>
{text}
</li>
<div style={{ display: show ? "block" : "none" }}>
<pre>
{childs.map((el) => (
<FlowRow
flow={el[1]}
text={el[1].path}
active={selected.includes(el[1].flow?.id ?? "")}
/>
))}
</pre>
</div>
</>
);
}

export default FlowTreeView;
14 changes: 14 additions & 0 deletions web/src/js/components/Header/MenuToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as eventLogActions from "../../ducks/eventLog";
import * as commandBarActions from "../../ducks/commandBar";
import { useAppDispatch, useAppSelector } from "../../ducks";
import * as optionsActions from "../../ducks/options";
import { toggleFlowViewType } from "../../ducks/ui/flow";

type MenuToggleProps = {
value: boolean;
Expand Down Expand Up @@ -68,3 +69,16 @@ export function CommandBarToggle() {
</MenuToggle>
);
}

export function TreeViewToggle() {
const visible = useAppSelector((state) => state.ui.flow.isTreeView);
const dispatch = useDispatch();
return (
<MenuToggle
value={visible}
onChange={() => dispatch(toggleFlowViewType())}
>
Tree View
</MenuToggle>
);
}
8 changes: 7 additions & 1 deletion web/src/js/components/Header/OptionMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as React from "react";
import { CommandBarToggle, EventlogToggle, OptionsToggle } from "./MenuToggle";
import {
CommandBarToggle,
EventlogToggle,
OptionsToggle,
TreeViewToggle,
} from "./MenuToggle";
import Button from "../common/Button";
import DocsLink from "../common/DocsLink";
import HideInStatic from "../common/HideInStatic";
Expand Down Expand Up @@ -49,6 +54,7 @@ export default function OptionMenu() {
<div className="menu-content">
<EventlogToggle />
<CommandBarToggle />
<TreeViewToggle />
</div>
<div className="menu-legend">View Options</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion web/src/js/components/MainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import FlowTable from "./FlowTable";
import FlowView from "./FlowView";
import { useAppSelector } from "../ducks";
import CaptureSetup from "./CaptureSetup";
import FlowTreeView from "./FlowTreeView";

export default function MainView() {
const hasSelection = useAppSelector(
(state) => !!state.flows.byId[state.flows.selected[0]]
);
const hasFlows = useAppSelector((state) => state.flows.list.length > 0);
const isTreeView = useAppSelector((state) => state.ui.flow.isTreeView);
return (
<div className="main-view">
{hasFlows ? <FlowTable /> : <CaptureSetup />}
{hasFlows ? (
isTreeView ? (
<FlowTreeView />
) : (
<FlowTable />
)
) : (
<CaptureSetup />
)}

{hasSelection && <Splitter key="splitter" />}
{hasSelection && <FlowView key="flowDetails" />}
Expand Down
14 changes: 13 additions & 1 deletion web/src/js/ducks/ui/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ import * as flowActions from "../flows";
import { tabsForFlow } from "../../components/FlowView";

export const SET_TAB = "UI_FLOWVIEW_SET_TAB",
SET_CONTENT_VIEW_FOR = "SET_CONTENT_VIEW_FOR";
SET_CONTENT_VIEW_FOR = "SET_CONTENT_VIEW_FOR",
TOGGLE_FLOW_VIEW_TYPE = "TOGGLE_FLOW_VIEW_TYPE";

interface UiFlowState {
tab: string;
contentViewFor: { [messageId: string]: string };
isTreeView: boolean;
}

export const defaultState: UiFlowState = {
tab: "request",
contentViewFor: {},
isTreeView: false,
};

export default function reducer(state = defaultState, action): UiFlowState {
Expand All @@ -30,6 +33,11 @@ export default function reducer(state = defaultState, action): UiFlowState {
...state,
tab: action.tab ? action.tab : "request",
};
case TOGGLE_FLOW_VIEW_TYPE:
return {
...state,
isTreeView: !state.isTreeView,
};

default:
return state;
Expand All @@ -43,3 +51,7 @@ export function selectTab(tab) {
export function setContentViewFor(messageId: string, contentView: string) {
return { type: SET_CONTENT_VIEW_FOR, messageId, contentView };
}

export function toggleFlowViewType() {
return { type: TOGGLE_FLOW_VIEW_TYPE };
}