transfer the argument suggestion into popup, show available commands, highlight currently typing argument

This commit is contained in:
zokutyou2@gmail.com 2021-07-13 20:02:31 +09:00
parent 5b229c2dcd
commit fc7455b914
4 changed files with 87 additions and 43 deletions

View File

@ -27,10 +27,8 @@ from mitmproxy.utils.strutils import always_str
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
""" """
Remove flow message content and cert to save transmission space. Remove flow message content and cert to save transmission space.
Args: Args:
flow: The original flow. flow: The original flow.
Sync with web/src/flow.ts. Sync with web/src/flow.ts.
""" """
f = { f = {
@ -454,12 +452,20 @@ class FlowContentView(RequestHandler):
class Commands(RequestHandler): class Commands(RequestHandler):
def get(self):
commands = {}
for (name, command) in self.master.commands.commands.items():
commands[name] = []
for parameter in command.parameters:
commands[name].append({"name": parameter.name})
self.write({"commands": commands})
def post(self): def post(self):
result = self.master.commands.execute(self.json["command"]) result = self.master.commands.execute(self.json["command"])
if result is None: if result is None:
self.write({"result": ""}) self.write({"result": ""})
return return
self.write({"result": str(result)}) self.write({ "result": result, "type": type(result).__name__ })
class Events(RequestHandler): class Events(RequestHandler):
@ -479,16 +485,6 @@ class Options(RequestHandler):
raise APIError(400, f"{err}") raise APIError(400, f"{err}")
class CommandArguments(RequestHandler):
def get(self):
arguments = {}
for (name, command) in self.master.commands.commands.items():
arguments[name] = []
for parameter in command.parameters:
arguments[name].append(parameter.name)
self.write(arguments)
class SaveOptions(RequestHandler): class SaveOptions(RequestHandler):
def post(self): def post(self):
# try: # try:
@ -541,7 +537,7 @@ class Application(tornado.web.Application):
(r"/", IndexHandler), (r"/", IndexHandler),
(r"/filter-help(?:\.json)?", FilterHelp), (r"/filter-help(?:\.json)?", FilterHelp),
(r"/updates", ClientConnection), (r"/updates", ClientConnection),
(r"/commands", Commands), (r"/commands(?:\.json)?", Commands),
(r"/events(?:\.json)?", Events), (r"/events(?:\.json)?", Events),
(r"/flows(?:\.json)?", Flows), (r"/flows(?:\.json)?", Flows),
(r"/flows/dump", DumpFlows), (r"/flows/dump", DumpFlows),
@ -559,7 +555,6 @@ class Application(tornado.web.Application):
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)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
FlowContentView), FlowContentView),
(r"/clear", ClearAll), (r"/clear", ClearAll),
(r"/arguments(?:\.json)?", CommandArguments),
(r"/options(?:\.json)?", Options), (r"/options(?:\.json)?", Options),
(r"/options/save", SaveOptions), (r"/options/save", SaveOptions),
(r"/conf\.js", Conf), (r"/conf\.js", Conf),

View File

@ -1,4 +1,4 @@
.command { .command-title {
background-color: #F2F2F2; background-color: #F2F2F2;
border: 1px solid #aaa; border: 1px solid #aaa;
} }
@ -14,4 +14,18 @@
.command-suggestion { .command-suggestion {
background-color: #9c9c9c; background-color: #9c9c9c;
}
.argument-suggestion {
background-color: hsla(209, 52%, 84%, 0.5) !important;
}
.command > .popover {
display: block;
position: relative;
max-width: none;
}
.available-commands {
overflow: auto;
} }

View File

@ -1,13 +1,10 @@
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer'
import CommandBar from '../../../components/CommandBar' import CommandBar from '../../../components/CommandBar'
import { render } from "../../test-utils"
describe('CommandBar Component', () => { test('CommandBar Component', async () => {
let commandBar = renderer.create( const {asFragment, store} = render(
<CommandBar />), <CommandBar/>
tree = commandBar.toJSON() );
expect(asFragment()).toMatchSnapshot();
it('should render correctly', () => {
expect(tree).toMatchSnapshot()
})
}) })

View File

@ -3,39 +3,76 @@ import classnames from 'classnames'
import { Key, fetchApi } from '../utils' import { Key, fetchApi } from '../utils'
import Filt from '../filt/command' import Filt from '../filt/command'
export function AvailableCommands({input, commands}) {
if (!commands) return null
let availableCommands = []
for (const [command, args] of Object.entries(commands)) {
if (command.startsWith(input)) {
availableCommands.push(command)
}
}
return <div className="available-commands popover bottom">Available Commands: {JSON.stringify(availableCommands)}</div>
}
export function ArgumentSuggestion({nextArgs, currentArg}){
let results = []
for (let i = 0; i < nextArgs.length; i++) {
if (i==currentArg) {
results.push(<mark>{nextArgs[i]}</mark>)
continue
}
results.push(<span>{nextArgs[i]} </span>)
}
return (<div className="argument-suggestion popover top">
<div className="arrow"/>
<div className="popover-content">
Argument suggestion: {results}
</div>
</div>)
}
export default function CommandBar() { export default function CommandBar() {
const [input, setInput] = useState("")
const [command, setCommand] = useState("") const [command, setCommand] = useState("")
const [results, setResults] = useState([]) const [results, setResults] = useState([])
const [history, setHistory] = useState([]) const [history, setHistory] = useState([])
const [currentPos, setCurrentPos] = useState(0) const [currentPos, setCurrentPos] = useState(0)
const [args, setArgs] = useState({}) const [allCommands, setAllCommands] = useState({})
const [nextArgs, setNextArgs] = useState([]) const [nextArgs, setNextArgs] = useState([])
const [currentArg, setCurrentArg] = useState(0)
const [commandHelp, setCommandHelp] = useState("")
useEffect(() => { useEffect(() => {
fetchApi('/arguments') fetchApi('/commands', { method: 'GET' })
.then(response => response.json()) .then(response => response.json())
.then(data => setArgs(data)) .then(data => setAllCommands(data))
}, []) }, [])
const parseCommand = (input) => { const parseCommand = (input) => {
const parts = Filt.parse(input) const parts = Filt.parse(input)
if (allCommands["commands"].hasOwnProperty(parts[0])){
setCommand(parts[0])
} else {
setCommand("")
}
const nextArgs = args[parts[0]] const nextArgs = allCommands["commands"][parts[0]]?.map(arg => arg.name)
if (nextArgs) { if (nextArgs) {
setNextArgs([parts[0], ...nextArgs]) setNextArgs([parts[0], ...nextArgs])
setCurrentArg(parts.length-1)
} }
} }
const onChange = (e) => { const onChange = (e) => {
setCommand(e.target.value) setInput(e.target.value)
} }
const onKeyDown = (e) => { const onKeyDown = (e) => {
if (e.keyCode === Key.ENTER) { if (e.keyCode === Key.ENTER) {
const body = {"command": command} const body = {"command": input}
const newHistory = Object.assign([], history) const newHistory = Object.assign([], history)
newHistory.splice(currentPos, 0, command) newHistory.splice(currentPos, 0, input)
fetchApi(`/commands`, { fetchApi(`/commands`, {
method: 'POST', method: 'POST',
@ -48,21 +85,21 @@ export default function CommandBar() {
.then(data => { .then(data => {
setHistory(newHistory) setHistory(newHistory)
setCurrentPos(currentPos + 1) setCurrentPos(currentPos + 1)
setNextArgs([])
if (data.result == "") return
setResults([...results, {"id": results.length, "result": data.result}]) setResults([...results, {"id": results.length, "result": data.result}])
}) })
setCommand("") setInput("")
} }
if (e.keyCode === Key.UP) { if (e.keyCode === Key.UP) {
if (currentPos > 0) { if (currentPos > 0) {
setCommand(history[currentPos - 1]) setInput(history[currentPos - 1])
setCurrentPos(currentPos - 1) setCurrentPos(currentPos - 1)
} }
} }
if (e.keyCode === Key.DOWN) { if (e.keyCode === Key.DOWN) {
setCommand(history[currentPos]) setInput(history[currentPos])
if (currentPos < history.length -1) { if (currentPos < history.length -1) {
setCurrentPos(currentPos + 1) setCurrentPos(currentPos + 1)
} }
@ -71,14 +108,14 @@ export default function CommandBar() {
} }
const onKeyUp = (e) => { const onKeyUp = (e) => {
if (command == "") return if (input == "") return
parseCommand(command) parseCommand(input)
e.stopPropagation() e.stopPropagation()
} }
return ( return (
<> <div className="command">
<div className="command"> <div className="command-title">
Command Result Command Result
</div> </div>
<div className="command-result"> <div className="command-result">
@ -88,7 +125,7 @@ export default function CommandBar() {
</div> </div>
))} ))}
</div> </div>
{ nextArgs ? <div className="command-suggestion">Argument suggestion: {nextArgs.join(" ")}</div> : null } { nextArgs.length > 0 && <ArgumentSuggestion nextArgs={nextArgs} currentArg={currentArg} /> }
<div className={classnames('command-input input-group')}> <div className={classnames('command-input input-group')}>
<span className="input-group-addon"> <span className="input-group-addon">
<i className={'fa fa-fw fa-terminal'}/> <i className={'fa fa-fw fa-terminal'}/>
@ -97,12 +134,13 @@ export default function CommandBar() {
type="text" type="text"
placeholder="Enter command" placeholder="Enter command"
className="form-control" className="form-control"
value={command} value={input}
onChange={onChange} onChange={onChange}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
/> />
</div> </div>
</> { !command && <AvailableCommands input={input} commands={allCommands["commands"]} /> }
</div>
) )
} }