mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 14:19:58 +00:00
Workspaces (#7)
* Got a hacky workspace implementation running * Removed some hax with reducer composition * Moved some more around * Moved files back out * Started on entities reducer * Split up some components * Moved nested modules back out of workspaces * Started on new Sidebar tree stuff * Better store stuff * Some more tweaks * Removed workspace update action * Re-implemented filtering in the Sidbare * Switch to get the newest response
This commit is contained in:
parent
97f579f5e5
commit
e9d64ebb23
@ -3,17 +3,17 @@ import Editor from './base/Editor'
|
||||
|
||||
class RequestBodyEditor extends Component {
|
||||
render () {
|
||||
const {request, onChange, className} = this.props;
|
||||
const mode = request.contentType || 'text/plain';
|
||||
const {body, contentType, requestId, onChange, className} = this.props;
|
||||
|
||||
return (
|
||||
<Editor
|
||||
value={request.body}
|
||||
value={body}
|
||||
className={className}
|
||||
debounceMillis={400}
|
||||
onChange={onChange}
|
||||
uniquenessKey={request._id}
|
||||
uniquenessKey={requestId}
|
||||
options={{
|
||||
mode: mode,
|
||||
mode: contentType,
|
||||
placeholder: 'request body here...'
|
||||
}}
|
||||
/>
|
||||
@ -22,10 +22,13 @@ class RequestBodyEditor extends Component {
|
||||
}
|
||||
|
||||
RequestBodyEditor.propTypes = {
|
||||
request: PropTypes.shape({
|
||||
body: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
// Functions
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// Other
|
||||
requestId: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
contentType: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default RequestBodyEditor;
|
||||
|
132
app/components/RequestPane.js
Normal file
132
app/components/RequestPane.js
Normal file
@ -0,0 +1,132 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
|
||||
|
||||
import KeyValueEditor from '../components/base/KeyValueEditor'
|
||||
import Dropdown from '../components/base/Dropdown'
|
||||
|
||||
import RequestBodyEditor from '../components/RequestBodyEditor'
|
||||
import RequestAuthEditor from '../components/RequestAuthEditor'
|
||||
import RequestUrlBar from '../components/RequestUrlBar'
|
||||
|
||||
class RequestPane extends Component {
|
||||
render () {
|
||||
const {
|
||||
request,
|
||||
sendRequest,
|
||||
updateRequestUrl,
|
||||
updateRequestMethod,
|
||||
updateRequestBody,
|
||||
updateRequestParams,
|
||||
updateRequestAuthentication,
|
||||
updateRequestHeaders
|
||||
} = this.props;
|
||||
|
||||
if (!request) {
|
||||
return (
|
||||
<section className="grid__cell section section--bordered grid--v grid--start">
|
||||
<header className="header bg-super-light section__header"></header>
|
||||
<div className="section__body grid__cell"></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="grid__cell section section--bordered">
|
||||
<div className="grid--v wide">
|
||||
<div className="header section__header">
|
||||
<RequestUrlBar
|
||||
uniquenessKey={request._id}
|
||||
sendRequest={() => sendRequest(request)}
|
||||
onUrlChange={updateRequestUrl}
|
||||
onMethodChange={updateRequestMethod}
|
||||
url={request.url}
|
||||
method={request.method}
|
||||
/>
|
||||
</div>
|
||||
<Tabs className="grid__cell grid--v section__body">
|
||||
<TabList className="grid grid--start">
|
||||
<Tab className="no-wrap grid grid--center">
|
||||
<button>JSON</button>
|
||||
<Dropdown>
|
||||
<button><i className="fa fa-caret-down"></i></button>
|
||||
<ul>
|
||||
{/*<li><button><i className="fa fa-picture-o"></i> File Upload</button></li>*/}
|
||||
<li><button><i className="fa fa-bars"></i> Form Data</button></li>
|
||||
<li><button><i className="fa fa-code"></i> XML</button></li>
|
||||
<li><button><i className="fa fa-file-text"></i> Plain Text</button></li>
|
||||
</ul>
|
||||
</Dropdown>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<button className="no-wrap">
|
||||
Params {request.params.length ? `(${request.params.length})` : ''}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<button className="no-wrap">
|
||||
Headers {request.headers.length ? `(${request.headers.length})` : ''}
|
||||
</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="grid__cell editor-wrapper">
|
||||
<RequestBodyEditor
|
||||
onChange={updateRequestBody}
|
||||
requestId={request._id}
|
||||
contentType={request.contentType}
|
||||
body={request.body}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div>
|
||||
<KeyValueEditor
|
||||
className="pad"
|
||||
namePlaceholder="name"
|
||||
valuePlaceholder="value"
|
||||
uniquenessKey={request._id}
|
||||
pairs={request.params}
|
||||
onChange={updateRequestParams}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div>
|
||||
<div className="pad">
|
||||
<label>Basic Authentication</label>
|
||||
<RequestAuthEditor
|
||||
request={request}
|
||||
onChange={updateRequestAuthentication}
|
||||
/>
|
||||
<br/>
|
||||
<label>Other Headers</label>
|
||||
<KeyValueEditor
|
||||
namePlaceholder="My-Header"
|
||||
valuePlaceholder="Value"
|
||||
uniquenessKey={request._id}
|
||||
pairs={request.headers}
|
||||
onChange={updateRequestHeaders}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RequestPane.propTypes = {
|
||||
// Functions
|
||||
sendRequest: PropTypes.func.isRequired,
|
||||
updateRequestUrl: PropTypes.func.isRequired,
|
||||
updateRequestMethod: PropTypes.func.isRequired,
|
||||
updateRequestBody: PropTypes.func.isRequired,
|
||||
updateRequestParams: PropTypes.func.isRequired,
|
||||
updateRequestAuthentication: PropTypes.func.isRequired,
|
||||
updateRequestHeaders: PropTypes.func.isRequired,
|
||||
|
||||
// Other
|
||||
request: PropTypes.object
|
||||
};
|
||||
|
||||
export default RequestPane;
|
@ -4,36 +4,42 @@ import Dropdown from './base/Dropdown';
|
||||
import {METHODS} from '../lib/constants';
|
||||
|
||||
class UrlInput extends Component {
|
||||
_handleFormSubmit (e) {
|
||||
e.preventDefault();
|
||||
this.props.sendRequest();
|
||||
}
|
||||
|
||||
render () {
|
||||
const {sendRequest, onUrlChange, onMethodChange, request} = this.props;
|
||||
const {onUrlChange, onMethodChange, uniquenessKey, url, method} = this.props;
|
||||
return (
|
||||
<div className="tall grid grid--center wide bg-super-light">
|
||||
<Dropdown className="tall">
|
||||
<button className="pad tall txt-md">
|
||||
{request.method} <i className="fa fa-caret-down"></i>
|
||||
{method}
|
||||
<i className="fa fa-caret-down"></i>
|
||||
</button>
|
||||
<ul>
|
||||
{METHODS.map((method) => (
|
||||
<li key={method}>
|
||||
<button onClick={onMethodChange.bind(null, method)}>
|
||||
{method}
|
||||
{METHODS.map(m => (
|
||||
<li key={m}>
|
||||
<button onClick={onMethodChange.bind(null, m)}>
|
||||
{m}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Dropdown>
|
||||
<form className="tall grid__cell form-control form-control--wide"
|
||||
onSubmit={e => {e.preventDefault(); sendRequest(request)}}>
|
||||
onSubmit={this._handleFormSubmit.bind(this)}>
|
||||
<DebouncingInput
|
||||
type="text"
|
||||
className="txt-md"
|
||||
placeholder="http://echo.insomnia.rest/status/200"
|
||||
value={request.url}
|
||||
value={url}
|
||||
debounceMillis={1000}
|
||||
uniquenessKey={request._id}
|
||||
uniquenessKey={uniquenessKey}
|
||||
onChange={onUrlChange}/>
|
||||
</form>
|
||||
<button className="btn btn--compact txt-lg" onClick={sendRequest.bind(null, request)}>
|
||||
<button className="btn btn--compact txt-lg" onClick={this._handleFormSubmit.bind(this)}>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
@ -45,10 +51,9 @@ UrlInput.propTypes = {
|
||||
sendRequest: PropTypes.func.isRequired,
|
||||
onUrlChange: PropTypes.func.isRequired,
|
||||
onMethodChange: PropTypes.func.isRequired,
|
||||
request: PropTypes.shape({
|
||||
uniquenessKey: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
method: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default UrlInput;
|
||||
|
90
app/components/ResponsePane.js
Normal file
90
app/components/ResponsePane.js
Normal file
@ -0,0 +1,90 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
|
||||
|
||||
import Dropdown from '../components/base/Dropdown'
|
||||
import Editor from '../components/base/Editor'
|
||||
import StatusTag from '../components/StatusTag'
|
||||
import SizeTag from '../components/SizeTag'
|
||||
import TimeTag from '../components/TimeTag'
|
||||
|
||||
class ResponsePane extends Component {
|
||||
render () {
|
||||
const {
|
||||
response
|
||||
} = this.props;
|
||||
|
||||
if (!response) {
|
||||
return (
|
||||
<section className="grid__cell section grid--v grid--start">
|
||||
<header className="header bg-light section__header"></header>
|
||||
<div className="section__body grid__cell"></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="grid__cell section">
|
||||
<div className="grid--v wide">
|
||||
<header
|
||||
className="grid grid--center header text-center bg-super-light txt-sm section__header">
|
||||
{!response ? null : (
|
||||
<div>
|
||||
<StatusTag
|
||||
statusCode={response.statusCode}
|
||||
statusMessage={response.statusMessage}
|
||||
/>
|
||||
<TimeTag milliseconds={response.millis}/>
|
||||
<SizeTag bytes={response.bytes}/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<Tabs className="grid__cell grid--v section__body">
|
||||
<TabList className="grid grid--start">
|
||||
<Tab className="no-wrap grid grid--center">
|
||||
<button>Preview</button>
|
||||
<Dropdown>
|
||||
<button><i className="fa fa-caret-down"></i></button>
|
||||
<ul>
|
||||
<li><button><i className="fa fa-eye"></i> Preview</button></li>
|
||||
<li><button><i className="fa fa-code"></i> Source</button></li>
|
||||
<li><button><i className="fa fa-file"></i> Raw</button></li>
|
||||
</ul>
|
||||
</Dropdown>
|
||||
</Tab>
|
||||
<Tab><button>Headers</button></Tab>
|
||||
</TabList>
|
||||
<TabPanel className="grid__cell editor-wrapper">
|
||||
<Editor
|
||||
value={response && response.body || ''}
|
||||
prettify={true}
|
||||
options={{
|
||||
mode: response && response.contentType || 'text/plain',
|
||||
readOnly: true,
|
||||
placeholder: 'nothing yet...'
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div className="wide">
|
||||
<div className="grid--v grid--start pad">
|
||||
{!response ? null : response.headers.map((h, i) => (
|
||||
<div className="grid grid__cell grid__cell--no-flex selectable" key={i}>
|
||||
<div className="grid__cell">{h.name}</div>
|
||||
<div className="grid__cell">{h.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ResponsePane.propTypes = {
|
||||
response: PropTypes.object
|
||||
};
|
||||
|
||||
export default ResponsePane;
|
@ -1,132 +1,83 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import WorkspaceDropdown from './../containers/WorkspaceDropdown'
|
||||
import RequestActionsDropdown from './../containers/RequestActionsDropdown'
|
||||
import RequestGroupActionsDropdown from './../containers/RequestGroupActionsDropdown'
|
||||
import DebouncingInput from './base/DebouncingInput'
|
||||
import MethodTag from './MethodTag'
|
||||
import * as db from '../database'
|
||||
import SidebarRequestGroupRow from './SidebarRequestGroupRow'
|
||||
import SidebarRequestRow from './SidebarRequestRow'
|
||||
|
||||
class Sidebar extends Component {
|
||||
onFilterChange (value) {
|
||||
this.props.changeFilter(value);
|
||||
}
|
||||
|
||||
renderRequestGroupRow (requestGroup = null) {
|
||||
const {
|
||||
activeFilter,
|
||||
activeRequest,
|
||||
addRequestToRequestGroup,
|
||||
toggleRequestGroup,
|
||||
requests
|
||||
} = this.props;
|
||||
|
||||
let filteredRequests = requests.filter(
|
||||
r => {
|
||||
// TODO: Move this to a lib file
|
||||
|
||||
if (!activeFilter) {
|
||||
_filterChildren (filter, children, extra = null) {
|
||||
return children.filter(child => {
|
||||
if (child.doc.type !== 'Request') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestGroupName = requestGroup ? requestGroup.name : '';
|
||||
const toMatch = `${requestGroupName}✌${r.method}✌${r.name}`.toLowerCase();
|
||||
const matchTokens = activeFilter.toLowerCase().split(' ');
|
||||
const request = child.doc;
|
||||
|
||||
const otherMatches = extra || '';
|
||||
const toMatch = `${request.method}❅${request.name}❅${otherMatches}`.toLowerCase();
|
||||
const matchTokens = filter.toLowerCase().split(' ');
|
||||
|
||||
for (let i = 0; i < matchTokens.length; i++) {
|
||||
let token = `${matchTokens[i]}`;
|
||||
if (toMatch.indexOf(token) === -1) {
|
||||
// Filter failed. Don't render children
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
if (!requestGroup) {
|
||||
filteredRequests = filteredRequests.filter(r => !r.parent);
|
||||
return filteredRequests.map(request => this.renderRequestRow(request));
|
||||
})
|
||||
}
|
||||
|
||||
// Grab all of the children for this request group
|
||||
filteredRequests = filteredRequests.filter(r => r.parent === requestGroup._id);
|
||||
_renderChildren (children, requestGroup) {
|
||||
const {filter} = this.props;
|
||||
|
||||
// Don't show folder if it was not in the filter
|
||||
if (activeFilter && !filteredRequests.length) {
|
||||
const filteredChildren = this._filterChildren(
|
||||
filter,
|
||||
children,
|
||||
requestGroup && requestGroup.name
|
||||
).sort((a, b) => a.doc._id > b.doc._id ? -1 : 1);
|
||||
|
||||
return filteredChildren.map(child => {
|
||||
if (child.doc.type === 'Request') {
|
||||
return (
|
||||
<SidebarRequestRow
|
||||
key={child.doc._id}
|
||||
activateRequest={this.props.activateRequest}
|
||||
isActive={child.doc._id === this.props.activeRequestId}
|
||||
request={child.doc}
|
||||
/>
|
||||
)
|
||||
} else if (child.doc.type === 'RequestGroup') {
|
||||
const requestGroup = child.doc;
|
||||
const isActive = !!child.children.find(c => c.doc._id === this.props.activeRequestId);
|
||||
|
||||
return (
|
||||
<SidebarRequestGroupRow
|
||||
key={requestGroup._id}
|
||||
isActive={isActive}
|
||||
hideIfNoChildren={filter}
|
||||
toggleRequestGroup={this.props.toggleRequestGroup}
|
||||
addRequestToRequestGroup={this.props.addRequestToRequestGroup}
|
||||
numChildren={child.children.length}
|
||||
requestGroup={requestGroup}
|
||||
>
|
||||
{this._renderChildren(child.children, requestGroup)}
|
||||
</SidebarRequestGroupRow>
|
||||
)
|
||||
} else {
|
||||
console.error('Unknown child type', child.doc.type);
|
||||
return null;
|
||||
}
|
||||
|
||||
const isActive = activeRequest && filteredRequests.find(r => r._id == activeRequest._id);
|
||||
|
||||
let folderIconClass = 'fa-folder';
|
||||
let expanded = !requestGroup.collapsed;
|
||||
folderIconClass += !expanded ? '' : '-open';
|
||||
folderIconClass += isActive ? '' : '-o';
|
||||
|
||||
const sidebarItemClassNames = classnames(
|
||||
'sidebar__item',
|
||||
'sidebar__item--bordered',
|
||||
{'sidebar__item--active': isActive}
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={requestGroup._id}>
|
||||
<div className={sidebarItemClassNames}>
|
||||
<div className="sidebar__item__row sidebar__item__row--heading">
|
||||
<button onClick={e => toggleRequestGroup(requestGroup)}>
|
||||
<i className={'fa ' + folderIconClass}></i>
|
||||
{requestGroup.name}
|
||||
</button>
|
||||
</div>
|
||||
<div className="sidebar__item__btn grid">
|
||||
<button onClick={(e) => addRequestToRequestGroup(requestGroup)}>
|
||||
<i className="fa fa-plus-circle"></i>
|
||||
</button>
|
||||
<RequestGroupActionsDropdown
|
||||
requestGroup={requestGroup}
|
||||
right={true}
|
||||
className="tall"/>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{expanded && !filteredRequests.length ? this.renderRequestRow() : null}
|
||||
{!expanded ? null : filteredRequests.map(request => this.renderRequestRow(request, requestGroup))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
renderRequestRow (request = null, requestGroup = null) {
|
||||
const {activeRequest, activateRequest} = this.props;
|
||||
const isActive = request && activeRequest && request._id === activeRequest._id;
|
||||
|
||||
return (
|
||||
<li key={request ? request._id : 'none'}>
|
||||
<div className={'sidebar__item ' + (isActive ? 'sidebar__item--active' : '')}>
|
||||
<div className="sidebar__item__row">
|
||||
{request ? (
|
||||
<button onClick={() => {activateRequest(request)}}>
|
||||
<MethodTag method={request.method}/> {request.name}
|
||||
</button>
|
||||
) : (
|
||||
<button className="italic">No Requests</button>
|
||||
)}
|
||||
</div>
|
||||
{request ? (
|
||||
<RequestActionsDropdown
|
||||
className="sidebar__item__btn"
|
||||
right={true}
|
||||
request={request}
|
||||
requestGroup={requestGroup}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {activeFilter, requestGroups} = this.props;
|
||||
const {filter, children} = this.props;
|
||||
|
||||
return (
|
||||
<section className="sidebar bg-dark grid--v section section--bordered">
|
||||
@ -136,8 +87,7 @@ class Sidebar extends Component {
|
||||
<div className="grid--v grid--start grid__cell section__body">
|
||||
<ul
|
||||
className="grid--v grid--start grid__cell sidebar__scroll hover-scrollbars sidebar__request-list">
|
||||
{this.renderRequestGroupRow(null)}
|
||||
{requestGroups.map(requestGroup => this.renderRequestGroupRow(requestGroup))}
|
||||
{this._renderChildren(children)}
|
||||
</ul>
|
||||
<div className="grid grid--center">
|
||||
<div className="grid__cell form-control form-control--underlined">
|
||||
@ -145,7 +95,7 @@ class Sidebar extends Component {
|
||||
type="text"
|
||||
placeholder="Filter Items"
|
||||
debounceMillis={300}
|
||||
value={activeFilter}
|
||||
value={filter}
|
||||
onChange={this.onFilterChange.bind(this)}/>
|
||||
</div>
|
||||
</div>
|
||||
@ -156,14 +106,19 @@ class Sidebar extends Component {
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
// Functions
|
||||
activateRequest: PropTypes.func.isRequired,
|
||||
toggleRequestGroup: PropTypes.func.isRequired,
|
||||
addRequestToRequestGroup: PropTypes.func.isRequired,
|
||||
changeFilter: PropTypes.func.isRequired,
|
||||
toggleRequestGroup: PropTypes.func.isRequired,
|
||||
activeFilter: PropTypes.string,
|
||||
requests: PropTypes.array.isRequired,
|
||||
requestGroups: PropTypes.array.isRequired,
|
||||
activeRequest: PropTypes.object
|
||||
|
||||
// Other
|
||||
children: PropTypes.array.isRequired,
|
||||
workspaceId: PropTypes.string.isRequired,
|
||||
|
||||
// Optional
|
||||
filter: PropTypes.string,
|
||||
activeRequestId: PropTypes.string
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
79
app/components/SidebarRequestGroupRow.js
Normal file
79
app/components/SidebarRequestGroupRow.js
Normal file
@ -0,0 +1,79 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import RequestGroupActionsDropdown from './../containers/RequestGroupActionsDropdown'
|
||||
import SidebarRequestRow from './SidebarRequestRow'
|
||||
|
||||
class SidebarRequestGroupRow extends Component {
|
||||
render () {
|
||||
const {
|
||||
children,
|
||||
hideIfNoChildren,
|
||||
requestGroup,
|
||||
isActive,
|
||||
toggleRequestGroup,
|
||||
addRequestToRequestGroup
|
||||
} = this.props;
|
||||
|
||||
// If we are supposed to have children, but aren't passed any, we are probably
|
||||
// filtering so don't render anything
|
||||
if (hideIfNoChildren && children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let folderIconClass = 'fa-folder';
|
||||
let expanded = !requestGroup.collapsed;
|
||||
folderIconClass += !expanded ? '' : '-open';
|
||||
folderIconClass += isActive ? '' : '-o';
|
||||
|
||||
const sidebarItemClassNames = classnames(
|
||||
'sidebar__item',
|
||||
'sidebar__item--bordered',
|
||||
{'sidebar__item--active': isActive}
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={requestGroup._id}>
|
||||
<div className={sidebarItemClassNames}>
|
||||
<div className="sidebar__item__row sidebar__item__row--heading">
|
||||
<button onClick={e => toggleRequestGroup(requestGroup)}>
|
||||
<i className={'fa ' + folderIconClass}></i>
|
||||
{requestGroup.name}
|
||||
</button>
|
||||
</div>
|
||||
<div className="sidebar__item__btn grid">
|
||||
<button onClick={(e) => addRequestToRequestGroup(requestGroup)}>
|
||||
<i className="fa fa-plus-circle"></i>
|
||||
</button>
|
||||
<RequestGroupActionsDropdown
|
||||
requestGroup={requestGroup}
|
||||
right={true}
|
||||
className="tall"/>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{!expanded || children.length > 0 ? null : (
|
||||
<SidebarRequestRow
|
||||
activateRequest={() => {}}
|
||||
isActive={false}
|
||||
request={null}
|
||||
/>
|
||||
)}
|
||||
{expanded ? children : null}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SidebarRequestGroupRow.propTypes = {
|
||||
// Functions
|
||||
toggleRequestGroup: PropTypes.func.isRequired,
|
||||
addRequestToRequestGroup: PropTypes.func.isRequired,
|
||||
|
||||
// Other
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
hideIfNoChildren: PropTypes.number.isRequired,
|
||||
requestGroup: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default SidebarRequestGroupRow;
|
47
app/components/SidebarRequestRow.js
Normal file
47
app/components/SidebarRequestRow.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import RequestActionsDropdown from './../containers/RequestActionsDropdown'
|
||||
import MethodTag from './MethodTag'
|
||||
|
||||
class SidebarRequestRow extends Component {
|
||||
render () {
|
||||
const {request, requestGroup, isActive, activateRequest} = this.props;
|
||||
|
||||
return (
|
||||
<li key={request ? request._id : 'none'}>
|
||||
<div className={'sidebar__item ' + (isActive ? 'sidebar__item--active' : '')}>
|
||||
<div className="sidebar__item__row">
|
||||
{request ? (
|
||||
<button onClick={() => {activateRequest(request)}}>
|
||||
<MethodTag method={request.method}/> {request.name}
|
||||
</button>
|
||||
) : (
|
||||
<button className="italic">No Requests</button>
|
||||
)}
|
||||
</div>
|
||||
{request ? (
|
||||
<RequestActionsDropdown
|
||||
className="sidebar__item__btn"
|
||||
right={true}
|
||||
request={request}
|
||||
requestGroup={requestGroup}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SidebarRequestRow.propTypes = {
|
||||
// Functions
|
||||
activateRequest: PropTypes.func.isRequired,
|
||||
|
||||
// Other
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
|
||||
// Optional
|
||||
requestGroup: PropTypes.object,
|
||||
request: PropTypes.object
|
||||
};
|
||||
|
||||
export default SidebarRequestRow;
|
@ -1,31 +1,20 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'
|
||||
|
||||
import Editor from '../components/base/Editor'
|
||||
import Prompts from './Prompts'
|
||||
import KeyValueEditor from '../components/base/KeyValueEditor'
|
||||
import RequestBodyEditor from '../components/RequestBodyEditor'
|
||||
import RequestAuthEditor from '../components/RequestAuthEditor'
|
||||
import RequestUrlBar from '../components/RequestUrlBar'
|
||||
import StatusTag from '../components/StatusTag'
|
||||
import SizeTag from '../components/SizeTag'
|
||||
import TimeTag from '../components/TimeTag'
|
||||
import Sidebar from '../components/Sidebar'
|
||||
import EnvironmentEditModal from '../components/EnvironmentEditModal'
|
||||
import RequestPane from '../components/RequestPane'
|
||||
import ResponsePane from '../components/ResponsePane'
|
||||
import Sidebar from '../components/Sidebar'
|
||||
|
||||
import * as GlobalActions from '../redux/modules/global'
|
||||
import * as RequestGroupActions from '../redux/modules/requestGroups'
|
||||
import * as RequestActions from '../redux/modules/requests'
|
||||
import * as ModalActions from '../redux/modules/modals'
|
||||
import * as TabActions from '../redux/modules/tabs'
|
||||
|
||||
import * as db from '../database'
|
||||
|
||||
// Don't inject component styles (use our own)
|
||||
Tabs.setUseDefaultStyles(false);
|
||||
|
||||
class App extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@ -35,183 +24,74 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_renderRequestPanel (actions, activeRequest, tabs) {
|
||||
if (!activeRequest) {
|
||||
return (
|
||||
<section className="grid__cell section section--bordered grid--v grid--start">
|
||||
<header className="header bg-super-light section__header"></header>
|
||||
<div className="section__body grid__cell"></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
_generateSidebarTree (parentId, entities) {
|
||||
const children = entities.filter(e => e.parentId === parentId);
|
||||
|
||||
return (
|
||||
<section className="grid__cell section section--bordered">
|
||||
<div className="grid--v wide">
|
||||
<div className="header section__header">
|
||||
<RequestUrlBar
|
||||
sendRequest={actions.requests.send}
|
||||
onUrlChange={url => {db.update(activeRequest, {url})}}
|
||||
onMethodChange={method => {db.update(activeRequest, {method})}}
|
||||
request={activeRequest}
|
||||
/>
|
||||
</div>
|
||||
<Tabs className="grid__cell grid--v section__body"
|
||||
onSelect={i => actions.tabs.select('request', i)}
|
||||
selectedIndex={tabs.request || 0}>
|
||||
<TabList className="grid grid--start">
|
||||
<Tab><button>Body</button></Tab>
|
||||
<Tab>
|
||||
<button className="no-wrap">
|
||||
Params {activeRequest.params.length ? `(${activeRequest.params.length})` : ''}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab><button>Auth</button></Tab>
|
||||
<Tab>
|
||||
<button className="no-wrap">
|
||||
Headers {activeRequest.headers.length ? `(${activeRequest.headers.length})` : ''}
|
||||
</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="grid__cell editor-wrapper">
|
||||
<RequestBodyEditor
|
||||
onChange={body => {db.update(activeRequest, {body})}}
|
||||
request={activeRequest}/>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div>
|
||||
<KeyValueEditor
|
||||
className="pad"
|
||||
namePlaceholder="name"
|
||||
valuePlaceholder="value"
|
||||
uniquenessKey={activeRequest._id}
|
||||
pairs={activeRequest.params}
|
||||
onChange={params => {db.update(activeRequest, {params})}}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div>
|
||||
<RequestAuthEditor
|
||||
className="pad"
|
||||
request={activeRequest}
|
||||
onChange={authentication => {db.update(activeRequest, {authentication})}}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div>
|
||||
<KeyValueEditor
|
||||
className="pad"
|
||||
namePlaceholder="My-Header"
|
||||
valuePlaceholder="Value"
|
||||
uniquenessKey={activeRequest._id}
|
||||
pairs={activeRequest.headers}
|
||||
onChange={headers => {db.update(activeRequest, {headers})}}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
if (children.length > 0) {
|
||||
return children.map(c => ({
|
||||
doc: c,
|
||||
children: this._generateSidebarTree(c._id, entities)
|
||||
}));
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
|
||||
_renderResponsePanel (actions, activeResponse, tabs) {
|
||||
if (!activeResponse) {
|
||||
return (
|
||||
<section className="grid__cell section grid--v grid--start">
|
||||
<header className="header bg-light section__header"></header>
|
||||
<div className="section__body grid__cell"></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="grid__cell section">
|
||||
<div className="grid--v wide">
|
||||
<header
|
||||
className="grid grid--center header text-center bg-super-light txt-sm section__header">
|
||||
{!activeResponse ? null : (
|
||||
<div>
|
||||
<StatusTag
|
||||
statusCode={activeResponse.statusCode}
|
||||
statusMessage={activeResponse.statusMessage}
|
||||
/>
|
||||
<TimeTag milliseconds={activeResponse.millis}/>
|
||||
<SizeTag bytes={activeResponse.bytes}/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<Tabs className="grid__cell grid--v section__body"
|
||||
onSelect={i => actions.tabs.select('response', i)}
|
||||
selectedIndex={tabs.response || 0}>
|
||||
<TabList className="grid grid--start">
|
||||
<Tab><button>Preview</button></Tab>
|
||||
<Tab><button>Raw</button></Tab>
|
||||
<Tab><button>Headers</button></Tab>
|
||||
</TabList>
|
||||
<TabPanel className="grid__cell editor-wrapper">
|
||||
<Editor
|
||||
value={activeResponse && activeResponse.body || ''}
|
||||
prettify={true}
|
||||
options={{
|
||||
mode: activeResponse && activeResponse.contentType || 'text/plain',
|
||||
readOnly: true,
|
||||
placeholder: 'nothing yet...'
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell editor-wrapper">
|
||||
<Editor
|
||||
value={activeResponse && activeResponse.body || ''}
|
||||
options={{
|
||||
lineWrapping: true,
|
||||
mode: 'text/plain',
|
||||
readOnly: true,
|
||||
placeholder: 'nothing yet...'
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="grid__cell grid__cell--scroll--v">
|
||||
<div className="wide">
|
||||
<div className="grid--v grid--start pad">
|
||||
{!activeResponse ? null : activeResponse.headers.map((h, i) => (
|
||||
<div className="grid grid__cell grid__cell--no-flex selectable" key={i}>
|
||||
<div className="grid__cell">{h.name}</div>
|
||||
<div className="grid__cell">{h.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {actions, requests, responses, requestGroups, tabs, modals} = this.props;
|
||||
const activeRequest = requests.all.find(r => r._id === requests.active);
|
||||
const activeResponse = activeRequest ? responses[activeRequest._id] : undefined;
|
||||
const {actions, modals, workspaces, requests, entities} = this.props;
|
||||
|
||||
// TODO: Factor this out into a selector
|
||||
let workspace = entities.workspaces[workspaces.activeId];
|
||||
if (!workspace) {
|
||||
workspace = entities.workspaces[Object.keys(entities.workspaces)[0]];
|
||||
}
|
||||
|
||||
const activeRequestId = workspace.activeRequestId;
|
||||
const activeRequest = activeRequestId ? entities.requests[activeRequestId] : null;
|
||||
|
||||
const responses = Object.keys(entities.responses).map(id => entities.responses[id]);
|
||||
const allRequests = Object.keys(entities.requests).map(id => entities.requests[id]);
|
||||
const allRequestGroups = Object.keys(entities.requestGroups).map(id => entities.requestGroups[id]);
|
||||
|
||||
const activeResponse = responses.sort(
|
||||
(a, b) => a._id > b._id ? -1 : 1
|
||||
).find(r => r.parentId === activeRequestId);
|
||||
|
||||
const children = this._generateSidebarTree(
|
||||
workspace._id,
|
||||
allRequests.concat(allRequestGroups)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid bg-super-dark tall">
|
||||
<Sidebar
|
||||
activateRequest={actions.requests.activate}
|
||||
workspaceId={workspace._id}
|
||||
activateRequest={r => db.update(workspace, {activeRequestId: r._id})}
|
||||
changeFilter={actions.requests.changeFilter}
|
||||
addRequestToRequestGroup={requestGroup => db.requestCreate({parent: requestGroup._id})}
|
||||
addRequestToRequestGroup={requestGroup => db.requestCreate({parentId: requestGroup._id})}
|
||||
toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})}
|
||||
activeRequest={activeRequest}
|
||||
activeFilter={requests.filter}
|
||||
requestGroups={requestGroups.all}
|
||||
requests={requests.all}/>
|
||||
activeRequestId={activeRequest ? activeRequest._id : null}
|
||||
filter={requests.filter}
|
||||
children={children}
|
||||
/>
|
||||
<div className="grid wide grid--collapse">
|
||||
{this._renderRequestPanel(actions, activeRequest, tabs)}
|
||||
{this._renderResponsePanel(actions, activeResponse, tabs)}
|
||||
<RequestPane
|
||||
request={activeRequest}
|
||||
sendRequest={actions.requests.send}
|
||||
updateRequestBody={body => db.update(activeRequest, {body})}
|
||||
updateRequestUrl={url => db.update(activeRequest, {url})}
|
||||
updateRequestMethod={method => db.update(activeRequest, {method})}
|
||||
updateRequestParams={params => db.update(activeRequest, {params})}
|
||||
updateRequestAuthentication={authentication => db.update(activeRequest, {authentication})}
|
||||
updateRequestHeaders={headers => db.update(activeRequest, {headers})}
|
||||
/>
|
||||
<ResponsePane
|
||||
response={activeResponse}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Prompts />
|
||||
|
||||
{modals.map(m => {
|
||||
if (m.id === EnvironmentEditModal.defaultProps.id) {
|
||||
return (
|
||||
@ -234,43 +114,36 @@ class App extends Component {
|
||||
App.propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
requests: PropTypes.shape({
|
||||
activate: PropTypes.func.isRequired,
|
||||
update: PropTypes.func.isRequired,
|
||||
remove: PropTypes.func.isRequired,
|
||||
send: PropTypes.func.isRequired,
|
||||
changeFilter: PropTypes.func.isRequired
|
||||
}),
|
||||
requestGroups: PropTypes.shape({
|
||||
remove: PropTypes.func.isRequired,
|
||||
update: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.func.isRequired
|
||||
}),
|
||||
modals: PropTypes.shape({
|
||||
hide: PropTypes.func.isRequired
|
||||
}),
|
||||
tabs: PropTypes.shape({
|
||||
select: PropTypes.func.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
requestGroups: PropTypes.shape({
|
||||
all: PropTypes.array.isRequired
|
||||
entities: PropTypes.shape({
|
||||
requests: PropTypes.object.isRequired,
|
||||
requestGroups: PropTypes.object.isRequired,
|
||||
responses: PropTypes.object.isRequired
|
||||
}).isRequired,
|
||||
workspaces: PropTypes.shape({
|
||||
activeId: PropTypes.string
|
||||
}).isRequired,
|
||||
requests: PropTypes.shape({
|
||||
all: PropTypes.array.isRequired,
|
||||
active: PropTypes.string // "required" but can be null
|
||||
filter: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
responses: PropTypes.object.isRequired,
|
||||
tabs: PropTypes.object.isRequired,
|
||||
modals: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
actions: state.actions,
|
||||
workspaces: state.workspaces,
|
||||
requests: state.requests,
|
||||
requestGroups: state.requestGroups,
|
||||
responses: state.responses,
|
||||
tabs: state.tabs,
|
||||
entities: state.entities,
|
||||
modals: state.modals
|
||||
};
|
||||
}
|
||||
@ -279,7 +152,6 @@ function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
actions: {
|
||||
global: bindActionCreators(GlobalActions, dispatch),
|
||||
tabs: bindActionCreators(TabActions, dispatch),
|
||||
modals: bindActionCreators(ModalActions, dispatch),
|
||||
requestGroups: bindActionCreators(RequestGroupActions, dispatch),
|
||||
requests: bindActionCreators(RequestActions, dispatch)
|
||||
|
@ -3,12 +3,14 @@ import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import * as ModalActions from '../redux/modules/modals'
|
||||
import * as RequestGroupActions from '../redux/modules/requestGroups'
|
||||
import * as RequestActions from '../redux/modules/requests'
|
||||
import PromptModal from '../components/base/PromptModal'
|
||||
|
||||
import * as db from '../database'
|
||||
import {MODAL_REQUEST_RENAME, MODAL_REQUEST_GROUP_RENAME} from '../lib/constants';
|
||||
import {
|
||||
MODAL_REQUEST_RENAME,
|
||||
MODAL_REQUEST_GROUP_RENAME,
|
||||
MODAL_WORKSPACE_RENAME
|
||||
} from '../lib/constants';
|
||||
|
||||
class Prompts extends Component {
|
||||
constructor (props) {
|
||||
@ -29,6 +31,14 @@ class Prompts extends Component {
|
||||
db.update(modal.data.requestGroup, {name})
|
||||
}
|
||||
};
|
||||
|
||||
this._prompts[MODAL_WORKSPACE_RENAME] = {
|
||||
header: 'Rename Workspace',
|
||||
submit: 'Rename',
|
||||
onSubmit: (modal, name) => {
|
||||
db.update(modal.data.workspace, {name})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -65,12 +75,6 @@ Prompts.propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
modals: PropTypes.shape({
|
||||
hide: PropTypes.func.isRequired
|
||||
}),
|
||||
requestGroups: PropTypes.shape({
|
||||
update: PropTypes.func.isRequired
|
||||
}),
|
||||
requests: PropTypes.shape({
|
||||
update: PropTypes.func.isRequired
|
||||
})
|
||||
}),
|
||||
modals: PropTypes.array.isRequired
|
||||
@ -86,9 +90,7 @@ function mapStateToProps (state) {
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
actions: {
|
||||
requests: bindActionCreators(RequestActions, dispatch),
|
||||
modals: bindActionCreators(ModalActions, dispatch),
|
||||
requestGroups: bindActionCreators(RequestGroupActions, dispatch)
|
||||
modals: bindActionCreators(ModalActions, dispatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class RequestActionsDropdown extends Component {
|
||||
<ul>
|
||||
<li>
|
||||
<button onClick={e => db.requestCopy(request)}>
|
||||
<i className="fa fa-copy"></i> Duplicate
|
||||
<i className="fa fa-copy"></i> Clone
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -38,8 +38,6 @@ class RequestGroupActionsDropdown extends Component {
|
||||
|
||||
RequestGroupActionsDropdown.propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
update: PropTypes.func.isRequired,
|
||||
remove: PropTypes.func.isRequired,
|
||||
showUpdateNamePrompt: PropTypes.func.isRequired,
|
||||
showEnvironmentEditModal: PropTypes.func.isRequired
|
||||
}),
|
||||
|
@ -8,6 +8,7 @@ import {connect} from 'react-redux'
|
||||
import Dropdown from '../components/base/Dropdown'
|
||||
import DropdownDivider from '../components/base/DropdownDivider'
|
||||
import * as RequestGroupActions from '../redux/modules/requestGroups'
|
||||
import * as WorkspaceActions from '../redux/modules/workspaces'
|
||||
import * as db from '../database'
|
||||
import importData from '../lib/import'
|
||||
|
||||
@ -20,24 +21,45 @@ class WorkspaceDropdown extends Component {
|
||||
}]
|
||||
};
|
||||
|
||||
// TODO: Factor this out into a selector
|
||||
const {entities, workspaces} = this.props;
|
||||
let workspace = entities.workspaces[workspaces.activeId];
|
||||
if (!workspace) {
|
||||
workspace = entities.workspaces[Object.keys(entities.workspaces)[0]];
|
||||
}
|
||||
|
||||
electron.remote.dialog.showOpenDialog(options, paths => {
|
||||
paths.map(path => {
|
||||
fs.readFile(path, 'utf8', (err, data) => {
|
||||
err || importData(data);
|
||||
err || importData(workspace, data);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
_workspaceCreate () {
|
||||
db.workspaceCreate({name: 'New Workspace'}).then(workspace => {
|
||||
this.props.actions.workspaces.activate(workspace);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {actions, loading, ...other} = this.props;
|
||||
const {actions, loading, workspaces, entities, ...other} = this.props;
|
||||
|
||||
const allWorkspaces = Object.keys(entities.workspaces).map(id => entities.workspaces[id]);
|
||||
|
||||
// TODO: Factor this out into a selector
|
||||
let workspace = entities.workspaces[workspaces.activeId];
|
||||
if (!workspace) {
|
||||
workspace = entities.workspaces[Object.keys(entities.workspaces)[0]];
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown right={true} {...other} className="block">
|
||||
<button className="btn header__content">
|
||||
<div className="grid grid--center">
|
||||
<div className="grid__cell">
|
||||
<h1 className="no-pad">Insomnia</h1>
|
||||
<h1 className="no-pad">{workspace.name}</h1>
|
||||
</div>
|
||||
<div className="no-wrap">
|
||||
{loading ? <i className="fa fa-refresh fa-spin txt-lg"></i> : ''}
|
||||
@ -50,45 +72,49 @@ class WorkspaceDropdown extends Component {
|
||||
<DropdownDivider name="Current Workspace"/>
|
||||
|
||||
<li>
|
||||
<button onClick={e => db.requestCreate()}>
|
||||
<button onClick={e => db.requestCreate({parentId: workspace._id})}>
|
||||
<i className="fa fa-plus-circle"></i> New Request
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={e => db.requestGroupCreate()}>
|
||||
<button onClick={e => db.requestGroupCreate({parentId: workspace._id})}>
|
||||
<i className="fa fa-folder"></i> New Request Group
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={e => actions.showEnvironmentEditModal()}>
|
||||
{/*<li>
|
||||
<button onClick={e => actions.requestGroups.showEnvironmentEditModal()}>
|
||||
<i className="fa fa-code"></i> Manage Environments
|
||||
</button>
|
||||
</li>
|
||||
</li>*/}
|
||||
<li>
|
||||
<button onClick={e => this._importDialog()}>
|
||||
<i className="fa fa-share-square-o"></i> Import/Export
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>
|
||||
<i className="fa fa-empty"></i> Delete <strong>Sendwithus</strong>
|
||||
<button onClick={e => actions.workspaces.showUpdateNamePrompt(workspace)}>
|
||||
<i className="fa fa-empty"></i> Rename <strong>{workspace.name}</strong>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={e => db.remove(workspace)}>
|
||||
<i className="fa fa-empty"></i> Delete <strong>{workspace.name}</strong>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<DropdownDivider name="Workspaces"/>
|
||||
|
||||
<li>
|
||||
<button>
|
||||
<i className="fa fa-random"></i> Switch to <strong>Sendwithus Track</strong>
|
||||
{allWorkspaces.map(w => {
|
||||
return w._id === workspace._id ? null : (
|
||||
<li key={w._id}>
|
||||
<button onClick={() => actions.workspaces.activate(w)}>
|
||||
<i className="fa fa-random"></i> Switch to <strong>{w.name}</strong>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li>
|
||||
<button>
|
||||
<i className="fa fa-random"></i> Switch to <strong>Default</strong>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>
|
||||
<button onClick={e => this._workspaceCreate()}>
|
||||
<i className="fa fa-blank"></i> Create Workspace
|
||||
</button>
|
||||
</li>
|
||||
@ -105,13 +131,27 @@ class WorkspaceDropdown extends Component {
|
||||
|
||||
WorkspaceDropdown.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
workspaces: PropTypes.shape({
|
||||
activeId: PropTypes.string
|
||||
}),
|
||||
entities: PropTypes.shape({
|
||||
workspaces: PropTypes.object.isRequired
|
||||
}).isRequired,
|
||||
actions: PropTypes.shape({
|
||||
requestGroups: PropTypes.shape({
|
||||
showEnvironmentEditModal: PropTypes.func.isRequired
|
||||
}),
|
||||
workspaces: PropTypes.shape({
|
||||
activate: PropTypes.func.isRequired,
|
||||
showUpdateNamePrompt: PropTypes.func.isRequired
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
workspaces: state.workspaces,
|
||||
entities: state.entities,
|
||||
actions: state.actions,
|
||||
loading: state.global.loading
|
||||
};
|
||||
@ -119,7 +159,10 @@ function mapStateToProps (state) {
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(RequestGroupActions, dispatch)
|
||||
actions: {
|
||||
requestGroups: bindActionCreators(RequestGroupActions, dispatch),
|
||||
workspaces: bindActionCreators(WorkspaceActions, dispatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,6 @@
|
||||
margin-bottom: 3px;
|
||||
border-radius: $radius-md;
|
||||
background: $bg-super-light;
|
||||
padding: $padding-xs;
|
||||
overflow: hidden;
|
||||
|
||||
li {
|
||||
@ -29,7 +28,7 @@
|
||||
padding: $padding-sm $padding-md $padding-sm $padding-sm;
|
||||
width: 100%;
|
||||
display: block;
|
||||
color: $font-super-light-bg;
|
||||
color: $font-super-light-bg !important;
|
||||
|
||||
&:hover {
|
||||
background: $hl-sm;
|
||||
|
@ -9,6 +9,7 @@ $border-color: $hl-md;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
height: $line-height-sm;
|
||||
line-height: $line-height-sm;
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
@ -21,12 +22,16 @@ $border-color: $hl-md;
|
||||
|
||||
.ReactTabs__Tab {
|
||||
align-self: flex-start;
|
||||
padding: $padding-sm / 4 $padding-md;
|
||||
height: $line-height-sm;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 1px solid $border-color;
|
||||
border-top: 0 !important;
|
||||
|
||||
* {
|
||||
color: $hl-xxl;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-left-color: transparent;
|
||||
}
|
||||
@ -35,26 +40,32 @@ $border-color: $hl-md;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
color: $hl-xxl;
|
||||
position: relative;
|
||||
& > * {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
padding-left: $padding-md / 4;
|
||||
padding-right: $padding-md / 4;
|
||||
|
||||
&:first-child {
|
||||
padding-left: $padding-md;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: $padding-md;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ReactTabs__Tab--selected {
|
||||
border: 1px solid $border-color;
|
||||
border-bottom-color: transparent;
|
||||
|
||||
button {
|
||||
* {
|
||||
color: inherit;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
& > button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,10 @@ $modal-width: 50rem;
|
||||
$breakpoint-md: 790px;
|
||||
$breakpoint-sm: 580px;
|
||||
|
||||
.txt-xs {
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
|
||||
.txt-sm {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ h3 {
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
padding-top: 1.5em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
hr {
|
||||
@ -40,6 +40,11 @@ hr {
|
||||
margin: $padding-md 0;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $hl-xxl;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
@ -2,6 +2,11 @@
|
||||
import * as methods from '../lib/constants';
|
||||
import {generateId} from './util'
|
||||
|
||||
export const TYPE_WORKSPACE = 'Workspace';
|
||||
export const TYPE_REQUEST_GROUP = 'RequestGroup';
|
||||
export const TYPE_REQUEST = 'Request';
|
||||
export const TYPE_RESPONSE = 'Response';
|
||||
|
||||
// We have to include the web version of PouchDB in app.html because
|
||||
// the NodeJS version defaults to LevelDB which is hard (impossible?)
|
||||
// to get working in Electron apps
|
||||
@ -10,19 +15,47 @@ let db = new PouchDB('insomnia.db', {adapter: 'websql'});
|
||||
// For browser console debugging
|
||||
global.db = db;
|
||||
|
||||
export let changes = db.changes({
|
||||
let changeListeners = {};
|
||||
|
||||
export function onChange (id, callback) {
|
||||
console.log(`-- Added DB Listener ${id} -- `);
|
||||
changeListeners[id] = callback;
|
||||
}
|
||||
|
||||
export function offChange (id) {
|
||||
console.log(`-- Removed DB Listener ${id} -- `);
|
||||
delete changeListeners[id];
|
||||
}
|
||||
|
||||
export function allDocs () {
|
||||
return db.allDocs({include_docs: true});
|
||||
}
|
||||
|
||||
db.changes({
|
||||
since: 'now',
|
||||
live: true,
|
||||
include_docs: true,
|
||||
return_docs: false
|
||||
}).on('change', function (res) {
|
||||
Object.keys(changeListeners).map(id => changeListeners[id](res))
|
||||
}).on('complete', function (info) {
|
||||
console.log('complete', info);
|
||||
}).on('error', function (err) {
|
||||
console.log('error', err);
|
||||
});
|
||||
|
||||
export function allDocs () {
|
||||
return db.allDocs({include_docs: true});
|
||||
/**
|
||||
* Initialize the database. This should be called once on app start.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function initDB () {
|
||||
console.log('-- Initializing Database --');
|
||||
return Promise.all([
|
||||
db.createIndex({index: {fields: ['parentId']}}),
|
||||
db.createIndex({index: {fields: ['type']}})
|
||||
]).catch(err => {
|
||||
console.error('Failed to PouchDB Indexes', err);
|
||||
});
|
||||
}
|
||||
|
||||
export function get (id) {
|
||||
@ -40,6 +73,7 @@ export function update (doc, patch = {}) {
|
||||
return db.put(updatedDoc).catch(e => {
|
||||
if (e.status === 409) {
|
||||
console.warn('Retrying document update for', updatedDoc);
|
||||
|
||||
get(doc._id).then(dbDoc => {
|
||||
update(dbDoc, patch);
|
||||
});
|
||||
@ -47,8 +81,20 @@ export function update (doc, patch = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getChildren (doc) {
|
||||
const parentId = doc._id;
|
||||
return db.find({selector: {parentId}});
|
||||
}
|
||||
|
||||
export function removeChildren (doc) {
|
||||
return getChildren(doc).then(res => res.docs.map(remove));
|
||||
}
|
||||
|
||||
export function remove (doc) {
|
||||
return update(doc, {_deleted: true});
|
||||
return Promise.all([
|
||||
update(doc, {_deleted: true}),
|
||||
removeChildren(doc)
|
||||
]);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~ //
|
||||
@ -56,7 +102,12 @@ export function remove (doc) {
|
||||
// ~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
function modelCreate (type, idPrefix, defaults, patch = {}) {
|
||||
const model = Object.assign(
|
||||
const baseDefaults = {
|
||||
parentId: null
|
||||
};
|
||||
|
||||
const doc = Object.assign(
|
||||
baseDefaults,
|
||||
defaults,
|
||||
patch,
|
||||
|
||||
@ -70,33 +121,30 @@ function modelCreate (type, idPrefix, defaults, patch = {}) {
|
||||
}
|
||||
);
|
||||
|
||||
update(model);
|
||||
|
||||
return model;
|
||||
return update(doc).then(() => doc);
|
||||
}
|
||||
|
||||
|
||||
// ~~~~~~~ //
|
||||
// REQUEST //
|
||||
// ~~~~~~~ //
|
||||
|
||||
export function requestCreate (patch = {}) {
|
||||
return modelCreate('Request', 'req', {
|
||||
return modelCreate(TYPE_REQUEST, 'req', {
|
||||
url: '',
|
||||
name: 'New Request',
|
||||
method: methods.METHOD_GET,
|
||||
activated: Date.now(),
|
||||
body: '',
|
||||
params: [],
|
||||
contentType: 'text/plain',
|
||||
headers: [],
|
||||
authentication: {},
|
||||
parent: null
|
||||
authentication: {}
|
||||
}, patch);
|
||||
}
|
||||
|
||||
export function requestCopy (originalRequest) {
|
||||
const name = `${originalRequest.name} (Copy)`;
|
||||
return requestCreate(Object.assign({}, originalRequest, {name}));
|
||||
export function requestCopy (request) {
|
||||
const name = `${request.name} (Copy)`;
|
||||
return requestCreate(Object.assign({}, request, {name}));
|
||||
}
|
||||
|
||||
|
||||
@ -105,22 +153,19 @@ export function requestCopy (originalRequest) {
|
||||
// ~~~~~~~~~~~~~ //
|
||||
|
||||
export function requestGroupCreate (patch = {}) {
|
||||
return modelCreate('RequestGroup', 'grp', {
|
||||
return modelCreate(TYPE_REQUEST_GROUP, 'grp', {
|
||||
collapsed: false,
|
||||
name: 'New Request Group',
|
||||
environment: {},
|
||||
parent: null
|
||||
environment: {}
|
||||
}, patch);
|
||||
}
|
||||
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// RESPONSE //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
export function responseCreate (patch = {}) {
|
||||
return modelCreate('Response', 'rsp', {
|
||||
requestId: null,
|
||||
return modelCreate(TYPE_RESPONSE, 'res', {
|
||||
statusCode: 0,
|
||||
statusMessage: '',
|
||||
contentType: 'text/plain',
|
||||
@ -131,32 +176,44 @@ export function responseCreate (patch = {}) {
|
||||
}, patch);
|
||||
}
|
||||
|
||||
db.createIndex({
|
||||
index: {fields: ['requestId']}
|
||||
}).catch(err => {
|
||||
console.error('Failed to create index', err);
|
||||
}).then(() => {
|
||||
console.log('-- Indexes Updated --');
|
||||
});
|
||||
|
||||
export function responseGetForRequest (request) {
|
||||
return db.find({
|
||||
selector: {
|
||||
requestId: request._id
|
||||
},
|
||||
sort: [{requestId: 'desc'}],
|
||||
limit: 1
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ~~~~~~~~~ //
|
||||
// WORKSPACE //
|
||||
// ~~~~~~~~~ //
|
||||
|
||||
export function workspaceCreate (patch = {}) {
|
||||
return modelCreate('Workspace', 'wsp', {
|
||||
name: 'New Request Group',
|
||||
return modelCreate(TYPE_WORKSPACE, 'wrk', {
|
||||
name: 'New Workspace',
|
||||
activeRequestId: null,
|
||||
environments: []
|
||||
}, patch);
|
||||
}
|
||||
|
||||
export function workspaceAll () {
|
||||
return db.find({
|
||||
selector: {type: 'Workspace'}
|
||||
}).then(res => {
|
||||
if (res.docs.length) {
|
||||
return res;
|
||||
} else {
|
||||
// No workspaces? Create first one and try again
|
||||
// TODO: Replace this with UI flow maybe?
|
||||
console.log('-- Creating First Workspace --');
|
||||
return workspaceCreate({name: 'Insomnia'}).then(() => {
|
||||
return workspaceAll();
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// SETTINGS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
// TODO: This
|
||||
// export function settingsCreate (patch = {}) {
|
||||
// return modelCreate('Settings', 'set', {
|
||||
// editorLineWrapping: false,
|
||||
// editorLineNumbers: true
|
||||
// }, patch);
|
||||
// }
|
||||
|
@ -5,7 +5,7 @@ const CHARS = '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'.split('')
|
||||
export function generateId (prefix) {
|
||||
let id = `${prefix}/${Date.now()}/`;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 13; i++) {
|
||||
id += CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
}
|
||||
|
||||
|
68
app/index.js
68
app/index.js
@ -1,75 +1,31 @@
|
||||
import React from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import {Provider} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import {Tabs} from 'react-tabs'
|
||||
|
||||
import createStore from './redux/create'
|
||||
import App from './containers/App'
|
||||
|
||||
import * as RequestGroupActions from './redux/modules/requestGroups'
|
||||
import * as RequestActions from './redux/modules/requests'
|
||||
import * as ResponseActions from './redux/modules/responses'
|
||||
import * as db from './database'
|
||||
|
||||
// Global CSS
|
||||
import './css/index.scss'
|
||||
import './css/lib/chrome/platform_app.css'
|
||||
import './css/lib/fontawesome/css/font-awesome.css'
|
||||
import {initStore} from './redux/initstore'
|
||||
import {initDB} from './database'
|
||||
|
||||
const store = createStore();
|
||||
// Don't inject component styles (use our own)
|
||||
Tabs.setUseDefaultStyles(false);
|
||||
|
||||
// Dispatch the initial load of data
|
||||
console.log('-- Init Insomnia --');
|
||||
export const store = createStore();
|
||||
|
||||
const actionFns = {
|
||||
RequestGroup: bindActionCreators(RequestGroupActions, store.dispatch),
|
||||
Request: bindActionCreators(RequestActions, store.dispatch),
|
||||
Response: bindActionCreators(ResponseActions, store.dispatch)
|
||||
};
|
||||
|
||||
function refreshDoc (doc) {
|
||||
const fns = actionFns[doc.type];
|
||||
|
||||
if (fns) {
|
||||
fns[doc._deleted ? 'remove' : 'update'](doc);
|
||||
} else if (doc.hasOwnProperty('type')) {
|
||||
console.warn('Unknown change', doc.type, doc);
|
||||
} else {
|
||||
// Probably a design doc update or something...
|
||||
}
|
||||
}
|
||||
|
||||
function watchDB () {
|
||||
console.log('-- Watching PouchDB --');
|
||||
|
||||
let buffer = [];
|
||||
let timeout = null;
|
||||
|
||||
// Debounce and buffer changes if they happen in quick succession
|
||||
db.changes.on('change', (response) => {
|
||||
const doc = response.doc;
|
||||
|
||||
buffer.push(doc);
|
||||
clearTimeout(timeout);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
buffer.map(refreshDoc);
|
||||
buffer = [];
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreDB() {
|
||||
db.allDocs().then(response => {
|
||||
response.rows.map(row => refreshDoc(row.doc));
|
||||
})
|
||||
}
|
||||
|
||||
watchDB();
|
||||
restoreDB();
|
||||
console.log('-- Loading App --');
|
||||
|
||||
initDB()
|
||||
.then(() => initStore(store.dispatch))
|
||||
.then(() => {
|
||||
console.log('-- Rendering App --');
|
||||
render(
|
||||
<Provider store={store}><App /></Provider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
});
|
||||
|
@ -17,6 +17,7 @@ export const METHODS = [
|
||||
METHOD_HEAD
|
||||
];
|
||||
|
||||
export const MODAL_REQUEST_RENAME = 'request.update.name';
|
||||
export const MODAL_WORKSPACE_RENAME = 'workspace.update.name';
|
||||
export const MODAL_REQUEST_GROUP_RENAME = 'requestgroup.update.name';
|
||||
export const MODAL_REQUEST_RENAME = 'request.update.name';
|
||||
export const MODAL_ENVIRONMENT_EDITOR = 'environment.edit';
|
||||
|
@ -4,15 +4,17 @@ const TYPE_REQUEST = 'request';
|
||||
const TYPE_REQUEST_GROUP = 'request_group';
|
||||
const FORMAT_MAP = {
|
||||
'json': 'application/json'
|
||||
// TODO: Fill these out
|
||||
};
|
||||
|
||||
function importRequestGroup (iRequestGroup, exportFormat) {
|
||||
function importRequestGroup (iRequestGroup, parentId, exportFormat) {
|
||||
if (exportFormat === 1) {
|
||||
const requestGroup = db.requestGroupCreate({
|
||||
db.requestGroupCreate({
|
||||
parentId,
|
||||
collapsed: true,
|
||||
name: iRequestGroup.name,
|
||||
environment: (iRequestGroup.environments || {}).base || {}
|
||||
});
|
||||
|
||||
}).then(requestGroup => {
|
||||
// Sometimes (maybe all the time, I can't remember) requests will be nested
|
||||
if (iRequestGroup.hasOwnProperty('requests')) {
|
||||
// Let's process them oldest to newest
|
||||
@ -21,10 +23,11 @@ function importRequestGroup (iRequestGroup, exportFormat) {
|
||||
r => importRequest(r, requestGroup._id, exportFormat)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function importRequest (iRequest, parent, exportFormat) {
|
||||
function importRequest (iRequest, parentId, exportFormat) {
|
||||
if (exportFormat === 1) {
|
||||
let auth = {};
|
||||
if (iRequest.authentication.username) {
|
||||
@ -35,6 +38,8 @@ function importRequest (iRequest, parent, exportFormat) {
|
||||
}
|
||||
|
||||
db.requestCreate({
|
||||
parentId,
|
||||
activated: 0, // Don't activate imported requests
|
||||
name: iRequest.name,
|
||||
url: iRequest.url,
|
||||
method: iRequest.method,
|
||||
@ -42,31 +47,32 @@ function importRequest (iRequest, parent, exportFormat) {
|
||||
headers: iRequest.headers || [],
|
||||
params: iRequest.params || [],
|
||||
contentType: FORMAT_MAP[iRequest.__insomnia.format] || 'text/plain',
|
||||
authentication: auth,
|
||||
parent: parent
|
||||
authentication: auth
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function (txt, callback) {
|
||||
export default function (workspace, txt) {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(txt);
|
||||
} catch (e) {
|
||||
return callback(new Error('Invalid Insomnia export'));
|
||||
// TODO: Handle these errors
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!data.hasOwnProperty('_type') || !data.hasOwnProperty('items')) {
|
||||
return callback(new Error('Invalid Insomnia export'));
|
||||
// TODO: Handle these errors
|
||||
return;
|
||||
}
|
||||
|
||||
data.items.filter(i => i._type === TYPE_REQUEST_GROUP).map(
|
||||
rg => importRequestGroup(rg, data.__export_format)
|
||||
data.items.reverse().filter(i => i._type === TYPE_REQUEST_GROUP).map(
|
||||
rg => importRequestGroup(rg, workspace._id, data.__export_format)
|
||||
);
|
||||
|
||||
data.items.filter(i => i._type === TYPE_REQUEST).map(
|
||||
r => importRequest(r, data.__export_format)
|
||||
data.items.reverse().filter(i => i._type === TYPE_REQUEST).map(
|
||||
r => importRequest(r, workspace._id, data.__export_format)
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import networkRequest from 'request'
|
||||
import render from './render'
|
||||
import * as db from '../database'
|
||||
|
||||
function makeRequest (unrenderedRequest, callback, context = {}) {
|
||||
function actuallySend (unrenderedRequest, callback, context = {}) {
|
||||
// SNEAKY HACK: Render nested object by converting it to JSON then rendering
|
||||
const template = JSON.stringify(unrenderedRequest);
|
||||
const request = JSON.parse(render(template, context));
|
||||
@ -35,6 +35,7 @@ function makeRequest (unrenderedRequest, callback, context = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this needs to account for existing URL params
|
||||
config.url += request.params.map((p, i) => {
|
||||
const name = encodeURIComponent(p.name);
|
||||
const value = encodeURIComponent(p.value);
|
||||
@ -47,7 +48,7 @@ function makeRequest (unrenderedRequest, callback, context = {}) {
|
||||
console.error('Request Failed', err, response);
|
||||
} else {
|
||||
db.responseCreate({
|
||||
requestId: request._id,
|
||||
parentId: request._id,
|
||||
statusCode: response.statusCode,
|
||||
statusMessage: response.statusMessage,
|
||||
contentType: response.headers['content-type'],
|
||||
@ -65,12 +66,12 @@ function makeRequest (unrenderedRequest, callback, context = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export default function (request, callback) {
|
||||
if (request.parent) {
|
||||
db.get(request.parent).then(
|
||||
requestGroup => makeRequest(request, callback, requestGroup.environment)
|
||||
export function send (request, callback) {
|
||||
if (request.parentId) {
|
||||
db.get(request.parentId).then(
|
||||
requestGroup => actuallySend(request, callback, requestGroup.environment)
|
||||
);
|
||||
} else {
|
||||
makeRequest(request, callback)
|
||||
actuallySend(request, callback)
|
||||
}
|
||||
}
|
@ -3,27 +3,22 @@ import thunkMiddleware from 'redux-thunk'
|
||||
import createLogger from 'redux-logger'
|
||||
import rootReducer from './reducer'
|
||||
|
||||
const loggerMiddleware = createLogger({
|
||||
collapsed: true
|
||||
});
|
||||
|
||||
export default function configureStore (initialState) {
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
applyMiddleware(
|
||||
thunkMiddleware,
|
||||
loggerMiddleware
|
||||
createLogger({collapsed: true})
|
||||
)
|
||||
);
|
||||
|
||||
if (module.hot) {
|
||||
// Enable Webpack hot module replacement for reducers
|
||||
module.hot.accept('./reducer', () => {
|
||||
const nextReducer = require('./reducer').default;
|
||||
const nextReducer = require('./reducer.js').default;
|
||||
store.replaceReducer(nextReducer);
|
||||
})
|
||||
}
|
||||
|
||||
return store
|
||||
return store;
|
||||
}
|
||||
|
48
app/redux/initstore.js
Normal file
48
app/redux/initstore.js
Normal file
@ -0,0 +1,48 @@
|
||||
import {bindActionCreators} from 'redux'
|
||||
import * as entitiesActions from './modules/entities'
|
||||
import * as db from '../database'
|
||||
|
||||
const CHANGE_ID = 'store.listener';
|
||||
|
||||
export function initStore (dispatch) {
|
||||
db.offChange(CHANGE_ID);
|
||||
|
||||
// New stuff...
|
||||
const entities = bindActionCreators(entitiesActions, dispatch);
|
||||
|
||||
const docChanged = doc => {
|
||||
if (!doc.hasOwnProperty('type')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// New stuff...
|
||||
entities[doc._deleted ? 'remove' : 'update'](doc);
|
||||
};
|
||||
|
||||
console.log('-- Restoring Store --');
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
return db.workspaceAll().then(res => {
|
||||
const restoreChildren = (doc) => {
|
||||
docChanged(doc);
|
||||
|
||||
return db.getChildren(doc).then(res => {
|
||||
// Done condition
|
||||
if (!res.docs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
res.docs.map(doc => restoreChildren(doc))
|
||||
);
|
||||
})
|
||||
};
|
||||
|
||||
return res.docs.map(restoreChildren)
|
||||
}).then(() => {
|
||||
console.log(`Restore took ${(Date.now() - start) / 1000} s`);
|
||||
}).then(() => {
|
||||
db.onChange(CHANGE_ID, res => docChanged(res.doc));
|
||||
});
|
||||
}
|
69
app/redux/modules/entities.js
Normal file
69
app/redux/modules/entities.js
Normal file
@ -0,0 +1,69 @@
|
||||
import {combineReducers} from 'redux'
|
||||
|
||||
import {TYPE_WORKSPACE, TYPE_REQUEST_GROUP, TYPE_REQUEST, TYPE_RESPONSE} from '../../database/index'
|
||||
import * as workspaceFns from './workspaces'
|
||||
|
||||
const ENTITY_UPDATE = 'entities/update';
|
||||
const ENTITY_REMOVE = 'entities/remove';
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// REDUCERS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
function genericEntityReducer (referenceName) {
|
||||
return function (state = {}, action) {
|
||||
const doc = action[referenceName];
|
||||
|
||||
if (!doc) {
|
||||
return state;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case ENTITY_UPDATE:
|
||||
return {...state, [doc._id]: doc};
|
||||
|
||||
case ENTITY_REMOVE:
|
||||
const newState = Object.assign({}, state);
|
||||
delete newState[action[referenceName]._id];
|
||||
return newState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
workspaces: genericEntityReducer('workspace'),
|
||||
requestGroups: genericEntityReducer('requestGroup'),
|
||||
requests: genericEntityReducer('request'),
|
||||
responses: genericEntityReducer('response')
|
||||
})
|
||||
|
||||
|
||||
// ~~~~~~~ //
|
||||
// ACTIONS //
|
||||
// ~~~~~~~ //
|
||||
|
||||
const updateFns = {
|
||||
[TYPE_WORKSPACE]: workspace => ({type: ENTITY_UPDATE, workspace}),
|
||||
[TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_UPDATE, requestGroup}),
|
||||
[TYPE_RESPONSE]: response => ({type: ENTITY_UPDATE, response}),
|
||||
[TYPE_REQUEST]: request => ({type: ENTITY_UPDATE, request})
|
||||
};
|
||||
|
||||
const removeFns = {
|
||||
[TYPE_WORKSPACE]: workspace => ({type: ENTITY_REMOVE, workspace}),
|
||||
[TYPE_REQUEST_GROUP]: requestGroup => ({type: ENTITY_REMOVE, requestGroup}),
|
||||
[TYPE_RESPONSE]: response => ({type: ENTITY_UPDATE, response}),
|
||||
[TYPE_REQUEST]: request => ({type: ENTITY_REMOVE, request})
|
||||
};
|
||||
|
||||
export function update (doc) {
|
||||
return updateFns[doc.type](doc);
|
||||
}
|
||||
|
||||
export function remove (doc) {
|
||||
return removeFns[doc.type](doc);
|
||||
}
|
@ -1,58 +1,20 @@
|
||||
import {combineReducers} from 'redux'
|
||||
|
||||
import {show} from './modals'
|
||||
import {MODAL_ENVIRONMENT_EDITOR, MODAL_REQUEST_GROUP_RENAME} from '../../lib/constants';
|
||||
import {MODAL_ENVIRONMENT_EDITOR, MODAL_REQUEST_GROUP_RENAME} from '../../lib/constants'
|
||||
|
||||
export const REQUEST_GROUP_UPDATE = 'requestgroups/update';
|
||||
export const REQUEST_GROUP_DELETE = 'requestgroups/delete';
|
||||
export const REQUEST_GROUP_TOGGLE = 'requestgroups/toggle';
|
||||
export const REQUEST_GROUP_TOGGLE = 'request-groups/toggle';
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// REDUCERS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
function allReducer (state = [], action) {
|
||||
switch (action.type) {
|
||||
|
||||
case REQUEST_GROUP_UPDATE:
|
||||
const i = state.findIndex(r => r._id === action.requestGroup._id);
|
||||
|
||||
if (i === -1) {
|
||||
return [action.requestGroup, ...state];
|
||||
} else {
|
||||
return [...state.slice(0, i), action.requestGroup, ...state.slice(i + 1)]
|
||||
}
|
||||
|
||||
case REQUEST_GROUP_TOGGLE:
|
||||
return state.map(
|
||||
rg => rg._id === action._id ? Object.assign({}, rg, {collapsed: !rg.collapsed}) : rg
|
||||
);
|
||||
|
||||
case REQUEST_GROUP_DELETE:
|
||||
return state.filter(rg => rg._id !== action.requestGroup._id);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
all: allReducer
|
||||
});
|
||||
// Nothing yet...
|
||||
|
||||
|
||||
// ~~~~~~~ //
|
||||
// ACTIONS //
|
||||
// ~~~~~~~ //
|
||||
|
||||
export function update (requestGroup) {
|
||||
return {type: REQUEST_GROUP_UPDATE, requestGroup};
|
||||
}
|
||||
|
||||
export function remove (requestGroup) {
|
||||
return {type: REQUEST_GROUP_DELETE, requestGroup};
|
||||
}
|
||||
|
||||
export function toggle (requestGroup) {
|
||||
return {type: REQUEST_GROUP_TOGGLE, requestGroup}
|
||||
}
|
||||
|
@ -1,86 +1,35 @@
|
||||
import {combineReducers} from 'redux'
|
||||
|
||||
import makeRequest from '../../lib/request'
|
||||
import * as network from '../../lib/network'
|
||||
import {loadStart, loadStop} from './global'
|
||||
import {show} from './modals'
|
||||
import {MODAL_REQUEST_RENAME} from '../../lib/constants'
|
||||
|
||||
export const REQUEST_UPDATE = 'requests/update';
|
||||
export const REQUEST_DELETE = 'requests/delete';
|
||||
export const REQUEST_ACTIVATE = 'requests/activate';
|
||||
export const REQUEST_CHANGE_FILTER = 'requests/filter';
|
||||
|
||||
const initialState = {
|
||||
filter: ''
|
||||
};
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// REDUCERS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
function allReducer (state = [], action) {
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case REQUEST_DELETE:
|
||||
return state.filter(r => r._id !== action.request._id);
|
||||
|
||||
case REQUEST_UPDATE:
|
||||
const i = state.findIndex(r => r._id === action.request._id);
|
||||
|
||||
if (i === -1) {
|
||||
return [action.request, ...state];
|
||||
} else {
|
||||
return [...state.slice(0, i), action.request, ...state.slice(i + 1)]
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function activeReducer (state = null, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case REQUEST_ACTIVATE:
|
||||
return action.request._id;
|
||||
|
||||
case REQUEST_DELETE:
|
||||
return state === action._id ? null : state;
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function filterReducer (state = '', action) {
|
||||
switch (action.type) {
|
||||
case REQUEST_CHANGE_FILTER:
|
||||
return action.filter;
|
||||
const filter = action.filter;
|
||||
return Object.assign({}, state, {filter});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
all: allReducer,
|
||||
filter: filterReducer,
|
||||
active: activeReducer
|
||||
});
|
||||
|
||||
|
||||
// ~~~~~~~ //
|
||||
// ACTIONS //
|
||||
// ~~~~~~~ //
|
||||
|
||||
export function remove (request) {
|
||||
return {type: REQUEST_DELETE, request};
|
||||
}
|
||||
|
||||
export function update (request) {
|
||||
return {type: REQUEST_UPDATE, request};
|
||||
}
|
||||
|
||||
export function activate (request) {
|
||||
return {type: REQUEST_ACTIVATE, request};
|
||||
}
|
||||
|
||||
export function changeFilter (filter) {
|
||||
return {type: REQUEST_CHANGE_FILTER, filter};
|
||||
}
|
||||
@ -89,7 +38,7 @@ export function send (request) {
|
||||
return dispatch => {
|
||||
dispatch(loadStart());
|
||||
|
||||
makeRequest(request, () => {
|
||||
network.send(request, () => {
|
||||
dispatch(loadStop());
|
||||
});
|
||||
}
|
||||
|
@ -1,29 +1,12 @@
|
||||
const RESPONSE_UPDATE = 'responses/update';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// REDUCERS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case RESPONSE_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
[action.response.requestId]: action.response
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
// None yet
|
||||
|
||||
|
||||
// ~~~~~~~ //
|
||||
// ACTIONS //
|
||||
// ~~~~~~~ //
|
||||
|
||||
export function update (response) {
|
||||
return {type: RESPONSE_UPDATE, response};
|
||||
}
|
||||
// None yet...
|
||||
|
@ -1,39 +1,26 @@
|
||||
import {combineReducers} from 'redux'
|
||||
import {MODAL_WORKSPACE_RENAME} from '../../lib/constants'
|
||||
import {show} from './modals'
|
||||
|
||||
export const WORKSPACE_UPDATE = 'workspaces/update';
|
||||
export const WORKSPACE_DELETE = 'workspaces/delete';
|
||||
export const WORKSPACE_ACTIVATE = 'workspaces/activate';
|
||||
|
||||
// ~~~~~~~~ //
|
||||
// REDUCERS //
|
||||
// ~~~~~~~~ //
|
||||
|
||||
function allReducer (state = [], action) {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function activeReducer (state = null, action) {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
|
||||
}
|
||||
}
|
||||
case WORKSPACE_ACTIVATE:
|
||||
return action.workspace._id;
|
||||
|
||||
function filterReducer (state = '', action) {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
all: allReducer,
|
||||
filter: filterReducer,
|
||||
active: activeReducer
|
||||
activeId: activeReducer
|
||||
});
|
||||
|
||||
|
||||
@ -41,15 +28,11 @@ export default combineReducers({
|
||||
// ACTIONS //
|
||||
// ~~~~~~~ //
|
||||
|
||||
export function remove (request) {
|
||||
return {type: WORKSPACE_DELETE, request};
|
||||
export function activate (workspace) {
|
||||
return {type: WORKSPACE_ACTIVATE, workspace};
|
||||
}
|
||||
|
||||
export function update (request) {
|
||||
return {type: WORKSPACE_UPDATE, request};
|
||||
export function showUpdateNamePrompt (workspace) {
|
||||
const defaultValue = workspace.name;
|
||||
return show(MODAL_WORKSPACE_RENAME, {defaultValue, workspace});
|
||||
}
|
||||
|
||||
export function activate (request) {
|
||||
return {type: WORKSPACE_ACTIVATE, request};
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
import {combineReducers} from 'redux'
|
||||
|
||||
import workspacesReducer from './modules/workspaces'
|
||||
import requestsReducer from './modules/requests'
|
||||
import tabsReducer from './modules/tabs'
|
||||
import globalReducer from './modules/global'
|
||||
import modalsReducer from './modules/modals'
|
||||
import requestGroupsReducer from './modules/requestGroups'
|
||||
import responsesReducer from './modules/responses'
|
||||
import workspaces from './modules/workspaces'
|
||||
import requestGroups from './modules/requestGroups'
|
||||
import requests from './modules/requests'
|
||||
import responses from './modules/responses'
|
||||
import global from './modules/global'
|
||||
import modals from './modules/modals'
|
||||
import entities from './modules/entities'
|
||||
|
||||
export default combineReducers({
|
||||
workspaces: workspacesReducer,
|
||||
requestGroups: requestGroupsReducer,
|
||||
requests: requestsReducer,
|
||||
responses: responsesReducer,
|
||||
modals: modalsReducer,
|
||||
global: globalReducer,
|
||||
tabs: tabsReducer
|
||||
workspaces,
|
||||
responses,
|
||||
requests,
|
||||
requestGroups,
|
||||
modals,
|
||||
global,
|
||||
entities
|
||||
});
|
||||
|
@ -19,6 +19,7 @@
|
||||
"react-tabs": "^0.5.3",
|
||||
"redux": "^3.3.1",
|
||||
"redux-logger": "^2.6.1",
|
||||
"redux-shortcuts": "0.0.1",
|
||||
"redux-thunk": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import path from 'path'
|
||||
import webpack from 'webpack'
|
||||
import baseConfig from './webpack.config.base'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user