diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-06-17 01:53:02 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-06-17 01:53:02 -0700 |
commit | fcf5dc8728816bae73a175ee021f8a11a1591567 (patch) | |
tree | 56ff25784a659f0e54894ca616e198cfe1a13ea0 /web/src/js/components/flowview | |
parent | 78785df16be237bfdbf4ee485639b61f06b4a47e (diff) | |
parent | 034287edcf00eb734cb67e62de58c3bfebf6bb44 (diff) | |
download | mitmproxy-fcf5dc8728816bae73a175ee021f8a11a1591567.tar.gz mitmproxy-fcf5dc8728816bae73a175ee021f8a11a1591567.tar.bz2 mitmproxy-fcf5dc8728816bae73a175ee021f8a11a1591567.zip |
Merge pull request #1267 from gzzhanghao/components
[web] Working on components
Diffstat (limited to 'web/src/js/components/flowview')
-rw-r--r-- | web/src/js/components/flowview/contentview.js | 267 | ||||
-rw-r--r-- | web/src/js/components/flowview/details.js | 181 | ||||
-rw-r--r-- | web/src/js/components/flowview/index.js | 114 | ||||
-rw-r--r-- | web/src/js/components/flowview/messages.js | 320 | ||||
-rw-r--r-- | web/src/js/components/flowview/nav.js | 61 |
5 files changed, 0 insertions, 943 deletions
diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js deleted file mode 100644 index cbac9a75..00000000 --- a/web/src/js/components/flowview/contentview.js +++ /dev/null @@ -1,267 +0,0 @@ -import React from "react"; -import _ from "lodash"; - -import {MessageUtils} from "../../flow/utils.js"; -import {formatSize} from "../../utils.js"; - -var ViewImage = React.createClass({ - propTypes: { - flow: React.PropTypes.object.isRequired, - message: React.PropTypes.object.isRequired, - }, - statics: { - regex: /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i, - matches: function (message) { - return ViewImage.regex.test(MessageUtils.getContentType(message)); - } - }, - render: function () { - var url = MessageUtils.getContentURL(this.props.flow, this.props.message); - return <div className="flowview-image"> - <img src={url} alt="preview" className="img-thumbnail"/> - </div>; - } -}); - -var ContentLoader = React.createClass({ - propTypes: { - flow: React.PropTypes.object.isRequired, - message: React.PropTypes.object.isRequired, - }, - getInitialState: function () { - return { - content: undefined, - request: undefined - } - }, - requestContent: function (nextProps) { - if (this.state.request) { - this.state.request.abort(); - } - var request = MessageUtils.getContent(nextProps.flow, nextProps.message); - this.setState({ - content: undefined, - request: request - }); - request.done(function (data) { - this.setState({content: data}); - }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) { - if (textStatus === "abort") { - return; - } - this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown}); - }.bind(this)).always(function () { - this.setState({request: undefined}); - }.bind(this)); - - }, - componentWillMount: function () { - this.requestContent(this.props); - }, - componentWillReceiveProps: function (nextProps) { - if (nextProps.message !== this.props.message) { - this.requestContent(nextProps); - } - }, - componentWillUnmount: function () { - if (this.state.request) { - this.state.request.abort(); - } - }, - render: function () { - if (!this.state.content) { - return <div className="text-center"> - <i className="fa fa-spinner fa-spin"></i> - </div>; - } - return React.cloneElement(this.props.children, { - content: this.state.content - }) - } -}); - -var ViewRaw = React.createClass({ - propTypes: { - content: React.PropTypes.string.isRequired, - }, - statics: { - textView: true, - matches: function (message) { - return true; - } - }, - render: function () { - return <pre>{this.props.content}</pre>; - } -}); - -var ViewJSON = React.createClass({ - propTypes: { - content: React.PropTypes.string.isRequired, - }, - statics: { - textView: true, - regex: /^application\/json$/i, - matches: function (message) { - return ViewJSON.regex.test(MessageUtils.getContentType(message)); - } - }, - render: function () { - var json = this.props.content; - try { - json = JSON.stringify(JSON.parse(json), null, 2); - } catch (e) { - // @noop - } - return <pre>{json}</pre>; - } -}); - -var ViewAuto = React.createClass({ - propTypes: { - message: React.PropTypes.object.isRequired, - flow: React.PropTypes.object.isRequired, - }, - statics: { - matches: function () { - return false; // don't match itself - }, - findView: function (message) { - for (var i = 0; i < all.length; i++) { - if (all[i].matches(message)) { - return all[i]; - } - } - return all[all.length - 1]; - } - }, - render: function () { - var { message, flow } = this.props - var View = ViewAuto.findView(this.props.message); - if (View.textView) { - return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader> - } else { - return <View message={message} flow={flow} /> - } - } -}); - -var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw]; - -var ContentEmpty = React.createClass({ - render: function () { - var message_name = this.props.flow.request === this.props.message ? "request" : "response"; - return <div className="alert alert-info">No {message_name} content.</div>; - } -}); - -var ContentMissing = React.createClass({ - render: function () { - var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; - return <div className="alert alert-info">{message_name} content missing.</div>; - } -}); - -var TooLarge = React.createClass({ - statics: { - isTooLarge: function (message) { - var max_mb = ViewImage.matches(message) ? 10 : 0.2; - return message.contentLength > 1024 * 1024 * max_mb; - } - }, - render: function () { - var size = formatSize(this.props.message.contentLength); - return <div className="alert alert-warning"> - <button onClick={this.props.onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button> - {size} content size. - </div>; - } -}); - -var ViewSelector = React.createClass({ - render: function () { - var views = []; - for (var i = 0; i < all.length; i++) { - var view = all[i]; - var className = "btn btn-default"; - if (view === this.props.active) { - className += " active"; - } - var text; - if (view === ViewAuto) { - text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", ""); - } else { - text = view.displayName.toLowerCase().replace("view", ""); - } - views.push( - <button - key={view.displayName} - onClick={this.props.selectView.bind(null, view)} - className={className}> - {text} - </button> - ); - } - - return <div className="view-selector btn-group btn-group-xs">{views}</div>; - } -}); - -var ContentView = React.createClass({ - getInitialState: function () { - return { - displayLarge: false, - View: ViewAuto - }; - }, - 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: React.PropTypes.object.isRequired, - message: React.PropTypes.object.isRequired, - }, - selectView: function (view) { - this.setState({ - View: view - }); - }, - displayLarge: function () { - this.setState({displayLarge: true}); - }, - componentWillReceiveProps: function (nextProps) { - if (nextProps.message !== this.props.message) { - this.setState(this.getInitialState()); - } - }, - render: function () { - var { flow, message } = this.props - var message = this.props.message; - if (message.contentLength === 0) { - return <ContentEmpty {...this.props}/>; - } else if (message.contentLength === null) { - return <ContentMissing {...this.props}/>; - } else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) { - return <TooLarge {...this.props} onClick={this.displayLarge}/>; - } - - var downloadUrl = MessageUtils.getContentURL(this.props.flow, message); - - return <div> - {this.state.View.textView ? ( - <ContentLoader flow={flow} message={message}><this.state.View content="" /></ContentLoader> - ) : ( - <this.state.View flow={flow} message={message} /> - )} - <div className="view-options text-center"> - <ViewSelector selectView={this.selectView} active={this.state.View} message={message}/> - - <a className="btn btn-default btn-xs" href={downloadUrl}> - <i className="fa fa-download"/> - </a> - </div> - </div>; - } -}); - -export default ContentView; diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js deleted file mode 100644 index 45fe1292..00000000 --- a/web/src/js/components/flowview/details.js +++ /dev/null @@ -1,181 +0,0 @@ -import React from "react"; -import _ from "lodash"; - -import {formatTimeStamp, formatTimeDelta} from "../../utils.js"; - -var TimeStamp = React.createClass({ - render: function () { - - if (!this.props.t) { - //should be return null, but that triggers a React bug. - return <tr></tr>; - } - - var ts = formatTimeStamp(this.props.t); - - var delta; - if (this.props.deltaTo) { - delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); - delta = <span className="text-muted">{"(" + delta + ")"}</span>; - } else { - delta = null; - } - - return <tr> - <td>{this.props.title + ":"}</td> - <td>{ts} {delta}</td> - </tr>; - } -}); - -var ConnectionInfo = React.createClass({ - - render: function () { - var conn = this.props.conn; - var address = conn.address.address.join(":"); - - var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug. - if (conn.sni) { - sni = <tr key="sni"> - <td> - <abbr title="TLS Server Name Indication">TLS SNI:</abbr> - </td> - <td>{conn.sni}</td> - </tr>; - } - return ( - <table className="connection-table"> - <tbody> - <tr key="address"> - <td>Address:</td> - <td>{address}</td> - </tr> - {sni} - </tbody> - </table> - ); - } -}); - -var CertificateInfo = React.createClass({ - render: function () { - //TODO: We should fetch human-readable certificate representation - // from the server - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - - var preStyle = {maxHeight: 100}; - return ( - <div> - {client_conn.cert ? <h4>Client Certificate</h4> : null} - {client_conn.cert ? <pre style={preStyle}>{client_conn.cert}</pre> : null} - - {server_conn.cert ? <h4>Server Certificate</h4> : null} - {server_conn.cert ? <pre style={preStyle}>{server_conn.cert}</pre> : null} - </div> - ); - } -}); - -var Timing = React.createClass({ - render: function () { - var flow = this.props.flow; - var sc = flow.server_conn; - var cc = flow.client_conn; - var req = flow.request; - var resp = flow.response; - - var 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 - } - ]; - - if (flow.response) { - timestamps.push( - { - title: "First response byte", - t: resp.timestamp_start, - deltaTo: req.timestamp_start - }, { - title: "Response complete", - t: resp.timestamp_end, - deltaTo: req.timestamp_start - } - ); - } - - //Add unique key for each row. - timestamps.forEach(function (e) { - e.key = e.title; - }); - - timestamps = _.sortBy(timestamps, 't'); - - var rows = timestamps.map(function (e) { - return <TimeStamp {...e}/>; - }); - - return ( - <div> - <h4>Timing</h4> - <table className="timing-table"> - <tbody> - {rows} - </tbody> - </table> - </div> - ); - } -}); - -var Details = React.createClass({ - render: function () { - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - return ( - <section> - - <h4>Client Connection</h4> - <ConnectionInfo conn={client_conn}/> - - <h4>Server Connection</h4> - <ConnectionInfo conn={server_conn}/> - - <CertificateInfo flow={flow}/> - - <Timing flow={flow}/> - - </section> - ); - } -}); - -export default Details;
\ No newline at end of file diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js deleted file mode 100644 index 7f5e9768..00000000 --- a/web/src/js/components/flowview/index.js +++ /dev/null @@ -1,114 +0,0 @@ -import React from "react"; - -import Nav from "./nav.js"; -import {Request, Response, Error} from "./messages.js"; -import Details from "./details.js"; -import Prompt from "../prompt.js"; - - -var allTabs = { - request: Request, - response: Response, - error: Error, - details: Details -}; - -var FlowView = React.createClass({ - getInitialState: function () { - return { - prompt: false - }; - }, - getTabs: function (flow) { - var tabs = []; - ["request", "response", "error"].forEach(function (e) { - if (flow[e]) { - tabs.push(e); - } - }); - tabs.push("details"); - return tabs; - }, - nextTab: function (i) { - var tabs = this.getTabs(this.props.flow); - var currentIndex = tabs.indexOf(this.props.tab); - // JS modulo operator doesn't correct negative numbers, make sure that we are positive. - var nextIndex = (currentIndex + i + tabs.length) % tabs.length; - this.selectTab(tabs[nextIndex]); - }, - selectTab: function (panel) { - this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`); - }, - promptEdit: function () { - var options; - switch (this.props.tab) { - case "request": - options = [ - "method", - "url", - {text: "http version", key: "v"}, - "header" - /*, "content"*/]; - break; - case "response": - options = [ - {text: "http version", key: "v"}, - "code", - "message", - "header" - /*, "content"*/]; - break; - case "details": - return; - default: - throw "Unknown tab for edit: " + this.props.tab; - } - - this.setState({ - prompt: { - done: function (k) { - this.setState({prompt: false}); - if (k) { - this.refs.tab.edit(k); - } - }.bind(this), - options: options - } - }); - }, - render: function () { - var flow = this.props.flow; - var tabs = this.getTabs(flow); - var active = this.props.tab; - - if (tabs.indexOf(active) < 0) { - if (active === "response" && flow.error) { - active = "error"; - } else if (active === "error" && flow.response) { - active = "response"; - } else { - active = tabs[0]; - } - } - - var prompt = null; - if (this.state.prompt) { - prompt = <Prompt {...this.state.prompt}/>; - } - - var Tab = allTabs[active]; - return ( - <div className="flow-detail" onScroll={this.adjustHead}> - <Nav ref="head" - flow={flow} - tabs={tabs} - active={active} - selectTab={this.selectTab}/> - <Tab ref="tab" flow={flow}/> - {prompt} - </div> - ); - } -}); - -export default FlowView; diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js deleted file mode 100644 index 2885b3b1..00000000 --- a/web/src/js/components/flowview/messages.js +++ /dev/null @@ -1,320 +0,0 @@ -import React from "react"; -import ReactDOM from 'react-dom'; -import _ from "lodash"; - -import {FlowActions} from "../../actions.js"; -import {RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion} from "../../flow/utils.js"; -import {Key, formatTimeStamp} from "../../utils.js"; -import ContentView from "./contentview.js"; -import {ValueEditor} from "../editor.js"; - -var Headers = React.createClass({ - propTypes: { - onChange: React.PropTypes.func.isRequired, - message: React.PropTypes.object.isRequired - }, - onChange: function (row, col, val) { - var nextHeaders = _.cloneDeep(this.props.message.headers); - 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: function () { - this.refs["0-key"].focus(); - }, - onTab: function (row, col, e) { - var headers = this.props.message.headers; - if (row === headers.length - 1 && col === 1) { - e.preventDefault(); - - var nextHeaders = _.cloneDeep(this.props.message.headers); - nextHeaders.push(["Name", "Value"]); - this.props.onChange(nextHeaders); - this._nextSel = (row + 1) + "-key"; - } - }, - componentDidUpdate: function () { - if (this._nextSel && this.refs[this._nextSel]) { - this.refs[this._nextSel].focus(); - this._nextSel = undefined; - } - }, - onRemove: function (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: function () { - - var rows = this.props.message.headers.map(function (header, i) { - - var kEdit = <HeaderEditor - ref={i + "-key"} - content={header[0]} - onDone={this.onChange.bind(null, i, 0)} - onRemove={this.onRemove.bind(null, i, 0)} - onTab={this.onTab.bind(null, i, 0)}/>; - var vEdit = <HeaderEditor - ref={i + "-value"} - content={header[1]} - onDone={this.onChange.bind(null, i, 1)} - onRemove={this.onRemove.bind(null, i, 1)} - onTab={this.onTab.bind(null, i, 1)}/>; - return ( - <tr key={i}> - <td className="header-name">{kEdit}:</td> - <td className="header-value">{vEdit}</td> - </tr> - ); - }.bind(this)); - return ( - <table className="header-table"> - <tbody> - {rows} - </tbody> - </table> - ); - } -}); - -var HeaderEditor = React.createClass({ - render: function () { - return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>; - }, - focus: function () { - ReactDOM.findDOMNode(this).focus(); - }, - onKeyDown: function (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.TAB: - if (!e.shiftKey) { - this.props.onTab(e); - } - break; - } - } -}); - -var RequestLine = React.createClass({ - render: function () { - var flow = this.props.flow; - var url = RequestUtils.pretty_url(flow.request); - var httpver = flow.request.http_version; - - return <div className="first-line request-line"> - <ValueEditor - ref="method" - content={flow.request.method} - onDone={this.onMethodChange} - inline/> - - <ValueEditor - ref="url" - content={url} - onDone={this.onUrlChange} - isValid={this.isValidUrl} - inline/> - - <ValueEditor - ref="httpVersion" - content={httpver} - onDone={this.onHttpVersionChange} - isValid={isValidHttpVersion} - inline/> - </div> - }, - isValidUrl: function (url) { - var u = parseUrl(url); - return !!u.host; - }, - onMethodChange: function (nextMethod) { - FlowActions.update( - this.props.flow, - {request: {method: nextMethod}} - ); - }, - onUrlChange: function (nextUrl) { - var props = parseUrl(nextUrl); - props.path = props.path || ""; - FlowActions.update( - this.props.flow, - {request: props} - ); - }, - onHttpVersionChange: function (nextVer) { - var ver = parseHttpVersion(nextVer); - FlowActions.update( - this.props.flow, - {request: {http_version: ver}} - ); - } -}); - -var ResponseLine = React.createClass({ - render: function () { - var flow = this.props.flow; - var httpver = flow.response.http_version; - return <div className="first-line response-line"> - <ValueEditor - ref="httpVersion" - content={httpver} - onDone={this.onHttpVersionChange} - isValid={isValidHttpVersion} - inline/> - - <ValueEditor - ref="code" - content={flow.response.status_code + ""} - onDone={this.onCodeChange} - isValid={this.isValidCode} - inline/> - - <ValueEditor - ref="msg" - content={flow.response.reason} - onDone={this.onMsgChange} - inline/> - </div>; - }, - isValidCode: function (code) { - return /^\d+$/.test(code); - }, - onHttpVersionChange: function (nextVer) { - var ver = parseHttpVersion(nextVer); - FlowActions.update( - this.props.flow, - {response: {http_version: ver}} - ); - }, - onMsgChange: function (nextMsg) { - FlowActions.update( - this.props.flow, - {response: {msg: nextMsg}} - ); - }, - onCodeChange: function (nextCode) { - nextCode = parseInt(nextCode); - FlowActions.update( - this.props.flow, - {response: {code: nextCode}} - ); - } -}); - -export var Request = React.createClass({ - render: function () { - var flow = this.props.flow; - return ( - <section className="request"> - <RequestLine ref="requestLine" flow={flow}/> - {/*<ResponseLine flow={flow}/>*/} - <Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/> - <hr/> - <ContentView flow={flow} message={flow.request}/> - </section> - ); - }, - edit: function (k) { - switch (k) { - case "m": - this.refs.requestLine.refs.method.focus(); - break; - case "u": - this.refs.requestLine.refs.url.focus(); - break; - case "v": - this.refs.requestLine.refs.httpVersion.focus(); - break; - case "h": - this.refs.headers.edit(); - break; - default: - throw "Unimplemented: " + k; - } - }, - onHeaderChange: function (nextHeaders) { - FlowActions.update(this.props.flow, { - request: { - headers: nextHeaders - } - }); - } -}); - -export var Response = React.createClass({ - render: function () { - var flow = this.props.flow; - return ( - <section className="response"> - {/*<RequestLine flow={flow}/>*/} - <ResponseLine ref="responseLine" flow={flow}/> - <Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/> - <hr/> - <ContentView flow={flow} message={flow.response}/> - </section> - ); - }, - edit: function (k) { - switch (k) { - case "c": - this.refs.responseLine.refs.status_code.focus(); - break; - case "m": - this.refs.responseLine.refs.msg.focus(); - break; - case "v": - this.refs.responseLine.refs.httpVersion.focus(); - break; - case "h": - this.refs.headers.edit(); - break; - default: - throw "Unimplemented: " + k; - } - }, - onHeaderChange: function (nextHeaders) { - FlowActions.update(this.props.flow, { - response: { - headers: nextHeaders - } - }); - } -}); - -export var Error = React.createClass({ - render: function () { - var flow = this.props.flow; - return ( - <section> - <div className="alert alert-warning"> - {flow.error.msg} - <div> - <small>{ formatTimeStamp(flow.error.timestamp) }</small> - </div> - </div> - </section> - ); - } -}); diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js deleted file mode 100644 index a12fd1fd..00000000 --- a/web/src/js/components/flowview/nav.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from "react"; - -import {FlowActions} from "../../actions.js"; - -var NavAction = React.createClass({ - onClick: function (e) { - e.preventDefault(); - this.props.onClick(); - }, - render: function () { - return ( - <a title={this.props.title} - href="#" - className="nav-action" - onClick={this.onClick}> - <i className={"fa fa-fw " + this.props.icon}></i> - </a> - ); - } -}); - -var Nav = React.createClass({ - render: function () { - var flow = this.props.flow; - - var tabs = this.props.tabs.map(function (e) { - var str = e.charAt(0).toUpperCase() + e.slice(1); - var className = this.props.active === e ? "active" : ""; - var onClick = function (event) { - this.props.selectTab(e); - event.preventDefault(); - }.bind(this); - return <a key={e} - href="#" - className={className} - onClick={onClick}>{str}</a>; - }.bind(this)); - - var acceptButton = null; - if(flow.intercepted){ - acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={FlowActions.accept.bind(null, flow)} />; - } - var revertButton = null; - if(flow.modified){ - revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={FlowActions.revert.bind(null, flow)} />; - } - - return ( - <nav ref="head" className="nav-tabs nav-tabs-sm"> - {tabs} - <NavAction title="[d]elete flow" icon="fa-trash" onClick={FlowActions.delete.bind(null, flow)} /> - <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={FlowActions.duplicate.bind(null, flow)} /> - <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={FlowActions.replay.bind(null, flow)} /> - {acceptButton} - {revertButton} - </nav> - ); - } -}); - -export default Nav;
\ No newline at end of file |