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});
}
export function showEnvironmentEditModal () {
return modals.show(ENVIRONMENT_EDITOR);
export function showEnvironmentEditModal (requestGroup) {
return modals.show(ENVIRONMENT_EDITOR, {requestGroup});
}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,31 @@
import fs from 'fs'
import React, {Component, PropTypes} from 'react'
import {bindActionCreators} from 'redux'
import {connect} from 'react-redux'
import Dropdown from '../base/Dropdown'
import * as RequestGroupActions from '../../actions/requestGroups'
import * as db from '../../database'
import importData from '../../lib/import'
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 () {
const {actions, loading, ...other} = this.props;
@ -23,16 +43,26 @@ class WorkspaceDropdown extends Component {
</div>
</button>
<ul>
<li><button onClick={e => db.requestCreate()}>
<i className="fa fa-plus-circle"></i> Add Request
</button></li>
<li><button onClick={e => db.requestGroupCreate()}>
<i className="fa fa-folder"></i> Add Request Group
</button></li>
<li><button onClick={e => actions.showEnvironmentEditModal()}>
<i className="fa fa-code"></i> Environments
</button></li>
<li><button><i className="fa fa-share-square-o"></i> Import/Export</button></li>
<li>
<button onClick={e => db.requestCreate()}>
<i className="fa fa-plus-circle"></i> Add Request
</button>
</li>
<li>
<button onClick={e => db.requestGroupCreate()}>
<i className="fa fa-folder"></i> Add Request Group
</button>
</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> Delete Workspace</button></li>
</ul>

View File

@ -7,22 +7,39 @@ import Editor from '../base/Editor'
import KeyValueEditor from '../base/KeyValueEditor'
import * as modalIds from '../../constants/modals'
class RequestGroupEnvironmentEditModal extends Component {
class EnvironmentEditModal extends Component {
constructor (props) {
super(props);
this.state = {
pairs: []
pairs: this._mapDataToPairs(props.requestGroup.environment)
}
}
_saveChanges () {
this.props.onChange(this.state.pairs);
this.props.onChange(this._mapPairsToData(this.state.pairs));
this.props.onClose();
}
_keyValueChange (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 () {
const editorOptions = {
mode: 'application/json',
@ -36,6 +53,7 @@ class RequestGroupEnvironmentEditModal extends Component {
<ModalBody className="grid--v wide pad">
<div>
<KeyValueEditor onChange={this._keyValueChange.bind(this)}
uniquenessKey={this.props.requestGroup._id}
pairs={this.state.pairs}
namePlaceholder="BASE_URL"
valuePlaceholder="https://api.insomnia.com/v1"/>
@ -53,13 +71,15 @@ class RequestGroupEnvironmentEditModal extends Component {
}
}
RequestGroupEnvironmentEditModal.propTypes = {
// requestGroup: PropTypes.object.isRequired,
EnvironmentEditModal.propTypes = {
requestGroup: PropTypes.shape({
environment: PropTypes.object.isRequired
}),
onChange: PropTypes.func.isRequired
};
RequestGroupEnvironmentEditModal.defaultProps = {
EnvironmentEditModal.defaultProps = {
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 RequestUrlBar from '../components/RequestUrlBar'
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 RequestGroupActions from '../actions/requestGroups'
import * as RequestActions from '../actions/requests'
import * as ModalActions from '../actions/modals'
import * as db from '../database'
@ -63,6 +64,7 @@ class App extends Component {
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide pad">
<KeyValueEditor
uniquenessKey={activeRequest._id}
pairs={activeRequest.params}
onChange={params => {db.update(activeRequest, {params})}}
/>
@ -79,6 +81,7 @@ class App extends Component {
<TabPanel className="grid__cell grid__cell--scroll--v">
<div className="wide pad">
<KeyValueEditor
uniquenessKey={activeRequest._id}
pairs={activeRequest.headers}
onChange={headers => {db.update(activeRequest, {headers})}}
/>
@ -153,17 +156,25 @@ class App extends Component {
return (
<div className="grid bg-super-dark tall">
<Prompts />
{!modals.find(m => m.id === RequestGroupEnvironmentEditModal.defaultProps.id) ? null : (
<RequestGroupEnvironmentEditModal
onClose={() => actions.hideModal(RequestGroupEnvironmentEditModal.defaultProps.id)}
onChange={v => console.log(v)}
/>
)}
{modals.map(m => {
if (m.id === EnvironmentEditModal.defaultProps.id) {
return (
<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
activateRequest={actions.requests.activate}
changeFilter={actions.requests.changeFilter}
addRequestToRequestGroup={requestGroup => db.requestCreate({parent: requestGroup._id})}
toggleRequestGroup={requestGroup => db.requestGroupToggle(requestGroup)}
toggleRequestGroup={requestGroup => db.update(requestGroup, {collapsed: !requestGroup.collapsed})}
activeRequest={activeRequest}
activeFilter={requests.filter}
requestGroups={requestGroups.all}
@ -193,6 +204,9 @@ App.propTypes = {
update: PropTypes.func.isRequired,
toggle: PropTypes.func.isRequired
}),
modals: PropTypes.shape({
hide: PropTypes.func.isRequired
}),
global: PropTypes.shape({
selectTab: PropTypes.func.isRequired
})
@ -224,6 +238,7 @@ function mapDispatchToProps (dispatch) {
return {
actions: {
global: bindActionCreators(GlobalActions, dispatch),
modals: bindActionCreators(ModalActions, dispatch),
requestGroups: bindActionCreators(RequestGroupActions, dispatch),
requests: bindActionCreators(RequestActions, dispatch)
}

View File

@ -1,5 +1,6 @@
import PouchDB from 'pouchdb';
import * as methods from '../constants/global';
import {generateId} from './util'
let db = new PouchDB('insomnia.db');
@ -20,6 +21,10 @@ export function allDocs () {
return db.allDocs({include_docs: true});
}
export function get (id) {
return db.get(id);
}
export function update (doc, patch = {}) {
const updatedDoc = Object.assign(
{},
@ -31,7 +36,7 @@ export function update (doc, patch = {}) {
return db.put(updatedDoc).catch(e => {
if (e.status === 409) {
console.warn('Retrying document update for', updatedDoc);
db.get(doc._id).then(dbDoc => {
get(doc._id).then(dbDoc => {
update(dbDoc, patch);
});
}
@ -47,7 +52,7 @@ export function remove (doc) {
// ~~~~~~~ //
export function requestCreate (patch = {}) {
update(Object.assign(
const request = Object.assign(
// Defaults
{
url: '',
@ -66,13 +71,17 @@ export function requestCreate (patch = {}) {
// Required Generated Fields
{
_id: `rq_${Date.now()}`,
_id: generateId('req'),
_rev: undefined,
type: 'Request',
created: Date.now(),
modified: Date.now()
}
));
);
update(request);
return request;
}
export function requestDuplicate (request) {
@ -84,7 +93,7 @@ export function requestDuplicate (request) {
// ~~~~~~~~~~~~~ //
export function requestGroupCreate (patch = {}) {
update(Object.assign(
const requestGroup = Object.assign(
// Default Fields
{
collapsed: false,
@ -98,17 +107,17 @@ export function requestGroupCreate (patch = {}) {
// Required Generated Fields
{
_id: `rg_${Date.now()}`,
_id: generateId('grp'),
_rev: undefined,
type: 'RequestGroup',
created: Date.now(),
modified: Date.now()
}
));
}
export function requestGroupToggle (requestGroup, patch = {}) {
return update(requestGroup, patch);
);
update(requestGroup);
return requestGroup;
}
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 render from './render'
import * as db from '../database'
export default function (request, callback) {
function makeRequest (request, callback) {
const config = {
url: request.url,
method: request.method,
body: request.body,
headers: {}
};
if (request.url.indexOf('://') === -1) {
config.url = request.url;
} else {
config.url = `https://${request.url}`;
}
if (request.authentication.username) {
config.auth = {
user: request.authentication.username,
@ -23,7 +29,7 @@ export default function (request, callback) {
config.headers[header.name] = header.value;
}
}
// TODO: this is just a POC. It breaks in a lot of cases
config.url += request.params.map((p, i) => {
const name = encodeURIComponent(p.name);
@ -31,12 +37,8 @@ export default function (request, callback) {
return `${i === 0 ? '?' : '&'}${name}=${value}`;
}).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) {
return callback(err);
} 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",
"concurrently": "^2.0.0",
"css-loader": "^0.23.1",
"electron-prebuilt": "^0.37.2",
"express": "latest",
"file-loader": "^0.8.5",
"jest": "^0.1.40",

View File

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