PouchDB, import, environments closes #13

This commit is contained in:
Gregory Schier 2016-04-16 18:52:10 -07:00
parent fc9d0c41fb
commit 097859aac1
14 changed files with 232 additions and 53 deletions

View File

@ -20,6 +20,6 @@ export function showUpdateNamePrompt (requestGroup) {
return modals.show(REQUEST_GROUP_RENAME, {defaultValue, requestGroup}); return modals.show(REQUEST_GROUP_RENAME, {defaultValue, requestGroup});
} }
export function showEnvironmentEditModal () { export function showEnvironmentEditModal (requestGroup) {
return modals.show(ENVIRONMENT_EDITOR); return modals.show(ENVIRONMENT_EDITOR, {requestGroup});
} }

View File

@ -13,6 +13,7 @@ class RequestAuthEditor extends Component {
return ( return (
<KeyValueEditor <KeyValueEditor
uniquenessKey={request._id}
pairs={pairs} pairs={pairs}
maxPairs={1} maxPairs={1}
namePlaceholder="Username" namePlaceholder="Username"

View File

@ -1,5 +1,4 @@
import React, {Component, PropTypes} from 'react' import React, {Component, PropTypes} from 'react'
import DebouncingInput from './DebouncingInput'
const NAME = 'name'; const NAME = 'name';
const VALUE = 'value'; const VALUE = 'value';
@ -131,6 +130,13 @@ class KeyValueEditor extends Component {
} }
} }
shouldComponentUpdate (nextProps) {
return (
nextProps.uniquenessKey !== this.props.uniquenessKey ||
nextProps.pairs.length !== this.state.pairs.length
)
}
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
this.setState({pairs: nextProps.pairs}) this.setState({pairs: nextProps.pairs})
} }
@ -153,7 +159,7 @@ class KeyValueEditor extends Component {
type="text" type="text"
placeholder={this.props.namePlaceholder || 'Name'} placeholder={this.props.namePlaceholder || 'Name'}
ref={`${i}.${NAME}`} ref={`${i}.${NAME}`}
value={pair.name} defaultValue={pair.name}
onChange={e => this._updatePair(i, {name: e.target.value})} onChange={e => this._updatePair(i, {name: e.target.value})}
onFocus={e => {this._focusedPair = i; this._focusedField = NAME}} onFocus={e => {this._focusedPair = i; this._focusedField = NAME}}
onBlur={e => {this._focusedPair = -1}} onBlur={e => {this._focusedPair = -1}}
@ -165,7 +171,7 @@ class KeyValueEditor extends Component {
type="text" type="text"
placeholder={this.props.valuePlaceholder || 'Value'} placeholder={this.props.valuePlaceholder || 'Value'}
ref={`${i}.${VALUE}`} ref={`${i}.${VALUE}`}
value={pair.value} defaultValue={pair.value}
onChange={e => this._updatePair(i, {value: e.target.value})} onChange={e => this._updatePair(i, {value: e.target.value})}
onFocus={e => {this._focusedPair = i; this._focusedField = VALUE}} onFocus={e => {this._focusedPair = i; this._focusedField = VALUE}}
onBlur={e => {this._focusedPair = -1}} onBlur={e => {this._focusedPair = -1}}
@ -210,6 +216,7 @@ class KeyValueEditor extends Component {
KeyValueEditor.propTypes = { KeyValueEditor.propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
uniquenessKey: PropTypes.string.isRequired,
pairs: PropTypes.array.isRequired, pairs: PropTypes.array.isRequired,
maxPairs: PropTypes.number, maxPairs: PropTypes.number,
namePlaceholder: PropTypes.string, namePlaceholder: PropTypes.string,

View File

@ -21,7 +21,7 @@ class RequestGroupActionsDropdown extends Component {
</button> </button>
</li> </li>
<li> <li>
<button> <button onClick={e => actions.showEnvironmentEditModal(requestGroup)}>
<i className="fa fa-code"></i> Environment <i className="fa fa-code"></i> Environment
</button> </button>
</li> </li>
@ -40,7 +40,8 @@ RequestGroupActionsDropdown.propTypes = {
actions: PropTypes.shape({ actions: PropTypes.shape({
update: PropTypes.func.isRequired, update: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired, remove: PropTypes.func.isRequired,
showUpdateNamePrompt: PropTypes.func.isRequired showUpdateNamePrompt: PropTypes.func.isRequired,
showEnvironmentEditModal: PropTypes.func.isRequired
}), }),
requestGroup: PropTypes.object requestGroup: PropTypes.object
}; };

View File

@ -1,11 +1,31 @@
import fs from 'fs'
import React, {Component, PropTypes} from 'react' import React, {Component, PropTypes} from 'react'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import Dropdown from '../base/Dropdown' import Dropdown from '../base/Dropdown'
import * as RequestGroupActions from '../../actions/requestGroups' import * as RequestGroupActions from '../../actions/requestGroups'
import * as db from '../../database' import * as db from '../../database'
import importData from '../../lib/import'
class WorkspaceDropdown extends Component { class WorkspaceDropdown extends Component {
_importDialog () {
const dialog = require('electron').remote.dialog;
const options = {
properties: ['openFile'],
filters: [{
name: 'Insomnia Imports', extensions: ['json']
}]
};
dialog.showOpenDialog(options, paths => {
paths.map(path => {
fs.readFile(path, 'utf8', (err, data) => {
err || importData(data);
})
})
});
}
render () { render () {
const {actions, loading, ...other} = this.props; const {actions, loading, ...other} = this.props;
@ -23,16 +43,26 @@ class WorkspaceDropdown extends Component {
</div> </div>
</button> </button>
<ul> <ul>
<li><button onClick={e => db.requestCreate()}> <li>
<i className="fa fa-plus-circle"></i> Add Request <button onClick={e => db.requestCreate()}>
</button></li> <i className="fa fa-plus-circle"></i> Add Request
<li><button onClick={e => db.requestGroupCreate()}> </button>
<i className="fa fa-folder"></i> Add Request Group </li>
</button></li> <li>
<li><button onClick={e => actions.showEnvironmentEditModal()}> <button onClick={e => db.requestGroupCreate()}>
<i className="fa fa-code"></i> Environments <i className="fa fa-folder"></i> Add Request Group
</button></li> </button>
<li><button><i className="fa fa-share-square-o"></i> Import/Export</button></li> </li>
<li>
<button onClick={e => actions.showEnvironmentEditModal()}>
<i className="fa fa-code"></i> Environments
</button>
</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> Toggle Sidebar</button></li> <li><button><i className="fa fa-empty"></i> Toggle Sidebar</button></li>
<li><button><i className="fa fa-empty"></i> Delete Workspace</button></li> <li><button><i className="fa fa-empty"></i> Delete Workspace</button></li>
</ul> </ul>

View File

@ -7,22 +7,39 @@ import Editor from '../base/Editor'
import KeyValueEditor from '../base/KeyValueEditor' import KeyValueEditor from '../base/KeyValueEditor'
import * as modalIds from '../../constants/modals' import * as modalIds from '../../constants/modals'
class RequestGroupEnvironmentEditModal extends Component { class EnvironmentEditModal extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
pairs: [] pairs: this._mapDataToPairs(props.requestGroup.environment)
} }
} }
_saveChanges () { _saveChanges () {
this.props.onChange(this.state.pairs); this.props.onChange(this._mapPairsToData(this.state.pairs));
this.props.onClose();
} }
_keyValueChange (pairs) { _keyValueChange (pairs) {
this.setState({pairs}); this.setState({pairs});
} }
_mapPairsToData (pairs) {
return pairs.reduce((prev, curr) => {
return Object.assign({}, prev, {[curr.name]: curr.value});
}, {});
}
_mapDataToPairs (data) {
return Object.keys(data).map(key => ({name: key, value: data[key]}));
}
componentWillReceiveProps (nextProps) {
this.setState({
pairs: this._mapDataToPairs(nextProps.requestGroup.environment)
})
}
render () { render () {
const editorOptions = { const editorOptions = {
mode: 'application/json', mode: 'application/json',
@ -36,6 +53,7 @@ class RequestGroupEnvironmentEditModal extends Component {
<ModalBody className="grid--v wide pad"> <ModalBody className="grid--v wide pad">
<div> <div>
<KeyValueEditor onChange={this._keyValueChange.bind(this)} <KeyValueEditor onChange={this._keyValueChange.bind(this)}
uniquenessKey={this.props.requestGroup._id}
pairs={this.state.pairs} pairs={this.state.pairs}
namePlaceholder="BASE_URL" namePlaceholder="BASE_URL"
valuePlaceholder="https://api.insomnia.com/v1"/> valuePlaceholder="https://api.insomnia.com/v1"/>
@ -53,13 +71,15 @@ class RequestGroupEnvironmentEditModal extends Component {
} }
} }
RequestGroupEnvironmentEditModal.propTypes = { EnvironmentEditModal.propTypes = {
// requestGroup: PropTypes.object.isRequired, requestGroup: PropTypes.shape({
environment: PropTypes.object.isRequired
}),
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired
}; };
RequestGroupEnvironmentEditModal.defaultProps = { EnvironmentEditModal.defaultProps = {
id: modalIds.ENVIRONMENT_EDITOR id: modalIds.ENVIRONMENT_EDITOR
}; };
export default RequestGroupEnvironmentEditModal; export default EnvironmentEditModal;

View File

@ -10,11 +10,12 @@ import RequestBodyEditor from '../components/RequestBodyEditor'
import RequestAuthEditor from '../components/RequestAuthEditor' import RequestAuthEditor from '../components/RequestAuthEditor'
import RequestUrlBar from '../components/RequestUrlBar' import RequestUrlBar from '../components/RequestUrlBar'
import Sidebar from '../components/Sidebar' import Sidebar from '../components/Sidebar'
import RequestGroupEnvironmentEditModal from '../components/modals/RequestGroupEnvironmentEditModal' import EnvironmentEditModal from '../components/modals/EnvironmentEditModal'
import * as GlobalActions from '../actions/global' import * as GlobalActions from '../actions/global'
import * as RequestGroupActions from '../actions/requestGroups' import * as RequestGroupActions from '../actions/requestGroups'
import * as RequestActions from '../actions/requests' import * as RequestActions from '../actions/requests'
import * as ModalActions from '../actions/modals'
import * as db from '../database' import * as db from '../database'
@ -63,6 +64,7 @@ class App extends Component {
<TabPanel className="grid__cell grid__cell--scroll--v"> <TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide pad"> <div className="wide pad">
<KeyValueEditor <KeyValueEditor
uniquenessKey={activeRequest._id}
pairs={activeRequest.params} pairs={activeRequest.params}
onChange={params => {db.update(activeRequest, {params})}} onChange={params => {db.update(activeRequest, {params})}}
/> />
@ -79,6 +81,7 @@ class App extends Component {
<TabPanel className="grid__cell grid__cell--scroll--v"> <TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide pad"> <div className="wide pad">
<KeyValueEditor <KeyValueEditor
uniquenessKey={activeRequest._id}
pairs={activeRequest.headers} pairs={activeRequest.headers}
onChange={headers => {db.update(activeRequest, {headers})}} onChange={headers => {db.update(activeRequest, {headers})}}
/> />
@ -153,17 +156,25 @@ class App extends Component {
return ( return (
<div className="grid bg-super-dark tall"> <div className="grid bg-super-dark tall">
<Prompts /> <Prompts />
{!modals.find(m => m.id === RequestGroupEnvironmentEditModal.defaultProps.id) ? null : ( {modals.map(m => {
<RequestGroupEnvironmentEditModal if (m.id === EnvironmentEditModal.defaultProps.id) {
onClose={() => actions.hideModal(RequestGroupEnvironmentEditModal.defaultProps.id)} return (
onChange={v => console.log(v)} <EnvironmentEditModal
/> key={m.id}
)} requestGroup={m.data.requestGroup}
onClose={() => actions.modals.hide(m.id)}
onChange={rg => db.update(m.data.requestGroup, {environment: rg.environment})}
/>
)
} else {
return null;
}
})}
<Sidebar <Sidebar
activateRequest={actions.requests.activate} activateRequest={actions.requests.activate}
changeFilter={actions.requests.changeFilter} changeFilter={actions.requests.changeFilter}
addRequestToRequestGroup={requestGroup => db.requestCreate({parent: requestGroup._id})} addRequestToRequestGroup={requestGroup => db.requestCreate({parent: requestGroup._id})}
toggleRequestGroup={requestGroup => db.requestGroupToggle(requestGroup)} toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})}
activeRequest={activeRequest} activeRequest={activeRequest}
activeFilter={requests.filter} activeFilter={requests.filter}
requestGroups={requestGroups.all} requestGroups={requestGroups.all}
@ -193,6 +204,9 @@ App.propTypes = {
update: PropTypes.func.isRequired, update: PropTypes.func.isRequired,
toggle: PropTypes.func.isRequired toggle: PropTypes.func.isRequired
}), }),
modals: PropTypes.shape({
hide: PropTypes.func.isRequired
}),
global: PropTypes.shape({ global: PropTypes.shape({
selectTab: PropTypes.func.isRequired selectTab: PropTypes.func.isRequired
}) })
@ -224,6 +238,7 @@ function mapDispatchToProps (dispatch) {
return { return {
actions: { actions: {
global: bindActionCreators(GlobalActions, dispatch), global: bindActionCreators(GlobalActions, dispatch),
modals: bindActionCreators(ModalActions, dispatch),
requestGroups: bindActionCreators(RequestGroupActions, dispatch), requestGroups: bindActionCreators(RequestGroupActions, dispatch),
requests: bindActionCreators(RequestActions, dispatch) requests: bindActionCreators(RequestActions, dispatch)
} }

View File

@ -1,5 +1,6 @@
import PouchDB from 'pouchdb'; import PouchDB from 'pouchdb';
import * as methods from '../constants/global'; import * as methods from '../constants/global';
import {generateId} from './util'
let db = new PouchDB('insomnia.db'); let db = new PouchDB('insomnia.db');
@ -20,6 +21,10 @@ export function allDocs () {
return db.allDocs({include_docs: true}); return db.allDocs({include_docs: true});
} }
export function get (id) {
return db.get(id);
}
export function update (doc, patch = {}) { export function update (doc, patch = {}) {
const updatedDoc = Object.assign( const updatedDoc = Object.assign(
{}, {},
@ -31,7 +36,7 @@ export function update (doc, patch = {}) {
return db.put(updatedDoc).catch(e => { return db.put(updatedDoc).catch(e => {
if (e.status === 409) { if (e.status === 409) {
console.warn('Retrying document update for', updatedDoc); console.warn('Retrying document update for', updatedDoc);
db.get(doc._id).then(dbDoc => { get(doc._id).then(dbDoc => {
update(dbDoc, patch); update(dbDoc, patch);
}); });
} }
@ -47,7 +52,7 @@ export function remove (doc) {
// ~~~~~~~ // // ~~~~~~~ //
export function requestCreate (patch = {}) { export function requestCreate (patch = {}) {
update(Object.assign( const request = Object.assign(
// Defaults // Defaults
{ {
url: '', url: '',
@ -66,13 +71,17 @@ export function requestCreate (patch = {}) {
// Required Generated Fields // Required Generated Fields
{ {
_id: `rq_${Date.now()}`, _id: generateId('req'),
_rev: undefined, _rev: undefined,
type: 'Request', type: 'Request',
created: Date.now(), created: Date.now(),
modified: Date.now() modified: Date.now()
} }
)); );
update(request);
return request;
} }
export function requestDuplicate (request) { export function requestDuplicate (request) {
@ -84,7 +93,7 @@ export function requestDuplicate (request) {
// ~~~~~~~~~~~~~ // // ~~~~~~~~~~~~~ //
export function requestGroupCreate (patch = {}) { export function requestGroupCreate (patch = {}) {
update(Object.assign( const requestGroup = Object.assign(
// Default Fields // Default Fields
{ {
collapsed: false, collapsed: false,
@ -98,17 +107,17 @@ export function requestGroupCreate (patch = {}) {
// Required Generated Fields // Required Generated Fields
{ {
_id: `rg_${Date.now()}`, _id: generateId('grp'),
_rev: undefined, _rev: undefined,
type: 'RequestGroup', type: 'RequestGroup',
created: Date.now(), created: Date.now(),
modified: Date.now() modified: Date.now()
} }
)); );
}
update(requestGroup);
export function requestGroupToggle (requestGroup, patch = {}) {
return update(requestGroup, patch); return requestGroup;
} }
export {changes}; export {changes};

11
app/database/util.js Normal file
View File

@ -0,0 +1,11 @@
const CHARS = '023456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ'.split('');
export function generateId (prefix) {
let id = `${prefix}_${Date.now()}-`;
for (let i = 0; i < 10; i++) {
id += CHARS[Math.floor(Math.random() * CHARS.length)];
}
return id;
}

70
app/lib/import.js Normal file
View File

@ -0,0 +1,70 @@
import * as db from '../database'
const TYPE_REQUEST = 'request';
const TYPE_REQUEST_GROUP = 'request_group';
const FORMAT_MAP = {
'json': 'application/json'
};
function importRequestGroup (iRequestGroup, exportFormat) {
if (exportFormat === 1) {
const requestGroup = db.requestGroupCreate({
name: iRequestGroup.name,
environment: (iRequestGroup.environments || {}).base || {}
});
// Sometimes (maybe all the time, I can't remember) requests will be nested
if (iRequestGroup.hasOwnProperty('requests')) {
iRequestGroup.requests.map(
r => importRequest(r, requestGroup._id, exportFormat)
);
}
}
}
function importRequest (iRequest, parent, exportFormat) {
if (exportFormat === 1) {
let auth = {};
if (iRequest.authentication.username) {
auth = {
username: iRequest.authentication.username,
password: iRequest.authentication.password
}
}
db.requestCreate({
name: iRequest.name,
url: iRequest.url,
method: iRequest.method,
body: iRequest.body,
headers: iRequest.headers || [],
params: iRequest.params || [],
contentType: FORMAT_MAP[iRequest.__insomnia.format] || 'text/plain',
authentication: auth,
parent: parent
});
}
}
export default function (txt, callback) {
let data;
try {
data = JSON.parse(txt);
} catch (e) {
return callback(new Error('Invalid Insomnia export'));
}
if (!data.hasOwnProperty('_type') || !data.hasOwnProperty('items')) {
return callback(new Error('Invalid Insomnia export'));
}
data.items.filter(i => i._type === TYPE_REQUEST_GROUP).map(
rg => importRequestGroup(rg, data.__export_format)
);
data.items.filter(i => i._type === TYPE_REQUEST).map(
r => importRequest(r, data.__export_format)
);
}

View File

@ -1,14 +1,20 @@
import networkRequest from 'request' import networkRequest from 'request'
import render from './render' import render from './render'
import * as db from '../database'
export default function (request, callback) { function makeRequest (request, callback) {
const config = { const config = {
url: request.url,
method: request.method, method: request.method,
body: request.body, body: request.body,
headers: {} headers: {}
}; };
if (request.url.indexOf('://') === -1) {
config.url = request.url;
} else {
config.url = `https://${request.url}`;
}
if (request.authentication.username) { if (request.authentication.username) {
config.auth = { config.auth = {
user: request.authentication.username, user: request.authentication.username,
@ -23,7 +29,7 @@ export default function (request, callback) {
config.headers[header.name] = header.value; config.headers[header.name] = header.value;
} }
} }
// TODO: this is just a POC. It breaks in a lot of cases // TODO: this is just a POC. It breaks in a lot of cases
config.url += request.params.map((p, i) => { config.url += request.params.map((p, i) => {
const name = encodeURIComponent(p.name); const name = encodeURIComponent(p.name);
@ -31,12 +37,8 @@ export default function (request, callback) {
return `${i === 0 ? '?' : '&'}${name}=${value}`; return `${i === 0 ? '?' : '&'}${name}=${value}`;
}).join(''); }).join('');
// SNEAKY HACK: Render nested object by converting it to JSON then rendering
const context = {template_id: 'tem_WWq2w9uJNR6Pqk8APkvsS3'};
const template = JSON.stringify(config);
const renderedConfig = JSON.parse(render(template, context));
networkRequest(renderedConfig, function (err, response) { networkRequest(config, function (err, response) {
if (err) { if (err) {
return callback(err); return callback(err);
} else { } else {
@ -52,3 +54,17 @@ export default function (request, callback) {
} }
}); });
} }
export default function (originalRequest, callback) {
// SNEAKY HACK: Render nested object by converting it to JSON then rendering
const template = JSON.stringify(originalRequest);
const request = JSON.parse(render(template, context));
if (request.parent) {
db.get(request.parent).then(
requestGroup => makeRequest(config, callback, requestGroup.environment)
);
} else {
makeRequest(request, callback)
}
}

View File

@ -36,7 +36,6 @@
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"concurrently": "^2.0.0", "concurrently": "^2.0.0",
"css-loader": "^0.23.1", "css-loader": "^0.23.1",
"electron-prebuilt": "^0.37.2",
"express": "latest", "express": "latest",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"jest": "^0.1.40", "jest": "^0.1.40",

View File

@ -2,7 +2,7 @@
const express = require('express'); const express = require('express');
const webpack = require('webpack'); const webpack = require('webpack');
const config = require('./dev.electron.config.js'); const config = require('./dev.config.js');
const app = express(); const app = express();
const compiler = webpack(config); const compiler = webpack(config);