[web] replay multiple selected flows (#7674)

Co-authored-by: Maximilian Hils <git@maximilianhils.com>
This commit is contained in:
Matteo Luppi 2025-04-29 12:45:19 +02:00 committed by GitHub
parent d48f9ae59c
commit 01a5cceeac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 23 additions and 17 deletions

View file

@ -58,7 +58,7 @@
([#7652](https://github.com/mitmproxy/mitmproxy/pull/7652), @lups2000)
- `mitmproxy.dns.Message` has been renamed to `mitmproxy.dns.DNSMessage`
([#7670](https://github.com/mitmproxy/mitmproxy/pull/7670), @mhils)
- Added support for selecting multiple flows in mitmweb using Ctrl+Click and Shift+Click. Multi-selection is now supported for deleting, duplicating, marking, reverting, resuming, and aborting flows.
- Added support for selecting multiple flows in mitmweb using Ctrl+Click and Shift+Click. Multi-selection is now supported for deleting, duplicating, marking, reverting, replaying ,resuming, and aborting flows.
([#7319](https://github.com/mitmproxy/mitmproxy/pull/7319), @lups2000, @mhils)
[tree-sitter]: https://tree-sitter.github.io/tree-sitter/

View file

@ -255,6 +255,8 @@ describe("flows actions", () => {
const tflow = TFlow();
tflow.intercepted = true;
tflow.modified = true;
// @ts-expect-error TFlow is Required<> for other tests.
tflow.websocket = undefined;
const ttcpflow = TTCPFlow();
beforeEach(() => {
@ -316,7 +318,7 @@ describe("flows actions", () => {
});
it("should handle replay action", () => {
store.dispatch(flowActions.replay(tflow));
store.dispatch(flowActions.replay([tflow]));
expect(fetchApi).toBeCalledWith(
"/flows/d91165be-ca1f-4612-88a9-c0f8696f3e29/replay",
{ method: "POST" },

View file

@ -172,7 +172,7 @@ export const quickactions: FlowColumn = ({ flow }) => {
<a
href="#"
className="quickaction"
onClick={() => dispatch(flowActions.replay(flow))}
onClick={() => dispatch(flowActions.replay([flow]))}
>
<i className="fa fa-fw fa-repeat text-primary" />
</a>

View file

@ -12,7 +12,7 @@ import {
duplicate as duplicateFlows,
kill as killFlows,
remove as removeFlows,
replay as replayFlow,
replay as replayFlows,
resume as resumeFlows,
revert as revertFlows,
mark as markFlows,
@ -29,9 +29,6 @@ export default function FlowMenu(): JSX.Element {
const selectedFlows = useAppSelector((state) => state.flows.selected);
const flow = selectedFlows[0];
const hasSingleFlowSelected = useAppSelector(
(state) => state.flows.selected.length === 1,
);
const canResumeOrKillAny = selectedFlows.some(canResumeOrKill);
if (selectedFlows.length === 0) return <div />;
@ -43,10 +40,8 @@ export default function FlowMenu(): JSX.Element {
<Button
title="[r]eplay flow"
icon="fa-repeat text-primary"
onClick={() => dispatch(replayFlow(flow))}
disabled={
!canReplay(flow) || !hasSingleFlowSelected
}
onClick={() => dispatch(replayFlows(selectedFlows))}
disabled={!selectedFlows.some(canReplay)}
>
Replay
</Button>

View file

@ -3,7 +3,12 @@ import { fetchApi } from "../utils";
import * as store from "./utils/store";
import Filt from "../filt/filt";
import { Flow } from "../flow";
import { canResumeOrKill, canRevert, sortFunctions } from "../flow/utils";
import {
canReplay,
canResumeOrKill,
canRevert,
sortFunctions,
} from "../flow/utils";
import { AppDispatch, RootState } from "./store";
import { State } from "./utils/store";
@ -255,8 +260,14 @@ export function duplicate(flows: Flow[]) {
);
}
export function replay(flow: Flow) {
return () => fetchApi(`/flows/${flow.id}/replay`, { method: "POST" });
export function replay(flows: Flow[]) {
flows = flows.filter(canReplay);
return () =>
Promise.all(
flows.map((flow) =>
fetchApi(`/flows/${flow.id}/replay`, { method: "POST" }),
),
);
}
export function revert(flows: Flow[]) {

View file

@ -107,9 +107,7 @@ export function onKeyDown(e: KeyboardEvent) {
}
case "r": {
if (selectedFlows.length === 1) {
dispatch(flowsActions.replay(flow));
}
dispatch(flowsActions.replay(selectedFlows));
break;
}