Various Improvements (#59)

* Better create, started response history

* Response history working

* A bunch
This commit is contained in:
Gregory Schier 2016-11-27 13:42:38 -08:00 committed by GitHub
parent 8aa274d21b
commit 6c1c03cef6
48 changed files with 881 additions and 372 deletions

View File

@ -35,9 +35,9 @@ export function getClientString () {
}
// Global Stuff
export const LOCALSTORAGE_KEY = 'insomnia.state';
export const DB_PERSIST_INTERVAL = 1000 * 60 * 10;
export const DEBOUNCE_MILLIS = 100;
export const MAX_RESPONSES = 20;
export const REQUEST_TIME_TO_SHOW_COUNTER = 1; // Seconds
export const GA_ID = 'UA-86416787-1';
export const GA_HOST = 'desktop.insomnia.rest';
@ -73,7 +73,7 @@ export const METHOD_HEAD = 'HEAD';
export const METHOD_FIND = 'FIND';
export const METHOD_PURGE = 'PURGE';
export const METHOD_DELETE_HARD = 'DELETEHARD';
export const METHODS = [
export const HTTP_METHODS = [
METHOD_GET,
METHOD_POST,
METHOD_PUT,
@ -113,7 +113,6 @@ export function getPreviewModeName (previewMode) {
// Content Types
export const CONTENT_TYPE_JSON = 'application/json';
export const CONTENT_TYPE_XML = 'application/xml';
export const CONTENT_TYPE_TEXT = 'text/plain';
export const CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data';
export const CONTENT_TYPE_FILE = 'application/octet-stream';

View File

@ -4,6 +4,7 @@ import fsPath from 'path';
import {DB_PERSIST_INTERVAL} from './constants';
import {generateId} from './misc';
import {getModel, initModel} from '../models';
import * as models from '../models/index';
export const CHANGE_INSERT = 'insert';
export const CHANGE_UPDATE = 'update';
@ -111,10 +112,15 @@ function notifyOfChange (event, doc, fromSync) {
// Helpers //
// ~~~~~~~ //
export function getMostRecentlyModified (type, query = {}) {
export async function getMostRecentlyModified (type, query = {}) {
const docs = await findMostRecentlyModified(type, query, 1);
return docs.length ? docs[0] : null;
}
export function findMostRecentlyModified (type, query = {}, limit = null) {
return new Promise(resolve => {
db[type].find(query).sort({modified: -1}).limit(1).exec((err, docs) => {
resolve(docs.length ? docs[0] : null);
db[type].find(query).sort({modified: -1}).limit(limit).exec((err, docs) => {
resolve(docs);
})
})
}
@ -262,11 +268,11 @@ export function docCreate (type, patch = {}) {
const doc = initModel(
type,
{_id: generateId(idPrefix)},
patch,
// Fields that the user can't touch
{
_id: generateId(idPrefix),
type: type,
modified: Date.now()
}
@ -347,6 +353,11 @@ export async function duplicate (originalDoc, patch = {}, first = true) {
// 2. Get all the children
for (const type of allTypes()) {
// Note: We never want to duplicate a response
if (type === models.response.type) {
continue;
}
const parentId = originalDoc._id;
const children = await find(type, {parentId});
for (const doc of children) {

View File

@ -116,6 +116,7 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
}, true);
} catch (e) {
const response = await models.response.create({
url: renderedRequest.url,
parentId: renderedRequest._id,
elapsedTime: 0,
statusMessage: 'Error',
@ -169,7 +170,9 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
}
await models.response.create({
url: originalUrl,
parentId: renderedRequest._id,
statusMessage: 'Error',
error: message
});
@ -223,6 +226,7 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
req.abort();
await models.response.create({
url: originalUrl,
parentId: renderedRequest._id,
elapsedTime: Date.now() - requestStartTime,
statusMessage: 'Cancelled',

View File

@ -1,11 +1,7 @@
import {METHOD_GET, getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA} from '../common/constants';
import {METHOD_GET, getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FILE} from '../common/constants';
import * as db from '../common/database';
import {getContentTypeHeader} from '../common/misc';
import {deconstructToParams} from '../common/querystring';
import {CONTENT_TYPE_JSON} from '../common/constants';
import {CONTENT_TYPE_XML} from '../common/constants';
import {CONTENT_TYPE_FILE} from '../common/constants';
import {CONTENT_TYPE_TEXT} from '../common/constants';
export const name = 'Request';
export const type = 'Request';
@ -91,8 +87,8 @@ export function update (request, patch) {
return db.docUpdate(request, patch);
}
export function updateMimeType (request, mimeType) {
let headers = [...request.headers];
export function updateMimeType (request, mimeType, doCreate = false) {
let headers = request.headers ? [...request.headers] : [];
const contentTypeHeader = getContentTypeHeader(headers);
// 1. Update Content-Type header
@ -121,7 +117,11 @@ export function updateMimeType (request, mimeType) {
body = newBodyRaw(request.body.text || '', mimeType);
}
return update(request, {headers, body});
if (doCreate) {
return create(Object.assign({}, request, {headers, body}));
} else {
return update(request, {headers, body});
}
}
export function duplicate (request) {

View File

@ -1,4 +1,5 @@
import * as db from '../common/database';
import {MAX_RESPONSES} from '../common/constants';
export const name = 'Response';
export const type = 'Response';
@ -24,12 +25,35 @@ export function migrate (doc) {
return doc;
}
export function create (patch = {}) {
export function getById (id) {
return db.get(type, id);
}
export function all () {
return db.all(type);
}
export async function removeForRequest (parentId) {
db.removeBulkSilently(type, {parentId});
}
export function findRecentForRequest (requestId, limit) {
return db.findMostRecentlyModified(type, {parentId: requestId}, limit);
}
export async function create (patch = {}) {
if (!patch.parentId) {
throw new Error('New Response missing `parentId`');
}
db.removeBulkSilently(type, {parentId: patch.parentId});
const {parentId} = patch;
// Delete all other responses before creating the new one
const allResponses = await db.findMostRecentlyModified(type, {parentId}, MAX_RESPONSES);
const recentIds = allResponses.map(r => r._id);
await db.removeBulkSilently(type, {parentId, _id: {$nin: recentIds}});
// Actually create the new response
return db.docCreate(type, patch);
}

View File

@ -131,7 +131,9 @@ class RequestPane extends PureComponent {
{" "}
{numBodyParams ? <span className="txt-sm">({numBodyParams})</span> : null}
</button>
<ContentTypeDropdown updateRequestMimeType={updateRequestMimeType} className="tall">
<ContentTypeDropdown onChange={updateRequestMimeType}
contentType={request.body.mimeType}
className="tall">
<i className="fa fa-caret-down"></i>
</ContentTypeDropdown>
</Tab>

View File

@ -1,21 +1,28 @@
import React, {Component, PropTypes} from 'react';
import {Dropdown, DropdownButton, DropdownItem} from './base/dropdown';
import {METHODS, DEBOUNCE_MILLIS, isMac} from '../../common/constants';
import {DEBOUNCE_MILLIS, isMac} from '../../common/constants';
import {trackEvent} from '../../analytics';
import MethodDropdown from './dropdowns/MethodDropdown';
class RequestUrlBar extends Component {
_handleFormSubmit (e) {
_handleFormSubmit = e => {
e.preventDefault();
this.props.handleSend();
}
};
_handleMethodChange = method => {
this.props.onMethodChange(method);
trackEvent('Request', 'Method Change', method);
};
_handleUrlChange = e => {
const url = e.target.value;
_handleUrlChange (url) {
clearTimeout(this._timeout);
this._timeout = setTimeout(() => {
this.props.onUrlChange(url);
}, DEBOUNCE_MILLIS);
}
};
componentDidMount () {
this._bodyKeydownHandler = e => {
@ -40,31 +47,20 @@ class RequestUrlBar extends Component {
}
render () {
const {onMethodChange, url, method} = this.props;
const {url, method} = this.props;
return (
<div className="urlbar">
<Dropdown>
<DropdownButton type="button">
{method} <i className="fa fa-caret-down"/>
</DropdownButton>
{METHODS.map(method => (
<DropdownItem key={method} className={`method-${method}`} onClick={() => {
onMethodChange(method);
trackEvent('Request', 'Method Change', method);
}}>
{method}
</DropdownItem>
))}
</Dropdown>
<form onSubmit={this._handleFormSubmit.bind(this)}>
<MethodDropdown onChange={this._handleMethodChange} method={method}>
{method} <i className="fa fa-caret-down"/>
</MethodDropdown>
<form onSubmit={this._handleFormSubmit}>
<div className="form-control">
<input
ref={n => this._input = n}
type="text"
placeholder="https://api.myproduct.com/v1/users"
defaultValue={url}
onClick={e => e.preventDefault()}
onChange={e => this._handleUrlChange(e.target.value)}/>
onChange={this._handleUrlChange}/>
</div>
<div className="no-wrap">
<button type="submit">

View File

@ -8,10 +8,12 @@ import StatusTag from './tags/StatusTag';
import TimeTag from './tags/TimeTag';
import PreviewModeDropdown from './dropdowns/PreviewModeDropdown';
import ResponseViewer from './viewers/ResponseViewer';
import ResponseHistoryDropdown from './dropdowns/ResponseHistoryDropdown';
import ResponseTimer from './ResponseTimer';
import ResponseHeadersViewer from './viewers/ResponseHeadersViewer';
import ResponseCookiesViewer from './viewers/ResponseCookiesViewer';
import * as models from '../../models';
import {REQUEST_TIME_TO_SHOW_COUNTER, MOD_SYM, PREVIEW_MODE_SOURCE, getPreviewModeName} from '../../common/constants';
import {MOD_SYM, PREVIEW_MODE_SOURCE, getPreviewModeName} from '../../common/constants';
import {getSetCookieHeaders} from '../../common/misc';
import {cancelCurrentRequest} from '../../common/network';
import {trackEvent} from '../../analytics';
@ -19,13 +21,14 @@ import {trackEvent} from '../../analytics';
class ResponsePane extends Component {
state = {response: null};
async _getResponse (request) {
if (!request) {
this.setState({response: null});
} else {
const response = await models.response.getLatestByParentId(request._id);
this.setState({response});
async _getResponse (requestId, responseId) {
let response = await models.response.getById(responseId);
if (!response) {
response = await models.response.getLatestByParentId(requestId);
}
this.setState({response});
}
async _handleDownloadResponseBody () {
@ -66,11 +69,15 @@ class ResponsePane extends Component {
}
componentWillReceiveProps (nextProps) {
this._getResponse(nextProps.request);
const activeRequestId = nextProps.request ? nextProps.request._id : null;
const activeResponseId = nextProps.activeResponseId;
this._getResponse(activeRequestId, activeResponseId);
}
componentDidMount () {
this._getResponse(this.props.request);
const activeRequestId = this.props.request ? this.props.request._id : null;
const activeResponseId = this.props.activeResponseId;
this._getResponse(activeRequestId, activeResponseId);
}
render () {
@ -78,51 +85,19 @@ class ResponsePane extends Component {
request,
previewMode,
handleSetPreviewMode,
handleSetActiveResponse,
handleDeleteResponses,
handleSetFilter,
loadStartTime,
editorLineWrapping,
editorFontSize,
filter,
activeResponseId,
showCookiesModal
} = this.props;
const {response} = this.state;
let timer = null;
if (loadStartTime >= 0) {
// Set a timer to update the UI again soon
// TODO: Move this into a child component so we don't rerender too much
setTimeout(() => {
this.forceUpdate();
}, 100);
// NOTE: subtract 200ms because the request has some time on either end
const millis = Date.now() - loadStartTime - 200;
const elapsedTime = Math.round(millis / 100) / 10;
timer = (
<div className="response-pane__overlay">
{elapsedTime > REQUEST_TIME_TO_SHOW_COUNTER ? (
<h2>{elapsedTime} seconds...</h2>
) : (
<h2>Loading...</h2>
)}
<br/>
<i className="fa fa-refresh fa-spin"></i>
<br/>
<div className="pad">
<button className="btn btn--clicky"
onClick={() => cancelCurrentRequest()}>
Cancel Request
</button>
</div>
</div>
)
}
if (!request) {
return (
<section className="response-pane pane">
@ -135,7 +110,11 @@ class ResponsePane extends Component {
if (!response) {
return (
<section className="response-pane pane">
{timer}
<ResponseTimer
className="response-pane__overlay"
handleCancel={cancelCurrentRequest}
loadStartTime={loadStartTime}
/>
<header className="pane__header"></header>
<div className="pane__body pane__body--placeholder">
@ -178,15 +157,31 @@ class ResponsePane extends Component {
return (
<section className="response-pane pane">
{timer}
<ResponseTimer
className="response-pane__overlay"
handleCancel={cancelCurrentRequest}
loadStartTime={loadStartTime}
/>
{!response ? null : (
<header className="pane__header">
<StatusTag
statusCode={response.statusCode}
statusMessage={response.statusMessage}
<header className="pane__header row-spaced">
<div className="no-wrap scrollable scrollable--no-bars pad-left">
<StatusTag
statusCode={response.statusCode}
statusMessage={response.statusMessage || null}
/>
<TimeTag milliseconds={response.elapsedTime} startTime={response.created}/>
<SizeTag bytes={response.bytesRead}/>
</div>
<ResponseHistoryDropdown
requestId={request._id}
isLatestResponseActive={!activeResponseId}
activeResponseId={response._id}
handleSetActiveResponse={handleSetActiveResponse}
handleDeleteResponses={handleDeleteResponses}
onChange={() => null}
className="tall pane__header__right"
right={true}
/>
<TimeTag milliseconds={response.elapsedTime}/>
<SizeTag bytes={response.bytesRead}/>
</header>
)}
<Tabs className="pane__body">
@ -265,6 +260,8 @@ ResponsePane.propTypes = {
handleSetFilter: PropTypes.func.isRequired,
showCookiesModal: PropTypes.func.isRequired,
handleSetPreviewMode: PropTypes.func.isRequired,
handleSetActiveResponse: PropTypes.func.isRequired,
handleDeleteResponses: PropTypes.func.isRequired,
// Required
previewMode: PropTypes.string.isRequired,
@ -272,6 +269,7 @@ ResponsePane.propTypes = {
editorFontSize: PropTypes.number.isRequired,
editorLineWrapping: PropTypes.bool.isRequired,
loadStartTime: PropTypes.number.isRequired,
activeResponseId: PropTypes.string.isRequired,
// Other
request: PropTypes.object,

View File

@ -0,0 +1,47 @@
import React, {Component, PropTypes} from 'react';
import {REQUEST_TIME_TO_SHOW_COUNTER} from '../../common/constants';
class ResponseTimer extends Component {
render () {
const {loadStartTime, className, handleCancel} = this.props;
if (loadStartTime < 0) {
return null;
}
// Set a timer to update the UI again soon
setTimeout(() => {
this.forceUpdate();
}, 100);
const millis = Date.now() - loadStartTime - 200;
const elapsedTime = Math.round(millis / 100) / 10;
return (
<div className={className}>
{elapsedTime > REQUEST_TIME_TO_SHOW_COUNTER ? (
<h2>{elapsedTime} seconds...</h2>
) : (
<h2>Loading...</h2>
)}
<br/>
<i className="fa fa-refresh fa-spin"></i>
<br/>
<div className="pad">
<button className="btn btn--clicky" onClick={handleCancel}>
Cancel Request
</button>
</div>
</div>
)
}
}
ResponseTimer.propTypes = {
handleCancel: PropTypes.func.isRequired,
loadStartTime: PropTypes.number.isRequired,
};
export default ResponseTimer;

View File

@ -5,6 +5,7 @@ import WorkspaceEnvironmentsEditModal from '../components/modals/WorkspaceEnviro
import CookiesModal from '../components/modals/CookiesModal';
import EnvironmentEditModal from '../components/modals/EnvironmentEditModal';
import RequestSwitcherModal from '../components/modals/RequestSwitcherModal';
import RequestCreateModal from '../components/modals/RequestCreateModal';
import GenerateCodeModal from '../components/modals/GenerateCodeModal';
import PromptModal from '../components/modals/PromptModal';
import AlertModal from '../components/modals/AlertModal';
@ -78,9 +79,15 @@ class Wrapper extends Component {
_handleImportFile = () => this.props.handleImportFileToWorkspace(this.props.activeWorkspace._id);
_handleExportWorkspaceToFile = () => this.props.handleExportFile(this.props.activeWorkspace._id);
_handleSetSidebarFilter = filter => this.props.handleSetSidebarFilter(this.props.activeWorkspace._id, filter);
_handleSetActiveResponse = responseId => this.props.handleSetActiveResponse(this.props.activeRequest._id, responseId);
_handleShowEnvironmentsModal = () => showModal(WorkspaceEnvironmentsEditModal, this.props.activeWorkspace);
_handleShowCookiesModal = () => showModal(CookiesModal, this.props.activeWorkspace);
_handleDeleteResponses = () => {
models.response.removeForRequest(this.props.activeRequest._id);
this._handleSetActiveResponse(null);
};
_handleSendRequestWithActiveEnvironment = () => {
const {activeRequest, activeEnvironment, handleSendRequestWithEnvironment} = this.props;
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
@ -106,40 +113,42 @@ class Wrapper extends Component {
render () {
const {
isLoading,
loadStartTime,
activeWorkspace,
activeRequest,
activeEnvironment,
sidebarHidden,
sidebarFilter,
sidebarWidth,
paneWidth,
forceRefreshCounter,
workspaces,
workspaceChildren,
settings,
activeRequest,
activeResponseId,
activeWorkspace,
environments,
responsePreviewMode,
responseFilter,
forceRefreshCounter,
handleActivateRequest,
handleCreateRequest,
handleCreateRequestForWorkspace,
handleCreateRequestGroup,
handleDuplicateRequest,
handleExportFile,
handleActivateRequest,
handleSetActiveWorkspace,
handleSetActiveEnvironment,
handleSetRequestGroupCollapsed,
handleMoveRequest,
handleMoveRequestGroup,
handleResetDragPane,
handleResetDragSidebar,
handleSetActiveEnvironment,
handleSetActiveWorkspace,
handleSetRequestGroupCollapsed,
handleSetRequestPaneRef,
handleSetResponsePaneRef,
handleSetSidebarRef,
handleStartDragSidebar,
handleResetDragSidebar,
handleStartDragPane,
handleResetDragPane,
handleStartDragSidebar,
isLoading,
loadStartTime,
paneWidth,
responseFilter,
responsePreviewMode,
settings,
sidebarChildren,
sidebarFilter,
sidebarHidden,
sidebarWidth,
workspaceChildren,
workspaces,
} = this.props;
const realSidebarWidth = sidebarHidden ? 0 : sidebarWidth;
@ -160,6 +169,7 @@ class Wrapper extends Component {
handleImportFile={this._handleImportFile}
handleExportFile={handleExportFile}
handleSetActiveWorkspace={handleSetActiveWorkspace}
handleDuplicateRequest={handleDuplicateRequest}
handleSetActiveEnvironment={handleSetActiveEnvironment}
moveRequest={handleMoveRequest}
moveRequestGroup={handleMoveRequestGroup}
@ -218,10 +228,13 @@ class Wrapper extends Component {
editorFontSize={settings.editorFontSize}
editorLineWrapping={settings.editorLineWrapping}
previewMode={responsePreviewMode}
activeResponseId={activeResponseId}
filter={responseFilter}
loadStartTime={loadStartTime}
showCookiesModal={this._handleShowCookiesModal}
handleSetActiveResponse={this._handleSetActiveResponse}
handleSetPreviewMode={this._handleSetPreviewMode}
handleDeleteResponses={this._handleDeleteResponses}
handleSetFilter={this._handleSetResponseFilter}
/>
@ -233,6 +246,7 @@ class Wrapper extends Component {
<PromptModal ref={registerModal}/>
<SignupModal ref={registerModal}/>
<PaymentModal ref={registerModal}/>
<RequestCreateModal ref={registerModal}/>
<PaymentNotificationModal ref={registerModal}/>
<EnvironmentEditModal
ref={registerModal}
@ -279,12 +293,14 @@ Wrapper.propTypes = {
handleMoveRequest: PropTypes.func.isRequired,
handleMoveRequestGroup: PropTypes.func.isRequired,
handleCreateRequest: PropTypes.func.isRequired,
handleDuplicateRequest: PropTypes.func.isRequired,
handleCreateRequestGroup: PropTypes.func.isRequired,
handleCreateRequestForWorkspace: PropTypes.func.isRequired,
handleSetRequestPaneRef: PropTypes.func.isRequired,
handleSetResponsePaneRef: PropTypes.func.isRequired,
handleSetResponsePreviewMode: PropTypes.func.isRequired,
handleSetResponseFilter: PropTypes.func.isRequired,
handleSetActiveResponse: PropTypes.func.isRequired,
handleSetSidebarRef: PropTypes.func.isRequired,
handleStartDragSidebar: PropTypes.func.isRequired,
handleResetDragSidebar: PropTypes.func.isRequired,
@ -299,6 +315,7 @@ Wrapper.propTypes = {
paneWidth: PropTypes.number.isRequired,
responsePreviewMode: PropTypes.string.isRequired,
responseFilter: PropTypes.string.isRequired,
activeResponseId: PropTypes.string.isRequired,
sidebarWidth: PropTypes.number.isRequired,
sidebarHidden: PropTypes.bool.isRequired,
sidebarFilter: PropTypes.string.isRequired,

View File

@ -221,11 +221,11 @@ class KeyValueEditor extends Component {
<FileInputButton
showFileName={true}
className="btn btn--clicky wide ellipsis txt-sm"
path={pair.fileName || ''}
onChange={fileName => {
this._updatePair(i, {fileName});
this.props.onChooseFile && this.props.onChooseFile();
}}
path={pair.fileName || ''}
/>
) : (
<input
@ -234,15 +234,13 @@ class KeyValueEditor extends Component {
ref={n => this._valueInputs[i] = n}
defaultValue={pair.value}
onChange={e => this._updatePair(i, {value: e.target.value})}
onBlur={() => this._focusedPair = -1}
onKeyDown={this._keyDown.bind(this)}
onFocus={e => {
this._focusedPair = i;
this._focusedField = VALUE;
this._focusedInput = e.target;
}}
onBlur={() => {
this._focusedPair = -1
}}
onKeyDown={this._keyDown.bind(this)}
/>
)}
</div>

View File

@ -6,47 +6,64 @@ import DropdownItem from './DropdownItem';
import DropdownDivider from './DropdownDivider';
class Dropdown extends Component {
state = {open: false, dropUp: false};
state = {
open: false,
dropUp: false,
focused: false,
};
_handleClick () {
this.toggle();
}
_addKeyListener () {
this._bodyKeydownHandler = e => {
if (!this.state.open) {
return;
}
// Catch all key presses if we're open
_handleKeyDown = e => {
// Catch all key presses if we're open
if (this.state.open) {
e.stopPropagation();
}
// Pressed escape?
if (e.keyCode === 27) {
e.preventDefault();
this.hide();
}
};
// Pressed escape?
if (this.state.open && e.keyCode === 27) {
e.preventDefault();
this.hide();
}
};
document.body.addEventListener('keydown', this._bodyKeydownHandler);
}
_checkSize = () => {
if (!this.state.open) {
return;
}
_removeKeyListener () {
document.body.removeEventListener('keydown', this._bodyKeydownHandler);
}
// Make the dropdown scroll if it drops off screen.
const rect = this._dropdownList.getBoundingClientRect();
const maxHeight = document.body.clientHeight - rect.top - 10;
this._dropdownList.style.maxHeight = `${maxHeight}px`;
};
_handleClick = () => {
this.toggle();
};
_handleMouseDown = e => {
// Intercept mouse down so that clicks don't trigger things like
// drag and drop.
e.preventDefault();
};
_addDropdownListRef = n => this._dropdownList = n;
componentDidUpdate () {
// Make the dropdown scroll if it drops off screen.
if (this.state.open) {
const rect = this._dropdownList.getBoundingClientRect();
const maxHeight = document.body.clientHeight - rect.top - 10;
this._dropdownList.style.maxHeight = `${maxHeight}px`;
}
this._checkSize();
}
componentDidMount () {
document.body.addEventListener('keydown', this._handleKeyDown);
window.addEventListener('resize', this._checkSize);
}
componentWillUnmount () {
document.body.removeEventListener('keydown', this._handleKeyDown);
window.removeEventListener('resize', this._checkSize);
}
hide () {
this.setState({open: false});
this._removeKeyListener();
}
show () {
@ -55,7 +72,6 @@ class Dropdown extends Component {
const dropUp = dropdownTop > bodyHeight * 0.65;
this.setState({open: true, dropUp});
this._addKeyListener();
}
toggle () {
@ -66,10 +82,6 @@ class Dropdown extends Component {
}
}
componentWillUnmount () {
this._removeKeyListener();
}
_getFlattenedChildren (children) {
let newChildren = [];
@ -119,18 +131,20 @@ class Dropdown extends Component {
if (dropdownButtons.length !== 1) {
console.error(`Dropdown needs exactly one DropdownButton! Got ${dropdownButtons.length}`, this.props);
} else if (dropdownItems.length === 0) {
console.error(`Dropdown needs at least one DropdownItem!`);
children = dropdownButtons;
} else {
children = [
dropdownButtons[0],
<ul key="items" ref={n => this._dropdownList = n}>{dropdownItems}</ul>
<ul key="items" ref={this._addDropdownListRef}>
{dropdownItems}
</ul>
]
}
return (
<div className={classes}
onClick={this._handleClick.bind(this)}
onMouseDown={e => e.preventDefault()}>
onClick={this._handleClick}
onMouseDown={this._handleMouseDown}>
{children}
<div className="dropdown__backdrop"></div>
</div>

View File

@ -1,7 +1,7 @@
import React from 'react';
const DropdownButton = ({children, ...props}) => (
<button {...props}>
<button type="button" {...props}>
{children}
</button>
);

View File

@ -17,7 +17,7 @@ const DropdownDivider = ({name}) => {
};
DropdownDivider.propTypes = {
name: PropTypes.string
name: PropTypes.any
};
export default DropdownDivider;

View File

@ -1,27 +1,57 @@
import React, {PropTypes} from 'react';
import React, {PureComponent, PropTypes} from 'react';
import classnames from 'classnames';
const DropdownItem = ({stayOpenAfterClick, buttonClass, onClick, children, className, ...props}) => {
const inner = (
<div className={classnames('dropdown__inner', className)}>
<span className="dropdown__text">{children}</span>
</div>
);
class DropdownItem extends PureComponent {
_handleClick = e => {
const {stayOpenAfterClick, onClick, disabled} = this.props;
const buttonProps = {
onClick: stayOpenAfterClick ? e => {e.stopPropagation(); onClick(e)} : onClick,
...props
if (stayOpenAfterClick) {
e.stopPropagation();
}
if (!onClick || disabled) {
return;
}
if (this.props.hasOwnProperty('value')) {
onClick(this.props.value, e);
} else {
onClick(e);
}
};
const button = React.createElement(buttonClass || 'button', buttonProps, inner);
return (
<li>{button}</li>
)
};
render () {
const {
buttonClass,
children,
className,
onClick, // Don't want this in ...props
...props
} = this.props;
const inner = (
<div className={classnames('dropdown__inner', className)}>
<span className="dropdown__text">{children}</span>
</div>
);
const buttonProps = {
type: 'button',
onClick: this._handleClick,
...props
};
const button = React.createElement(buttonClass || 'button', buttonProps, inner);
return (
<li>{button}</li>
)
}
}
DropdownItem.propTypes = {
buttonClass: PropTypes.any,
stayOpenAfterClick: PropTypes.bool
stayOpenAfterClick: PropTypes.bool,
value: PropTypes.any,
};
export default DropdownItem;

View File

@ -5,43 +5,55 @@ import {trackEvent} from '../../../analytics/index';
import * as constants from '../../../common/constants';
import {getContentTypeName} from '../../../common/constants';
const EMPTY_MIME_TYPE = null;
class ContentTypeDropdown extends Component {
_renderDropdownItem (mimeType, iconClass, forcedName = null) {
_handleChangeMimeType = mimeType => {
this.props.onChange(mimeType);
trackEvent('Request', 'Content-Type Change', contentTypesMap[mimeType]);
};
_renderDropdownItem (mimeType, forcedName = null) {
const contentType = typeof this.props.contentType !== 'string' ?
EMPTY_MIME_TYPE : this.props.contentType;
const iconClass = mimeType === contentType ? 'fa-check' : 'fa-empty';
return (
<DropdownItem onClick={e => {
this.props.updateRequestMimeType(mimeType);
trackEvent('Request', 'Content-Type Change', contentTypesMap[mimeType]);
}}>
<i className={`fa ${iconClass || 'fa-empty'}`}/>
<DropdownItem onClick={this._handleChangeMimeType} value={mimeType}>
<i className={`fa ${iconClass}`}/>
{forcedName || getContentTypeName(mimeType)}
</DropdownItem>
)
}
render () {
const {children, className} = this.props;
const {children, className, ...extraProps} = this.props;
return (
<Dropdown debug="true">
<Dropdown debug="true" {...extraProps}>
<DropdownButton className={className}>
{children}
</DropdownButton>
<DropdownDivider name="Form Data"/>
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA, 'fa-bars')}
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED, 'fa-bars')}
<DropdownDivider name="Raw Text"/>
{this._renderDropdownItem(constants.CONTENT_TYPE_JSON, 'fa-code')}
{this._renderDropdownItem(constants.CONTENT_TYPE_XML, 'fa-code')}
{this._renderDropdownItem(constants.CONTENT_TYPE_OTHER, 'fa-code')}
<DropdownDivider name="Other"/>
{this._renderDropdownItem(constants.CONTENT_TYPE_FILE, 'fa-file-o')}
{this._renderDropdownItem(null, 'fa-ban', 'No Body')}
<DropdownDivider name={<span><i className="fa fa-bars"></i> Form Data</span>}/>
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_DATA)}
{this._renderDropdownItem(constants.CONTENT_TYPE_FORM_URLENCODED)}
<DropdownDivider name={<span><i className="fa fa-code"></i> Raw Text</span>}/>
{this._renderDropdownItem(constants.CONTENT_TYPE_JSON)}
{this._renderDropdownItem(constants.CONTENT_TYPE_XML)}
{this._renderDropdownItem(constants.CONTENT_TYPE_OTHER)}
<DropdownDivider name={<span><i className="fa fa-ellipsis-h"></i> Other</span>}/>
{this._renderDropdownItem(constants.CONTENT_TYPE_FILE)}
{this._renderDropdownItem(EMPTY_MIME_TYPE, 'No Body')}
</Dropdown>
)
}
}
ContentTypeDropdown.propTypes = {
updateRequestMimeType: PropTypes.func.isRequired
onChange: PropTypes.func.isRequired,
// Optional
contentType: PropTypes.string, // Can be null
};
export default ContentTypeDropdown;

View File

@ -0,0 +1,31 @@
import React, {PropTypes, Component} from 'react';
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown';
import * as constants from '../../../common/constants';
class MethodDropdown extends Component {
render () {
const {method, onChange, right, ...extraProps} = this.props;
return (
<Dropdown className="method-dropdown" right={right}>
<DropdownButton type="button" {...extraProps}>
{method} <i className="fa fa-caret-down"/>
</DropdownButton>
{constants.HTTP_METHODS.map(method => (
<DropdownItem key={method}
className={`http-method-${method}`}
onClick={onChange}
value={method}>
{method}
</DropdownItem>
))}
</Dropdown>
)
}
}
MethodDropdown.propTypes = {
onChange: PropTypes.func.isRequired,
method: PropTypes.string.isRequired,
};
export default MethodDropdown;

View File

@ -1,27 +1,37 @@
import React, {PropTypes} from 'react';
import React, {PureComponent, PropTypes} from 'react';
import {Dropdown, DropdownDivider, DropdownButton, DropdownItem} from '../base/dropdown';
import {PREVIEW_MODES, getPreviewModeName} from '../../../common/constants';
import {trackEvent} from '../../../analytics/index';
const PreviewModeDropdown = ({updatePreviewMode, download}) => (
<Dropdown>
<DropdownButton className="tall">
<i className="fa fa-caret-down"></i>
</DropdownButton>
{PREVIEW_MODES.map(previewMode => (
<DropdownItem key={previewMode} onClick={() => {
updatePreviewMode(previewMode);
trackEvent('Response', 'Preview Mode Change', previewMode);
}}>
{getPreviewModeName(previewMode)}
</DropdownItem>
))}
<DropdownDivider></DropdownDivider>
<DropdownItem onClick={download}>
Download
</DropdownItem>
</Dropdown>
);
class PreviewModeDropdown extends PureComponent {
_handleClick = previewMode => {
this.props.updatePreviewMode(previewMode);
trackEvent('Response', 'Preview Mode Change', mode);
};
render () {
const {download, previewMode} = this.props;
return (
<Dropdown>
<DropdownButton className="tall">
<i className="fa fa-caret-down"></i>
</DropdownButton>
<DropdownDivider name="Preview Mode"/>
{PREVIEW_MODES.map(mode => (
<DropdownItem key={mode} onClick={this._handleClick} value={mode}>
{previewMode === mode ? <i className="fa fa-check"/> : <i className="fa fa-empty"/>}
{getPreviewModeName(mode)}
</DropdownItem>
))}
<DropdownDivider name="Response"/>
<DropdownItem onClick={download}>
<i className="fa fa-save"></i>
Save to File
</DropdownItem>
</Dropdown>
)
}
}
PreviewModeDropdown.propTypes = {
// Functions

View File

@ -9,7 +9,19 @@ import {trackEvent} from '../../../analytics/index';
class RequestActionsDropdown extends Component {
async _promptUpdateName () {
_handleDuplicate = () => {
const {request, handleDuplicateRequest} = this.props;
handleDuplicateRequest(request);
trackEvent('Request', 'Duplicate', 'Request Action');
};
_handleGenerateCode = () => {
const {request} = this.props;
showModal(GenerateCodeModal, request);
trackEvent('Request', 'Generate Code', 'Request Action');
};
_handlePromptUpdateName = async () => {
const {request} = this.props;
const name = await showModal(PromptModal, {
@ -19,7 +31,15 @@ class RequestActionsDropdown extends Component {
});
models.request.update(request, {name});
}
trackEvent('Request', 'Rename', 'Request Action');
};
_handleRemove = () => {
const {request} = this.props;
models.request.remove(request);
trackEvent('Request', 'Delete', 'Action');
};
render () {
const {request, ...other} = this.props;
@ -29,31 +49,17 @@ class RequestActionsDropdown extends Component {
<DropdownButton>
<i className="fa fa-caret-down"></i>
</DropdownButton>
<DropdownItem onClick={e => {
models.request.duplicate(request);
trackEvent('Request', 'Duplicate', 'Request Action');
}}>
<DropdownItem onClick={this._handleDuplicate}>
<i className="fa fa-copy"></i> Duplicate
<DropdownHint char="D"></DropdownHint>
</DropdownItem>
<DropdownItem onClick={e => {
this._promptUpdateName();
trackEvent('Request', 'Rename', 'Request Action');
}}>
<DropdownItem onClick={this._handlePromptUpdateName}>
<i className="fa fa-edit"></i> Rename
</DropdownItem>
<DropdownItem onClick={e => {
showModal(GenerateCodeModal, request);
trackEvent('Request', 'Generate Code', 'Request Action');
}}>
<DropdownItem onClick={this._handleGenerateCode}>
<i className="fa fa-code"></i> Generate Code
</DropdownItem>
<DropdownItem buttonClass={PromptButton}
onClick={e => {
models.request.remove(request);
trackEvent('Request', 'Delete', 'Action');
}}
addIcon={true}>
<DropdownItem buttonClass={PromptButton} onClick={this._handleRemove} addIcon={true}>
<i className="fa fa-trash-o"></i> Delete
</DropdownItem>
</Dropdown>
@ -62,7 +68,8 @@ class RequestActionsDropdown extends Component {
}
RequestActionsDropdown.propTypes = {
request: PropTypes.object.isRequired
handleDuplicateRequest: PropTypes.func.isRequired,
request: PropTypes.object.isRequired,
};
export default RequestActionsDropdown;

View File

@ -0,0 +1,100 @@
import React, {PropTypes, Component} from 'react';
import {Dropdown, DropdownButton, DropdownItem, DropdownDivider} from '../base/dropdown';
import SizeTag from '../tags/SizeTag';
import StatusTag from '../tags/StatusTag';
import TimeTag from '../tags/TimeTag';
import * as models from '../../../models/index';
import PromptButton from '../base/PromptButton';
class ResponseHistoryDropdown extends Component {
state = {
responses: [],
};
_handleDeleteResponses = () => {
this.props.handleDeleteResponses(this.props.requestId);
};
async _load (requestId) {
const responses = await models.response.findRecentForRequest(requestId);
// NOTE: this is bad practice, but I can't figure out a better way.
// This component may not be mounted if the user switches to a request that
// doesn't have a response
if (!this._unmounted) {
this.setState({responses});
}
}
componentWillUnmount () {
this._unmounted = true;
}
componentWillReceiveProps (nextProps) {
this._load(nextProps.requestId);
}
componentDidMount () {
this._unmounted = false;
this._load(this.props.requestId);
}
renderDropdownItem = (response, i) => {
const {activeResponseId, handleSetActiveResponse} = this.props;
const active = response._id === activeResponseId;
return (
<DropdownItem key={response._id}
disabled={active}
value={i === 0 ? null : response._id}
onClick={handleSetActiveResponse}>
{active ? <i className="fa fa-thumb-tack"/> : <i className="fa fa-empty"/>}
<StatusTag statusCode={response.statusCode}
statusMessage={response.statusMessage || 'Error'}
small={true}/>
<TimeTag milliseconds={response.elapsedTime} small={true}/>
<SizeTag bytes={response.bytesRead} small={true}/>
</DropdownItem>
)
};
render () {
const {
activeResponseId,
handleSetActiveResponse,
handleDeleteResponses,
isLatestResponseActive,
...extraProps
} = this.props;
const {responses} = this.state;
return (
<Dropdown {...extraProps}>
<DropdownButton className="btn btn--super-compact tall">
{isLatestResponseActive ?
<i className="fa fa-history"/> :
<i className="fa fa-thumb-tack"/>}
</DropdownButton>
<DropdownDivider name="Response History"/>
<DropdownItem buttonClass={PromptButton}
addIcon={true}
onClick={this._handleDeleteResponses}>
<i className="fa fa-trash-o"/>
Clear History
</DropdownItem>
<DropdownDivider name="Past Responses"/>
{responses.map(this.renderDropdownItem)}
</Dropdown>
)
}
}
ResponseHistoryDropdown.propTypes = {
handleSetActiveResponse: PropTypes.func.isRequired,
handleDeleteResponses: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
requestId: PropTypes.string.isRequired,
activeResponseId: PropTypes.string.isRequired,
isLatestResponseActive: PropTypes.bool.isRequired,
};
export default ResponseHistoryDropdown;

View File

@ -78,7 +78,7 @@ class BodyEditor extends PureComponent {
)
} else {
return (
<div className="pad center-container text-center">
<div className="pad valign-center text-center">
<p className="pad super-faint text-sm text-center">
<i className="fa fa-ban" style={{fontSize: '6rem', opacity: 0.2}}></i>
<br/>

View File

@ -160,7 +160,7 @@ class PaymentModal extends Component {
<Nl2Br className="notice info">{message}</Nl2Br>
) : null}
<div>
<div className="pad-left-half">
<div className="pad-left-sm">
<div className="inline-block text-center"
style={{width: '50%'}}>
<input
@ -222,7 +222,7 @@ class PaymentModal extends Component {
<label htmlFor="payment-expiry">
Expiration Date
</label>
<div className="pad-left-half pad-top-sm">
<div className="pad-left-sm pad-top-sm">
<select name="payment-expiration-month"
id="payment-expiration-month"
ref={n => this._expiryMonthInput = n}>

View File

@ -12,6 +12,16 @@ import {trackEvent} from '../../../analytics';
let hidePaymentNotificationUntilNextLaunch = false;
class PaymentNotificationModal extends Component {
_handleHide = () => {
this.hide();
};
_handleProceedToPayment = () => {
this.hide();
showModal(PaymentModal);
trackEvent('Billing', 'Trial Ended', 'Proceed')
};
show () {
// Don't trigger automatically if user has dismissed it already
if (hidePaymentNotificationUntilNextLaunch) {
@ -42,23 +52,17 @@ class PaymentNotificationModal extends Component {
</p>
<br/>
<p className="pad-top">
<button className="btn btn--compact btn--outlined" onClick={e => {
this.hide();
showModal(PaymentModal);
trackEvent('Billing', 'Trial Ended', 'Proceed')
}}>
<button className="btn btn--compact btn--outlined"
onClick={this._handleProceedToPayment}>
Proceed to Billing
</button>
</p>
</div>
</ModalBody>
<ModalFooter>
<button className="btn" onClick={e => {
this.hide();
showModal(PaymentModal);
}}>Maybe Later
<button className="btn" onClick={this._handleHide}>
Maybe Later
</button>
<div></div>
</ModalFooter>
</Modal>

View File

@ -0,0 +1,151 @@
import React, {Component} from 'react';
import ContentTypeDropdown from '../dropdowns/ContentTypeDropdown';
import MethodDropdown from '../dropdowns/MethodDropdown';
import Modal from '../base/Modal';
import ModalBody from '../base/ModalBody';
import ModalHeader from '../base/ModalHeader';
import ModalFooter from '../base/ModalFooter';
import {getContentTypeName, METHOD_GET, METHOD_HEAD, METHOD_OPTIONS} from '../../../common/constants';
import * as models from '../../../models/index';
import {trackEvent} from '../../../analytics/index';
class RequestCreateModal extends Component {
constructor (props) {
super(props);
let contentType;
try {
contentType = JSON.parse(localStorage.getItem('insomnia::createRequest::contentType'));
} catch (e) {
}
let method;
try {
method = JSON.parse(localStorage.getItem('insomnia::createRequest::method'));
} catch (e) {
}
this.state = {
selectedContentType: contentType || null,
selectedMethod: method || METHOD_GET,
parentId: null,
};
}
_handleSubmit = async e => {
e.preventDefault();
const {parentId, selectedContentType, selectedMethod} = this.state;
const request = models.initModel(models.request.type, {
parentId,
name: this._input.value,
method: selectedMethod,
});
const finalRequest = await models.request.updateMimeType(
request,
selectedContentType,
true,
);
this._onSubmitCallback(finalRequest);
this.hide();
};
_handleChangeSelectedContentType = selectedContentType => {
this.setState({selectedContentType});
localStorage.setItem(
'insomnia::createRequest::contentType',
JSON.stringify(selectedContentType)
);
trackEvent('Request Create', 'Content Type Change', selectedContentType);
};
_handleChangeSelectedMethod = selectedMethod => {
this.setState({selectedMethod});
localStorage.setItem(
'insomnia::createRequest::method',
JSON.stringify(selectedMethod)
);
trackEvent('Request Create', 'Method Change', selectedMethod);
};
_handleHide = () => this.hide();
hide () {
this.modal.hide();
}
show ({parentId}) {
this.modal.show();
this._input.value = 'My Request';
this.setState({parentId});
// Need to do this after render because modal focuses itself too
setTimeout(() => {
this._input.focus();
this._input.select();
}, 100);
return new Promise(resolve => this._onSubmitCallback = resolve);
}
render () {
const {selectedContentType, selectedMethod} = this.state;
const shouldNotHaveBody =
selectedMethod === METHOD_GET ||
selectedMethod === METHOD_HEAD ||
selectedMethod === METHOD_OPTIONS;
return (
<Modal ref={m => this.modal = m}>
<ModalHeader>Create HTTP Request</ModalHeader>
<ModalBody noScroll={true}>
<form onSubmit={this._handleSubmit} className="pad row-fill">
<div className="form-control form-control--outlined form-control--wide wide">
<input ref={n => this._input = n} type="text"/>
</div>
<div className="pad-left-sm">
<MethodDropdown
className="btn btn--clicky no-wrap"
right={true}
method={selectedMethod}
onChange={this._handleChangeSelectedMethod}
/>
</div>
{!shouldNotHaveBody ? (
<div className="pad-left-sm">
<ContentTypeDropdown className="btn btn--clicky no-wrap"
right={true}
onChange={this._handleChangeSelectedContentType}>
{getContentTypeName(selectedContentType)}
{" "}
<i className="fa fa-caret-down"></i>
</ContentTypeDropdown>
</div>
) : null}
</form>
</ModalBody>
<ModalFooter>
<div className="margin-left faint italic txt-sm tall">
* hint: 'TIP: Import Curl command by pasting it into the URL bar'
</div>
<div>
<button className="btn" onClick={this._handleHide}>
Cancel
</button>
<button className="btn" onClick={this._handleSubmit}>
Create
</button>
</div>
</ModalFooter>
</Modal>
)
}
}
RequestCreateModal.propTypes = {};
export default RequestCreateModal;

View File

@ -128,7 +128,7 @@ class SignupModal extends Component {
</span>
</p>
<div className="text-left">
<label htmlFor="signup-password-confirm" className="pad-left-half">
<label htmlFor="signup-password-confirm" className="pad-left-sm">
Confirm your Password
</label>
<div className="form-control form-control--outlined">

View File

@ -131,7 +131,7 @@ const SettingsGeneral = ({settings, updateSetting}) => (
</div>
</div>
<div className="inline-block" style={{width: '50%'}}>
<div className="pad-left-half">
<div className="pad-left-sm">
<label htmlFor="setting-https-proxy">
HTTPS Proxy
</label>

View File

@ -41,6 +41,7 @@ class Sidebar extends PureComponent {
handleChangeFilter,
isLoading,
handleCreateRequest,
handleDuplicateRequest,
handleCreateRequestGroup,
handleSetRequestGroupCollapsed,
moveRequest,
@ -92,6 +93,7 @@ class Sidebar extends PureComponent {
handleCreateRequest={handleCreateRequest}
handleCreateRequestGroup={handleCreateRequestGroup}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
handleDuplicateRequest={handleDuplicateRequest}
moveRequest={moveRequest}
moveRequestGroup={moveRequestGroup}
filter={filter}
@ -122,6 +124,7 @@ Sidebar.propTypes = {
moveRequestGroup: PropTypes.func.isRequired,
handleCreateRequest: PropTypes.func.isRequired,
handleCreateRequestGroup: PropTypes.func.isRequired,
handleDuplicateRequest: PropTypes.func.isRequired,
showEnvironmentsModal: PropTypes.func.isRequired,
showCookiesModal: PropTypes.func.isRequired,

View File

@ -37,6 +37,7 @@ class SidebarChildren extends PureComponent {
handleCreateRequest,
handleCreateRequestGroup,
handleSetRequestGroupCollapsed,
handleDuplicateRequest,
moveRequest,
moveRequestGroup,
handleActivateRequest,
@ -61,6 +62,7 @@ class SidebarChildren extends PureComponent {
handleActivateRequest={handleActivateRequest}
requestCreate={handleCreateRequest}
isActive={child.doc._id === activeRequestId}
handleDuplicateRequest={handleDuplicateRequest}
request={child.doc}
workspace={workspace}
/>
@ -129,6 +131,7 @@ SidebarChildren.propTypes = {
handleCreateRequest: PropTypes.func.isRequired,
handleCreateRequestGroup: PropTypes.func.isRequired,
handleSetRequestGroupCollapsed: PropTypes.func.isRequired,
handleDuplicateRequest: PropTypes.func.isRequired,
moveRequest: PropTypes.func.isRequired,
moveRequestGroup: PropTypes.func.isRequired,
children: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@ -38,6 +38,7 @@ class SidebarRequestRow extends PureComponent {
render () {
const {
handleDuplicateRequest,
connectDragSource,
connectDropTarget,
isDragging,
@ -85,6 +86,7 @@ class SidebarRequestRow extends PureComponent {
<div className="sidebar__actions">
<RequestActionsDropdown
handleDuplicateRequest={handleDuplicateRequest}
right={true}
request={request}
requestGroup={requestGroup}
@ -103,6 +105,7 @@ class SidebarRequestRow extends PureComponent {
SidebarRequestRow.propTypes = {
// Functions
handleActivateRequest: PropTypes.func.isRequired,
handleDuplicateRequest: PropTypes.func.isRequired,
requestCreate: PropTypes.func.isRequired,
moveRequest: PropTypes.func.isRequired,

View File

@ -14,7 +14,7 @@ const MethodTag = ({method, fullNames}) => {
}
return (
<div className={'tag tag--no-bg tag--small method-' + method}>
<div className={'tag tag--no-bg tag--small http-method-' + method}>
<span className='tag__inner'>{methodName}</span>
</div>
)

View File

@ -1,18 +1,24 @@
import React, {PropTypes} from 'react';
import classnames from 'classnames';
import * as misc from '../../../common/misc';
const SizeTag = props => {
const responseSizeString = misc.describeByteSize(props.bytes);
const SizeTag = ({bytes, small, className}) => {
const responseSizeString = misc.describeByteSize(bytes);
return (
<div className="tag">
<div className={classnames('tag', {'tag--small': small}, className)}
title={`${bytes} bytes`}>
<strong>SIZE</strong>&nbsp;{responseSizeString}
</div>
);
};
SizeTag.propTypes = {
bytes: PropTypes.number.isRequired
// Required
bytes: PropTypes.number.isRequired,
// Optional
small: PropTypes.bool,
};
export default SizeTag;

View File

@ -5,7 +5,7 @@ import {
STATUS_CODE_PEBKAC
} from '../../../common/constants';
const StatusTag = ({statusMessage, statusCode}) => {
const StatusTag = ({statusMessage, statusCode, small}) => {
statusCode = String(statusCode);
let colorClass;
@ -42,14 +42,21 @@ const StatusTag = ({statusMessage, statusCode}) => {
const description = RESPONSE_CODE_DESCRIPTIONS[statusCode] || 'Unknown Response Code';
return (
<div className={classnames('tag', colorClass)} title={description}>
<strong>{statusCode}</strong> {statusMessage || backupStatusMessage}
<div className={classnames('tag', colorClass, {'tag--small': small})}
title={description}>
<strong>{statusCode}</strong>
{" "}
{typeof statusMessage === 'string' ? statusMessage : backupStatusMessage}
</div>
);
};
StatusTag.propTypes = {
// Required
statusCode: PropTypes.number.isRequired,
// Optional
small: PropTypes.bool,
statusMessage: PropTypes.string
};

View File

@ -1,6 +1,7 @@
import React, {PropTypes} from 'react';
import classnames from 'classnames';
const TimeTag = ({milliseconds}) => {
const TimeTag = ({milliseconds, startTime, small, className}) => {
let unit = 'ms';
let number = milliseconds;
@ -15,15 +16,21 @@ const TimeTag = ({milliseconds}) => {
// Round to 2 decimal places
number = Math.round(number * 100) / 100;
let description = `${milliseconds} milliseconds`;
return (
<div className="tag">
<div className={classnames('tag', {'tag--small': small}, className)} title={description}>
<strong>TIME</strong> {number} {unit}
</div>
)
}
};
TimeTag.propTypes = {
milliseconds: PropTypes.number.isRequired
// Required
milliseconds: PropTypes.number.isRequired,
// Optional
small: PropTypes.bool,
startTime: PropTypes.bool,
};
export default TimeTag;

View File

@ -24,6 +24,7 @@ import * as db from '../../common/database';
import * as models from '../../models';
import {trackEvent, trackLegacyEvent} from '../../analytics';
import {selectEntitiesLists, selectActiveWorkspace, selectSidebarChildren, selectWorkspaceRequestsAndRequestGroups} from '../redux/selectors';
import RequestCreateModal from '../components/modals/RequestCreateModal';
class App extends Component {
@ -87,14 +88,7 @@ class App extends Component {
// Request Duplicate
'mod+d': async () => {
const {activeWorkspace, activeRequest, handleSetActiveRequest} = this.props;
if (!activeRequest) {
return;
}
const request = await models.request.duplicate(activeRequest);
handleSetActiveRequest(activeWorkspace._id, request._id);
this._requestDuplicate(this.props.activeRequest);
trackEvent('HotKey', 'Request Duplicate');
}
};
@ -115,20 +109,23 @@ class App extends Component {
};
_requestCreate = async (parentId) => {
const name = await showModal(PromptModal, {
headerName: 'Create New Request',
defaultValue: 'My Request',
selectText: true,
submitName: 'Create',
hint: 'TIP: Import Curl command by pasting it into the URL bar'
});
const request = await showModal(RequestCreateModal, {parentId});
const {activeWorkspace, handleSetActiveRequest} = this.props;
const request = await models.request.create({parentId, name});
handleSetActiveRequest(activeWorkspace._id, request._id);
};
_requestDuplicate = async (request) => {
const {activeWorkspace, handleSetActiveRequest} = this.props;
if (!request) {
return;
}
const newRequest = await models.request.duplicate(request);
handleSetActiveRequest(activeWorkspace._id, newRequest._id);
};
_requestCreateForWorkspace = () => {
this._requestCreate(this.props.activeWorkspace._id);
};
@ -303,6 +300,7 @@ class App extends Component {
handleStartDragPane={this._startDragPane}
handleResetDragPane={this._resetDragPane}
handleCreateRequest={this._requestCreate}
handleDuplicateRequest={this._requestDuplicate}
handleCreateRequestGroup={this._requestGroupCreate}
{...this.props}
/>
@ -331,6 +329,7 @@ function mapStateToProps (state, props) {
const {
loadingRequestIds,
activeResponseIds,
previewModes,
responseFilters,
} = requestMeta;
@ -364,6 +363,9 @@ function mapStateToProps (state, props) {
const responsePreviewMode = previewModes[activeRequestId] || PREVIEW_MODE_SOURCE;
const responseFilter = responseFilters[activeRequestId] || '';
// Response Stuff
const activeResponseId = activeResponseIds[activeRequestId] || '';
// Environment stuff
const activeEnvironmentId = activeEnvironmentIds[activeWorkspaceId];
const activeEnvironment = entities.environments[activeEnvironmentId];
@ -382,6 +384,7 @@ function mapStateToProps (state, props) {
loadStartTime,
activeWorkspace,
activeRequest,
activeResponseId,
sidebarHidden,
sidebarFilter,
sidebarWidth,
@ -416,6 +419,7 @@ function mapDispatchToProps (dispatch) {
handleSendRequestWithEnvironment: requests.send,
handleSetResponsePreviewMode: requests.setPreviewMode,
handleSetResponseFilter: requests.setResponseFilter,
handleSetActiveResponse: requests.setActiveResponse,
handleSetActiveWorkspace: legacyActions.global.setActiveWorkspace,
handleImportFileToWorkspace: legacyActions.global.importFile,

View File

@ -0,0 +1,12 @@
@import '../constants/dimensions';
.method-dropdown {
.dropdown__inner::before {
content: '\25cf';
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.1);
}
.dropdown__text {
padding-left: @padding-sm;
}
}

View File

@ -39,7 +39,7 @@
grid-template-rows: auto minmax(0, 1fr) auto;
color: @font-super-light-bg;
border-radius: @radius-md;
overflow: hidden;
overflow: visible;
box-sizing: border-box;
box-shadow: 0 0 2rem 0 rgba(0, 0, 0, 0.2);
width: @modal-width;

View File

@ -7,6 +7,7 @@
grid-template-columns: 100%;
.pane__header {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
@ -20,6 +21,11 @@
background: @bg-light;
border-left: 1px solid @hl-xs;
}
.pane__header__right {
box-shadow: -@padding-md 0 @padding-md -@padding-sm fade(@bg-super-light, 85%);
background: @bg-super-light;
}
}
.pane__body {

View File

@ -10,7 +10,7 @@
left: 0;
bottom: 0;
background: fade(@bg-super-dark, 80%);
z-index: 10;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;

View File

@ -112,6 +112,12 @@
overflow: hidden;
text-overflow: ellipsis;
}
i.fa {
// Bump the drop down caret down a bit
position: relative;
top: 1px;
}
}
.btn {

View File

@ -7,19 +7,19 @@
margin-right: 1em;
line-height: 1em;
box-sizing: border-box;
border-radius: @radius-md;
border-radius: @radius-sm;
text-align: center;
background: @hl-sm;
border: 1px solid rgba(0, 0, 0, 0.07);
border: 1px solid rgba(0, 0, 0, 0.05);
white-space: nowrap;
&:last-child {
margin-right: 0;
}
&.tag--small {
padding: @padding-xs;
padding: @padding-xxs @padding-xs;
font-size: @font-size-xs;
border-radius: @radius-sm;
}
&.tag--no-bg {

View File

@ -26,26 +26,6 @@
}
}
.dropdown__inner {
border-radius: 0;
font-size: @font-size-md;
padding: 0;
&::before {
content: '\25cf';
color: inherit;
font-weight: bold;
position: relative;
font-size: 1.2em;
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.1);
}
}
.dropdown__text {
color: @hl;
padding-left: @padding-sm;
}
input {
min-width: 0;
}

View File

@ -34,81 +34,81 @@
@surprise: #9b81ff;
@info: #24cfff;
[class^="method-"],
[class*=" method-"] {
[class^="http-method-"],
[class*=" http-method-"] {
// Default method color
color: @hl;
}
.success,
.method-POST {
.http-method-POST {
color: @success !important;
}
.notice,
.method-PATCH {
.http-method-PATCH {
color: @notice !important;
}
.warning,
.method-PUT {
.http-method-PUT {
color: @warning !important;
}
.danger,
.method-DELETE {
.http-method-DELETE {
color: @danger !important;
}
.info,
.method-OPTIONS,
.method-HEAD {
.http-method-OPTIONS,
.http-method-HEAD {
color: @info !important;
}
.surprise,
.method-GET {
.http-method-GET {
color: @surprise !important;
}
.bg-success,
.bg-method-POST {
.bg-http-method-POST {
background: @success !important;
text-shadow: 0 0 0.05em darken(@success, 20);
color: #fff;
}
.bg-notice,
.bg-method-PATCH {
.bg-http-method-PATCH {
background: @notice !important;
color: #fff;
text-shadow: 0 0 0.05em darken(@notice, 20);
}
.bg-warning,
.bg-method-PUT {
.bg-http-method-PUT {
background: @warning !important;
color: #fff;
text-shadow: 0 0 0.05em darken(@warning, 20);
}
.bg-danger,
.bg-method-DELETE {
.bg-http-method-DELETE {
background: @danger !important;
color: #fff;
text-shadow: 0 0 0.05em darken(@danger, 20);
}
.bg-info,
.bg-method-OPTIONS,
.bg-method-HEAD {
.bg-http-method-OPTIONS,
.bg-http-method-HEAD {
background: @info !important;
color: #fff;
text-shadow: 0 0 0.05em darken(@info, 20);
}
.bg-surprise,
.bg-method-GET {
.bg-http-method-GET {
background: @surprise !important;
color: #fff;
text-shadow: 0 0 0.05em darken(@surprise, 20);

View File

@ -20,8 +20,8 @@
@line-height-lg: 4.5rem;
@line-height-md: 4rem;
@line-height-sm: 3rem;
@line-height-xs: 2.4rem;
@line-height-xxs: 2rem;
@line-height-xs: 2.6rem;
@line-height-xxs: 2.1rem;
@height-nav: @line-height-md;
/* Sidebar */
@ -31,7 +31,7 @@
@scrollbar-width: 0.75rem;
/* Borders */
@radius-sm: 0.15rem;
@radius-sm: 0.2rem;
@radius-md: 0.3rem;
/* Dropdowns */

View File

@ -35,3 +35,4 @@
@import 'components/keyvalueeditor';
@import 'components/editable';
@import 'components/responsepane';
@import 'components/methoddropdown';

View File

@ -150,16 +150,6 @@ code, pre, .monospace {
opacity: 0.7;
}
.center-container {
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
justify-content: center;
height: 100%;
width: 100%;
}
.auto-margin {
margin: auto;
}
@ -176,6 +166,40 @@ code, pre, .monospace {
vertical-align: bottom;
}
.row-fill {
display: flex;
flex-direction: row;
align-items: center;
align-content: stretch;
width: 100%;
}
.row-spaced {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between !important;
width: 100%;
}
.row-stretch {
display: flex;
flex-direction: row;
align-content: stretch;
align-items: stretch;
width: 100%;
}
.valign-center {
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
justify-content: center;
height: 100%;
width: 100%;
}
.pointer {
cursor: pointer;
}
@ -212,6 +236,10 @@ i.fa {
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.wrap {
white-space: normal;
}
@ -271,15 +299,19 @@ i.fa {
padding-bottom: 0;
}
.no-pad-left {
padding-left: 0;
}
.no-pad-top {
padding-top: 0;
}
.pad-left-half {
.pad-left-sm {
padding-left: @padding-md / 2;
}
.pad-right-half {
.pad-right-sm {
padding-right: @padding-md / 2;
}
@ -327,8 +359,13 @@ i.fa {
.scrollable {
overflow: auto;
position: relative;
&.scrollable--no-bars::-webkit-scrollbar {
display: none;
}
}
.scrollable-container {
position: relative;

View File

@ -6,8 +6,8 @@ export default function configureStore () {
const middleware = [thunkMiddleware];
if (__DEV__) {
// const createLogger = require('redux-logger');
// middleware.push(createLogger({collapsed: true}));
const createLogger = require('redux-logger');
middleware.push(createLogger({collapsed: true}));
}
const store = createStore(reducer, applyMiddleware(...middleware));

View File

@ -25,29 +25,6 @@ const COMMAND_TRIAL_END = 'app/billing/trial-end';
// REDUCERS //
// ~~~~~~~~ //
/** Helper to update requestGroup metadata */
function updateRequestGroupMeta (state = {}, requestGroupId, value, key) {
const newState = Object.assign({}, state);
newState[requestGroupId] = newState[requestGroupId] || {};
newState[requestGroupId][key] = value;
return newState;
}
function requestGroupMetaReducer (state = {}, action) {
switch (action.type) {
case REQUEST_GROUP_TOGGLE_COLLAPSE:
const meta = state[action.requestGroupId];
return updateRequestGroupMeta(
state,
action.requestGroupId,
meta ? !meta.collapsed : false,
'collapsed'
);
default:
return state;
}
}
function activeWorkspaceReducer (state = null, action) {
switch (action.type) {
case SET_ACTIVE_WORKSPACE:
@ -68,19 +45,9 @@ function loadingReducer (state = false, action) {
}
}
function commandReducer (state = {}, action) {
switch (action.type) {
// Nothing yet...
default:
return state;
}
}
export default combineReducers({
isLoading: loadingReducer,
requestGroupMeta: requestGroupMetaReducer,
activeWorkspaceId: activeWorkspaceReducer,
command: commandReducer,
});

View File

@ -8,6 +8,7 @@ const START_LOADING = 'requests/start-loading';
const STOP_LOADING = 'requests/stop-loading';
const SET_PREVIEW_MODE = 'requests/preview-mode';
const SET_RESPONSE_FILTER = 'requests/response-filter';
const SET_ACTIVE_RESPONSE = 'requests/active-response';
// ~~~~~~~~ //
@ -18,6 +19,7 @@ export default combineReducers({
loadingRequestIds: loadingReducer,
..._makePropertyReducer(SET_PREVIEW_MODE, 'previewModes', 'previewMode'),
..._makePropertyReducer(SET_RESPONSE_FILTER, 'responseFilters', 'filter'),
..._makePropertyReducer(SET_ACTIVE_RESPONSE, 'activeResponseIds', 'responseId'),
});
function loadingReducer (state = {}, action) {
@ -54,6 +56,11 @@ export function setResponseFilter (requestId, filter) {
return {type: SET_RESPONSE_FILTER, requestId, filter};
}
export function setActiveResponse (requestId, responseId) {
_setMeta(requestId, 'activeResponseIds', responseId);
return {type: SET_ACTIVE_RESPONSE, requestId, responseId};
}
export function send(requestId, environmentId) {
return async function (dispatch) {
dispatch(startLoading(requestId));
@ -67,6 +74,10 @@ export function send(requestId, environmentId) {
// It's OK
}
// Unset pinned response
dispatch(setActiveResponse(requestId, null));
// Stop loading
dispatch(stopLoading(requestId));
}
}
@ -87,6 +98,7 @@ export function init () {
callAction('previewModes', setPreviewMode);
callAction('responseFilters', setResponseFilter);
callAction('activeResponseIds', setActiveResponse);
}
}