mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-25 01:29:48 +00:00
major mitmweb upgrades
This commit is contained in:
parent
46cf75d01e
commit
65aa0a6ef5
@ -5,7 +5,8 @@ import logging
|
|||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import ClassVar, Optional
|
from itertools import islice
|
||||||
|
from typing import ClassVar, Optional, Sequence, Union
|
||||||
|
|
||||||
import tornado.escape
|
import tornado.escape
|
||||||
import tornado.web
|
import tornado.web
|
||||||
@ -13,7 +14,7 @@ import tornado.websocket
|
|||||||
|
|
||||||
import mitmproxy.flow
|
import mitmproxy.flow
|
||||||
import mitmproxy.tools.web.master # noqa
|
import mitmproxy.tools.web.master # noqa
|
||||||
from mitmproxy import contentviews
|
from mitmproxy import certs, command, contentviews
|
||||||
from mitmproxy import flowfilter
|
from mitmproxy import flowfilter
|
||||||
from mitmproxy import http
|
from mitmproxy import http
|
||||||
from mitmproxy import io
|
from mitmproxy import io
|
||||||
@ -21,7 +22,27 @@ from mitmproxy import log
|
|||||||
from mitmproxy import optmanager
|
from mitmproxy import optmanager
|
||||||
from mitmproxy import version
|
from mitmproxy import version
|
||||||
from mitmproxy.addons import export
|
from mitmproxy.addons import export
|
||||||
|
from mitmproxy.http import HTTPFlow
|
||||||
|
from mitmproxy.tcp import TCPFlow, TCPMessage
|
||||||
|
from mitmproxy.tools.console.common import SYMBOL_MARK, render_marker
|
||||||
from mitmproxy.utils.strutils import always_str
|
from mitmproxy.utils.strutils import always_str
|
||||||
|
from mitmproxy.websocket import WebSocketMessage
|
||||||
|
|
||||||
|
|
||||||
|
def cert_to_json(certs: Sequence[certs.Cert]) -> Optional[dict]:
|
||||||
|
if not certs:
|
||||||
|
return None
|
||||||
|
cert = certs[0]
|
||||||
|
return {
|
||||||
|
"keyinfo": cert.keyinfo,
|
||||||
|
"sha256": cert.fingerprint().hex(),
|
||||||
|
"notbefore": int(cert.notbefore.timestamp()),
|
||||||
|
"notafter": int(cert.notafter.timestamp()),
|
||||||
|
"serial": str(cert.serial),
|
||||||
|
"subject": cert.subject,
|
||||||
|
"issuer": cert.issuer,
|
||||||
|
"altnames": cert.altnames,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||||
@ -37,7 +58,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"is_replay": flow.is_replay,
|
"is_replay": flow.is_replay,
|
||||||
"type": flow.type,
|
"type": flow.type,
|
||||||
"modified": flow.modified(),
|
"modified": flow.modified(),
|
||||||
"marked": flow.marked,
|
"marked": render_marker(flow.marked).replace(SYMBOL_MARK, "🔴") if flow.marked else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if flow.client_conn:
|
if flow.client_conn:
|
||||||
@ -46,6 +67,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"peername": flow.client_conn.peername,
|
"peername": flow.client_conn.peername,
|
||||||
"sockname": flow.client_conn.sockname,
|
"sockname": flow.client_conn.sockname,
|
||||||
"tls_established": flow.client_conn.tls_established,
|
"tls_established": flow.client_conn.tls_established,
|
||||||
|
"cert": cert_to_json(flow.client_conn.certificate_list),
|
||||||
"sni": flow.client_conn.sni,
|
"sni": flow.client_conn.sni,
|
||||||
"cipher": flow.client_conn.cipher,
|
"cipher": flow.client_conn.cipher,
|
||||||
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||||
@ -53,11 +75,6 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"timestamp_start": flow.client_conn.timestamp_start,
|
"timestamp_start": flow.client_conn.timestamp_start,
|
||||||
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
|
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
|
||||||
"timestamp_end": flow.client_conn.timestamp_end,
|
"timestamp_end": flow.client_conn.timestamp_end,
|
||||||
|
|
||||||
# Legacy properties
|
|
||||||
"address": flow.client_conn.peername,
|
|
||||||
"cipher_name": flow.client_conn.cipher,
|
|
||||||
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if flow.server_conn:
|
if flow.server_conn:
|
||||||
@ -67,6 +84,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"sockname": flow.server_conn.sockname,
|
"sockname": flow.server_conn.sockname,
|
||||||
"address": flow.server_conn.address,
|
"address": flow.server_conn.address,
|
||||||
"tls_established": flow.server_conn.tls_established,
|
"tls_established": flow.server_conn.tls_established,
|
||||||
|
"cert": cert_to_json(flow.server_conn.certificate_list),
|
||||||
"sni": flow.server_conn.sni,
|
"sni": flow.server_conn.sni,
|
||||||
"cipher": flow.server_conn.cipher,
|
"cipher": flow.server_conn.cipher,
|
||||||
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
||||||
@ -75,10 +93,6 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
|
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
|
||||||
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
|
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
|
||||||
"timestamp_end": flow.server_conn.timestamp_end,
|
"timestamp_end": flow.server_conn.timestamp_end,
|
||||||
# Legacy properties
|
|
||||||
"ip_address": flow.server_conn.peername,
|
|
||||||
"source_address": flow.server_conn.sockname,
|
|
||||||
"alpn_proto_negotiated": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
|
||||||
}
|
}
|
||||||
if flow.error:
|
if flow.error:
|
||||||
f["error"] = flow.error.get_state()
|
f["error"] = flow.error.get_state()
|
||||||
@ -87,7 +101,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
content_length: Optional[int]
|
content_length: Optional[int]
|
||||||
content_hash: Optional[str]
|
content_hash: Optional[str]
|
||||||
if flow.request:
|
if flow.request:
|
||||||
if flow.request.raw_content:
|
if flow.request.raw_content is not None:
|
||||||
content_length = len(flow.request.raw_content)
|
content_length = len(flow.request.raw_content)
|
||||||
content_hash = hashlib.sha256(flow.request.raw_content).hexdigest()
|
content_hash = hashlib.sha256(flow.request.raw_content).hexdigest()
|
||||||
else:
|
else:
|
||||||
@ -109,7 +123,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"pretty_host": flow.request.pretty_host,
|
"pretty_host": flow.request.pretty_host,
|
||||||
}
|
}
|
||||||
if flow.response:
|
if flow.response:
|
||||||
if flow.response.raw_content:
|
if flow.response.raw_content is not None:
|
||||||
content_length = len(flow.response.raw_content)
|
content_length = len(flow.response.raw_content)
|
||||||
content_hash = hashlib.sha256(flow.response.raw_content).hexdigest()
|
content_hash = hashlib.sha256(flow.response.raw_content).hexdigest()
|
||||||
else:
|
else:
|
||||||
@ -129,6 +143,18 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
if flow.response.data.trailers:
|
if flow.response.data.trailers:
|
||||||
f["response"]["trailers"] = tuple(flow.response.data.trailers.items(True))
|
f["response"]["trailers"] = tuple(flow.response.data.trailers.items(True))
|
||||||
|
|
||||||
|
if flow.websocket:
|
||||||
|
f["websocket"] = {
|
||||||
|
"messages_meta": {
|
||||||
|
"count": len(flow.websocket.messages),
|
||||||
|
"timestamp_last": flow.websocket.messages[-1].timestamp if flow.websocket.messages else None,
|
||||||
|
},
|
||||||
|
"closed_by_client": flow.websocket.closed_by_client,
|
||||||
|
"close_code": flow.websocket.close_code,
|
||||||
|
"close_reason": flow.websocket.close_reason,
|
||||||
|
"timestamp_end": flow.websocket.timestamp_end,
|
||||||
|
}
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
@ -147,7 +173,7 @@ class APIError(tornado.web.HTTPError):
|
|||||||
class RequestHandler(tornado.web.RequestHandler):
|
class RequestHandler(tornado.web.RequestHandler):
|
||||||
application: "Application"
|
application: "Application"
|
||||||
|
|
||||||
def write(self, chunk):
|
def write(self, chunk: Union[str, bytes, dict, list]):
|
||||||
# Writing arrays on the top level is ok nowadays.
|
# Writing arrays on the top level is ok nowadays.
|
||||||
# http://flask.pocoo.org/docs/0.11/security/#json-security
|
# http://flask.pocoo.org/docs/0.11/security/#json-security
|
||||||
if isinstance(chunk, list):
|
if isinstance(chunk, list):
|
||||||
@ -217,7 +243,7 @@ class IndexHandler(RequestHandler):
|
|||||||
def get(self):
|
def get(self):
|
||||||
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
||||||
assert token
|
assert token
|
||||||
self.render("index.html", static=False, version=version.VERSION)
|
self.render("index.html")
|
||||||
|
|
||||||
|
|
||||||
class FilterHelp(RequestHandler):
|
class FilterHelp(RequestHandler):
|
||||||
@ -278,14 +304,6 @@ class DumpFlows(RequestHandler):
|
|||||||
bio.close()
|
bio.close()
|
||||||
|
|
||||||
|
|
||||||
class ExportFlow(RequestHandler):
|
|
||||||
def post(self, flow_id, format):
|
|
||||||
out = export.formats[format](self.flow)
|
|
||||||
self.write({
|
|
||||||
"export": always_str(out, "utf8", "backslashreplace")
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class ClearAll(RequestHandler):
|
class ClearAll(RequestHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
self.view.clear()
|
self.view.clear()
|
||||||
@ -329,12 +347,12 @@ class FlowHandler(RequestHandler):
|
|||||||
self.view.remove([self.flow])
|
self.view.remove([self.flow])
|
||||||
|
|
||||||
def put(self, flow_id):
|
def put(self, flow_id):
|
||||||
flow = self.flow
|
flow: mitmproxy.flow.Flow = self.flow
|
||||||
flow.backup()
|
flow.backup()
|
||||||
try:
|
try:
|
||||||
for a, b in self.json.items():
|
for a, b in self.json.items():
|
||||||
if a == "request" and hasattr(flow, "request"):
|
if a == "request" and hasattr(flow, "request"):
|
||||||
request = flow.request
|
request: mitmproxy.http.Request = flow.request
|
||||||
for k, v in b.items():
|
for k, v in b.items():
|
||||||
if k in ["method", "scheme", "host", "path", "http_version"]:
|
if k in ["method", "scheme", "host", "path", "http_version"]:
|
||||||
setattr(request, k, str(v))
|
setattr(request, k, str(v))
|
||||||
@ -354,7 +372,7 @@ class FlowHandler(RequestHandler):
|
|||||||
raise APIError(400, f"Unknown update request.{k}: {v}")
|
raise APIError(400, f"Unknown update request.{k}: {v}")
|
||||||
|
|
||||||
elif a == "response" and hasattr(flow, "response"):
|
elif a == "response" and hasattr(flow, "response"):
|
||||||
response = flow.response
|
response: mitmproxy.http.Response = flow.response
|
||||||
for k, v in b.items():
|
for k, v in b.items():
|
||||||
if k in ["msg", "http_version"]:
|
if k in ["msg", "http_version"]:
|
||||||
setattr(response, k, str(v))
|
setattr(response, k, str(v))
|
||||||
@ -372,6 +390,8 @@ class FlowHandler(RequestHandler):
|
|||||||
response.text = v
|
response.text = v
|
||||||
else:
|
else:
|
||||||
raise APIError(400, f"Unknown update response.{k}: {v}")
|
raise APIError(400, f"Unknown update response.{k}: {v}")
|
||||||
|
elif a == "marked":
|
||||||
|
flow.marked = b
|
||||||
else:
|
else:
|
||||||
raise APIError(400, f"Unknown update {a}: {b}")
|
raise APIError(400, f"Unknown update {a}: {b}")
|
||||||
except APIError:
|
except APIError:
|
||||||
@ -409,9 +429,6 @@ class FlowContent(RequestHandler):
|
|||||||
def get(self, flow_id, message):
|
def get(self, flow_id, message):
|
||||||
message = getattr(self.flow, message)
|
message = getattr(self.flow, message)
|
||||||
|
|
||||||
if not message.raw_content:
|
|
||||||
raise APIError(400, "No content.")
|
|
||||||
|
|
||||||
content_encoding = message.headers.get("Content-Encoding", None)
|
content_encoding = message.headers.get("Content-Encoding", None)
|
||||||
if content_encoding:
|
if content_encoding:
|
||||||
content_encoding = re.sub(r"[^\w]", "", content_encoding)
|
content_encoding = re.sub(r"[^\w]", "", content_encoding)
|
||||||
@ -436,40 +453,88 @@ class FlowContent(RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class FlowContentView(RequestHandler):
|
class FlowContentView(RequestHandler):
|
||||||
def get(self, flow_id, message, content_view):
|
def message_to_json(
|
||||||
message = getattr(self.flow, message)
|
self,
|
||||||
|
viewname: str,
|
||||||
|
message: Union[http.Message, TCPMessage, WebSocketMessage],
|
||||||
|
flow: Union[HTTPFlow, TCPFlow],
|
||||||
|
max_lines: Optional[int] = None
|
||||||
|
):
|
||||||
|
description, lines, error = contentviews.get_message_content_view(viewname, message, flow)
|
||||||
|
if error:
|
||||||
|
self.master.log.error(error)
|
||||||
|
if max_lines:
|
||||||
|
lines = islice(lines, max_lines)
|
||||||
|
|
||||||
description, lines, error = contentviews.get_message_content_view(
|
return dict(
|
||||||
content_view.replace('_', ' '), message, self.flow
|
|
||||||
)
|
|
||||||
# if error:
|
|
||||||
# add event log
|
|
||||||
|
|
||||||
self.write(dict(
|
|
||||||
lines=list(lines),
|
lines=list(lines),
|
||||||
description=description
|
description=description,
|
||||||
))
|
)
|
||||||
|
|
||||||
|
def get(self, flow_id, message, content_view):
|
||||||
|
flow = self.flow
|
||||||
|
assert isinstance(flow, (HTTPFlow, TCPFlow))
|
||||||
|
|
||||||
|
if self.request.arguments.get("lines"):
|
||||||
|
max_lines = int(self.request.arguments["lines"][0])
|
||||||
|
else:
|
||||||
|
max_lines = None
|
||||||
|
|
||||||
|
if message == "messages":
|
||||||
|
if isinstance(flow, HTTPFlow) and flow.websocket:
|
||||||
|
messages = flow.websocket.messages
|
||||||
|
elif isinstance(flow, TCPFlow):
|
||||||
|
messages = flow.messages
|
||||||
|
else:
|
||||||
|
raise APIError(400, f"This flow has no messages.")
|
||||||
|
msgs = []
|
||||||
|
for m in messages:
|
||||||
|
d = self.message_to_json(content_view, m, flow, max_lines)
|
||||||
|
d["from_client"] = m.from_client
|
||||||
|
d["timestamp"] = m.timestamp
|
||||||
|
msgs.append(d)
|
||||||
|
if max_lines:
|
||||||
|
max_lines -= len(d["lines"])
|
||||||
|
if max_lines <= 0:
|
||||||
|
break
|
||||||
|
self.write(msgs)
|
||||||
|
else:
|
||||||
|
message = getattr(self.flow, message)
|
||||||
|
self.write(self.message_to_json(content_view, message, flow, max_lines))
|
||||||
|
|
||||||
|
|
||||||
class Commands(RequestHandler):
|
class Commands(RequestHandler):
|
||||||
def get(self):
|
def get(self) -> None:
|
||||||
commands = {}
|
commands = {}
|
||||||
for (name, command) in self.master.commands.commands.items():
|
for (name, cmd) in self.master.commands.commands.items():
|
||||||
commands[name] = {
|
commands[name] = {
|
||||||
"args": [],
|
"help": cmd.help,
|
||||||
"signature_help": command.signature_help(),
|
"parameters": [
|
||||||
"description": command.help
|
{
|
||||||
|
"name": param.name,
|
||||||
|
"type": command.typename(param.type),
|
||||||
|
"kind": str(param.kind),
|
||||||
|
}
|
||||||
|
for param in cmd.parameters
|
||||||
|
],
|
||||||
|
"return_type": command.typename(cmd.return_type) if cmd.return_type else None,
|
||||||
|
"signature_help": cmd.signature_help(),
|
||||||
}
|
}
|
||||||
for parameter in command.parameters:
|
self.write(commands)
|
||||||
commands[name]["args"].append(parameter.name)
|
|
||||||
self.write({"commands": commands})
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
result = self.master.commands.execute(self.json["command"])
|
class ExecuteCommand(RequestHandler):
|
||||||
if result is None:
|
def post(self, cmd: str):
|
||||||
self.write({"result": ""})
|
# TODO: We should parse query strings here, this API is painful.
|
||||||
return
|
try:
|
||||||
self.write({"result": result, "type": type(result).__name__, "history": self.master.commands.execute("commands.history.get")})
|
args = self.json['arguments']
|
||||||
|
except APIError:
|
||||||
|
args = []
|
||||||
|
result = self.master.commands.call_strings(cmd, args)
|
||||||
|
self.write({
|
||||||
|
"value": result,
|
||||||
|
"type": command.typename(type(result)) if result is not None else "none"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class Events(RequestHandler):
|
class Events(RequestHandler):
|
||||||
@ -512,7 +577,7 @@ class Conf(RequestHandler):
|
|||||||
conf = {
|
conf = {
|
||||||
"static": False,
|
"static": False,
|
||||||
"version": version.VERSION,
|
"version": version.VERSION,
|
||||||
"contentViews": [v.name for v in contentviews.views]
|
"contentViews": [v.name for v in contentviews.views if v.name != "Query"]
|
||||||
}
|
}
|
||||||
self.write(f"MITMWEB_CONF = {json.dumps(conf)};")
|
self.write(f"MITMWEB_CONF = {json.dumps(conf)};")
|
||||||
self.set_header("content-type", "application/javascript")
|
self.set_header("content-type", "application/javascript")
|
||||||
@ -542,6 +607,7 @@ class Application(tornado.web.Application):
|
|||||||
(r"/filter-help(?:\.json)?", FilterHelp),
|
(r"/filter-help(?:\.json)?", FilterHelp),
|
||||||
(r"/updates", ClientConnection),
|
(r"/updates", ClientConnection),
|
||||||
(r"/commands(?:\.json)?", Commands),
|
(r"/commands(?:\.json)?", Commands),
|
||||||
|
(r"/commands/(?P<cmd>[a-z.]+)", ExecuteCommand),
|
||||||
(r"/events(?:\.json)?", Events),
|
(r"/events(?:\.json)?", Events),
|
||||||
(r"/flows(?:\.json)?", Flows),
|
(r"/flows(?:\.json)?", Flows),
|
||||||
(r"/flows/dump", DumpFlows),
|
(r"/flows/dump", DumpFlows),
|
||||||
@ -553,10 +619,9 @@ class Application(tornado.web.Application):
|
|||||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
|
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
|
||||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
|
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
|
||||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
|
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
|
||||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/export/(?P<format>[a-z][a-z_]+).json", ExportFlow),
|
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response|messages)/content.data", FlowContent),
|
||||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content.data", FlowContent),
|
|
||||||
(
|
(
|
||||||
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
|
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response|messages)/content/(?P<content_view>[0-9a-zA-Z\-\_%]+)(?:\.json)?",
|
||||||
FlowContentView),
|
FlowContentView),
|
||||||
(r"/clear", ClearAll),
|
(r"/clear", ClearAll),
|
||||||
(r"/options(?:\.json)?", Options),
|
(r"/options(?:\.json)?", Options),
|
||||||
|
BIN
mitmproxy/tools/web/static/images/resourceWebSocketIcon.png
vendored
Normal file
BIN
mitmproxy/tools/web/static/images/resourceWebSocketIcon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -265,6 +265,18 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
"description": "Raw"
|
"description": "Raw"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_commands(self):
|
||||||
|
resp = self.fetch("/commands")
|
||||||
|
assert resp.code == 200
|
||||||
|
assert get_json(resp)["set"]["help"]
|
||||||
|
|
||||||
|
def test_command_execute(self):
|
||||||
|
resp = self.fetch("/commands/unknown", method="POST")
|
||||||
|
assert resp.code == 500
|
||||||
|
resp = self.fetch("/commands/commands.history.get", method="POST")
|
||||||
|
assert resp.code == 200
|
||||||
|
assert get_json(resp) == ["unknown", "commands.history.get"]
|
||||||
|
|
||||||
def test_events(self):
|
def test_events(self):
|
||||||
resp = self.fetch("/events")
|
resp = self.fetch("/events")
|
||||||
assert resp.code == 200
|
assert resp.code == 200
|
||||||
@ -368,14 +380,14 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
print("/** Auto-generated by test_app.py:TestApp.test_generate_options_js */")
|
print("/** Auto-generated by test_app.py:TestApp.test_generate_options_js */")
|
||||||
|
|
||||||
print("export interface OptionsState {")
|
print("export interface OptionsState {")
|
||||||
for _, opt in m.options.items():
|
for _, opt in sorted(m.options.items()):
|
||||||
print(f" {opt.name}: {ts_type(opt.typespec)}")
|
print(f" {opt.name}: {ts_type(opt.typespec)}")
|
||||||
print("}")
|
print("}")
|
||||||
print("")
|
print("")
|
||||||
print("export type Option = keyof OptionsState")
|
print("export type Option = keyof OptionsState")
|
||||||
print("")
|
print("")
|
||||||
print("export const defaultState: OptionsState = {")
|
print("export const defaultState: OptionsState = {")
|
||||||
for _, opt in m.options.items():
|
for _, opt in sorted(m.options.items()):
|
||||||
print(f" {opt.name}: {json.dumps(opt.default)},".replace(": null", ": undefined"))
|
print(f" {opt.name}: {json.dumps(opt.default)},".replace(": null", ": undefined"))
|
||||||
print("}")
|
print("}")
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ function styles_app_dev() {
|
|||||||
|
|
||||||
|
|
||||||
function esbuild(dev) {
|
function esbuild(dev) {
|
||||||
return gulp.src('src/js/app.jsx').pipe(
|
return gulp.src('src/js/app.tsx').pipe(
|
||||||
gulpEsbuild({
|
gulpEsbuild({
|
||||||
outfile: 'app.js',
|
outfile: 'app.js',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
//TODO: Move into some utils
|
|
||||||
.monospace() {
|
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-detail {
|
.flow-detail {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow:hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -15,45 +10,52 @@
|
|||||||
|
|
||||||
section {
|
section {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
>article{
|
flex: 1;
|
||||||
overflow: auto;
|
padding: 5px 12px 10px;
|
||||||
padding: 5px 12px 0;
|
|
||||||
}
|
> footer {
|
||||||
>footer {
|
|
||||||
box-shadow: 0 0 3px gray;
|
box-shadow: 0 0 3px gray;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height:23px;
|
height: 23px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section.detail, section.error{
|
|
||||||
overflow: auto;
|
|
||||||
padding: 5px 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.first-line {
|
.first-line {
|
||||||
.monospace();
|
font-family: @font-family-monospace;
|
||||||
background-color: #428bca;
|
background-color: #428bca;
|
||||||
color: white;
|
color: white;
|
||||||
margin: 0 -8px;
|
margin: 0 -8px 2px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.inline-input.editable {
|
|
||||||
border-color: rgba(255,255,255,0.5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.request-line {
|
|
||||||
margin-bottom: 2px;
|
.contentview {
|
||||||
|
margin: 0 -12px;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
h5 {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre button:not(:only-child) {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 0 0 5px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,30 +66,40 @@
|
|||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
|
|
||||||
border: solid transparent 1px;
|
border: solid transparent 1px;
|
||||||
&.editable {
|
|
||||||
border-color: #ccc;
|
&:hover {
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 1.25%), 0 2px 4px rgba(0, 0, 0, 5%), 0 2px 6px rgba(0, 0, 0, 2.5%);
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[placeholder]:empty:not(:focus-visible):before {
|
||||||
|
content: attr(placeholder);
|
||||||
|
color: lightgray;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[contenteditable] {
|
&[contenteditable] {
|
||||||
|
outline-width: 0;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 5%), 0 2px 4px rgba(0, 0, 0, 20%), 0 2px 6px rgba(0, 0, 0, 10%);
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
|
||||||
&.has-warning {
|
&.has-warning {
|
||||||
color: rgb(255, 184, 184);
|
color: rgb(255, 184, 184);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-success {
|
&.has-success {
|
||||||
//color: green;
|
//color: green;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-all-content-btn{
|
|
||||||
float: right;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-detail table {
|
.flow-detail table {
|
||||||
.monospace();
|
td:nth-child(2) {
|
||||||
|
font-family: @font-family-monospace;
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@ -109,34 +121,45 @@
|
|||||||
|
|
||||||
.connection-table {
|
.connection-table {
|
||||||
td:first-child {
|
td:first-child {
|
||||||
width: 50%;
|
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-table {
|
.headers, .trailers {
|
||||||
td {
|
.kv-row {
|
||||||
line-height: 1.3em;
|
margin-bottom: .3em;
|
||||||
}
|
max-height: 12.4ex;
|
||||||
.header-name {
|
overflow-y: auto;
|
||||||
width: 33%;
|
|
||||||
}
|
|
||||||
.header-value {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This exists so that you can copy
|
.kv-key {
|
||||||
// and paste headers out of mitmweb.
|
font-weight: bold;
|
||||||
.header-colon {
|
}
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
.kv-value {
|
||||||
|
font-family: @font-family-monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-input {
|
.inline-input {
|
||||||
display: inline-block;
|
background-color: white;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kv-add-row {
|
||||||
|
opacity: 0;
|
||||||
|
color: #666;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
transition: all 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .kv-add-row {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
min-height: 2ex;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-table, .timing-table {
|
.connection-table, .timing-table {
|
||||||
@ -146,3 +169,23 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dl.cert-attributes {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
dt, dd {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
flex: 0 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
flex: 0 0 calc(100% - 2em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -111,8 +111,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.col-path {
|
.col-path {
|
||||||
|
.marker {
|
||||||
|
}
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-repeat {
|
.fa-repeat {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.flowview-image {
|
.flowview-image {
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
footer {
|
footer {
|
||||||
box-shadow: 0 -1px 3px lightgray;
|
box-shadow: 0 -1px 3px lightgray;
|
||||||
padding: 0px 10px 3px;
|
padding: 0 0 4px 3px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,10 @@ header {
|
|||||||
height: @menu-height - @menu-legend-height;
|
height: @menu-height - @menu-legend-height;
|
||||||
display: flow-root;
|
display: flow-root;
|
||||||
|
|
||||||
> .btn {
|
> a {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
> .btn, > a > .btn {
|
||||||
height: @menu-height - @menu-legend-height;
|
height: @menu-height - @menu-legend-height;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 1px;
|
margin: 0 1px;
|
||||||
|
@ -43,4 +43,8 @@
|
|||||||
|
|
||||||
.resource-icon-redirect {
|
.resource-icon-redirect {
|
||||||
background-image: url(images/resourceRedirectIcon.png);
|
background-image: url(images/resourceRedirectIcon.png);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-icon-websocket {
|
||||||
|
background-image: url(images/resourceWebSocketIcon.png);
|
||||||
|
}
|
||||||
|
@ -38,12 +38,7 @@
|
|||||||
|
|
||||||
.nav-tabs-sm {
|
.nav-tabs-sm {
|
||||||
> a {
|
> a {
|
||||||
padding: 0px 7px;
|
padding: 0 7px;
|
||||||
margin: 2px 2px -1px;
|
margin: 2px 2px -1px;
|
||||||
}
|
}
|
||||||
> a.nav-action {
|
|
||||||
float: right;
|
|
||||||
padding: 0;
|
|
||||||
margin: 1px 0 0px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,3 +3,5 @@
|
|||||||
@navbar-default-color: #303030;
|
@navbar-default-color: #303030;
|
||||||
@navbar-default-bg: #ffffff;
|
@navbar-default-bg: #ffffff;
|
||||||
@navbar-default-border: #e0e0e0;
|
@navbar-default-border: #e0e0e0;
|
||||||
|
@font-family-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
@font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
BIN
web/src/images/resourceWebSocketIcon.png
Normal file
BIN
web/src/images/resourceWebSocketIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import CommandBar from '../../../components/CommandBar'
|
import CommandBar from '../../../components/CommandBar'
|
||||||
import { render } from "../../test-utils"
|
import { render } from "../../test-utils"
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
@ -13,4 +13,4 @@ test('CommandBar Component', async () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import withContentLoader from '../../../components/ContentView/ContentLoader'
|
import withContentLoader from '../../../components/contentviews/useContent'
|
||||||
import { TFlow } from '../../ducks/tutils'
|
import { TFlow } from '../../ducks/tutils'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import mockXMLHttpRequest from 'mock-xmlhttprequest'
|
import mockXMLHttpRequest from 'mock-xmlhttprequest'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import ContentViewOptions from '../../../components/ContentView/ContentViewOptions'
|
import ContentViewOptions from '../../../components/ContentView/ContentViewOptions'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import DownloadContentButton from '../../../components/ContentView/DownloadContentButton'
|
import DownloadContentButton from '../../../components/ContentView/DownloadContentButton'
|
||||||
import { TFlow } from '../../ducks/tutils'
|
import { TFlow } from '../../ducks/tutils'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { ContentEmpty, ContentMissing, ContentTooLarge } from '../../../components/ContentView/MetaViews'
|
import { ContentEmpty, ContentMissing, ContentTooLarge } from '../../../components/ContentView/MetaViews'
|
||||||
import { TFlow } from '../../ducks/tutils'
|
import { TFlow } from '../../ducks/tutils'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import ShowFullContentButton from '../../../components/ContentView/ShowFullContentButton'
|
import ShowFullContentButton from '../../../components/ContentView/ShowFullContentButton'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import UploadContentButton from '../../../components/ContentView/UploadContentButton'
|
import UploadContentButton from '../../../components/ContentView/UploadContentButton'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import ViewSelector from '../../../components/ContentView/ViewSelector'
|
import ViewSelector from '../../../components/ContentView/ViewSelector'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import ContentView from '../../components/ContentView'
|
import ContentView from '../../components/ContentView'
|
||||||
import { TStore, TFlow } from '../ducks/tutils'
|
import { TStore, TFlow } from '../ducks/tutils'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import EventLogList from '../../../components/EventLog/EventList'
|
import EventLogList from '../../../components/EventLog/EventList'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
jest.mock('../../components/EventLog/EventList')
|
jest.mock('../../components/EventLog/EventList')
|
||||||
|
|
||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import EventLog, {PureEventLog} from '../../components/EventLog'
|
import EventLog, {PureEventLog} from '../../components/EventLog'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import {
|
import {
|
||||||
IconColumn,
|
icon,
|
||||||
MethodColumn,
|
method,
|
||||||
PathColumn,
|
path,
|
||||||
QuickActionsColumn,
|
quickactions,
|
||||||
SizeColumn,
|
size,
|
||||||
StatusColumn,
|
status,
|
||||||
TimeColumn,
|
time,
|
||||||
TimeStampColumn,
|
timestamp,
|
||||||
TLSColumn
|
tls
|
||||||
} from '../../../components/FlowTable/FlowColumns'
|
} from '../../../components/FlowTable/FlowColumns'
|
||||||
import {TFlow, TStore} from '../../ducks/tutils'
|
import {TFlow, TStore} from '../../ducks/tutils'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
@ -18,109 +18,109 @@ describe('Flowcolumns Components', () => {
|
|||||||
|
|
||||||
let tflow = TFlow()
|
let tflow = TFlow()
|
||||||
it('should render TLSColumn', () => {
|
it('should render TLSColumn', () => {
|
||||||
let tlsColumn = renderer.create(<TLSColumn flow={tflow}/>),
|
let tlsColumn = renderer.create(<tls flow={tflow}/>),
|
||||||
tree = tlsColumn.toJSON()
|
tree = tlsColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render IconColumn', () => {
|
it('should render IconColumn', () => {
|
||||||
let tflow = TFlow(),
|
let tflow = TFlow(),
|
||||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>),
|
iconColumn = renderer.create(<icon flow={tflow}/>),
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
// plain
|
// plain
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// not modified
|
// not modified
|
||||||
tflow.response.status_code = 304
|
tflow.response.status_code = 304
|
||||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>)
|
iconColumn = renderer.create(<icon flow={tflow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// redirect
|
// redirect
|
||||||
tflow.response.status_code = 302
|
tflow.response.status_code = 302
|
||||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>)
|
iconColumn = renderer.create(<icon flow={tflow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// image
|
// image
|
||||||
let imageFlow = TFlow()
|
let imageFlow = TFlow()
|
||||||
imageFlow.response.headers = [['Content-Type', 'image/jpeg']]
|
imageFlow.response.headers = [['Content-Type', 'image/jpeg']]
|
||||||
iconColumn = renderer.create(<IconColumn flow={imageFlow}/>)
|
iconColumn = renderer.create(<icon flow={imageFlow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// javascript
|
// javascript
|
||||||
let jsFlow = TFlow()
|
let jsFlow = TFlow()
|
||||||
jsFlow.response.headers = [['Content-Type', 'application/x-javascript']]
|
jsFlow.response.headers = [['Content-Type', 'application/x-javascript']]
|
||||||
iconColumn = renderer.create(<IconColumn flow={jsFlow}/>)
|
iconColumn = renderer.create(<icon flow={jsFlow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// css
|
// css
|
||||||
let cssFlow = TFlow()
|
let cssFlow = TFlow()
|
||||||
cssFlow.response.headers = [['Content-Type', 'text/css']]
|
cssFlow.response.headers = [['Content-Type', 'text/css']]
|
||||||
iconColumn = renderer.create(<IconColumn flow={cssFlow}/>)
|
iconColumn = renderer.create(<icon flow={cssFlow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// html
|
// html
|
||||||
let htmlFlow = TFlow()
|
let htmlFlow = TFlow()
|
||||||
htmlFlow.response.headers = [['Content-Type', 'text/html']]
|
htmlFlow.response.headers = [['Content-Type', 'text/html']]
|
||||||
iconColumn = renderer.create(<IconColumn flow={htmlFlow}/>)
|
iconColumn = renderer.create(<icon flow={htmlFlow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// default
|
// default
|
||||||
let fooFlow = TFlow()
|
let fooFlow = TFlow()
|
||||||
fooFlow.response.headers = [['Content-Type', 'foo']]
|
fooFlow.response.headers = [['Content-Type', 'foo']]
|
||||||
iconColumn = renderer.create(<IconColumn flow={fooFlow}/>)
|
iconColumn = renderer.create(<icon flow={fooFlow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// no response
|
// no response
|
||||||
tflow.response = null
|
tflow.response = null
|
||||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>)
|
iconColumn = renderer.create(<icon flow={tflow}/>)
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render pathColumn', () => {
|
it('should render pathColumn', () => {
|
||||||
let tflow = TFlow(),
|
let tflow = TFlow(),
|
||||||
pathColumn = renderer.create(<PathColumn flow={tflow}/>),
|
pathColumn = renderer.create(<path flow={tflow}/>),
|
||||||
tree = pathColumn.toJSON()
|
tree = pathColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
|
|
||||||
tflow.error.msg = 'Connection killed.'
|
tflow.error.msg = 'Connection killed.'
|
||||||
tflow.intercepted = true
|
tflow.intercepted = true
|
||||||
pathColumn = renderer.create(<PathColumn flow={tflow}/>)
|
pathColumn = renderer.create(<path flow={tflow}/>)
|
||||||
tree = pathColumn.toJSON()
|
tree = pathColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render MethodColumn', () => {
|
it('should render MethodColumn', () => {
|
||||||
let methodColumn = renderer.create(<MethodColumn flow={tflow}/>),
|
let methodColumn = renderer.create(<method flow={tflow}/>),
|
||||||
tree = methodColumn.toJSON()
|
tree = methodColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render StatusColumn', () => {
|
it('should render StatusColumn', () => {
|
||||||
let statusColumn = renderer.create(<StatusColumn flow={tflow}/>),
|
let statusColumn = renderer.create(<status flow={tflow}/>),
|
||||||
tree = statusColumn.toJSON()
|
tree = statusColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render SizeColumn', () => {
|
it('should render SizeColumn', () => {
|
||||||
let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
|
let sizeColumn = renderer.create(<size flow={tflow}/>),
|
||||||
tree = sizeColumn.toJSON()
|
tree = sizeColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render TimeColumn', () => {
|
it('should render TimeColumn', () => {
|
||||||
let tflow = TFlow(),
|
let tflow = TFlow(),
|
||||||
timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
|
timeColumn = renderer.create(<time flow={tflow}/>),
|
||||||
tree = timeColumn.toJSON()
|
tree = timeColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
|
|
||||||
tflow.response = null
|
tflow.response = null
|
||||||
timeColumn = renderer.create(<TimeColumn flow={tflow}/>)
|
timeColumn = renderer.create(<time flow={tflow}/>)
|
||||||
tree = timeColumn.toJSON()
|
tree = timeColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render TimeStampColumn', () => {
|
it('should render TimeStampColumn', () => {
|
||||||
let timeStampColumn = renderer.create(<TimeStampColumn flow={tflow}/>),
|
let timeStampColumn = renderer.create(<timestamp flow={tflow}/>),
|
||||||
tree = timeStampColumn.toJSON()
|
tree = timeStampColumn.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
@ -129,7 +129,7 @@ describe('Flowcolumns Components', () => {
|
|||||||
let store = TStore(),
|
let store = TStore(),
|
||||||
provider = renderer.create(
|
provider = renderer.create(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QuickActionsColumn flow={tflow}/>
|
<quickactions flow={tflow}/>
|
||||||
</Provider>),
|
</Provider>),
|
||||||
tree = provider.toJSON()
|
tree = provider.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import FlowRow from '../../../components/FlowTable/FlowRow'
|
import FlowRow from '../../../components/FlowTable/FlowRow'
|
||||||
import {testState} from '../../ducks/tutils'
|
import {testState} from '../../ducks/tutils'
|
||||||
import {fireEvent, render, screen} from "../../test-utils";
|
import {fireEvent, render, screen} from "../../test-utils";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import FlowTableHead from '../../../components/FlowTable/FlowTableHead'
|
import FlowTableHead from '../../../components/FlowTable/FlowTableHead'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
import {TStore} from '../../ducks/tutils'
|
import {TStore} from '../../ducks/tutils'
|
||||||
@ -23,7 +23,7 @@ test("FlowTableHead Component", async () => {
|
|||||||
fireEvent.click(screen.getByText("Size"))
|
fireEvent.click(screen.getByText("Size"))
|
||||||
|
|
||||||
expect(store.getActions()).toStrictEqual([
|
expect(store.getActions()).toStrictEqual([
|
||||||
setSort("SizeColumn", false)
|
setSort("size", false)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import {PureFlowTable as FlowTable} from '../../components/FlowTable'
|
import {PureFlowTable as FlowTable} from '../../components/FlowTable'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import Details, { TimeStamp, ConnectionInfo, CertificateInfo, Timing } from '../../../components/FlowView/Details'
|
import Connection, { TimeStamp, ConnectionInfo, CertificateInfo, Timing } from '../../../components/FlowView/Connection'
|
||||||
import { TFlow } from '../../ducks/tutils'
|
import { TFlow } from '../../ducks/tutils'
|
||||||
|
|
||||||
let tflow = TFlow()
|
let tflow = TFlow()
|
||||||
@ -43,7 +43,7 @@ describe('Timing Component', () => {
|
|||||||
|
|
||||||
describe('Details Component', () => {
|
describe('Details Component', () => {
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
let details = renderer.create(<Details flow={tflow}/>),
|
let details = renderer.create(<Connection flow={tflow}/>),
|
||||||
tree = details.toJSON()
|
tree = details.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
@ -53,7 +53,6 @@ describe('Details Component', () => {
|
|||||||
|
|
||||||
tflowServerAddressNull.server_conn.address = null
|
tflowServerAddressNull.server_conn.address = null
|
||||||
tflowServerAddressNull.server_conn.ip_address = null
|
tflowServerAddressNull.server_conn.ip_address = null
|
||||||
tflowServerAddressNull.server_conn.alpn_proto_negotiated = null
|
|
||||||
tflowServerAddressNull.server_conn.sni = null
|
tflowServerAddressNull.server_conn.sni = null
|
||||||
tflowServerAddressNull.server_conn.ssl_established = false
|
tflowServerAddressNull.server_conn.ssl_established = false
|
||||||
tflowServerAddressNull.server_conn.tls_version = null
|
tflowServerAddressNull.server_conn.tls_version = null
|
||||||
@ -61,8 +60,8 @@ describe('Details Component', () => {
|
|||||||
tflowServerAddressNull.server_conn.timestamp_ssl_setup = null
|
tflowServerAddressNull.server_conn.timestamp_ssl_setup = null
|
||||||
tflowServerAddressNull.server_conn.timestamp_start = null
|
tflowServerAddressNull.server_conn.timestamp_start = null
|
||||||
tflowServerAddressNull.server_conn.timestamp_end = null
|
tflowServerAddressNull.server_conn.timestamp_end = null
|
||||||
|
|
||||||
let details = renderer.create(<Details flow={tflowServerAddressNull}/>),
|
let details = renderer.create(<Connection flow={tflowServerAddressNull}/>),
|
||||||
tree = details.toJSON()
|
tree = details.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import Headers, { HeaderEditor } from '../../../components/FlowView/Headers'
|
import KeyValueListEditor, { HeaderEditor } from '../../../components/editors/KeyValueListEditor'
|
||||||
import { Key } from '../../../utils'
|
import { Key } from '../../../utils'
|
||||||
|
|
||||||
describe('HeaderEditor Component', () => {
|
describe('HeaderEditor Component', () => {
|
||||||
@ -54,12 +54,12 @@ describe('Headers Component', () => {
|
|||||||
let changeFn = jest.fn(),
|
let changeFn = jest.fn(),
|
||||||
mockMessage = { headers: [['k1', 'v1'], ['k2', '']] }
|
mockMessage = { headers: [['k1', 'v1'], ['k2', '']] }
|
||||||
it('should handle correctly', () => {
|
it('should handle correctly', () => {
|
||||||
let headers = renderer.create(<Headers onChange={changeFn} message={mockMessage}/>),
|
let headers = renderer.create(<KeyValueListEditor onChange={changeFn} message={mockMessage}/>),
|
||||||
tree = headers.toJSON()
|
tree = headers.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
let headers = TestUtils.renderIntoDocument(<Headers onChange={changeFn} message={mockMessage}/>),
|
let headers = TestUtils.renderIntoDocument(<KeyValueListEditor onChange={changeFn} message={mockMessage}/>),
|
||||||
headerEditors = TestUtils.scryRenderedComponentsWithType(headers, HeaderEditor),
|
headerEditors = TestUtils.scryRenderedComponentsWithType(headers, HeaderEditor),
|
||||||
key1Editor = headerEditors[0],
|
key1Editor = headerEditors[0],
|
||||||
value1Editor = headerEditors[1],
|
value1Editor = headerEditors[1],
|
||||||
@ -123,7 +123,7 @@ describe('Headers Component', () => {
|
|||||||
|
|
||||||
it('should not delete last row when handle remove', () => {
|
it('should not delete last row when handle remove', () => {
|
||||||
mockMessage = { headers: [['', '']] }
|
mockMessage = { headers: [['', '']] }
|
||||||
headers = TestUtils.renderIntoDocument(<Headers onChange={changeFn} message={mockMessage}/>)
|
headers = TestUtils.renderIntoDocument(<KeyValueListEditor onChange={changeFn} message={mockMessage}/>)
|
||||||
headers.onChange(0, 0, '')
|
headers.onChange(0, 0, '')
|
||||||
expect(changeFn).toBeCalledWith([['Name', 'Value']])
|
expect(changeFn).toBeCalledWith([['Name', 'Value']])
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
jest.mock('../../../components/ContentView', () => () => null)
|
jest.mock('../../../components/ContentView', () => () => null)
|
||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import {ErrorView, Request, Response} from '../../../components/FlowView/Messages'
|
import {ErrorView, Request, Response} from '../../../components/FlowView/HttpMessages'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
import {TFlow, TStore} from '../../ducks/tutils'
|
import {TFlow, TStore} from '../../ducks/tutils'
|
||||||
import {updateEdit} from '../../../ducks/ui/flow'
|
import {updateEdit} from '../../../ducks/ui/flow'
|
||||||
import {parseUrl} from '../../../flow/utils'
|
import {parseUrl} from '../../../flow/utils'
|
||||||
import ContentView from '../../../components/ContentView'
|
import ContentView from '../../../components/ContentView'
|
||||||
import ContentViewOptions from '../../../components/ContentView/ContentViewOptions'
|
import ContentViewOptions from '../../../components/ContentView/ContentViewOptions'
|
||||||
import Headers from '../../../components/FlowView/Headers'
|
import KeyValueListEditor from '../../../components/editors/KeyValueListEditor'
|
||||||
import ValueEditor from '../../../components/ValueEditor/ValueEditor'
|
import ValueEditor from '../../../components/editors/ValueEditor'
|
||||||
|
|
||||||
global.fetch = jest.fn()
|
global.fetch = jest.fn()
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ describe('Request Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle change on flow request header', () => {
|
it('should handle change on flow request header', () => {
|
||||||
let headers = provider.root.findAllByType(Headers).filter(headers => headers.props.type === 'headers')[0]
|
let headers = provider.root.findAllByType(KeyValueListEditor).filter(headers => headers.props.type === 'headers')[0]
|
||||||
headers.props.onChange('foo')
|
headers.props.onChange('foo')
|
||||||
expect(store.getActions()).toEqual([updateEdit({request: {headers: 'foo'}})])
|
expect(store.getActions()).toEqual([updateEdit({request: {headers: 'foo'}})])
|
||||||
})
|
})
|
||||||
@ -118,7 +118,7 @@ describe('Response Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle change on flow response headers', () => {
|
it('should handle change on flow response headers', () => {
|
||||||
let headers = provider.root.findAllByType(Headers).filter(headers => headers.props.type === 'headers')[0]
|
let headers = provider.root.findAllByType(KeyValueListEditor).filter(headers => headers.props.type === 'headers')[0]
|
||||||
headers.props.onChange('foo')
|
headers.props.onChange('foo')
|
||||||
expect(store.getActions()).toEqual([updateEdit({response: {headers: 'foo'}})])
|
expect(store.getActions()).toEqual([updateEdit({response: {headers: 'foo'}})])
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import Nav, { NavAction } from '../../../components/FlowView/Nav'
|
import Nav, { NavAction } from '../../../components/FlowView/Nav'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
|
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
|
||||||
import {TFlow} from '../../ducks/tutils'
|
import {TFlow} from '../../ducks/tutils'
|
||||||
import {render} from "../../test-utils"
|
import {render} from "../../test-utils"
|
||||||
|
@ -19,7 +19,7 @@ exports[`HeaderEditor Component should render correctly 1`] = `
|
|||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Headers Component should handle correctly 1`] = `
|
exports[`KeyValueListEditor Component should handle correctly 1`] = `
|
||||||
<table
|
<table
|
||||||
className="header-table"
|
className="header-table"
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import ConnectionIndicator from '../../../components/Header/ConnectionIndicator'
|
import ConnectionIndicator from '../../../components/Header/ConnectionIndicator'
|
||||||
import * as connectionActions from '../../../ducks/connection'
|
import * as connectionActions from '../../../ducks/connection'
|
||||||
import {render} from "../../test-utils"
|
import {render} from "../../test-utils"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import FileMenu from '../../../components/Header/FileMenu'
|
import FileMenu from '../../../components/Header/FileMenu'
|
||||||
import {Provider} from "react-redux";
|
import {Provider} from "react-redux";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import FilterDocs from '../../../components/Header/FilterDocs'
|
import FilterDocs from '../../../components/Header/FilterDocs'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import FilterInput from '../../../components/Header/FilterInput'
|
import FilterInput from '../../../components/Header/FilterInput'
|
||||||
import FilterDocs from '../../../components/Header/FilterDocs'
|
import FilterDocs from '../../../components/Header/FilterDocs'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
jest.mock('../../../flow/utils')
|
jest.mock('../../../flow/utils')
|
||||||
|
|
||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import FlowMenu from '../../../components/Header/FlowMenu'
|
import FlowMenu from '../../../components/Header/FlowMenu'
|
||||||
import { TFlow, TStore }from '../../ducks/tutils'
|
import { TFlow, TStore }from '../../ducks/tutils'
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import MainMenu from '../../../components/Header/MainMenu'
|
import StartMenu from '../../../components/Header/StartMenu'
|
||||||
import {render} from "../../test-utils"
|
import {render} from "../../test-utils"
|
||||||
|
|
||||||
test("MainMenu", () => {
|
test("MainMenu", () => {
|
||||||
const {asFragment} = render(<MainMenu/>);
|
const {asFragment} = render(<StartMenu/>);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle'
|
import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import OptionMenu from '../../../components/Header/OptionMenu'
|
import OptionMenu from '../../../components/Header/OptionMenu'
|
||||||
|
@ -1,6 +1,104 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`MainMenu 1`] = `
|
exports[`MainMenu 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="main-menu"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="filter-input input-group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="input-group-addon"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-fw fa-search"
|
||||||
|
style="color: black;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Search"
|
||||||
|
type="text"
|
||||||
|
value="~d address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="filter-input input-group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="input-group-addon"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-fw fa-tag"
|
||||||
|
style="color: rgb(0, 0, 0);"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Highlight"
|
||||||
|
type="text"
|
||||||
|
value="~u /path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-legend"
|
||||||
|
>
|
||||||
|
Find
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="filter-input input-group"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="input-group-addon"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-fw fa-pause"
|
||||||
|
style="color: rgb(68, 68, 68);"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Intercept"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-sm btn btn-default"
|
||||||
|
title="[a]ccept all"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-forward text-success"
|
||||||
|
/>
|
||||||
|
Resume All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-legend"
|
||||||
|
>
|
||||||
|
Intercept
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`StartMenu 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="main-menu"
|
class="main-menu"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import Modal from '../../../components/Modal/Modal'
|
import Modal from '../../../components/Modal/Modal'
|
||||||
import {render} from "../../test-utils"
|
import {render} from "../../test-utils"
|
||||||
import {setActiveModal} from "../../../ducks/ui/modal";
|
import {setActiveModal} from "../../../ducks/ui/modal";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { PureOptionDefault } from '../../../components/Modal/OptionModal'
|
import { PureOptionDefault } from '../../../components/Modal/OptionModal'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import { Options, ChoicesOption } from '../../../components/Modal/Option'
|
import { Options, ChoicesOption } from '../../../components/Modal/Option'
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import ValidateEditor from '../../../components/ValueEditor/ValidateEditor'
|
import ValidateEditor from '../../../components/editors/ValidateEditor'
|
||||||
|
|
||||||
describe('ValidateEditor Component', () => {
|
describe('ValidateEditor Component', () => {
|
||||||
let validateFn = jest.fn( content => content.length == 3),
|
let validateFn = jest.fn( content => content.length == 3),
|
||||||
@ -9,14 +9,14 @@ describe('ValidateEditor Component', () => {
|
|||||||
|
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
let validateEditor = renderer.create(
|
let validateEditor = renderer.create(
|
||||||
<ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
|
<ValidateEditor content="foo" onEditDone={doneFn} isValid={validateFn}/>
|
||||||
),
|
),
|
||||||
tree = validateEditor.toJSON()
|
tree = validateEditor.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
let validateEditor = TestUtils.renderIntoDocument(
|
let validateEditor = TestUtils.renderIntoDocument(
|
||||||
<ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
|
<ValidateEditor content="foo" onEditDone={doneFn} isValid={validateFn}/>
|
||||||
)
|
)
|
||||||
it('should handle componentWillReceiveProps', () => {
|
it('should handle componentWillReceiveProps', () => {
|
||||||
let mockProps = {
|
let mockProps = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import ValueEditor from '../../../components/ValueEditor/ValueEditor'
|
import ValueEditor from '../../../components/editors/ValueEditor'
|
||||||
import { Key } from '../../../utils'
|
import { Key } from '../../../utils'
|
||||||
|
|
||||||
describe('ValueEditor Component', () => {
|
describe('ValueEditor Component', () => {
|
||||||
@ -9,14 +9,14 @@ describe('ValueEditor Component', () => {
|
|||||||
let mockFn = jest.fn()
|
let mockFn = jest.fn()
|
||||||
it ('should render correctly', () => {
|
it ('should render correctly', () => {
|
||||||
let valueEditor = renderer.create(
|
let valueEditor = renderer.create(
|
||||||
<ValueEditor content="foo" onDone={mockFn}/>
|
<ValueEditor content="foo" onEditDone={mockFn}/>
|
||||||
),
|
),
|
||||||
tree = valueEditor.toJSON()
|
tree = valueEditor.toJSON()
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
let valueEditor = TestUtils.renderIntoDocument(
|
let valueEditor = TestUtils.renderIntoDocument(
|
||||||
<ValueEditor content="<script>foo</script>" onDone={mockFn}/>
|
<ValueEditor content="<script>foo</script>" onEditDone={mockFn}/>
|
||||||
)
|
)
|
||||||
it('should handle this.blur', () => {
|
it('should handle this.blur', () => {
|
||||||
valueEditor.input.blur = jest.fn()
|
valueEditor.input.blur = jest.fn()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import Button from '../../../components/common/Button'
|
import Button from '../../../components/common/Button'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import DocsLink from '../../../components/common/DocsLink'
|
import DocsLink from '../../../components/common/DocsLink'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react"
|
import * as React from "react";
|
||||||
import Dropdown, {Divider, MenuItem, SubMenu} from '../../../components/common/Dropdown'
|
import Dropdown, {Divider, MenuItem, SubMenu} from '../../../components/common/Dropdown'
|
||||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import FileChooser from '../../../components/common/FileChooser'
|
import FileChooser from '../../../components/common/FileChooser'
|
||||||
import {render} from '@testing-library/react'
|
import {render} from '@testing-library/react'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import Splitter from '../../../components/common/Splitter'
|
import Splitter from '../../../components/common/Splitter'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import ToggleButton from '../../../components/common/ToggleButton'
|
import ToggleButton from '../../../components/common/ToggleButton'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import ToggleInputButton from '../../../components/common/ToggleInputButton'
|
import ToggleInputButton from '../../../components/common/ToggleInputButton'
|
||||||
import { Key } from '../../../utils'
|
import { Key } from '../../../utils'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react"
|
import * as React from "react";
|
||||||
import ReactDOM from "react-dom"
|
import ReactDOM from "react-dom"
|
||||||
import AutoScroll from '../../../components/helpers/AutoScroll'
|
import AutoScroll from '../../../components/helpers/AutoScroll'
|
||||||
import { calcVScroll } from '../../../components/helpers/VirtualScroll'
|
import { calcVScroll } from '../../../components/helpers/VirtualScroll'
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import reduceEventLog, * as eventLogActions from '../../ducks/eventLog'
|
import reduceEventLog, * as eventLogActions from '../../ducks/eventLog'
|
||||||
import reduceStore from '../../ducks/utils/store'
|
import reduce from '../../ducks/utils/store'
|
||||||
|
|
||||||
describe('event log reducer', () => {
|
describe('event log reducer', () => {
|
||||||
it('should return initial state', () => {
|
it('should return initial state', () => {
|
||||||
expect(reduceEventLog(undefined, {})).toEqual({
|
expect(reduceEventLog(undefined, {})).toEqual({
|
||||||
visible: false,
|
visible: false,
|
||||||
filters: { debug: false, info: true, web: true, warn: true, error: true },
|
filters: { debug: false, info: true, web: true, warn: true, error: true },
|
||||||
...reduceStore(undefined, {}),
|
...reduce(undefined, {}),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ describe('event log reducer', () => {
|
|||||||
expect(reduceEventLog(state, eventLogActions.toggleFilter('info'))).toEqual({
|
expect(reduceEventLog(state, eventLogActions.toggleFilter('info'))).toEqual({
|
||||||
visible: false,
|
visible: false,
|
||||||
filters: { ...state.filters, info: false},
|
filters: { ...state.filters, info: false},
|
||||||
...reduceStore(state, {})
|
...reduce(state, {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ describe('event log reducer', () => {
|
|||||||
expect(reduceEventLog(state, eventLogActions.toggleVisibility())).toEqual({
|
expect(reduceEventLog(state, eventLogActions.toggleVisibility())).toEqual({
|
||||||
visible: true,
|
visible: true,
|
||||||
filters: {...state.filters},
|
filters: {...state.filters},
|
||||||
...reduceStore(undefined, {})
|
...reduce(undefined, {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ jest.mock('../../utils')
|
|||||||
|
|
||||||
import reduceFlows from "../../ducks/flows"
|
import reduceFlows from "../../ducks/flows"
|
||||||
import * as flowActions from "../../ducks/flows"
|
import * as flowActions from "../../ducks/flows"
|
||||||
import reduceStore from "../../ducks/utils/store"
|
import reduce from "../../ducks/utils/store"
|
||||||
import { fetchApi } from "../../utils"
|
import { fetchApi } from "../../utils"
|
||||||
import { createStore } from "./tutils"
|
import { createStore } from "./tutils"
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ describe('flow reducer', () => {
|
|||||||
filter: null,
|
filter: null,
|
||||||
sort: { column: null, desc: false },
|
sort: { column: null, desc: false },
|
||||||
selected: [],
|
selected: [],
|
||||||
...reduceStore(undefined, {})
|
...reduce(undefined, {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import reduceStore, * as storeActions from '../../../ducks/utils/store'
|
import reduce, * as storeActions from '../../../ducks/utils/store'
|
||||||
|
|
||||||
describe('store reducer', () => {
|
describe('store reducer', () => {
|
||||||
it('should return initial state', () => {
|
it('should return initial state', () => {
|
||||||
expect(reduceStore(undefined, {})).toEqual({
|
expect(reduce(undefined, {})).toEqual({
|
||||||
byId: {},
|
byId: {},
|
||||||
list: [],
|
list: [],
|
||||||
listIndex: {},
|
listIndex: {},
|
||||||
@ -14,8 +14,8 @@ describe('store reducer', () => {
|
|||||||
it('should handle add action', () => {
|
it('should handle add action', () => {
|
||||||
let a = {id: 1},
|
let a = {id: 1},
|
||||||
b = {id: 9},
|
b = {id: 9},
|
||||||
state = reduceStore(undefined, {})
|
state = reduce(undefined, {})
|
||||||
expect(state = reduceStore(state, storeActions.add(a))).toEqual({
|
expect(state = reduce(state, storeActions.add(a))).toEqual({
|
||||||
byId: { 1: a },
|
byId: { 1: a },
|
||||||
listIndex: { 1: 0 },
|
listIndex: { 1: 0 },
|
||||||
list: [ a ],
|
list: [ a ],
|
||||||
@ -23,7 +23,7 @@ describe('store reducer', () => {
|
|||||||
viewIndex: { 1: 0 },
|
viewIndex: { 1: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(state = reduceStore(state, storeActions.add(b))).toEqual({
|
expect(state = reduce(state, storeActions.add(b))).toEqual({
|
||||||
byId: { 1: a, 9: b },
|
byId: { 1: a, 9: b },
|
||||||
listIndex: { 1: 0, 9: 1 },
|
listIndex: { 1: 0, 9: 1 },
|
||||||
list: [ a, b ],
|
list: [ a, b ],
|
||||||
@ -33,7 +33,7 @@ describe('store reducer', () => {
|
|||||||
|
|
||||||
// add item and sort them
|
// add item and sort them
|
||||||
let c = {id: 0}
|
let c = {id: 0}
|
||||||
expect(reduceStore(state, storeActions.add(c, undefined,
|
expect(reduce(state, storeActions.add(c, undefined,
|
||||||
(a, b) => {return a.id - b.id}))).toEqual({
|
(a, b) => {return a.id - b.id}))).toEqual({
|
||||||
byId: {...state.byId, 0: c },
|
byId: {...state.byId, 0: c },
|
||||||
list: [...state.list, c ],
|
list: [...state.list, c ],
|
||||||
@ -46,15 +46,15 @@ describe('store reducer', () => {
|
|||||||
|
|
||||||
it('should not add the item with duplicated id', () => {
|
it('should not add the item with duplicated id', () => {
|
||||||
let a = {id: 1},
|
let a = {id: 1},
|
||||||
state = reduceStore(undefined, storeActions.add(a))
|
state = reduce(undefined, storeActions.add(a))
|
||||||
expect(reduceStore(state, storeActions.add(a))).toEqual(state)
|
expect(reduce(state, storeActions.add(a))).toEqual(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle update action', () => {
|
it('should handle update action', () => {
|
||||||
let a = {id: 1, foo: "foo"},
|
let a = {id: 1, foo: "foo"},
|
||||||
updated = {...a, foo: "bar"},
|
updated = {...a, foo: "bar"},
|
||||||
state = reduceStore(undefined, storeActions.add(a))
|
state = reduce(undefined, storeActions.add(a))
|
||||||
expect(reduceStore(state, storeActions.update(updated))).toEqual({
|
expect(reduce(state, storeActions.update(updated))).toEqual({
|
||||||
byId: { 1: updated },
|
byId: { 1: updated },
|
||||||
list: [ updated ],
|
list: [ updated ],
|
||||||
listIndex: { 1: 0 },
|
listIndex: { 1: 0 },
|
||||||
@ -65,8 +65,8 @@ describe('store reducer', () => {
|
|||||||
|
|
||||||
it('should handle update action with filter', () => {
|
it('should handle update action with filter', () => {
|
||||||
let a = {id: 0}, b = {id: 1},
|
let a = {id: 0}, b = {id: 1},
|
||||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
state = reduce(undefined, storeActions.receive([a, b]))
|
||||||
state = reduceStore(state, storeActions.update(b,
|
state = reduce(state, storeActions.update(b,
|
||||||
item => {return item.id != 1}))
|
item => {return item.id != 1}))
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
byId: { 0: a, 1: b },
|
byId: { 0: a, 1: b },
|
||||||
@ -75,7 +75,7 @@ describe('store reducer', () => {
|
|||||||
view: [ a ],
|
view: [ a ],
|
||||||
viewIndex: { 0: 0 }
|
viewIndex: { 0: 0 }
|
||||||
})
|
})
|
||||||
expect(reduceStore(state, storeActions.update(b,
|
expect(reduce(state, storeActions.update(b,
|
||||||
item => {return item.id != 0}))).toEqual({
|
item => {return item.id != 0}))).toEqual({
|
||||||
byId: { 0: a, 1: b },
|
byId: { 0: a, 1: b },
|
||||||
list: [ a, b ],
|
list: [ a, b ],
|
||||||
@ -88,8 +88,8 @@ describe('store reducer', () => {
|
|||||||
it('should handle update action with sort', () => {
|
it('should handle update action with sort', () => {
|
||||||
let a = {id: 2},
|
let a = {id: 2},
|
||||||
b = {id: 3},
|
b = {id: 3},
|
||||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
state = reduce(undefined, storeActions.receive([a, b]))
|
||||||
expect(reduceStore(state, storeActions.update(b, undefined,
|
expect(reduce(state, storeActions.update(b, undefined,
|
||||||
(a, b) => {return b.id - a.id}))).toEqual({
|
(a, b) => {return b.id - a.id}))).toEqual({
|
||||||
// sort by id in descending order
|
// sort by id in descending order
|
||||||
byId: { 2: a, 3: b },
|
byId: { 2: a, 3: b },
|
||||||
@ -99,8 +99,8 @@ describe('store reducer', () => {
|
|||||||
viewIndex: { 2: 1, 3: 0 },
|
viewIndex: { 2: 1, 3: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
let state1 = reduceStore(undefined, storeActions.receive([b, a]))
|
let state1 = reduce(undefined, storeActions.receive([b, a]))
|
||||||
expect(reduceStore(state1, storeActions.update(b, undefined,
|
expect(reduce(state1, storeActions.update(b, undefined,
|
||||||
(a, b) => {return a.id - b.id}))).toEqual({
|
(a, b) => {return a.id - b.id}))).toEqual({
|
||||||
// sort by id in ascending order
|
// sort by id in ascending order
|
||||||
byId: { 2: a, 3: b },
|
byId: { 2: a, 3: b },
|
||||||
@ -114,8 +114,8 @@ describe('store reducer', () => {
|
|||||||
it('should set filter', () => {
|
it('should set filter', () => {
|
||||||
let a = { id: 1 },
|
let a = { id: 1 },
|
||||||
b = { id: 2 },
|
b = { id: 2 },
|
||||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
state = reduce(undefined, storeActions.receive([a, b]))
|
||||||
expect(reduceStore(state, storeActions.setFilter(
|
expect(reduce(state, storeActions.setFilter(
|
||||||
item => {return item.id != 1}
|
item => {return item.id != 1}
|
||||||
))).toEqual({
|
))).toEqual({
|
||||||
byId: { 1 :a, 2: b },
|
byId: { 1 :a, 2: b },
|
||||||
@ -129,8 +129,8 @@ describe('store reducer', () => {
|
|||||||
it('should set sort', () => {
|
it('should set sort', () => {
|
||||||
let a = { id: 1 },
|
let a = { id: 1 },
|
||||||
b = { id: 2 },
|
b = { id: 2 },
|
||||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
state = reduce(undefined, storeActions.receive([a, b]))
|
||||||
expect(reduceStore(state, storeActions.setSort(
|
expect(reduce(state, storeActions.setSort(
|
||||||
(a, b) => { return b.id - a.id }
|
(a, b) => { return b.id - a.id }
|
||||||
))).toEqual({
|
))).toEqual({
|
||||||
byId: { 1: a, 2: b },
|
byId: { 1: a, 2: b },
|
||||||
@ -143,8 +143,8 @@ describe('store reducer', () => {
|
|||||||
|
|
||||||
it('should handle remove action', () => {
|
it('should handle remove action', () => {
|
||||||
let a = { id: 1 }, b = { id: 2},
|
let a = { id: 1 }, b = { id: 2},
|
||||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
state = reduce(undefined, storeActions.receive([a, b]))
|
||||||
expect(reduceStore(state, storeActions.remove(1))).toEqual({
|
expect(reduce(state, storeActions.remove(1))).toEqual({
|
||||||
byId: { 2: b },
|
byId: { 2: b },
|
||||||
list: [ b ],
|
list: [ b ],
|
||||||
listIndex: { 2: 0 },
|
listIndex: { 2: 0 },
|
||||||
@ -152,13 +152,13 @@ describe('store reducer', () => {
|
|||||||
viewIndex: { 2: 0 },
|
viewIndex: { 2: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(reduceStore(state, storeActions.remove(3))).toEqual(state)
|
expect(reduce(state, storeActions.remove(3))).toEqual(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle receive list', () => {
|
it('should handle receive list', () => {
|
||||||
let a = { id: 1 }, b = { id: 2 },
|
let a = { id: 1 }, b = { id: 2 },
|
||||||
list = [ a, b ]
|
list = [ a, b ]
|
||||||
expect(reduceStore(undefined, storeActions.receive(list))).toEqual({
|
expect(reduce(undefined, storeActions.receive(list))).toEqual({
|
||||||
byId: { 1: a, 2: b },
|
byId: { 1: a, 2: b },
|
||||||
list: [ a, b ],
|
list: [ a, b ],
|
||||||
listIndex: {1: 0, 2: 1},
|
listIndex: {1: 0, 2: 1},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import {render as rtlRender} from '@testing-library/react'
|
import {render as rtlRender} from '@testing-library/react'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
// Import your own reducer
|
// Import your own reducer
|
||||||
|
@ -71,20 +71,3 @@ describe('getDiff', () => {
|
|||||||
expect(utils.getDiff(obj1, obj2)).toEqual({ b: {foo: 2}, c:[4]})
|
expect(utils.getDiff(obj1, obj2)).toEqual({ b: {foo: 2}, c:[4]})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('pure', () => {
|
|
||||||
let tFunc = function({ className }) {
|
|
||||||
return (<p className={ className }>foo</p>)
|
|
||||||
},
|
|
||||||
puredFunc = utils.pure(tFunc),
|
|
||||||
f = new puredFunc('bar')
|
|
||||||
|
|
||||||
it('should display function name', () => {
|
|
||||||
expect(utils.pure(tFunc).displayName).toEqual('tFunc')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render properties', () => {
|
|
||||||
expect(f.render()).toEqual(tFunc('bar'))
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import {render} from 'react-dom'
|
import {render} from 'react-dom'
|
||||||
import {Provider} from 'react-redux'
|
import {Provider} from 'react-redux'
|
||||||
|
|
||||||
@ -11,14 +11,17 @@ import {store} from "./ducks";
|
|||||||
|
|
||||||
|
|
||||||
useUrlState(store)
|
useUrlState(store)
|
||||||
|
// @ts-ignore
|
||||||
if (window.MITMWEB_STATIC) {
|
if (window.MITMWEB_STATIC) {
|
||||||
|
// @ts-ignore
|
||||||
window.backend = new StaticBackend(store)
|
window.backend = new StaticBackend(store)
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
window.backend = new WebSocketBackend(store)
|
window.backend = new WebSocketBackend(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('error', msg => {
|
window.addEventListener('error', (e: ErrorEvent) => {
|
||||||
store.dispatch(addLog(msg))
|
store.dispatch(addLog(`${e.message}\n${e.error.stack}`))
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
@ -37,8 +37,8 @@ export function Results({results}: ResultProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resultElement) {
|
if (resultElement) {
|
||||||
resultElement.current.addEventListener('DOMNodeInserted', event => {
|
resultElement.current.addEventListener('DOMNodeInserted', (event) => {
|
||||||
const { currentTarget: target } = event;
|
const target = event.currentTarget as Element;
|
||||||
target.scroll({ top: target.scrollHeight, behavior: 'auto' });
|
target.scroll({ top: target.scrollHeight, behavior: 'auto' });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@ export default function CommandBar() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
setAllCommands(data["commands"])
|
setAllCommands(data["commands"])
|
||||||
setCompletionCandidate(getAvailableCommands(data["commands"]))
|
setCompletionCandidate(getAvailableCommands(data["commands"]))
|
||||||
setAvailableCommands(Object.keys(data["commands"]))
|
setAvailableCommands(Object.keys(data))
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -213,4 +213,4 @@ export default function CommandBar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import { Edit, ViewServer, ViewImage } from './ContentView/ContentViews'
|
|
||||||
import * as MetaViews from './ContentView/MetaViews'
|
|
||||||
import ShowFullContentButton from './ContentView/ShowFullContentButton'
|
|
||||||
|
|
||||||
|
|
||||||
import { displayLarge, updateEdit } from '../ducks/ui/flow'
|
|
||||||
|
|
||||||
ContentView.propTypes = {
|
|
||||||
// It may seem a bit weird at the first glance:
|
|
||||||
// Every view takes the flow and the message as props, e.g.
|
|
||||||
// <Auto flow={flow} message={flow.request}/>
|
|
||||||
flow: PropTypes.object.isRequired,
|
|
||||||
message: PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2)
|
|
||||||
|
|
||||||
function ContentView(props) {
|
|
||||||
const { flow, message, contentView, isDisplayLarge, displayLarge, onContentChange, readonly } = props
|
|
||||||
|
|
||||||
if (message.contentLength === 0 && readonly) {
|
|
||||||
return <MetaViews.ContentEmpty {...props}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.contentLength === null && readonly) {
|
|
||||||
return <MetaViews.ContentMissing {...props}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDisplayLarge && ContentView.isContentTooLarge(message)) {
|
|
||||||
return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
let view;
|
|
||||||
if(contentView === "Edit") {
|
|
||||||
view = <Edit flow={flow} message={message} onChange={onContentChange}/>
|
|
||||||
} else {
|
|
||||||
view = <ViewServer flow={flow} message={message} contentView={contentView}/>
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="contentview">
|
|
||||||
{view}
|
|
||||||
<ShowFullContentButton/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
state => ({
|
|
||||||
contentView: state.ui.flow.contentView,
|
|
||||||
isDisplayLarge: state.ui.flow.displayLarge,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
displayLarge,
|
|
||||||
updateEdit
|
|
||||||
}
|
|
||||||
)(ContentView)
|
|
@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import CodeMirror from "../../contrib/CodeMirror"
|
|
||||||
|
|
||||||
|
|
||||||
type CodeEditorProps = {
|
|
||||||
content: string,
|
|
||||||
onChange: Function,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CodeEditor ( { content, onChange}: CodeEditorProps ){
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
lineNumbers: true
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
|
|
||||||
<CodeMirror value={content} onChange={onChange} options={options}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { MessageUtils } from '../../flow/utils'
|
|
||||||
import { Flow, HTTPMessage } from '../../flow'
|
|
||||||
|
|
||||||
type ContentLoaderProps = {
|
|
||||||
content: string,
|
|
||||||
contentView: object,
|
|
||||||
flow: Flow,
|
|
||||||
message: HTTPMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContentLoaderStates = {
|
|
||||||
content: string | undefined,
|
|
||||||
request: { abort: () => void }| undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function withContentLoader(View) {
|
|
||||||
|
|
||||||
return class extends React.Component<ContentLoaderProps, ContentLoaderStates> {
|
|
||||||
static displayName: string = View.displayName || View.name
|
|
||||||
static matches: (message: any) => boolean = View.matches
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
content: undefined,
|
|
||||||
request: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.updateContent(this.props)
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
if (
|
|
||||||
nextProps.message.content !== this.props.message.content ||
|
|
||||||
nextProps.message.contentHash !== this.props.message.contentHash ||
|
|
||||||
nextProps.contentView !== this.props.contentView
|
|
||||||
) {
|
|
||||||
this.updateContent(nextProps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.state.request) {
|
|
||||||
this.state.request.abort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateContent(props) {
|
|
||||||
if (this.state.request) {
|
|
||||||
this.state.request.abort()
|
|
||||||
}
|
|
||||||
// We have a few special cases where we do not need to make an HTTP request.
|
|
||||||
if (props.message.content !== undefined) {
|
|
||||||
return this.setState({request: undefined, content: props.message.content})
|
|
||||||
}
|
|
||||||
if (props.message.contentLength === 0) {
|
|
||||||
return this.setState({request: undefined, content: ""})
|
|
||||||
}
|
|
||||||
|
|
||||||
let requestUrl = MessageUtils.getContentURL(props.flow, props.message, props.contentView)
|
|
||||||
|
|
||||||
// We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable.
|
|
||||||
let request = new XMLHttpRequest();
|
|
||||||
request.addEventListener("load", this.requestComplete.bind(this, request));
|
|
||||||
request.addEventListener("error", this.requestFailed.bind(this, request));
|
|
||||||
request.open("GET", requestUrl);
|
|
||||||
request.send();
|
|
||||||
this.setState({request, content: undefined})
|
|
||||||
}
|
|
||||||
|
|
||||||
requestComplete(request, e) {
|
|
||||||
if (request !== this.state.request) {
|
|
||||||
return // Stale request
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
content: request.responseText,
|
|
||||||
request: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
requestFailed(request, e) {
|
|
||||||
if (request !== this.state.request) {
|
|
||||||
return // Stale request
|
|
||||||
}
|
|
||||||
console.error(e)
|
|
||||||
// FIXME: Better error handling
|
|
||||||
this.setState({
|
|
||||||
content: "Error getting content.",
|
|
||||||
request: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.state.content !== undefined ? (
|
|
||||||
<View content={this.state.content} {...this.props}/>
|
|
||||||
) : (
|
|
||||||
<div className="text-center">
|
|
||||||
<i className="fa fa-spinner fa-spin"></i>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,28 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ViewSelector from './ViewSelector'
|
|
||||||
import UploadContentButton from './UploadContentButton'
|
|
||||||
import DownloadContentButton from './DownloadContentButton'
|
|
||||||
import { useAppSelector } from "../../ducks";
|
|
||||||
import { Flow, HTTPMessage } from '../../flow'
|
|
||||||
|
|
||||||
type ContentViewOptionsProps = {
|
|
||||||
flow: Flow,
|
|
||||||
message: HTTPMessage,
|
|
||||||
uploadContent: (content: string) => Promise<Response>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ContentViewOptions({ flow, message, uploadContent }: ContentViewOptionsProps) {
|
|
||||||
const contentViewDescription = useAppSelector(state => state.ui.flow.viewDescription)
|
|
||||||
const readonly = useAppSelector(state => state.ui.flow.modifiedFlow);
|
|
||||||
return (
|
|
||||||
<div className="view-options">
|
|
||||||
{readonly ? <ViewSelector /> : <span><b>View:</b> edit</span>}
|
|
||||||
|
|
||||||
<DownloadContentButton flow={flow} message={message}/>
|
|
||||||
|
|
||||||
{!readonly && <UploadContentButton uploadContent={uploadContent}/> }
|
|
||||||
|
|
||||||
{readonly && <span>{contentViewDescription}</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
|
|
||||||
import withContentLoader from './ContentLoader'
|
|
||||||
import { MessageUtils } from '../../flow/utils'
|
|
||||||
import CodeEditor from './CodeEditor'
|
|
||||||
import { useAppDispatch, useAppSelector } from "../../ducks";
|
|
||||||
|
|
||||||
|
|
||||||
const isImage = /^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i
|
|
||||||
ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg))
|
|
||||||
|
|
||||||
type ViewImageProps = {
|
|
||||||
flow: object,
|
|
||||||
message: object,
|
|
||||||
}
|
|
||||||
|
|
||||||
function ViewImage({ flow, message }: ViewImageProps) {
|
|
||||||
return (
|
|
||||||
<div className="flowview-image">
|
|
||||||
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type EditProps = {
|
|
||||||
content: string,
|
|
||||||
onChange: (content: string) => any,
|
|
||||||
}
|
|
||||||
|
|
||||||
function PureEdit({ content, onChange }: EditProps) {
|
|
||||||
return <CodeEditor content={content} onChange={onChange}/>
|
|
||||||
}
|
|
||||||
const Edit = withContentLoader(PureEdit)
|
|
||||||
|
|
||||||
type PureViewServerProps = {
|
|
||||||
flow: object,
|
|
||||||
message: object,
|
|
||||||
content: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type PureViewServerStates = {
|
|
||||||
lines: [style: string, text: string][][],
|
|
||||||
description: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PureViewServer({flow, message, content}: PureViewServerProps) {
|
|
||||||
const [data, setData] = useState<PureViewServerStates>({
|
|
||||||
lines: [],
|
|
||||||
description: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch(),
|
|
||||||
showFullContent: boolean = useAppSelector(state => state.ui.flow.showFullContent),
|
|
||||||
maxLines: number = useAppSelector(state => state.ui.flow.maxContentLines)
|
|
||||||
|
|
||||||
let lines = showFullContent ? data.lines : data.lines?.slice(0, maxLines)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setContentView({flow, message, content})
|
|
||||||
}, [flow, message, content])
|
|
||||||
|
|
||||||
const setContentView = (props) => {
|
|
||||||
try {
|
|
||||||
setData(JSON.parse(props.content))
|
|
||||||
}catch(err) {
|
|
||||||
setData({lines: [], description: err.message})
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setContentViewDescription(props.contentView !== data.description ? data.description : ''))
|
|
||||||
dispatch(setContent(data.lines))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{ViewImage.matches(message) && <ViewImage flow={flow} message={message}/>}
|
|
||||||
<pre>
|
|
||||||
{lines.map((line, i) =>
|
|
||||||
<div key={`line${i}`}>
|
|
||||||
{line.map((element, j) => {
|
|
||||||
let [style, text] = element
|
|
||||||
return (
|
|
||||||
<span key={`tuple${j}`} className={style}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const ViewServer = withContentLoader(PureViewServer)
|
|
||||||
|
|
||||||
export { Edit, ViewServer, ViewImage }
|
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { MessageUtils } from "../../flow/utils"
|
|
||||||
import { Flow, HTTPMessage } from '../../flow'
|
|
||||||
|
|
||||||
type DownloadContentButtonProps = {
|
|
||||||
flow: Flow,
|
|
||||||
message: HTTPMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DownloadContentButton({ flow, message }: DownloadContentButtonProps) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a className="btn btn-default btn-xs"
|
|
||||||
href={MessageUtils.getContentURL(flow, message)}
|
|
||||||
title="Download the content of the flow.">
|
|
||||||
<i className="fa fa-download"/>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { formatSize } from '../../utils'
|
|
||||||
import UploadContentButton from './UploadContentButton'
|
|
||||||
import DownloadContentButton from './DownloadContentButton'
|
|
||||||
import { HTTPFlow, HTTPMessage } from '../../flow'
|
|
||||||
|
|
||||||
interface ContentProps {
|
|
||||||
flow: HTTPFlow,
|
|
||||||
message: HTTPMessage,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContentTooLargeProps extends ContentProps {
|
|
||||||
onClick: () => void,
|
|
||||||
uploadContent: () => any,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function ContentEmpty({ flow, message }: ContentProps) {
|
|
||||||
return (
|
|
||||||
<div className="alert alert-info">
|
|
||||||
No {flow.request === message ? 'request' : 'response'} content.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContentMissing({ flow, message }: ContentProps) {
|
|
||||||
return (
|
|
||||||
<div className="alert alert-info">
|
|
||||||
{flow.request === message ? 'Request' : 'Response'} content missing.
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContentTooLarge({ message, onClick, uploadContent, flow }: ContentTooLargeProps) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="alert alert-warning">
|
|
||||||
|
|
||||||
<button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
|
|
||||||
{formatSize(message.contentLength)} content size.
|
|
||||||
</div>
|
|
||||||
<div className="view-options text-center">
|
|
||||||
<UploadContentButton uploadContent={uploadContent}/>
|
|
||||||
|
|
||||||
<DownloadContentButton flow={flow} message={message}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Button from '../common/Button';
|
|
||||||
import { setShowFullContent } from '../../ducks/ui/flow'
|
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
|
||||||
|
|
||||||
export default function ShowFullContentButton() {
|
|
||||||
const dispatch = useAppDispatch(),
|
|
||||||
showFullContent = useAppSelector(state => state.ui.flow.showFullContent),
|
|
||||||
visibleLines = useAppSelector(state => state.ui.flow.maxContentLines),
|
|
||||||
contentLines = useAppSelector(state => state.ui.flow.content.length)
|
|
||||||
|
|
||||||
return (
|
|
||||||
!showFullContent ? (
|
|
||||||
<div>
|
|
||||||
<Button className="view-all-content-btn btn-xs" onClick={() => dispatch(setShowFullContent())}>
|
|
||||||
Show full content
|
|
||||||
</Button>
|
|
||||||
<span className="pull-right"> {visibleLines}/{contentLines} are visible </span>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import FileChooser from '../common/FileChooser'
|
|
||||||
|
|
||||||
type UploadContentButtonProps = {
|
|
||||||
uploadContent: (content: string) => Promise<Response>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UploadContentButton({ uploadContent }: UploadContentButtonProps) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FileChooser
|
|
||||||
icon="fa-upload"
|
|
||||||
title="Upload a file to replace the content."
|
|
||||||
onOpenFile={uploadContent}
|
|
||||||
className="btn btn-default btn-xs"/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {setContentView} from '../../ducks/ui/flow';
|
|
||||||
import Dropdown, {MenuItem} from '../common/Dropdown'
|
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
|
||||||
|
|
||||||
|
|
||||||
export default React.memo(function ViewSelector() {
|
|
||||||
const dispatch = useAppDispatch(),
|
|
||||||
contentViews = useAppSelector(state => state.conf.contentViews || []),
|
|
||||||
activeView = useAppSelector(state => state.ui.flow.contentView);
|
|
||||||
|
|
||||||
let inner = <span><b>View:</b> {activeView.toLowerCase()} <span className="caret"/></span>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
text={inner}
|
|
||||||
className="btn btn-default btn-xs pull-left"
|
|
||||||
options={{placement: "top-start"}}>
|
|
||||||
{contentViews.map(name =>
|
|
||||||
<MenuItem key={name} onClick={() => dispatch(setContentView(name))}>
|
|
||||||
{name.toLowerCase().replace('_', ' ')}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Dropdown>
|
|
||||||
)
|
|
||||||
});
|
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {useDispatch} from 'react-redux'
|
import {useDispatch} from 'react-redux'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {RequestUtils, ResponseUtils} from '../../flow/utils'
|
import {endTime, getTotalSize, RequestUtils, ResponseUtils, startTime} from '../../flow/utils'
|
||||||
import {fetchApi, formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||||
import * as flowActions from "../../ducks/flows";
|
import * as flowActions from "../../ducks/flows";
|
||||||
import {addInterceptFilter} from "../../ducks/options"
|
import {addInterceptFilter} from "../../ducks/options"
|
||||||
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
||||||
import {Flow} from "../../flow";
|
import {Flow} from "../../flow";
|
||||||
|
import {copy} from "../../flow/export";
|
||||||
|
|
||||||
|
|
||||||
type FlowColumnProps = {
|
type FlowColumnProps = {
|
||||||
@ -16,38 +17,38 @@ type FlowColumnProps = {
|
|||||||
interface FlowColumn {
|
interface FlowColumn {
|
||||||
(props: FlowColumnProps): JSX.Element;
|
(props: FlowColumnProps): JSX.Element;
|
||||||
|
|
||||||
headerClass: string;
|
headerName: string; // Shown in the UI
|
||||||
headerName: string;
|
sortKey: (flow: Flow) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TLSColumn: FlowColumn = ({flow}) => {
|
export const tls: FlowColumn = ({flow}) => {
|
||||||
return (
|
return (
|
||||||
<td className={classnames('col-tls', flow.client_conn.tls_established ? 'col-tls-https' : 'col-tls-http')}/>
|
<td className={classnames('col-tls', flow.client_conn.tls_established ? 'col-tls-https' : 'col-tls-http')}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
tls.headerName = ''
|
||||||
|
tls.sortKey = flow => flow.type === "http" && flow.request.scheme
|
||||||
|
|
||||||
TLSColumn.headerClass = 'col-tls'
|
export const icon: FlowColumn = ({flow}) => {
|
||||||
TLSColumn.headerName = ''
|
|
||||||
|
|
||||||
export const IconColumn: FlowColumn = ({flow}) => {
|
|
||||||
return (
|
return (
|
||||||
<td className="col-icon">
|
<td className="col-icon">
|
||||||
<div className={classnames('resource-icon', getIcon(flow))}/>
|
<div className={classnames('resource-icon', getIcon(flow))}/>
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
icon.headerName = ''
|
||||||
IconColumn.headerClass = 'col-icon'
|
icon.sortKey = flow => 0
|
||||||
IconColumn.headerName = ''
|
|
||||||
|
|
||||||
const getIcon = (flow: Flow): string => {
|
const getIcon = (flow: Flow): string => {
|
||||||
if (flow.type !== "http" || !flow.response) {
|
if (flow.type !== "http" || !flow.response) {
|
||||||
return 'resource-icon-plain'
|
return 'resource-icon-plain'
|
||||||
}
|
}
|
||||||
|
if (flow.websocket) {
|
||||||
|
return 'resource-icon-websocket'
|
||||||
|
}
|
||||||
|
|
||||||
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
||||||
|
|
||||||
// @todo We should assign a type to the flow somewhere else.
|
|
||||||
if (flow.response.status_code === 304) {
|
if (flow.response.status_code === 304) {
|
||||||
return 'resource-icon-not-modified'
|
return 'resource-icon-not-modified'
|
||||||
}
|
}
|
||||||
@ -70,7 +71,7 @@ const getIcon = (flow: Flow): string => {
|
|||||||
return 'resource-icon-plain'
|
return 'resource-icon-plain'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PathColumn: FlowColumn = ({flow}) => {
|
export const path: FlowColumn = ({flow}) => {
|
||||||
let err;
|
let err;
|
||||||
if (flow.error) {
|
if (flow.error) {
|
||||||
if (flow.error.msg === "Connection killed.") {
|
if (flow.error.msg === "Connection killed.") {
|
||||||
@ -88,24 +89,23 @@ export const PathColumn: FlowColumn = ({flow}) => {
|
|||||||
<i className="fa fa-fw fa-pause pull-right"/>
|
<i className="fa fa-fw fa-pause pull-right"/>
|
||||||
)}
|
)}
|
||||||
{err}
|
{err}
|
||||||
|
<span className="marker pull-right">{flow.marked}</span>
|
||||||
{flow.type === "http" ? RequestUtils.pretty_url(flow.request) : null}
|
{flow.type === "http" ? RequestUtils.pretty_url(flow.request) : null}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
path.headerName = 'Path'
|
||||||
|
path.sortKey = flow => flow.type === "http" && RequestUtils.pretty_url(flow.request)
|
||||||
|
|
||||||
PathColumn.headerClass = 'col-path'
|
export const method: FlowColumn = ({flow}) => {
|
||||||
PathColumn.headerName = 'Path'
|
|
||||||
|
|
||||||
export const MethodColumn: FlowColumn = ({flow}) => {
|
|
||||||
return (
|
return (
|
||||||
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toLowerCase()}</td>
|
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toLowerCase()}</td>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
method.headerName = 'Method'
|
||||||
|
method.sortKey = flow => flow.type === "http" && flow.request.method
|
||||||
|
|
||||||
MethodColumn.headerClass = 'col-method'
|
export const status: FlowColumn = ({flow}) => {
|
||||||
MethodColumn.headerName = 'Method'
|
|
||||||
|
|
||||||
export const StatusColumn: FlowColumn = ({flow}) => {
|
|
||||||
let color = 'darkred';
|
let color = 'darkred';
|
||||||
|
|
||||||
if (flow.type !== "http" || !flow.response)
|
if (flow.type !== "http" || !flow.response)
|
||||||
@ -127,75 +127,65 @@ export const StatusColumn: FlowColumn = ({flow}) => {
|
|||||||
<td className="col-status" style={{color: color}}>{flow.response.status_code}</td>
|
<td className="col-status" style={{color: color}}>{flow.response.status_code}</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
status.headerName = 'Status'
|
||||||
|
status.sortKey = flow => flow.type === "http" && flow.response && flow.response.status_code
|
||||||
|
|
||||||
StatusColumn.headerClass = 'col-status'
|
export const size: FlowColumn = ({flow}) => {
|
||||||
StatusColumn.headerName = 'Status'
|
|
||||||
|
|
||||||
export const SizeColumn: FlowColumn = ({flow}) => {
|
|
||||||
return (
|
return (
|
||||||
<td className="col-size">{formatSize(getTotalSize(flow))}</td>
|
<td className="col-size">{formatSize(getTotalSize(flow))}</td>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
size.headerName = 'Size'
|
||||||
|
size.sortKey = flow => getTotalSize(flow)
|
||||||
|
|
||||||
const getTotalSize = (flow: Flow): number => {
|
|
||||||
if (flow.type !== "http")
|
|
||||||
return 0
|
|
||||||
let total = flow.request.contentLength
|
|
||||||
if (flow.response) {
|
|
||||||
total += flow.response.contentLength || 0
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
SizeColumn.headerClass = 'col-size'
|
export const time: FlowColumn = ({flow}) => {
|
||||||
SizeColumn.headerName = 'Size'
|
const start = startTime(flow), end = endTime(flow);
|
||||||
|
|
||||||
export const TimeColumn: FlowColumn = ({flow}) => {
|
|
||||||
return (
|
return (
|
||||||
<td className="col-time">
|
<td className="col-time">
|
||||||
{flow.type === "http" && flow.response?.timestamp_end ? (
|
{start && end ? (
|
||||||
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
|
formatTimeDelta(1000 * (end - start))
|
||||||
) : (
|
) : (
|
||||||
'...'
|
'...'
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
time.headerName = 'Time'
|
||||||
|
time.sortKey = flow => {
|
||||||
|
const start = startTime(flow), end = endTime(flow);
|
||||||
|
return start && end && end - start;
|
||||||
|
}
|
||||||
|
|
||||||
TimeColumn.headerClass = 'col-time'
|
export const timestamp: FlowColumn = ({flow}) => {
|
||||||
TimeColumn.headerName = 'Time'
|
const start = startTime(flow);
|
||||||
|
|
||||||
export const TimeStampColumn: FlowColumn = ({flow}) => {
|
|
||||||
return (
|
return (
|
||||||
<td className="col-start">
|
<td className="col-start">
|
||||||
{flow.type === "http" && flow.request.timestamp_start ? (
|
{start ? (
|
||||||
formatTimeStamp(flow.request.timestamp_start)
|
formatTimeStamp(start)
|
||||||
) : (
|
) : (
|
||||||
'...'
|
'...'
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
timestamp.headerName = 'Start time'
|
||||||
|
timestamp.sortKey = flow => startTime(flow)
|
||||||
|
|
||||||
TimeStampColumn.headerClass = 'col-timestamp'
|
const markers = {
|
||||||
TimeStampColumn.headerName = 'TimeStamp'
|
":red_circle:": "🔴",
|
||||||
|
":orange_circle:": "🟠",
|
||||||
|
":yellow_circle:": "🟡",
|
||||||
|
":green_circle:": "🟢",
|
||||||
|
":large_blue_circle:": "🔵",
|
||||||
|
":purple_circle:": "🟣",
|
||||||
|
":brown_circle:": "🟤",
|
||||||
|
}
|
||||||
|
|
||||||
export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
export const quickactions: FlowColumn = ({flow}) => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
let [open, setOpen] = useState(false)
|
let [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const copy = (format: string) => {
|
|
||||||
if (!flow) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchApi(`/flows/${flow.id}/export/${format}.json`, {method: 'POST'})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
navigator.clipboard.writeText(data.export)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let resume_or_replay: React.ReactNode | null = null;
|
let resume_or_replay: React.ReactNode | null = null;
|
||||||
if (flow.intercepted) {
|
if (flow.intercepted) {
|
||||||
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
||||||
@ -221,11 +211,21 @@ export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
|||||||
onOpen={setOpen}
|
onOpen={setOpen}
|
||||||
options={{placement: "bottom-end"}}>
|
options={{placement: "bottom-end"}}>
|
||||||
<SubMenu title="Copy...">
|
<SubMenu title="Copy...">
|
||||||
<MenuItem onClick={() => copy("raw_request")}>Copy raw request</MenuItem>
|
<MenuItem onClick={() => copy(flow, "raw_request")}>Copy raw request</MenuItem>
|
||||||
<MenuItem onClick={() => copy("raw_response")}>Copy raw response</MenuItem>
|
<MenuItem onClick={() => copy(flow, "raw_response")}>Copy raw response</MenuItem>
|
||||||
<MenuItem onClick={() => copy("raw")}>Copy raw request and response</MenuItem>
|
<MenuItem onClick={() => copy(flow, "raw")}>Copy raw request and response</MenuItem>
|
||||||
<MenuItem onClick={() => copy("curl")}>Copy as cURL</MenuItem>
|
<MenuItem onClick={() => copy(flow, "curl")}>Copy as cURL</MenuItem>
|
||||||
<MenuItem onClick={() => copy("httpie")}>Copy as HTTPie</MenuItem>
|
<MenuItem onClick={() => copy(flow, "httpie")}>Copy as HTTPie</MenuItem>
|
||||||
|
</SubMenu>
|
||||||
|
<SubMenu title="Mark..." className="markers-menu">
|
||||||
|
<MenuItem onClick={() => dispatch(flowActions.update(flow, {marked: ""}))}>⚪ (no marker)</MenuItem>
|
||||||
|
{Object.entries(markers).map(([name, sym]) =>
|
||||||
|
<MenuItem
|
||||||
|
key={name}
|
||||||
|
onClick={() => dispatch(flowActions.update(flow, {marked: name}))}>
|
||||||
|
{sym} {name.replace(/[:_]/g, " ")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<SubMenu title="Intercept requests like this">
|
<SubMenu title="Intercept requests like this">
|
||||||
<MenuItem onClick={() => filt(`~q ${flow.request.host}`)}>
|
<MenuItem onClick={() => filt(`~q ${flow.request.host}`)}>
|
||||||
@ -260,21 +260,5 @@ export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickActionsColumn.headerClass = 'col-quickactions'
|
quickactions.headerName = ''
|
||||||
QuickActionsColumn.headerName = ''
|
quickactions.sortKey = flow => 0;
|
||||||
|
|
||||||
|
|
||||||
export const columns: { [key: string]: FlowColumn } = {};
|
|
||||||
for (let col of [
|
|
||||||
TLSColumn,
|
|
||||||
IconColumn,
|
|
||||||
PathColumn,
|
|
||||||
MethodColumn,
|
|
||||||
StatusColumn,
|
|
||||||
TimeStampColumn,
|
|
||||||
SizeColumn,
|
|
||||||
TimeColumn,
|
|
||||||
QuickActionsColumn,
|
|
||||||
]) {
|
|
||||||
columns[col.name.replace(/Column$/, "").toLowerCase()] = col;
|
|
||||||
}
|
|
||||||
|
@ -3,7 +3,7 @@ import classnames from 'classnames'
|
|||||||
import {Flow} from "../../flow";
|
import {Flow} from "../../flow";
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
import {select} from '../../ducks/flows'
|
import {select} from '../../ducks/flows'
|
||||||
import {columns, QuickActionsColumn} from "./FlowColumns";
|
import * as columns from "./FlowColumns";
|
||||||
|
|
||||||
type FlowRowProps = {
|
type FlowRowProps = {
|
||||||
flow: Flow
|
flow: Flow
|
||||||
@ -33,7 +33,7 @@ export default React.memo(function FlowRow({flow, selected, highlighted}: FlowRo
|
|||||||
dispatch(select(flow.id));
|
dispatch(select(flow.id));
|
||||||
}, [flow]);
|
}, [flow]);
|
||||||
|
|
||||||
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
|
const displayColumns = displayColumnNames.map(x => columns[x]).filter(x => x).concat(columns.quickactions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={className} onClick={onClick}>
|
<tr className={className} onClick={onClick}>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {columns, QuickActionsColumn} from './FlowColumns'
|
import * as columns from './FlowColumns'
|
||||||
|
|
||||||
import {setSort} from '../../ducks/flows'
|
import {setSort} from '../../ducks/flows'
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
@ -12,12 +12,12 @@ export default React.memo(function FlowTableHead() {
|
|||||||
displayColumnNames = useAppSelector(state => state.options.web_columns);
|
displayColumnNames = useAppSelector(state => state.options.web_columns);
|
||||||
|
|
||||||
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
||||||
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
|
const displayColumns = displayColumnNames.map(x => columns[x]).filter(x => x).concat(columns.quickactions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
{displayColumns.map(Column => (
|
{displayColumns.map(Column => (
|
||||||
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
|
<th className={classnames(`col-${Column.name}`, sortColumn === Column.name && sortType)}
|
||||||
key={Column.name}
|
key={Column.name}
|
||||||
onClick={() => dispatch(setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc))}>
|
onClick={() => dispatch(setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc))}>
|
||||||
{Column.headerName}
|
{Column.headerName}
|
||||||
|
@ -1,44 +1,66 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import _ from 'lodash'
|
import {FunctionComponent} from "react"
|
||||||
|
import {Request, Response} from './FlowView/HttpMessages'
|
||||||
|
import Connection from './FlowView/Connection'
|
||||||
|
import Error from "./FlowView/Error"
|
||||||
|
import Timing from "./FlowView/Timing"
|
||||||
|
import WebSocket from "./FlowView/WebSocket"
|
||||||
|
|
||||||
import Nav from './FlowView/Nav'
|
import {selectTab} from '../ducks/ui/flow'
|
||||||
import { ErrorView as Error, Request, Response } from './FlowView/Messages'
|
|
||||||
import Details from './FlowView/Details'
|
|
||||||
|
|
||||||
import { selectTab } from '../ducks/ui/flow'
|
|
||||||
import {useAppDispatch, useAppSelector} from "../ducks";
|
import {useAppDispatch, useAppSelector} from "../ducks";
|
||||||
|
import {Flow} from "../flow";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
export const allTabs = { Request, Response, Error, Details }
|
type TabProps = {
|
||||||
|
flow: Flow
|
||||||
|
}
|
||||||
|
|
||||||
|
export const allTabs: { [name: string]: FunctionComponent<TabProps> } = {
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
error: Error,
|
||||||
|
connection: Connection,
|
||||||
|
timing: Timing,
|
||||||
|
websocket: WebSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tabsForFlow(flow: Flow): string[] {
|
||||||
|
const tabs = ['request', 'response', 'websocket', 'error'].filter(k => flow[k])
|
||||||
|
tabs.push("connection")
|
||||||
|
tabs.push("timing")
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
export default function FlowView() {
|
export default function FlowView() {
|
||||||
const dispatch = useAppDispatch(),
|
const dispatch = useAppDispatch(),
|
||||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]),
|
||||||
|
tabs = tabsForFlow(flow);
|
||||||
|
|
||||||
let tabName = useAppSelector(state => state.ui.flow.tab)
|
let active = useAppSelector(state => state.ui.flow.tab)
|
||||||
|
if (tabs.indexOf(active) < 0) {
|
||||||
// only display available tab names
|
if (active === 'response' && flow.error) {
|
||||||
const tabs = ['request', 'response', 'error'].filter(k => flow[k])
|
active = 'error'
|
||||||
tabs.push("details")
|
} else if (active === 'error' && "response" in flow) {
|
||||||
|
active = 'response'
|
||||||
if (tabs.indexOf(tabName) < 0) {
|
|
||||||
if (tabName === 'response' && flow.error) {
|
|
||||||
tabName = 'error'
|
|
||||||
} else if (tabName === 'error' && flow.response) {
|
|
||||||
tabName = 'response'
|
|
||||||
} else {
|
} else {
|
||||||
tabName = tabs[0]
|
active = tabs[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const Tab = allTabs[active];
|
||||||
const Tab = allTabs[_.capitalize(tabName)]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-detail">
|
<div className="flow-detail">
|
||||||
<Nav
|
<nav className="nav-tabs nav-tabs-sm">
|
||||||
tabs={tabs}
|
{tabs.map(tabId => (
|
||||||
active={tabName}
|
<a key={tabId} href="#" className={classnames({active: active === tabId})}
|
||||||
onSelectTab={(tab: string) => dispatch(selectTab(tab))}
|
onClick={event => {
|
||||||
/>
|
event.preventDefault()
|
||||||
|
dispatch(selectTab(tabId))
|
||||||
|
}}>
|
||||||
|
{allTabs[tabId].name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
<Tab flow={flow}/>
|
<Tab flow={flow}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
151
web/src/js/components/FlowView/Connection.tsx
Normal file
151
web/src/js/components/FlowView/Connection.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {formatTimeStamp} from '../../utils'
|
||||||
|
import {Client, Flow, Server} from '../../flow'
|
||||||
|
|
||||||
|
|
||||||
|
type ConnectionInfoProps = {
|
||||||
|
conn: Client | Server
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectionInfo({conn}: ConnectionInfoProps) {
|
||||||
|
let address_info: JSX.Element | null = null;
|
||||||
|
if ("address" in conn) {
|
||||||
|
// Server
|
||||||
|
address_info = <>
|
||||||
|
<tr>
|
||||||
|
<td>Address:</td>
|
||||||
|
<td>{conn.address?.join(':')}</td>
|
||||||
|
</tr>
|
||||||
|
{conn.peername && (
|
||||||
|
<tr>
|
||||||
|
<td>Resolved address:</td>
|
||||||
|
<td>{conn.peername.join(':')}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{conn.sockname && (
|
||||||
|
<tr>
|
||||||
|
<td>Source address:</td>
|
||||||
|
<td>{conn.sockname.join(':')}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
// Client
|
||||||
|
if (conn.peername?.[0]) {
|
||||||
|
address_info = <>
|
||||||
|
<tr>
|
||||||
|
<td>Address:</td>
|
||||||
|
<td>{conn.peername?.join(':')}</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<table className="connection-table">
|
||||||
|
<tbody>
|
||||||
|
{address_info}
|
||||||
|
{conn.sni ? (
|
||||||
|
<tr>
|
||||||
|
<td><abbr title="TLS Server Name Indication">SNI</abbr>:</td>
|
||||||
|
<td>{conn.sni}</td>
|
||||||
|
</tr>
|
||||||
|
): null}
|
||||||
|
{conn.alpn ? (
|
||||||
|
<tr>
|
||||||
|
<td><abbr title="ALPN protocol negotiated">ALPN</abbr>:</td>
|
||||||
|
<td>{conn.alpn}</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
{conn.tls_version ? (
|
||||||
|
<tr>
|
||||||
|
<td>TLS Version:</td>
|
||||||
|
<td>{conn.tls_version}</td>
|
||||||
|
</tr>
|
||||||
|
): null}
|
||||||
|
{conn.cipher ? (
|
||||||
|
<tr>
|
||||||
|
<td>TLS Cipher:</td>
|
||||||
|
<td>{conn.cipher}</td>
|
||||||
|
</tr>
|
||||||
|
): null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrList(data: [string, string][]): JSX.Element {
|
||||||
|
return <dl className="cert-attributes">
|
||||||
|
{data.map(([k, v]) =>
|
||||||
|
<React.Fragment key={k}>
|
||||||
|
<dt>{k}</dt>
|
||||||
|
<dd>{v}</dd>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CertificateInfo({flow}: { flow: Flow }): JSX.Element {
|
||||||
|
const cert = flow.server_conn?.cert;
|
||||||
|
if (!cert)
|
||||||
|
return <></>;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<h4 key="name">Server Certificate</h4>
|
||||||
|
<table className="certificate-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>{cert.keyinfo[0]}, {cert.keyinfo[1]} bits</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>SHA256 digest</td>
|
||||||
|
<td>{cert.sha256}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Valid from</td>
|
||||||
|
<td>{formatTimeStamp(cert.notbefore, {milliseconds: false})}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Valid to</td>
|
||||||
|
<td>{formatTimeStamp(cert.notafter, {milliseconds: false})}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Subject Alternative Names</td>
|
||||||
|
<td>{cert.altnames.join(", ")}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Subject</td>
|
||||||
|
<td>{attrList(cert.subject)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Issuer</td>
|
||||||
|
<td>{attrList(cert.issuer)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Serial</td>
|
||||||
|
<td>{cert.serial}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Connection({flow}: { flow: Flow }) {
|
||||||
|
return (
|
||||||
|
<section className="detail">
|
||||||
|
<h4>Client Connection</h4>
|
||||||
|
<ConnectionInfo conn={flow.client_conn}/>
|
||||||
|
|
||||||
|
{
|
||||||
|
flow.server_conn?.address &&
|
||||||
|
<>
|
||||||
|
<h4>Server Connection</h4>
|
||||||
|
<ConnectionInfo conn={flow.server_conn}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CertificateInfo flow={flow}/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -1,173 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {formatTimeDelta, formatTimeStamp} from '../../utils'
|
|
||||||
import { Flow, HTTPMessage, Connection } from '../../flow'
|
|
||||||
|
|
||||||
type TimeStampProps = {
|
|
||||||
t: number,
|
|
||||||
deltaTo: number,
|
|
||||||
title: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TimeStamp({t, deltaTo, title}: TimeStampProps) {
|
|
||||||
return t ? (
|
|
||||||
<tr>
|
|
||||||
<td>{title}:</td>
|
|
||||||
<td>
|
|
||||||
{formatTimeStamp(t)}
|
|
||||||
{deltaTo && (
|
|
||||||
<span className="text-muted">
|
|
||||||
({formatTimeDelta(1000 * (t - deltaTo))})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
<tr></tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnectionInfoProps = {
|
|
||||||
conn: Connection,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConnectionInfo({conn}: ConnectionInfoProps) {
|
|
||||||
return (
|
|
||||||
<table className="connection-table">
|
|
||||||
<tbody>
|
|
||||||
<tr key="address">
|
|
||||||
<td>Address:</td>
|
|
||||||
<td>{conn.address?.join(':')}</td>
|
|
||||||
</tr>
|
|
||||||
{conn.sni && (
|
|
||||||
<tr key="sni">
|
|
||||||
<td><abbr title="TLS Server Name Indication">TLS SNI:</abbr></td>
|
|
||||||
<td>{conn.sni}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{conn.tls_version && (
|
|
||||||
<tr key="tls_version">
|
|
||||||
<td>TLS version:</td>
|
|
||||||
<td>{conn.tls_version}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{conn.cipher_name && (
|
|
||||||
<tr key="cipher_name">
|
|
||||||
<td>cipher name:</td>
|
|
||||||
<td>{conn.cipher_name}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{conn.alpn_proto_negotiated && (
|
|
||||||
<tr key="ALPN">
|
|
||||||
<td><abbr title="ALPN protocol negotiated">ALPN:</abbr></td>
|
|
||||||
<td>{conn.alpn_proto_negotiated}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{conn.ip_address && (
|
|
||||||
<tr key="ip_address">
|
|
||||||
<td>Resolved address:</td>
|
|
||||||
<td>{conn.ip_address.join(':')}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{conn.source_address && (
|
|
||||||
<tr key="source_address">
|
|
||||||
<td>Source address:</td>
|
|
||||||
<td>{conn.source_address.join(':')}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CertificateInfo({flow}) {
|
|
||||||
// @todo We should fetch human-readable certificate representation from the server
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{flow.client_conn.cert && [
|
|
||||||
<h4 key="name">Client Certificate</h4>,
|
|
||||||
<pre key="value" style={{maxHeight: 100}}>{flow.client_conn.cert}</pre>
|
|
||||||
]}
|
|
||||||
|
|
||||||
{flow.server_conn.cert && [
|
|
||||||
<h4 key="name">Server Certificate</h4>,
|
|
||||||
<pre key="value" style={{maxHeight: 100}}>{flow.server_conn.cert}</pre>
|
|
||||||
]}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Timing({flow}) {
|
|
||||||
const {server_conn: sc, client_conn: cc, request: req, response: res} = flow
|
|
||||||
|
|
||||||
const timestamps = [
|
|
||||||
{
|
|
||||||
title: "Server conn. initiated",
|
|
||||||
t: sc.timestamp_start,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, {
|
|
||||||
title: "Server conn. TCP handshake",
|
|
||||||
t: sc.timestamp_tcp_setup,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, {
|
|
||||||
title: "Server conn. SSL handshake",
|
|
||||||
t: sc.timestamp_ssl_setup,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, {
|
|
||||||
title: "Client conn. established",
|
|
||||||
t: cc.timestamp_start,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, {
|
|
||||||
title: "Client conn. SSL handshake",
|
|
||||||
t: cc.timestamp_ssl_setup,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, {
|
|
||||||
title: "First request byte",
|
|
||||||
t: req.timestamp_start
|
|
||||||
}, {
|
|
||||||
title: "Request complete",
|
|
||||||
t: req.timestamp_end,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, res && {
|
|
||||||
title: "First response byte",
|
|
||||||
t: res.timestamp_start,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}, res && {
|
|
||||||
title: "Response complete",
|
|
||||||
t: res.timestamp_end,
|
|
||||||
deltaTo: req.timestamp_start
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4>Timing</h4>
|
|
||||||
<table className="timing-table">
|
|
||||||
<tbody>
|
|
||||||
{timestamps.filter(v => v).sort((a, b) => a.t - b.t).map(item => (
|
|
||||||
<TimeStamp key={item.title} {...item}/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Details({flow}) {
|
|
||||||
return (
|
|
||||||
<section className="detail">
|
|
||||||
<h4>Client Connection</h4>
|
|
||||||
<ConnectionInfo conn={flow.client_conn}/>
|
|
||||||
|
|
||||||
{flow.server_conn.address &&
|
|
||||||
[
|
|
||||||
<h4 key="sc">Server Connection</h4>,
|
|
||||||
<ConnectionInfo key="sc-ci" conn={flow.server_conn}/>
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
<CertificateInfo flow={flow}/>
|
|
||||||
|
|
||||||
<Timing flow={flow}/>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
20
web/src/js/components/FlowView/Error.tsx
Normal file
20
web/src/js/components/FlowView/Error.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {HTTPFlow} from "../../flow";
|
||||||
|
import {formatTimeStamp} from "../../utils";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type ErrorProps = {
|
||||||
|
flow: HTTPFlow & { error: Error }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Error({flow}: ErrorProps) {
|
||||||
|
return (
|
||||||
|
<section className="error">
|
||||||
|
<div className="alert alert-warning">
|
||||||
|
{flow.error.msg}
|
||||||
|
<div>
|
||||||
|
<small>{formatTimeStamp(flow.error.timestamp)}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -1,162 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import ValueEditor from '../ValueEditor/ValueEditor'
|
|
||||||
import { Key } from '../../utils'
|
|
||||||
|
|
||||||
export class HeaderEditor extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.onKeyDown = this.onKeyDown.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let { onTab, ...props } = this.props
|
|
||||||
return <ValueEditor
|
|
||||||
{...props}
|
|
||||||
onKeyDown={this.onKeyDown}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
ReactDOM.findDOMNode(this).focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(e) {
|
|
||||||
switch (e.keyCode) {
|
|
||||||
case Key.BACKSPACE:
|
|
||||||
var s = window.getSelection().getRangeAt(0)
|
|
||||||
if (s.startOffset === 0 && s.endOffset === 0) {
|
|
||||||
this.props.onRemove(e)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case Key.ENTER:
|
|
||||||
case Key.TAB:
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
this.props.onTab(e)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Headers extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
message: PropTypes.object.isRequired,
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
type: 'headers',
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(row, col, val) {
|
|
||||||
const nextHeaders = _.cloneDeep(this.props.message[this.props.type])
|
|
||||||
|
|
||||||
nextHeaders[row][col] = val
|
|
||||||
|
|
||||||
if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
|
|
||||||
// do not delete last row
|
|
||||||
if (nextHeaders.length === 1) {
|
|
||||||
nextHeaders[0][0] = 'Name'
|
|
||||||
nextHeaders[0][1] = 'Value'
|
|
||||||
} else {
|
|
||||||
nextHeaders.splice(row, 1)
|
|
||||||
// manually move selection target if this has been the last row.
|
|
||||||
if (row === nextHeaders.length) {
|
|
||||||
this._nextSel = `${row - 1}-value`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(nextHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
edit() {
|
|
||||||
this.refs['0-key'].focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
onTab(row, col, e) {
|
|
||||||
const headers = this.props.message[this.props.type]
|
|
||||||
|
|
||||||
if (col === 0) {
|
|
||||||
this._nextSel = `${row}-value`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (row !== headers.length - 1) {
|
|
||||||
this._nextSel = `${row + 1}-key`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const nextHeaders = _.cloneDeep(this.props.message[this.props.type])
|
|
||||||
nextHeaders.push(['Name', 'Value'])
|
|
||||||
this.props.onChange(nextHeaders)
|
|
||||||
this._nextSel = `${row + 1}-key`
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (this._nextSel && this.refs[this._nextSel]) {
|
|
||||||
this.refs[this._nextSel].focus()
|
|
||||||
this._nextSel = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(row, col, e) {
|
|
||||||
if (col === 1) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.refs[`${row}-key`].focus()
|
|
||||||
} else if (row > 0) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.refs[`${row - 1}-value`].focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { message, readonly } = this.props
|
|
||||||
if (message[this.props.type]) {
|
|
||||||
return (
|
|
||||||
<table className="header-table">
|
|
||||||
<tbody>
|
|
||||||
{message[this.props.type].map((header, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="header-name">
|
|
||||||
<HeaderEditor
|
|
||||||
ref={`${i}-key`}
|
|
||||||
content={header[0]}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={val => this.onChange(i, 0, val)}
|
|
||||||
onRemove={event => this.onRemove(i, 0, event)}
|
|
||||||
onTab={event => this.onTab(i, 0, event)}
|
|
||||||
/>
|
|
||||||
<span className="header-colon">:</span>
|
|
||||||
</td>
|
|
||||||
<td className="header-value">
|
|
||||||
<HeaderEditor
|
|
||||||
ref={`${i}-value`}
|
|
||||||
content={header[1]}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={val => this.onChange(i, 1, val)}
|
|
||||||
onRemove={event => this.onRemove(i, 1, event)}
|
|
||||||
onTab={event => this.onTab(i, 1, event)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<table className="header-table">
|
|
||||||
<tbody>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
146
web/src/js/components/FlowView/HttpMessages.tsx
Normal file
146
web/src/js/components/FlowView/HttpMessages.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import {isValidHttpVersion, MessageUtils, parseUrl, RequestUtils} from '../../flow/utils'
|
||||||
|
import ValidateEditor from '../editors/ValidateEditor'
|
||||||
|
import ValueEditor from '../editors/ValueEditor'
|
||||||
|
|
||||||
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
|
import {HTTPFlow, HTTPMessage, HTTPResponse} from '../../flow'
|
||||||
|
import * as flowActions from '../../ducks/flows'
|
||||||
|
import KeyValueListEditor from "../editors/KeyValueListEditor";
|
||||||
|
import HttpMessage from "../contentviews/HttpMessage";
|
||||||
|
|
||||||
|
|
||||||
|
type RequestLineProps = {
|
||||||
|
flow: HTTPFlow,
|
||||||
|
}
|
||||||
|
|
||||||
|
function RequestLine({flow}: RequestLineProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="first-line request-line">
|
||||||
|
<div>
|
||||||
|
<ValidateEditor
|
||||||
|
content={flow.request.method}
|
||||||
|
onEditDone={method => dispatch(flowActions.update(flow, {request: {method}}))}
|
||||||
|
isValid={method => method.length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ValidateEditor
|
||||||
|
content={RequestUtils.pretty_url(flow.request)}
|
||||||
|
onEditDone={url => dispatch(flowActions.update(flow, {request: {path: '', ...parseUrl(url)}}))}
|
||||||
|
isValid={url => !!parseUrl(url)?.host}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ValidateEditor
|
||||||
|
content={flow.request.http_version}
|
||||||
|
onEditDone={http_version => dispatch(flowActions.update(flow, {request: {http_version}}))}
|
||||||
|
isValid={isValidHttpVersion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ResponseLineProps = {
|
||||||
|
flow: HTTPFlow & { response: HTTPResponse },
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResponseLine({flow}: ResponseLineProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="first-line response-line">
|
||||||
|
<ValidateEditor
|
||||||
|
content={flow.response.http_version}
|
||||||
|
onEditDone={nextVer => dispatch(flowActions.update(flow, {response: {http_version: nextVer}}))}
|
||||||
|
isValid={isValidHttpVersion}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ValidateEditor
|
||||||
|
content={flow.response.status_code + ''}
|
||||||
|
onEditDone={code => dispatch(flowActions.update(flow, {response: {code: parseInt(code)}}))}
|
||||||
|
isValid={code => /^\d+$/.test(code)}
|
||||||
|
/>
|
||||||
|
{flow.response.http_version !== "HTTP/2.0" &&
|
||||||
|
<>
|
||||||
|
<ValueEditor
|
||||||
|
content={flow.response.reason}
|
||||||
|
onEditDone={msg => dispatch(flowActions.update(flow, {response: {msg}}))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type HeadersProps = {
|
||||||
|
flow: HTTPFlow,
|
||||||
|
message: HTTPMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
function Headers({flow, message}: HeadersProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const part = flow.request === message ? "request" : "response";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyValueListEditor
|
||||||
|
className="headers"
|
||||||
|
data={message.headers}
|
||||||
|
onChange={headers => dispatch(flowActions.update(flow, {[part]: {headers}}))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrailersProps = {
|
||||||
|
flow: HTTPFlow,
|
||||||
|
message: HTTPMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
function Trailers({flow, message}: TrailersProps) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const part = flow.request === message ? "request" : "response";
|
||||||
|
const hasTrailers = !!MessageUtils.get_first_header(message, /^trailer$/i)
|
||||||
|
|
||||||
|
if (!hasTrailers)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<hr/>
|
||||||
|
<h5>HTTP Trailers</h5>
|
||||||
|
<KeyValueListEditor
|
||||||
|
className="trailers"
|
||||||
|
data={message.trailers}
|
||||||
|
onChange={trailers => dispatch(flowActions.update(flow, {[part]: {trailers}}))}
|
||||||
|
/>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Message = React.memo(function Message({flow, message}: { flow: HTTPFlow, message: HTTPMessage }) {
|
||||||
|
const part = flow.request === message ? "request" : "response";
|
||||||
|
const FirstLine = flow.request === message ? RequestLine : ResponseLine;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={part}>
|
||||||
|
<FirstLine flow={flow}/>
|
||||||
|
<Headers flow={flow} message={message}/>
|
||||||
|
<hr/>
|
||||||
|
<HttpMessage key={flow.id + part} flow={flow} message={message}/>
|
||||||
|
<Trailers flow={flow} message={message}/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export function Request() {
|
||||||
|
const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as HTTPFlow;
|
||||||
|
return <Message flow={flow} message={flow.request}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Response() {
|
||||||
|
const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as HTTPFlow & { response: HTTPResponse }
|
||||||
|
return <Message flow={flow} message={flow.response}/>;
|
||||||
|
}
|
@ -1,198 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils'
|
|
||||||
import { formatTimeStamp } from '../../utils'
|
|
||||||
import ContentView from '../ContentView'
|
|
||||||
import ContentViewOptions from '../ContentView/ContentViewOptions'
|
|
||||||
import ValidateEditor from '../ValueEditor/ValidateEditor'
|
|
||||||
import ValueEditor from '../ValueEditor/ValueEditor'
|
|
||||||
import HideInStatic from '../common/HideInStatic'
|
|
||||||
|
|
||||||
import Headers from './Headers'
|
|
||||||
import { updateEdit as updateFlow } from '../../ducks/ui/flow'
|
|
||||||
import { uploadContent } from '../../ducks/flows'
|
|
||||||
import ToggleEdit from './ToggleEdit'
|
|
||||||
import { useAppDispatch, useAppSelector } from "../../ducks";
|
|
||||||
import { HTTPFlow, HTTPMessage } from '../../flow'
|
|
||||||
|
|
||||||
|
|
||||||
type RequestLineProps = {
|
|
||||||
flow: HTTPFlow,
|
|
||||||
readonly: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
function RequestLine({ flow, readonly }: RequestLineProps) {
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="first-line request-line">
|
|
||||||
<div>
|
|
||||||
<ValueEditor
|
|
||||||
content={flow.request.method}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={method => dispatch(updateFlow({ request: { method } }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ValidateEditor
|
|
||||||
content={RequestUtils.pretty_url(flow.request)}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={url => dispatch(updateFlow({ request: {path: '', ...parseUrl(url)}}))}
|
|
||||||
isValid={url => !!parseUrl(url).host}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ValidateEditor
|
|
||||||
content={flow.request.http_version}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={http_version => dispatch(updateFlow({ request: { http_version } }))}
|
|
||||||
isValid={isValidHttpVersion}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseLineProps = {
|
|
||||||
flow: HTTPFlow,
|
|
||||||
readonly: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResponseLine({ flow, readonly }: ResponseLineProps) {
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="first-line response-line">
|
|
||||||
<ValidateEditor
|
|
||||||
content={flow.response?.http_version}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={nextVer => dispatch(updateFlow({ response: { http_version: nextVer } }))}
|
|
||||||
isValid={isValidHttpVersion}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ValidateEditor
|
|
||||||
content={flow.response?.status_code + ''}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={code => dispatch(updateFlow({ response: { code: parseInt(code) } }))}
|
|
||||||
isValid={code => /^\d+$/.test(code)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ValueEditor
|
|
||||||
content={flow.response?.reason}
|
|
||||||
readonly={readonly}
|
|
||||||
onDone={msg => dispatch(updateFlow({ response: { msg } }))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Request() {
|
|
||||||
const dispatch = useAppDispatch(),
|
|
||||||
flow = useAppSelector(state => state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]]),
|
|
||||||
isEdit = useAppSelector(state => !!state.ui.flow.modifiedFlow)
|
|
||||||
|
|
||||||
let noContent = !isEdit && (flow.request.contentLength == 0 || flow.request.contentLength == null)
|
|
||||||
return (
|
|
||||||
<section className="request">
|
|
||||||
<article>
|
|
||||||
<ToggleEdit/>
|
|
||||||
<RequestLine
|
|
||||||
flow={flow}
|
|
||||||
readonly={!isEdit} />
|
|
||||||
<Headers
|
|
||||||
message={flow.request}
|
|
||||||
readonly={!isEdit}
|
|
||||||
onChange={headers => dispatch(updateFlow({ request: { headers } }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
<ContentView
|
|
||||||
readonly={!isEdit}
|
|
||||||
flow={flow}
|
|
||||||
onContentChange={content => dispatch(updateFlow({ request: {content}}))}
|
|
||||||
message={flow.request}/>
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
<Headers
|
|
||||||
message={flow.request}
|
|
||||||
readonly={!isEdit}
|
|
||||||
onChange={trailers => dispatch(updateFlow({ request: { trailers } }))}
|
|
||||||
type='trailers'
|
|
||||||
/>
|
|
||||||
</article>
|
|
||||||
<HideInStatic>
|
|
||||||
{!noContent &&
|
|
||||||
<footer>
|
|
||||||
<ContentViewOptions
|
|
||||||
flow={flow}
|
|
||||||
message={flow.request}
|
|
||||||
uploadContent={content => dispatch(uploadContent(flow, content, "request"))}/>
|
|
||||||
</footer>
|
|
||||||
}
|
|
||||||
</HideInStatic>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Response() {
|
|
||||||
const dispatch = useAppDispatch(),
|
|
||||||
flow = useAppSelector(state => state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]]),
|
|
||||||
isEdit = useAppSelector(state => !!state.ui.flow.modifiedFlow)
|
|
||||||
|
|
||||||
let noContent = !isEdit && (flow.response.contentLength == 0 || flow.response.contentLength == null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="response">
|
|
||||||
<article>
|
|
||||||
<ToggleEdit/>
|
|
||||||
<ResponseLine
|
|
||||||
flow={flow}
|
|
||||||
readonly={!isEdit} />
|
|
||||||
<Headers
|
|
||||||
message={flow.response}
|
|
||||||
readonly={!isEdit}
|
|
||||||
onChange={headers => dispatch(updateFlow({ response: { headers } }))}
|
|
||||||
/>
|
|
||||||
<hr/>
|
|
||||||
<ContentView
|
|
||||||
readonly={!isEdit}
|
|
||||||
flow={flow}
|
|
||||||
onContentChange={content => dispatch(updateFlow({ response: {content}}))}
|
|
||||||
message={flow.response}
|
|
||||||
/>
|
|
||||||
<hr/>
|
|
||||||
<Headers
|
|
||||||
message={flow.response}
|
|
||||||
readonly={!isEdit}
|
|
||||||
onChange={trailers => dispatch(updateFlow({ response: { trailers } }))}
|
|
||||||
type='trailers'
|
|
||||||
/>
|
|
||||||
</article>
|
|
||||||
<HideInStatic>
|
|
||||||
{!noContent &&
|
|
||||||
<footer >
|
|
||||||
<ContentViewOptions
|
|
||||||
flow={flow}
|
|
||||||
message={flow.response}
|
|
||||||
uploadContent={content => dispatch(uploadContent(flow, content, "response"))} />
|
|
||||||
</footer>
|
|
||||||
}
|
|
||||||
</HideInStatic>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorViewProps = {
|
|
||||||
flow: HTTPFlow
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ErrorView({ flow }: ErrorViewProps) {
|
|
||||||
return (
|
|
||||||
<section className="error">
|
|
||||||
<div className="alert alert-warning">
|
|
||||||
{flow.error?.msg}
|
|
||||||
<div>
|
|
||||||
<small>{formatTimeStamp(flow.error?.timestamp)}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
type NavActionProps = {
|
|
||||||
icon: string,
|
|
||||||
title: string,
|
|
||||||
onClick: (e: any) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavAction({ icon, title, onClick }: NavActionProps) {
|
|
||||||
return (
|
|
||||||
<a title={title}
|
|
||||||
href="#"
|
|
||||||
className="nav-action"
|
|
||||||
onClick={event => {
|
|
||||||
event.preventDefault()
|
|
||||||
onClick(event)
|
|
||||||
}}>
|
|
||||||
<i className={`fa fa-fw ${icon}`}></i>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type NavProps = {
|
|
||||||
active: string,
|
|
||||||
tabs: string[],
|
|
||||||
onSelectTab: (e: string) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Nav({ active, tabs, onSelectTab }: NavProps) {
|
|
||||||
return (
|
|
||||||
<nav className="nav-tabs nav-tabs-sm">
|
|
||||||
{tabs.map(tab => (
|
|
||||||
<a key={tab}
|
|
||||||
href="#"
|
|
||||||
className={classnames({ active: active === tab })}
|
|
||||||
onClick={event => {
|
|
||||||
event.preventDefault()
|
|
||||||
onSelectTab(tab)
|
|
||||||
}}>
|
|
||||||
{_.capitalize(tab)}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
94
web/src/js/components/FlowView/Timing.tsx
Normal file
94
web/src/js/components/FlowView/Timing.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import {Flow} from "../../flow";
|
||||||
|
import * as React from "react";
|
||||||
|
import {formatTimeDelta, formatTimeStamp} from "../../utils";
|
||||||
|
|
||||||
|
export type TimeStampProps = {
|
||||||
|
t: number,
|
||||||
|
deltaTo?: number,
|
||||||
|
title: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimeStamp({t, deltaTo, title}: TimeStampProps) {
|
||||||
|
return t ? (
|
||||||
|
<tr>
|
||||||
|
<td>{title}:</td>
|
||||||
|
<td>
|
||||||
|
{formatTimeStamp(t)}
|
||||||
|
{deltaTo && (
|
||||||
|
<span className="text-muted">
|
||||||
|
({formatTimeDelta(1000 * (t - deltaTo))})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<tr/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Timing({flow}: { flow: Flow }) {
|
||||||
|
let ref: number;
|
||||||
|
if (flow.type === "http") {
|
||||||
|
ref = flow.request.timestamp_start
|
||||||
|
} else {
|
||||||
|
ref = flow.client_conn.timestamp_start
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamps: Partial<TimeStampProps>[] = [
|
||||||
|
{
|
||||||
|
title: "Server conn. initiated",
|
||||||
|
t: flow.server_conn?.timestamp_start,
|
||||||
|
deltaTo: ref
|
||||||
|
}, {
|
||||||
|
title: "Server conn. TCP handshake",
|
||||||
|
t: flow.server_conn?.timestamp_tcp_setup,
|
||||||
|
deltaTo: ref
|
||||||
|
}, {
|
||||||
|
title: "Server conn. TLS handshake",
|
||||||
|
t: flow.server_conn?.timestamp_tls_setup,
|
||||||
|
deltaTo: ref
|
||||||
|
}, {
|
||||||
|
title: "Client conn. established",
|
||||||
|
t: flow.client_conn.timestamp_start,
|
||||||
|
deltaTo: flow.type === "http" ? ref : undefined
|
||||||
|
}, {
|
||||||
|
title: "Client conn. TLS handshake",
|
||||||
|
t: flow.client_conn.timestamp_tls_setup,
|
||||||
|
deltaTo: ref
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (flow.type === "http") {
|
||||||
|
timestamps.push(...[
|
||||||
|
{
|
||||||
|
title: "First request byte",
|
||||||
|
t: flow.request.timestamp_start
|
||||||
|
}, {
|
||||||
|
title: "Request complete",
|
||||||
|
t: flow.request.timestamp_end,
|
||||||
|
deltaTo: ref
|
||||||
|
}, {
|
||||||
|
title: "First response byte",
|
||||||
|
t: flow.response?.timestamp_start,
|
||||||
|
deltaTo: ref
|
||||||
|
}, {
|
||||||
|
title: "Response complete",
|
||||||
|
t: flow.response?.timestamp_end,
|
||||||
|
deltaTo: ref
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="timing">
|
||||||
|
<h4>Timing</h4>
|
||||||
|
<table className="timing-table">
|
||||||
|
<tbody>
|
||||||
|
{timestamps
|
||||||
|
.filter((v): v is TimeStampProps => !!v.t)
|
||||||
|
.sort((a, b) => a.t - b.t)
|
||||||
|
.map(props => <TimeStamp key={props.title} {...props}/>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
|
||||||
import { startEdit, stopEdit } from '../../ducks/ui/flow'
|
|
||||||
|
|
||||||
|
|
||||||
export default function ToggleEdit() {
|
|
||||||
const dispatch = useAppDispatch(),
|
|
||||||
isEdit = useAppSelector(state => !!state.ui.flow.modifiedFlow),
|
|
||||||
modifiedFlow = useAppSelector(state => state.ui.flow.modifiedFlow|| state.flows.byId[state.flows.selected[0]]),
|
|
||||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="edit-flow-container">
|
|
||||||
{isEdit ?
|
|
||||||
<a className="edit-flow" title="Finish Edit" onClick={() => dispatch(stopEdit(flow, modifiedFlow))}>
|
|
||||||
<i className="fa fa-check"/>
|
|
||||||
</a>
|
|
||||||
:
|
|
||||||
<a className="edit-flow" title="Edit Flow" onClick={() => dispatch(startEdit(flow))}>
|
|
||||||
<i className="fa fa-pencil"/>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
66
web/src/js/components/FlowView/WebSocket.tsx
Normal file
66
web/src/js/components/FlowView/WebSocket.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {HTTPFlow, WebSocketData} from "../../flow";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useCallback, useMemo, useState} from "react";
|
||||||
|
import {ContentViewData, SHOW_MAX_LINES, useContent} from "../contentviews/useContent";
|
||||||
|
import {MessageUtils} from "../../flow/utils";
|
||||||
|
import ViewSelector from "../contentviews/ViewSelector";
|
||||||
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
|
import {setContentViewFor} from "../../ducks/ui/flow";
|
||||||
|
import LineRenderer from "../contentviews/LineRenderer";
|
||||||
|
import {formatTimeStamp} from "../../utils";
|
||||||
|
|
||||||
|
|
||||||
|
export default function WebSocket({flow}: { flow: HTTPFlow & { websocket: WebSocketData } }) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const contentView = useAppSelector(state => state.ui.flow.contentViewFor[flow.id + "ws"] || "Auto");
|
||||||
|
let [maxLines, setMaxLines] = useState<number>(SHOW_MAX_LINES);
|
||||||
|
const showMore = useCallback(() => setMaxLines(Math.max(1024, maxLines * 2)), [maxLines]);
|
||||||
|
const content = useContent(
|
||||||
|
MessageUtils.getContentURL(flow, "messages", contentView, maxLines + 1),
|
||||||
|
flow.id + flow.websocket.messages_meta.count
|
||||||
|
);
|
||||||
|
const messages = useMemo<ContentViewData[] | undefined>(() => content && JSON.parse(content), [content]) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="websocket">
|
||||||
|
<h4>WebSocket</h4>
|
||||||
|
<div className="contentview">
|
||||||
|
<div className="controls">
|
||||||
|
<h5>{flow.websocket.messages_meta.count} Messages</h5>
|
||||||
|
<ViewSelector value={contentView}
|
||||||
|
onChange={cv => dispatch(setContentViewFor(flow.id + "ws", cv))}/>
|
||||||
|
</div>
|
||||||
|
{messages.map((d: ContentViewData, i) => {
|
||||||
|
const className = `fa fa-fw fa-arrow-${d.from_client ? "right text-primary" : "left text-danger"}`;
|
||||||
|
const renderer = <div key={i}>
|
||||||
|
<small>
|
||||||
|
<i className={className}/>
|
||||||
|
<span className="pull-right">{d.timestamp && formatTimeStamp(d.timestamp)}</span>
|
||||||
|
</small>
|
||||||
|
<LineRenderer lines={d.lines} maxLines={maxLines} showMore={showMore}/>
|
||||||
|
</div>;
|
||||||
|
maxLines -= d.lines.length;
|
||||||
|
return renderer;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<CloseSummary websocket={flow.websocket}/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function CloseSummary({websocket}: {websocket: WebSocketData}){
|
||||||
|
if(!websocket.timestamp_end)
|
||||||
|
return null;
|
||||||
|
const reason = websocket.close_reason ? `(${websocket.close_reason})` : ""
|
||||||
|
return <div>
|
||||||
|
<i className="fa fa-fw fa-window-close text-muted"/>
|
||||||
|
|
||||||
|
Closed by {websocket.closed_by_client ? "client": "server"} with code {websocket.close_code} {reason}.
|
||||||
|
|
||||||
|
<small className="pull-right">
|
||||||
|
{formatTimeStamp(websocket.timestamp_end)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from 'react'
|
||||||
import {formatSize} from '../utils'
|
import {formatSize} from '../utils'
|
||||||
import HideInStatic from '../components/common/HideInStatic'
|
import HideInStatic from '../components/common/HideInStatic'
|
||||||
import {useAppSelector} from "../ducks";
|
import {useAppSelector} from "../ducks";
|
||||||
@ -7,7 +7,7 @@ export default function Footer() {
|
|||||||
const version = useAppSelector(state => state.conf.version);
|
const version = useAppSelector(state => state.conf.version);
|
||||||
let {
|
let {
|
||||||
mode, intercept, showhost, upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
|
mode, intercept, showhost, upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
|
||||||
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server
|
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server, ssl_insecure
|
||||||
} = useAppSelector(state => state.options);
|
} = useAppSelector(state => state.options);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -18,6 +18,9 @@ export default function Footer() {
|
|||||||
{intercept && (
|
{intercept && (
|
||||||
<span className="label label-success">Intercept: {intercept}</span>
|
<span className="label label-success">Intercept: {intercept}</span>
|
||||||
)}
|
)}
|
||||||
|
{ssl_insecure && (
|
||||||
|
<span className="label label-danger">ssl_insecure</span>
|
||||||
|
)}
|
||||||
{showhost && (
|
{showhost && (
|
||||||
<span className="label label-success">showhost</span>
|
<span className="label label-success">showhost</span>
|
||||||
)}
|
)}
|
||||||
@ -57,8 +60,8 @@ export default function Footer() {
|
|||||||
</span>)
|
</span>)
|
||||||
}
|
}
|
||||||
</HideInStatic>
|
</HideInStatic>
|
||||||
<span className="label label-info" title="Mitmproxy Version">
|
<span className="label label-default" title="Mitmproxy Version">
|
||||||
{version}
|
mitmproxy {version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import React, { Component } from 'react'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
import MainMenu from './Header/MainMenu'
|
|
||||||
import OptionMenu from './Header/OptionMenu'
|
|
||||||
import FileMenu from './Header/FileMenu'
|
|
||||||
import FlowMenu from './Header/FlowMenu'
|
|
||||||
import {setActiveMenu} from '../ducks/ui/header'
|
|
||||||
import ConnectionIndicator from "./Header/ConnectionIndicator"
|
|
||||||
import HideInStatic from './common/HideInStatic'
|
|
||||||
|
|
||||||
class Header extends Component {
|
|
||||||
static entries = [MainMenu, OptionMenu]
|
|
||||||
|
|
||||||
handleClick(active, e) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.props.setActiveMenu(active.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { selectedFlowId, activeMenu} = this.props
|
|
||||||
|
|
||||||
let entries = [...Header.entries]
|
|
||||||
if(selectedFlowId)
|
|
||||||
entries.push(FlowMenu)
|
|
||||||
|
|
||||||
// Make sure to have a fallback in case FlowMenu is selected but we don't have any flows
|
|
||||||
// (e.g. because they are all deleted or not yet received)
|
|
||||||
const Active = _.find(entries, (e) => e.title == activeMenu) || MainMenu
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<nav className="nav-tabs nav-tabs-lg">
|
|
||||||
<FileMenu/>
|
|
||||||
{entries.map(Entry => (
|
|
||||||
<a key={Entry.title}
|
|
||||||
href="#"
|
|
||||||
className={classnames({ active: Entry === Active})}
|
|
||||||
onClick={e => this.handleClick(Entry, e)}>
|
|
||||||
{Entry.title}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
<HideInStatic>
|
|
||||||
<ConnectionIndicator/>
|
|
||||||
</HideInStatic>
|
|
||||||
</nav>
|
|
||||||
<div>
|
|
||||||
<Active/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
state => ({
|
|
||||||
selectedFlowId: state.flows.selected[0],
|
|
||||||
activeMenu: state.ui.header.activeMenu,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
setActiveMenu,
|
|
||||||
}
|
|
||||||
)(Header)
|
|
64
web/src/js/components/Header.tsx
Normal file
64
web/src/js/components/Header.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, {useState} from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import StartMenu from './Header/StartMenu'
|
||||||
|
import OptionMenu from './Header/OptionMenu'
|
||||||
|
import FileMenu from './Header/FileMenu'
|
||||||
|
import FlowMenu from './Header/FlowMenu'
|
||||||
|
import ConnectionIndicator from "./Header/ConnectionIndicator"
|
||||||
|
import HideInStatic from './common/HideInStatic'
|
||||||
|
import {useAppSelector} from "../ducks";
|
||||||
|
|
||||||
|
interface Menu {
|
||||||
|
(): JSX.Element;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const selectedFlows = useAppSelector(state => state.flows.selected.filter(id => id in state.flows.byId)),
|
||||||
|
[ActiveMenu, setActiveMenu] = useState<Menu>(() => StartMenu),
|
||||||
|
[wasFlowSelected, setWasFlowSelected] = useState(false);
|
||||||
|
|
||||||
|
let entries: Menu[] = [StartMenu, OptionMenu];
|
||||||
|
if (selectedFlows.length > 0) {
|
||||||
|
if (!wasFlowSelected) {
|
||||||
|
setActiveMenu(() => FlowMenu);
|
||||||
|
setWasFlowSelected(true);
|
||||||
|
}
|
||||||
|
entries.push(FlowMenu)
|
||||||
|
} else {
|
||||||
|
if (wasFlowSelected) {
|
||||||
|
setWasFlowSelected(false);
|
||||||
|
}
|
||||||
|
if (ActiveMenu === FlowMenu) {
|
||||||
|
setActiveMenu(() => StartMenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(active: Menu, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
setActiveMenu(() => active)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<nav className="nav-tabs nav-tabs-lg">
|
||||||
|
<FileMenu/>
|
||||||
|
{entries.map(Entry => (
|
||||||
|
<a key={Entry.title}
|
||||||
|
href="#"
|
||||||
|
className={classnames({active: Entry === ActiveMenu})}
|
||||||
|
onClick={e => handleClick(Entry, e)}>
|
||||||
|
{Entry.title}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<HideInStatic>
|
||||||
|
<ConnectionIndicator/>
|
||||||
|
</HideInStatic>
|
||||||
|
</nav>
|
||||||
|
<div>
|
||||||
|
<ActiveMenu/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react"
|
import * as React from "react";
|
||||||
import {ConnectionState} from "../../ducks/connection"
|
import {ConnectionState} from "../../ducks/connection"
|
||||||
import {useAppSelector} from "../../ducks";
|
import {useAppSelector} from "../../ducks";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import {useDispatch} from 'react-redux'
|
import {useDispatch} from 'react-redux'
|
||||||
import FileChooser from '../common/FileChooser'
|
import FileChooser from '../common/FileChooser'
|
||||||
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
|
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
|
||||||
@ -9,10 +9,7 @@ import HideInStatic from "../common/HideInStatic";
|
|||||||
export default React.memo(function FileMenu() {
|
export default React.memo(function FileMenu() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return (
|
return (
|
||||||
<Dropdown className="pull-left special" text="mitmproxy" options={{"placement": "bottom-start"}}>
|
<Dropdown className="pull-left special" text="File" options={{"placement": "bottom-start"}}>
|
||||||
<MenuItem onClick={() => confirm('Delete all flows?') && dispatch(flowsActions.clear())}>
|
|
||||||
<i className="fa fa-fw fa-trash"/> Clear All
|
|
||||||
</MenuItem>
|
|
||||||
<li>
|
<li>
|
||||||
<FileChooser
|
<FileChooser
|
||||||
icon="fa-folder-open"
|
icon="fa-folder-open"
|
||||||
@ -30,6 +27,9 @@ export default React.memo(function FileMenu() {
|
|||||||
<MenuItem onClick={() => dispatch(flowsActions.download())}>
|
<MenuItem onClick={() => dispatch(flowsActions.download())}>
|
||||||
<i className="fa fa-fw fa-floppy-o"/> Save...
|
<i className="fa fa-fw fa-floppy-o"/> Save...
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => confirm('Delete all flows?') && dispatch(flowsActions.clear())}>
|
||||||
|
<i className="fa fa-fw fa-trash"/> Clear All
|
||||||
|
</MenuItem>
|
||||||
<HideInStatic>
|
<HideInStatic>
|
||||||
<Divider/>
|
<Divider/>
|
||||||
<li>
|
<li>
|
||||||
|
@ -1,76 +1,121 @@
|
|||||||
import React from "react"
|
import * as React from "react";
|
||||||
import Button from "../common/Button"
|
import Button from "../common/Button"
|
||||||
import { MessageUtils } from "../../flow/utils.js"
|
import {MessageUtils} from "../../flow/utils.js"
|
||||||
import HideInStatic from "../common/HideInStatic";
|
import HideInStatic from "../common/HideInStatic";
|
||||||
import { useAppDispatch, useAppSelector } from "../../ducks";
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
import {
|
import {
|
||||||
resume as resumeFlow,
|
|
||||||
replay as replayFlow,
|
|
||||||
duplicate as duplicateFlow,
|
duplicate as duplicateFlow,
|
||||||
revert as revertFlow,
|
kill as killFlow,
|
||||||
remove as removeFlow,
|
remove as removeFlow,
|
||||||
kill as killFlow
|
replay as replayFlow,
|
||||||
|
resume as resumeFlow,
|
||||||
|
revert as revertFlow
|
||||||
} from "../../ducks/flows"
|
} from "../../ducks/flows"
|
||||||
|
import Dropdown, {MenuItem} from "../common/Dropdown";
|
||||||
|
import {copy} from "../../flow/export";
|
||||||
|
import {Flow} from "../../flow";
|
||||||
|
|
||||||
FlowMenu.title = 'Flow'
|
FlowMenu.title = 'Flow'
|
||||||
|
|
||||||
export default function FlowMenu() {
|
export default function FlowMenu(): JSX.Element {
|
||||||
const dispatch = useAppDispatch(),
|
const dispatch = useAppDispatch(),
|
||||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
||||||
|
|
||||||
if (!flow)
|
if (!flow)
|
||||||
return <div/>
|
return <div/>
|
||||||
return (
|
return (
|
||||||
<div className="flow-menu">
|
<div className="flow-menu">
|
||||||
<HideInStatic>
|
<HideInStatic>
|
||||||
<div className="menu-group">
|
<div className="menu-group">
|
||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
|
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
|
||||||
onClick={() => dispatch(replayFlow(flow))}>
|
onClick={() => dispatch(replayFlow(flow))}
|
||||||
Replay
|
disabled={!(flow.type === "http" && !flow.websocket)}
|
||||||
</Button>
|
>
|
||||||
<Button title="[D]uplicate flow" icon="fa-copy text-info"
|
Replay
|
||||||
onClick={() => dispatch(duplicateFlow(flow))}>
|
</Button>
|
||||||
Duplicate
|
<Button title="[D]uplicate flow" icon="fa-copy text-info"
|
||||||
</Button>
|
onClick={() => dispatch(duplicateFlow(flow))}>
|
||||||
<Button disabled={!flow || !flow.modified} title="revert changes to flow [V]"
|
Duplicate
|
||||||
icon="fa-history text-warning" onClick={() => dispatch(revertFlow(flow))}>
|
</Button>
|
||||||
Revert
|
<Button disabled={!flow || !flow.modified} title="revert changes to flow [V]"
|
||||||
</Button>
|
icon="fa-history text-warning" onClick={() => dispatch(revertFlow(flow))}>
|
||||||
<Button title="[d]elete flow" icon="fa-trash text-danger"
|
Revert
|
||||||
onClick={() => dispatch(removeFlow(flow))}>
|
</Button>
|
||||||
Delete
|
<Button title="[d]elete flow" icon="fa-trash text-danger"
|
||||||
</Button>
|
onClick={() => dispatch(removeFlow(flow))}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="menu-legend">Flow Modification</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-legend">Flow Modification</div>
|
|
||||||
</div>
|
|
||||||
</HideInStatic>
|
</HideInStatic>
|
||||||
|
|
||||||
<div className="menu-group">
|
<div className="menu-group">
|
||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<Button title="download" icon="fa-download"
|
<DownloadButton flow={flow}/>
|
||||||
onClick={() => window.location = MessageUtils.getContentURL(flow, flow.response)}>
|
<Dropdown className="" text={
|
||||||
Download
|
<Button title="Export flow." icon="fa-clone" onClick={() => 1}>Export▾</Button>
|
||||||
</Button>
|
} options={{"placement": "bottom-start"}}>
|
||||||
|
<MenuItem onClick={() => copy(flow, "raw_request")}>Copy raw request</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "raw_response")}>Copy raw response</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "raw")}>Copy raw request and response</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "curl")}>Copy as cURL</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "httpie")}>Copy as HTTPie</MenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-legend">Export</div>
|
<div className="menu-legend">Export</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HideInStatic>
|
<HideInStatic>
|
||||||
<div className="menu-group">
|
<div className="menu-group">
|
||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
|
<Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
|
||||||
icon="fa-play text-success" onClick={() => dispatch(resumeFlow(flow))}>
|
icon="fa-play text-success" onClick={() => dispatch(resumeFlow(flow))}>
|
||||||
Resume
|
Resume
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]"
|
<Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]"
|
||||||
icon="fa-times text-danger" onClick={() => dispatch(killFlow(flow))}>
|
icon="fa-times text-danger" onClick={() => dispatch(killFlow(flow))}>
|
||||||
Abort
|
Abort
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="menu-legend">Interception</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-legend">Interception</div>
|
|
||||||
</div>
|
|
||||||
</HideInStatic>
|
</HideInStatic>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function DownloadButton({flow}: { flow: Flow }) {
|
||||||
|
if (flow.type !== "http")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (flow.request.contentLength && !flow.response?.contentLength) {
|
||||||
|
return <Button icon="fa-download"
|
||||||
|
onClick={() => window.location.href = MessageUtils.getContentURL(flow, flow.request)}
|
||||||
|
>Download</Button>
|
||||||
|
}
|
||||||
|
if (flow.response) {
|
||||||
|
const response = flow.response;
|
||||||
|
if (!flow.request.contentLength && flow.response.contentLength) {
|
||||||
|
return <Button icon="fa-download"
|
||||||
|
onClick={() => window.location.href = MessageUtils.getContentURL(flow, response)}
|
||||||
|
>Download</Button>
|
||||||
|
}
|
||||||
|
if (flow.request.contentLength && flow.response.contentLength) {
|
||||||
|
return <Dropdown text={
|
||||||
|
<Button icon="fa-download" onClick={() => 1}>Download▾</Button>
|
||||||
|
} options={{"placement": "bottom-start"}}>
|
||||||
|
<MenuItem onClick={() => window.location.href = MessageUtils.getContentURL(flow, flow.request)}>Download
|
||||||
|
request</MenuItem>
|
||||||
|
<MenuItem onClick={() => window.location.href = MessageUtils.getContentURL(flow, response)}>Download
|
||||||
|
response</MenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import React, {ChangeEvent} from "react"
|
import * as React from "react";
|
||||||
import {useDispatch} from "react-redux"
|
import {useDispatch} from "react-redux"
|
||||||
import {toggleVisibility} from "../../ducks/eventLog"
|
import * as eventLogActions from "../../ducks/eventLog"
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
import * as commandBarActions from "../../ducks/commandBar"
|
||||||
import * as optionsActions from "../../ducks/options";
|
import {useAppDispatch, useAppSelector} from "../../ducks"
|
||||||
|
import * as optionsActions from "../../ducks/options"
|
||||||
|
|
||||||
|
|
||||||
type MenuToggleProps = {
|
type MenuToggleProps = {
|
||||||
value: boolean
|
value: boolean
|
||||||
onChange: (e: ChangeEvent) => void
|
onChange: (e: React.ChangeEvent) => void
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +52,23 @@ export function EventlogToggle() {
|
|||||||
return (
|
return (
|
||||||
<MenuToggle
|
<MenuToggle
|
||||||
value={visible}
|
value={visible}
|
||||||
onChange={() => dispatch(toggleVisibility())}
|
onChange={() => dispatch(eventLogActions.toggleVisibility())}
|
||||||
>
|
>
|
||||||
Display Event Log
|
Display Event Log
|
||||||
</MenuToggle>
|
</MenuToggle>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CommandBarToggle() {
|
||||||
|
const dispatch = useDispatch(),
|
||||||
|
visible = useAppSelector(state => state.commandBar.visible);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuToggle
|
||||||
|
value={visible}
|
||||||
|
onChange={() => dispatch(commandBarActions.toggleVisibility())}
|
||||||
|
>
|
||||||
|
Display Command Bar
|
||||||
|
</MenuToggle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from "react"
|
import * as React from "react";
|
||||||
import { EventlogToggle, OptionsToggle } from "./MenuToggle"
|
import {CommandBarToggle, EventlogToggle, OptionsToggle} from "./MenuToggle"
|
||||||
import Button from "../common/Button"
|
import Button from "../common/Button"
|
||||||
import DocsLink from "../common/DocsLink"
|
import DocsLink from "../common/DocsLink"
|
||||||
import HideInStatic from "../common/HideInStatic";
|
import HideInStatic from "../common/HideInStatic";
|
||||||
@ -44,6 +44,7 @@ export default function OptionMenu() {
|
|||||||
<div className="menu-group">
|
<div className="menu-group">
|
||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<EventlogToggle/>
|
<EventlogToggle/>
|
||||||
|
<CommandBarToggle/>
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-legend">View Options</div>
|
<div className="menu-legend">View Options</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react"
|
import * as React from "react";
|
||||||
import FilterInput from "./FilterInput"
|
import FilterInput from "./FilterInput"
|
||||||
import * as flowsActions from "../../ducks/flows"
|
import * as flowsActions from "../../ducks/flows"
|
||||||
import {setFilter, setHighlight} from "../../ducks/flows"
|
import {setFilter, setHighlight} from "../../ducks/flows"
|
||||||
@ -6,9 +6,9 @@ import Button from "../common/Button"
|
|||||||
import {update as updateOptions} from "../../ducks/options";
|
import {update as updateOptions} from "../../ducks/options";
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
|
|
||||||
MainMenu.title = "Start"
|
StartMenu.title = "Start"
|
||||||
|
|
||||||
export default function MainMenu() {
|
export default function StartMenu() {
|
||||||
return (
|
return (
|
||||||
<div className="main-menu">
|
<div className="main-menu">
|
||||||
<div className="menu-group">
|
<div className="menu-group">
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import Splitter from './common/Splitter'
|
import Splitter from './common/Splitter'
|
||||||
import FlowTable from './FlowTable'
|
import FlowTable from './FlowTable'
|
||||||
import FlowView from './FlowView'
|
import FlowView from './FlowView'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import * as React from "react"
|
||||||
import ModalList from './ModalList'
|
import ModalList from './ModalList'
|
||||||
import { useAppSelector } from "../../ducks";
|
import { useAppSelector } from "../../ducks";
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user