convert components in FlowView, Header, Modal, ValueEditor into typescript

This commit is contained in:
zokutyou2@gmail.com 2021-07-19 15:47:30 +09:00
parent e0b8a48392
commit 29997bca4b
36 changed files with 380 additions and 615 deletions

View File

@ -2,75 +2,29 @@ jest.mock('../../../flow/utils')
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer' import renderer from 'react-test-renderer'
import ConnectedFlowMenu, { FlowMenu } from '../../../components/Header/FlowMenu' import FlowMenu from '../../../components/Header/FlowMenu'
import { TFlow, TStore }from '../../ducks/tutils' import { TFlow, TStore }from '../../ducks/tutils'
import { MessageUtils } from "../../../flow/utils" import { MessageUtils } from "../../../flow/utils"
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
describe('FlowMenu Component', () => { describe('FlowMenu Component', () => {
let actions = { let tflow = new TFlow(),
resumeFlow: jest.fn(), store = new TStore()
killFlow: jest.fn(),
replayFlow: jest.fn(),
duplicateFlow: jest.fn(),
removeFlow: jest.fn(),
revertFlow: jest.fn()
},
tflow = new TFlow()
tflow.modified = true tflow.modified = true
tflow.intercepted = true tflow.intercepted = true
global.fetch = jest.fn()
it('should render correctly without flow', () => { let flowMenu = renderer.create(
let flowMenu = renderer.create( <Provider store={store}>
<FlowMenu removeFlow={actions.removeFlow} <FlowMenu />
killFlow={actions.killFlow} </Provider>
replayFlow={actions.replayFlow} ),
duplicateFlow={actions.duplicateFlow}
resumeFlow={actions.resumeFlow}
revertFlow={actions.revertFlow}/>),
tree = flowMenu.toJSON()
expect(tree).toMatchSnapshot()
})
let flowMenu = renderer.create(<FlowMenu
flow={tflow}
removeFlow={actions.removeFlow}
killFlow={actions.killFlow}
replayFlow={actions.replayFlow}
duplicateFlow={actions.duplicateFlow}
resumeFlow={actions.resumeFlow}
revertFlow={actions.revertFlow}/>),
tree = flowMenu.toJSON() tree = flowMenu.toJSON()
it('should render correctly with flow', () => { it('should render correctly with flow', () => {
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
let menu_content_1 = tree.children[0].children[0]
it('should handle replayFlow', () => {
let button = menu_content_1.children[0]
button.props.onClick()
expect(actions.replayFlow).toBeCalledWith(tflow)
})
it('should handle duplicateFlow', () => {
let button = menu_content_1.children[1]
button.props.onClick()
expect(actions.duplicateFlow).toBeCalledWith(tflow)
})
it('should handle revertFlow', () => {
let button = menu_content_1.children[2]
button.props.onClick()
expect(actions.revertFlow).toBeCalledWith(tflow)
})
it('should handle removeFlow', () => {
let button = menu_content_1.children[3]
button.props.onClick()
expect(actions.removeFlow).toBeCalledWith(tflow)
})
let menu_content_2 = tree.children[1].children[0] let menu_content_2 = tree.children[1].children[0]
it('should handle download', () => { it('should handle download', () => {
let button = menu_content_2.children[0] let button = menu_content_2.children[0]
@ -78,24 +32,4 @@ describe('FlowMenu Component', () => {
expect(MessageUtils.getContentURL).toBeCalledWith(tflow, tflow.response) expect(MessageUtils.getContentURL).toBeCalledWith(tflow, tflow.response)
}) })
let menu_content_3 = tree.children[2].children[0]
it('should handle resumeFlow', () => {
let button = menu_content_3.children[0]
button.props.onClick()
expect(actions.resumeFlow).toBeCalledWith(tflow)
})
it('should handle killFlow', () => {
let button = menu_content_3.children[1]
button.props.onClick()
expect(actions.killFlow).toBeCalledWith(tflow)
})
it('should connect to state', () => {
let store = TStore(),
provider = renderer.create(<Provider store={store}><ConnectedFlowMenu/></Provider>),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
}) })

View File

@ -1,121 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlowMenu Component should connect to state 1`] = `
<div
className="flow-menu"
>
<div
className="menu-group"
>
<div
className="menu-content"
>
<button
className="btn btn-default"
onClick={[Function]}
title="[r]eplay flow"
>
<i
className="fa fa-fw fa-repeat text-primary"
/>
Replay
</button>
<button
className="btn btn-default"
onClick={[Function]}
title="[D]uplicate flow"
>
<i
className="fa fa-fw fa-copy text-info"
/>
Duplicate
</button>
<button
className="btn btn-default"
disabled={true}
title="revert changes to flow [V]"
>
<i
className="fa fa-fw fa-history text-warning"
/>
Revert
</button>
<button
className="btn btn-default"
onClick={[Function]}
title="[d]elete flow"
>
<i
className="fa fa-fw fa-trash text-danger"
/>
Delete
</button>
</div>
<div
className="menu-legend"
>
Flow Modification
</div>
</div>
<div
className="menu-group"
>
<div
className="menu-content"
>
<button
className="btn btn-default"
onClick={[Function]}
title="download"
>
<i
className="fa fa-fw fa-download"
/>
Download
</button>
</div>
<div
className="menu-legend"
>
Export
</div>
</div>
<div
className="menu-group"
>
<div
className="menu-content"
>
<button
className="btn btn-default"
disabled={true}
title="[a]ccept intercepted flow"
>
<i
className="fa fa-fw fa-play text-success"
/>
Resume
</button>
<button
className="btn btn-default"
disabled={true}
title="kill intercepted flow [x]"
>
<i
className="fa fa-fw fa-times text-danger"
/>
Abort
</button>
</div>
<div
className="menu-legend"
>
Interception
</div>
</div>
</div>
`;
exports[`FlowMenu Component should render correctly with flow 1`] = ` exports[`FlowMenu Component should render correctly with flow 1`] = `
<div <div
className="flow-menu" className="flow-menu"
@ -234,5 +118,3 @@ exports[`FlowMenu Component should render correctly with flow 1`] = `
</div> </div>
</div> </div>
`; `;
exports[`FlowMenu Component should render correctly without flow 1`] = `<div />`;

View File

@ -21,6 +21,8 @@ export {TFlow}
const tflow1: HTTPFlow = TFlow(); const tflow1: HTTPFlow = TFlow();
const tflow2: HTTPFlow = TFlow(); const tflow2: HTTPFlow = TFlow();
tflow1.modified = true
tflow1.intercepted = true
tflow2.id = "flow2"; tflow2.id = "flow2";
tflow2.request.path = "/second"; tflow2.request.path = "/second";

View File

@ -1,14 +1,13 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import CodeMirror from "../../contrib/CodeMirror" import CodeMirror from "../../contrib/CodeMirror"
CodeEditor.propTypes = { type CodeEditorProps = {
content: PropTypes.string.isRequired, content: string,
onChange: PropTypes.func.isRequired, onChange: Function,
} }
export default function CodeEditor ( { content, onChange} ){ export default function CodeEditor ( { content, onChange}: CodeEditorProps ){
let options = { let options = {
lineNumbers: true lineNumbers: true

View File

@ -1,14 +1,12 @@
import React, { Component } from 'react' import React from 'react'
import { MessageUtils } from '../../flow/utils' import { MessageUtils } from '../../flow/utils'
import { Flow, HTTPMessage } from '../../flow'
type ContentLoaderProps = { type ContentLoaderProps = {
content: string, content: string,
contentView: object, contentView: object,
flow: object, flow: Flow,
message: { message: HTTPMessage,
content: string,
contentHash: string,
},
} }
type ContentLoaderStates = { type ContentLoaderStates = {

View File

@ -3,11 +3,12 @@ import ViewSelector from './ViewSelector'
import UploadContentButton from './UploadContentButton' import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton' import DownloadContentButton from './DownloadContentButton'
import { useAppSelector } from "../../ducks"; import { useAppSelector } from "../../ducks";
import { Flow, HTTPMessage } from '../../flow'
type ContentViewOptionsProps = { type ContentViewOptionsProps = {
flow: object, flow: Flow,
message: object, message: HTTPMessage,
uploadContent: () => void, uploadContent: (content: string) => Promise<Response>,
} }
export default function ContentViewOptions({ flow, message, uploadContent }: ContentViewOptionsProps) { export default function ContentViewOptions({ flow, message, uploadContent }: ContentViewOptionsProps) {

View File

@ -24,7 +24,7 @@ function ViewImage({ flow, message }: ViewImageProps) {
type EditProps = { type EditProps = {
content: string, content: string,
onChange: () => void, onChange: (content: string) => any,
} }
function PureEdit({ content, onChange }: EditProps) { function PureEdit({ content, onChange }: EditProps) {
@ -39,7 +39,7 @@ type PureViewServerProps = {
} }
type PureViewServerStates = { type PureViewServerStates = {
lines: string[][], lines: [style: string, text: string][][],
description: string, description: string,
} }

View File

@ -1,9 +1,10 @@
import React from 'react' import React from 'react'
import { MessageUtils } from "../../flow/utils" import { MessageUtils } from "../../flow/utils"
import { Flow, HTTPMessage } from '../../flow'
type DownloadContentButtonProps = { type DownloadContentButtonProps = {
flow: object, flow: Flow,
message: object, message: HTTPMessage,
} }
export default function DownloadContentButton({ flow, message }: DownloadContentButtonProps) { export default function DownloadContentButton({ flow, message }: DownloadContentButtonProps) {

View File

@ -2,10 +2,11 @@ import React from 'react'
import { formatSize } from '../../utils' import { formatSize } from '../../utils'
import UploadContentButton from './UploadContentButton' import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton' import DownloadContentButton from './DownloadContentButton'
import { HTTPFlow, HTTPMessage } from '../../flow'
interface ContentProps { interface ContentProps {
flow: { request: object }, flow: HTTPFlow,
message: { contentLength: number }, message: HTTPMessage,
} }
interface ContentTooLargeProps extends ContentProps { interface ContentTooLargeProps extends ContentProps {

View File

@ -10,13 +10,14 @@ export default function ShowFullContentButton() {
contentLines = useAppSelector(state => state.ui.flow.content.length) contentLines = useAppSelector(state => state.ui.flow.content.length)
return ( return (
!showFullContent && !showFullContent ? (
<div> <div>
<Button className="view-all-content-btn btn-xs" onClick={() => dispatch(setShowFullContent())}> <Button className="view-all-content-btn btn-xs" onClick={() => dispatch(setShowFullContent())}>
Show full content Show full content
</Button> </Button>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span> <span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div> </div>
) : null
) )
} }

View File

@ -2,7 +2,7 @@ import React from 'react'
import FileChooser from '../common/FileChooser' import FileChooser from '../common/FileChooser'
type UploadContentButtonProps = { type UploadContentButtonProps = {
uploadContent: () => any, uploadContent: (content: string) => Promise<Response>,
} }
export default function UploadContentButton({ uploadContent }: UploadContentButtonProps) { export default function UploadContentButton({ uploadContent }: UploadContentButtonProps) {

View File

@ -1,5 +1,4 @@
import React, { Component } from 'react' import React from 'react'
import { connect } from 'react-redux'
import _ from 'lodash' import _ from 'lodash'
import Nav from './FlowView/Nav' import Nav from './FlowView/Nav'
@ -7,10 +6,15 @@ import { ErrorView as Error, Request, Response } from './FlowView/Messages'
import Details from './FlowView/Details' import Details from './FlowView/Details'
import { selectTab } from '../ducks/ui/flow' import { selectTab } from '../ducks/ui/flow'
import {useAppDispatch, useAppSelector} from "../ducks";
export const allTabs = { Request, Response, Error, Details } export const allTabs = { Request, Response, Error, Details }
function FlowView({ flow, tabName, selectTab }) { export default function FlowView() {
const dispatch = useAppDispatch(),
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
let tabName = useAppSelector(state => state.ui.flow.tab)
// only display available tab names // only display available tab names
const tabs = ['request', 'response', 'error'].filter(k => flow[k]) const tabs = ['request', 'response', 'error'].filter(k => flow[k])
@ -33,19 +37,9 @@ function FlowView({ flow, tabName, selectTab }) {
<Nav <Nav
tabs={tabs} tabs={tabs}
active={tabName} active={tabName}
onSelectTab={selectTab} onSelectTab={(tab: string) => dispatch(selectTab(tab))}
/> />
<Tab flow={flow}/> <Tab flow={flow}/>
</div> </div>
) )
} }
export default connect(
state => ({
flow: state.flows.byId[state.flows.selected[0]],
tabName: state.ui.flow.tab,
}),
{
selectTab,
}
)(FlowView)

View File

@ -1,7 +1,14 @@
import React from 'react' import React from 'react'
import {formatTimeDelta, formatTimeStamp} from '../../utils' import {formatTimeDelta, formatTimeStamp} from '../../utils'
import { Flow, HTTPMessage } from '../../flow'
export function TimeStamp({t, deltaTo, title}) { type TimeStampProps = {
t: number,
deltaTo: number,
title: string,
}
export function TimeStamp({t, deltaTo, title}: TimeStampProps) {
return t ? ( return t ? (
<tr> <tr>
<td>{title}:</td> <td>{title}:</td>
@ -19,7 +26,19 @@ export function TimeStamp({t, deltaTo, title}) {
) )
} }
export function ConnectionInfo({conn}) { type ConnectionInfoProps = {
conn: {
address: string[],
sni: string,
tls_version: string,
cipher_name: string,
alpn_proto_negotiated: string,
ip_address: string[],
source_address: string[],
},
}
export function ConnectionInfo({conn}: ConnectionInfoProps) {
return ( return (
<table className="connection-table"> <table className="connection-table">
<tbody> <tbody>

View File

@ -1,203 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
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 { startEdit, updateEdit } from '../../ducks/ui/flow'
import * as FlowActions from '../../ducks/flows'
import ToggleEdit from './ToggleEdit'
function RequestLine({ flow, readonly, updateFlow }) {
return (
<div className="first-line request-line">
<div>
<ValueEditor
content={flow.request.method}
readonly={readonly}
onDone={method => updateFlow({ request: { method } })}
/>
&nbsp;
<ValidateEditor
content={RequestUtils.pretty_url(flow.request)}
readonly={readonly}
onDone={url => updateFlow({ request: {path: '', ...parseUrl(url)}})}
isValid={url => !!parseUrl(url).host}
/>
&nbsp;
<ValidateEditor
content={flow.request.http_version}
readonly={readonly}
onDone={http_version => updateFlow({ request: { http_version } })}
isValid={isValidHttpVersion}
/>
</div>
</div>
)
}
function ResponseLine({ flow, readonly, updateFlow }) {
return (
<div className="first-line response-line">
<ValidateEditor
content={flow.response.http_version}
readonly={readonly}
onDone={nextVer => updateFlow({ response: { http_version: nextVer } })}
isValid={isValidHttpVersion}
/>
&nbsp;
<ValidateEditor
content={flow.response.status_code + ''}
readonly={readonly}
onDone={code => updateFlow({ response: { code: parseInt(code) } })}
isValid={code => /^\d+$/.test(code)}
/>
&nbsp;
<ValueEditor
content={flow.response.reason}
readonly={readonly}
onDone={msg => updateFlow({ response: { msg } })}
/>
</div>
)
}
const Message = connect(
state => ({
flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]],
isEdit: !!state.ui.flow.modifiedFlow,
}),
{
updateFlow: updateEdit,
uploadContent: FlowActions.uploadContent
}
)
export class Request extends Component {
render() {
const { flow, isEdit, updateFlow, uploadContent } = this.props
let noContent = !isEdit && (flow.request.contentLength == 0 || flow.request.contentLength == null)
return (
<section className="request">
<article>
<ToggleEdit/>
<RequestLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers
message={flow.request}
readonly={!isEdit}
onChange={headers => updateFlow({ request: { headers } })}
/>
<hr/>
<ContentView
readonly={!isEdit}
flow={flow}
onContentChange={content => updateFlow({ request: {content}})}
message={flow.request}/>
<hr/>
<Headers
message={flow.request}
readonly={!isEdit}
onChange={trailers => updateFlow({ request: { trailers } })}
type='trailers'
/>
</article>
<HideInStatic>
{!noContent &&
<footer>
<ContentViewOptions
flow={flow}
readonly={!isEdit}
message={flow.request}
uploadContent={content => uploadContent(flow, content, "request")}/>
</footer>
}
</HideInStatic>
</section>
)
}
}
Request = Message(Request)
export class Response extends Component {
render() {
const { flow, isEdit, updateFlow, uploadContent } = this.props
let noContent = !isEdit && (flow.response.contentLength == 0 || flow.response.contentLength == null)
return (
<section className="response">
<article>
<ToggleEdit/>
<ResponseLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers
message={flow.response}
readonly={!isEdit}
onChange={headers => updateFlow({ response: { headers } })}
/>
<hr/>
<ContentView
readonly={!isEdit}
flow={flow}
onContentChange={content => updateFlow({ response: {content}})}
message={flow.response}
/>
<hr/>
<Headers
message={flow.response}
readonly={!isEdit}
onChange={trailers => updateFlow({ response: { trailers } })}
type='trailers'
/>
</article>
<HideInStatic>
{!noContent &&
<footer >
<ContentViewOptions
flow={flow}
message={flow.response}
uploadContent={content => uploadContent(flow, content, "response")}
readonly={!isEdit}/>
</footer>
}
</HideInStatic>
</section>
)
}
}
Response = Message(Response)
ErrorView.propTypes = {
flow: PropTypes.object.isRequired,
}
export function ErrorView({ flow }) {
return (
<section className="error">
<div className="alert alert-warning">
{flow.error.msg}
<div>
<small>{formatTimeStamp(flow.error.timestamp)}</small>
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,198 @@
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 } }))}
/>
&nbsp;
<ValidateEditor
content={RequestUtils.pretty_url(flow.request)}
readonly={readonly}
onDone={url => dispatch(updateFlow({ request: {path: '', ...parseUrl(url)}}))}
isValid={url => !!parseUrl(url).host}
/>
&nbsp;
<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}
/>
&nbsp;
<ValidateEditor
content={flow.response?.status_code + ''}
readonly={readonly}
onDone={code => dispatch(updateFlow({ response: { code: parseInt(code) } }))}
isValid={code => /^\d+$/.test(code)}
/>
&nbsp;
<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>
)
}

View File

@ -1,16 +1,14 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import _ from 'lodash' import _ from 'lodash'
NavAction.propTypes = { type NavActionProps = {
icon: PropTypes.string.isRequired, icon: string,
title: PropTypes.string.isRequired, title: string,
onClick: PropTypes.func.isRequired, onClick: (e: any) => void,
} }
export function NavAction({ icon, title, onClick }) { export function NavAction({ icon, title, onClick }: NavActionProps) {
return ( return (
<a title={title} <a title={title}
href="#" href="#"
@ -24,13 +22,13 @@ export function NavAction({ icon, title, onClick }) {
) )
} }
Nav.propTypes = { type NavProps = {
active: PropTypes.string.isRequired, active: string,
tabs: PropTypes.array.isRequired, tabs: string[],
onSelectTab: PropTypes.func.isRequired, onSelectTab: (e: string) => void,
} }
export default function Nav({ active, tabs, onSelectTab }) { export default function Nav({ active, tabs, onSelectTab }: NavProps) {
return ( return (
<nav className="nav-tabs nav-tabs-sm"> <nav className="nav-tabs nav-tabs-sm">
{tabs.map(tab => ( {tabs.map(tab => (

View File

@ -1,40 +1,25 @@
import React, { Component } from 'react' import React from 'react'
import PropTypes from 'prop-types' import {useAppDispatch, useAppSelector} from "../../ducks";
import { connect } from 'react-redux'
import { startEdit, stopEdit } from '../../ducks/ui/flow' import { startEdit, stopEdit } from '../../ducks/ui/flow'
ToggleEdit.propTypes = {
isEdit: PropTypes.bool.isRequired,
flow: PropTypes.object.isRequired,
startEdit: PropTypes.func.isRequired,
stopEdit: PropTypes.func.isRequired,
}
function ToggleEdit({ isEdit, startEdit, stopEdit, flow, modifiedFlow }) { 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 ( return (
<div className="edit-flow-container"> <div className="edit-flow-container">
{isEdit ? {isEdit ?
<a className="edit-flow" title="Finish Edit" onClick={() => stopEdit(flow, modifiedFlow)}> <a className="edit-flow" title="Finish Edit" onClick={() => dispatch(stopEdit(flow, modifiedFlow))}>
<i className="fa fa-check"/> <i className="fa fa-check"/>
</a> </a>
: :
<a className="edit-flow" title="Edit Flow" onClick={() => startEdit(flow)}> <a className="edit-flow" title="Edit Flow" onClick={() => dispatch(startEdit(flow))}>
<i className="fa fa-pencil"/> <i className="fa fa-pencil"/>
</a> </a>
} }
</div> </div>
) )
} }
export default connect(
state => ({
isEdit: !!state.ui.flow.modifiedFlow,
modifiedFlow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]],
flow: state.flows.byId[state.flows.selected[0]]
}),
{
startEdit,
stopEdit,
}
)(ToggleEdit)

View File

@ -1,5 +1,4 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import classnames from 'classnames' import classnames from 'classnames'
import MainMenu from './Header/MainMenu' import MainMenu from './Header/MainMenu'

View File

@ -1,13 +1,20 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { fetchApi } from "../../utils"; import { fetchApi } from "../../utils";
type FilterDocsProps = {
selectHandler: (cmd: string) => void,
}
export default class FilterDocs extends Component { type FilterDocsStates = {
doc: {commands: string[][]}
}
export default class FilterDocs extends Component<FilterDocsProps, FilterDocsStates> {
// @todo move to redux // @todo move to redux
static xhr = null static xhr: Promise<any>
static doc = null static doc: {commands: string[][]}
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
@ -18,7 +25,7 @@ export default class FilterDocs extends Component {
if (!FilterDocs.xhr) { if (!FilterDocs.xhr) {
FilterDocs.xhr = fetchApi('/filter-help').then(response => response.json()) FilterDocs.xhr = fetchApi('/filter-help').then(response => response.json())
FilterDocs.xhr.catch(() => { FilterDocs.xhr.catch(() => {
FilterDocs.xhr = null FilterDocs.xhr = Promise.resolve()
}) })
} }
if (!this.state.doc) { if (!this.state.doc) {
@ -43,7 +50,7 @@ export default class FilterDocs extends Component {
</tr> </tr>
))} ))}
<tr key="docs-link"> <tr key="docs-link">
<td colSpan="2"> <td colSpan={2}>
<a href="https://mitmproxy.org/docs/latest/concepts-filters/" <a href="https://mitmproxy.org/docs/latest/concepts-filters/"
target="_blank"> target="_blank">
<i className="fa fa-external-link"/> <i className="fa fa-external-link"/>

View File

@ -1,24 +1,23 @@
import React from "react" import React from "react"
import PropTypes from 'prop-types'
import { connect } from "react-redux"
import Button from "../common/Button" import Button from "../common/Button"
import { MessageUtils } from "../../flow/utils.js" import { MessageUtils } from "../../flow/utils.js"
import * as flowsActions from "../../ducks/flows"
import HideInStatic from "../common/HideInStatic"; import HideInStatic from "../common/HideInStatic";
import { useAppDispatch, useAppSelector } from "../../ducks";
import {
resume as resumeFlow,
replay as replayFlow,
duplicate as duplicateFlow,
revert as revertFlow,
remove as removeFlow,
kill as killFlow
} from "../../ducks/flows"
FlowMenu.title = 'Flow' FlowMenu.title = 'Flow'
FlowMenu.propTypes = { export default function FlowMenu() {
flow: PropTypes.object, const dispatch = useAppDispatch(),
resumeFlow: PropTypes.func.isRequired, flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
killFlow: PropTypes.func.isRequired,
replayFlow: PropTypes.func.isRequired,
duplicateFlow: PropTypes.func.isRequired,
removeFlow: PropTypes.func.isRequired,
revertFlow: PropTypes.func.isRequired
}
export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
if (!flow) if (!flow)
return <div/> return <div/>
return ( return (
@ -27,19 +26,19 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
<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={() => replayFlow(flow)}> onClick={() => dispatch(replayFlow(flow))}>
Replay Replay
</Button> </Button>
<Button title="[D]uplicate flow" icon="fa-copy text-info" <Button title="[D]uplicate flow" icon="fa-copy text-info"
onClick={() => duplicateFlow(flow)}> onClick={() => dispatch(duplicateFlow(flow))}>
Duplicate Duplicate
</Button> </Button>
<Button disabled={!flow || !flow.modified} title="revert changes to flow [V]" <Button disabled={!flow || !flow.modified} title="revert changes to flow [V]"
icon="fa-history text-warning" onClick={() => revertFlow(flow)}> icon="fa-history text-warning" onClick={() => dispatch(revertFlow(flow))}>
Revert Revert
</Button> </Button>
<Button title="[d]elete flow" icon="fa-trash text-danger" <Button title="[d]elete flow" icon="fa-trash text-danger"
onClick={() => removeFlow(flow)}> onClick={() => dispatch(removeFlow(flow))}>
Delete Delete
</Button> </Button>
</div> </div>
@ -61,11 +60,11 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
<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={() => 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={() => killFlow(flow)}> icon="fa-times text-danger" onClick={() => dispatch(killFlow(flow))}>
Abort Abort
</Button> </Button>
</div> </div>
@ -75,17 +74,3 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow
</div> </div>
) )
} }
export default connect(
state => ({
flow: state.flows.byId[state.flows.selected[0]],
}),
{
resumeFlow: flowsActions.resume,
killFlow: flowsActions.kill,
replayFlow: flowsActions.replay,
duplicateFlow: flowsActions.duplicate,
removeFlow: flowsActions.remove,
revertFlow: flowsActions.revert,
}
)(FlowMenu)

View File

@ -1,21 +1,24 @@
import React from "react" import React from "react"
import { connect } from "react-redux"
import { EventlogToggle, OptionsToggle } from "./MenuToggle" import { 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";
import * as modalActions from "../../ducks/ui/modal" import * as modalActions from "../../ducks/ui/modal"
import { useAppDispatch } from "../../ducks";
OptionMenu.title = 'Options' OptionMenu.title = 'Options'
function OptionMenu({ openOptions }) { export default function OptionMenu() {
const dispatch = useAppDispatch()
const openOptions = () => modalActions.setActiveModal('OptionModal')
return ( return (
<div> <div>
<HideInStatic> <HideInStatic>
<div className="menu-group"> <div className="menu-group">
<div className="menu-content"> <div className="menu-content">
<Button title="Open Options" icon="fa-cogs text-primary" <Button title="Open Options" icon="fa-cogs text-primary"
onClick={openOptions}> onClick={() => dispatch(openOptions())}>
Edit Options <sup>alpha</sup> Edit Options <sup>alpha</sup>
</Button> </Button>
</div> </div>
@ -47,10 +50,3 @@ function OptionMenu({ openOptions }) {
</div> </div>
) )
} }
export default connect(
null,
{
openOptions: () => modalActions.setActiveModal('OptionModal')
}
)(OptionMenu)

View File

@ -1,15 +1,11 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
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'
import {useAppSelector} from "../ducks";
MainView.propTypes = { export default function MainView() {
hasSelection: PropTypes.bool.isRequired, const hasSelection = useAppSelector(state => !!state.flows.byId[state.flows.selected[0]])
}
function MainView({ hasSelection }) {
return ( return (
<div className="main-view"> <div className="main-view">
<FlowTable/> <FlowTable/>
@ -18,10 +14,3 @@ function MainView({ hasSelection }) {
</div> </div>
) )
} }
export default connect(
state => ({
hasSelection: !!state.flows.byId[state.flows.selected[0]]
}),
{}
)(MainView)

View File

@ -1,24 +1,13 @@
import React, { Component } from 'react' import React from 'react'
import { connect } from 'react-redux'
import ModalList from './ModalList' import ModalList from './ModalList'
import { useAppSelector } from "../../ducks";
class PureModal extends Component {
constructor(props, context) { export default function PureModal() {
super(props, context) const activeModal = useAppSelector(state => state.ui.modal.activeModal)
} const ActiveModal = ModalList.find(m => m.name === activeModal )
render() { return(
const { activeModal } = this.props activeModal ? <ActiveModal/> : <div/>
const ActiveModal = ModalList.find(m => m.name === activeModal ) )
return(
activeModal ? <ActiveModal/> : <div/>
)
}
} }
export default connect(
state => ({
activeModal: state.ui.modal.activeModal
})
)(PureModal)

View File

@ -1,47 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { onKeyDown } from '../ducks/ui/keyboard'
import MainView from './MainView'
import Header from './Header'
import CommandBar from './CommandBar'
import EventLog from './EventLog'
import Footer from './Footer'
import Modal from './Modal/Modal'
class ProxyAppMain extends Component {
componentDidMount() {
window.addEventListener('keydown', this.props.onKeyDown);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.props.onKeyDown);
}
render() {
const { showEventLog } = this.props
return (
<div id="container" tabIndex="0">
<Header/>
<MainView />
<CommandBar />
{showEventLog && (
<EventLog key="eventlog"/>
)}
<Footer />
<Modal/>
</div>
)
}
}
export default connect(
state => ({
showEventLog: state.eventLog.visible,
}),
{
onKeyDown,
}
)(ProxyAppMain)

View File

@ -0,0 +1,35 @@
import React, { useEffect } from 'react'
import { onKeyDown } from '../ducks/ui/keyboard'
import MainView from './MainView'
import Header from './Header'
import CommandBar from './CommandBar'
import EventLog from './EventLog'
import Footer from './Footer'
import Modal from './Modal/Modal'
import {useAppDispatch, useAppSelector} from "../ducks";
export default function ProxyAppMain() {
const dispatch = useAppDispatch(),
showEventLog = useAppSelector(state => state.eventLog.visible)
useEffect(() => {
window.addEventListener('keydown', (e) => dispatch(onKeyDown(e)));
return function cleanup() {
window.removeEventListener('keydown', (e) => dispatch(onKeyDown(e)));
}
})
return (
<div id="container" tabIndex={0}>
<Header/>
<MainView />
<CommandBar />
{showEventLog && (
<EventLog key="eventlog"/>
)}
<Footer />
<Modal/>
</div>
)
}

View File

@ -1,19 +1,20 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ValueEditor from './ValueEditor' import ValueEditor from './ValueEditor'
import classnames from 'classnames' import classnames from 'classnames'
type ValidateEditorProps = {
content: string | undefined,
readonly: boolean,
onDone: (content: string) => void,
className?: string,
isValid: (content: string) => boolean,
}
export default class ValidateEditor extends Component { type ValidateEditorStates = {
valid: boolean,
static propTypes = { }
content: PropTypes.string.isRequired,
readonly: PropTypes.bool,
onDone: PropTypes.func.isRequired,
className: PropTypes.string,
isValid: PropTypes.func.isRequired,
}
export default class ValidateEditor extends Component<ValidateEditorProps, ValidateEditorStates> {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { valid: props.isValid(props.content) } this.state = { valid: props.isValid(props.content) }

View File

@ -1,7 +1,7 @@
import React from "react" import React from "react"
type DocLinkProps = { type DocLinkProps = {
children: React.ReactNode, children?: React.ReactNode,
resource: string resource: string
} }

View File

@ -5,5 +5,5 @@ type HideInStaticProps = {
} }
export default function HideInStatic({ children }: HideInStaticProps) { export default function HideInStatic({ children }: HideInStaticProps) {
return (window.MITMWEB_CONF && window.MITMWEB_CONF.static) ? null : [children] return (window.MITMWEB_CONF && window.MITMWEB_CONF.static) ? null : <>{[children]}</>
} }

View File

@ -2,7 +2,7 @@ import React from 'react'
type ToggleButtonProps = { type ToggleButtonProps = {
checked: boolean, checked: boolean,
onToggle: () => void, onToggle: () => any,
text: string text: string
} }

View File

@ -5,7 +5,7 @@ import { Key } from '../../utils'
type ToggleInputButtonProps = { type ToggleInputButtonProps = {
name: string, name: string,
txt: string, txt: string,
onToggleChanged: (string) => void, onToggleChanged: Function,
checked: boolean, checked: boolean,
placeholder: string, placeholder: string,
inputType: string, inputType: string,

View File

@ -67,6 +67,7 @@ export interface HTTPMessage {
trailers?: Headers trailers?: Headers
contentLength: number contentLength: number
contentHash: string contentHash: string
content?: string
timestamp_start: number timestamp_start: number
timestamp_end?: number timestamp_end?: number
} }

View File

@ -59,7 +59,7 @@ export var formatTimeDelta = function (milliseconds) {
export var formatTimeStamp = function (seconds, utc_to_local=true) { export var formatTimeStamp = function (seconds, utc_to_local=true) {
var utc = new Date(seconds * 1000); var utc = new Date(seconds * 1000);
if (utc_to_local && !process.env.JEST_WORKER_ID) { if (utc_to_local) {
var local = utc.getTime() - utc.getTimezoneOffset() * 60 * 1000; var local = utc.getTime() - utc.getTimezoneOffset() * 60 * 1000;
var ts = new Date(local).toISOString(); var ts = new Date(local).toISOString();
} else { } else {