JSONPath filtering (#37)

* POC JSONPath support

* Add help for JSONPath

* Now save response filter on request

* Fixed test
This commit is contained in:
Gregory Schier 2016-09-08 17:52:14 -07:00 committed by GitHub
parent c9766e0768
commit 2bfc4516ac
11 changed files with 149 additions and 20 deletions

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "insomnia",
"version": "3.3.2",
"version": "3.3.3",
"productName": "Insomnia",
"longName": "Insomnia REST Client",
"description": "A simple and beautiful REST API client",
@ -13,6 +13,7 @@
"electron-context-menu": "^0.4.0",
"electron-squirrel-startup": "^1.0.0",
"httpsnippet": "git@github.com:gschier/httpsnippet.git#39d2fb0449f33711e5cc71a4d42b0e5b808426b4",
"jsonpath": "^0.2.7",
"nedb": "^1.8.0",
"node-localstorage": "^1.3.0",
"nunjucks": "git@github.com:gschier/nunjucks.git#80485468cd577f1a1a8067bedf6c5bfa878712ea",

View File

@ -44,9 +44,11 @@ class ResponsePane extends Component {
request,
previewMode,
updatePreviewMode,
updateResponseFilter,
loadingRequests,
editorLineWrapping,
editorFontSize,
responseFilter,
showCookiesModal
} = this.props;
@ -184,6 +186,8 @@ class ResponsePane extends Component {
key={response._id}
contentType={response.contentType}
previewMode={response.error ? PREVIEW_MODE_SOURCE : previewMode}
filter={response.error ? '' : responseFilter}
updateFilter={response.error ? null : updateResponseFilter}
body={response.error ? response.error : response.body}
error={!!response.error}
editorLineWrapping={editorLineWrapping}
@ -213,10 +217,12 @@ class ResponsePane extends Component {
ResponsePane.propTypes = {
// Functions
updatePreviewMode: PropTypes.func.isRequired,
updateResponseFilter: PropTypes.func.isRequired,
showCookiesModal: PropTypes.func.isRequired,
// Required
previewMode: PropTypes.string.isRequired,
responseFilter: PropTypes.string.isRequired,
loadingRequests: PropTypes.object.isRequired,
editorFontSize: PropTypes.number.isRequired,
editorLineWrapping: PropTypes.bool.isRequired,

View File

@ -2,6 +2,7 @@ import React, {Component, PropTypes} from 'react';
import {getDOMNode} from 'react-dom';
import CodeMirror from 'codemirror';
import classnames from 'classnames';
import JSONPath from 'jsonpath-plus';
import {DEBOUNCE_MILLIS} from '../../lib/constants';
import 'codemirror/mode/css/css';
import 'codemirror/mode/htmlmixed/htmlmixed';
@ -35,6 +36,9 @@ import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/json-lint';
import 'codemirror/addon/lint/lint.css';
import '../../css/components/editor.less';
import {getModal} from '../modals/index';
import AlertModal from '../modals/AlertModal';
import Link from '../base/Link';
const BASE_CODEMIRROR_OPTIONS = {
@ -63,9 +67,12 @@ const BASE_CODEMIRROR_OPTIONS = {
};
class Editor extends Component {
constructor () {
super();
this.state = {isFocused: false}
constructor (props) {
super(props);
this.state = {
filter: props.filter || ''
};
this._originalCode = '';
}
componentWillUnmount () {
@ -122,6 +129,7 @@ class Editor extends Component {
});
}
// Do this a bit later so we don't block the render process
setTimeout(() => {
this._codemirrorSetValue(value || '');
}, 50);
@ -129,6 +137,14 @@ class Editor extends Component {
this._codemirrorSetOptions();
}
_isJSON (mode) {
if (!mode) {
return false;
}
return mode.indexOf('json') !== -1
}
/**
* Sets options on the CodeMirror editor while also sanitizing them
*/
@ -147,7 +163,7 @@ class Editor extends Component {
// Strip of charset if there is one
options.mode = options.mode ? options.mode.split(';')[0] : 'text/plain';
if (options.mode.indexOf('json') !== -1) {
if (this._isJSON(options.mode)) {
// set LD JSON because it highlights the keys a different color
options.mode = {name: 'javascript', jsonld: true}
}
@ -183,11 +199,18 @@ class Editor extends Component {
* @param code the code to set in the editor
*/
_codemirrorSetValue (code) {
this._originalCode = code;
this._ignoreNextChange = true;
if (this.props.prettify) {
try {
code = JSON.stringify(JSON.parse(code), null, '\t');
let obj = JSON.parse(code);
if (this.props.updateFilter && this.state.filter) {
obj = JSONPath({json: obj, path: this.state.filter});
}
code = JSON.stringify(obj, null, '\t');
} catch (e) {
// That's Ok, just leave it
// TODO: support more than just JSON prettifying
@ -197,12 +220,60 @@ class Editor extends Component {
this.codeMirror.setValue(code);
}
_handleFilterChange (filter) {
clearTimeout(this._filterTimeout);
this._filterTimeout = setTimeout(() => {
this.setState({filter});
this._codemirrorSetValue(this._originalCode);
if (this.props.updateFilter) {
this.props.updateFilter(filter);
}
}, DEBOUNCE_MILLIS);
}
_showFilterHelp () {
getModal(AlertModal).show({
headerName: 'Response Filtering Help',
message: (
<div>
<p>
Use <Link href="http://schier.co">JSONPath</Link> to filter the
response body. Here are some examples that you might use on a
book store API.
</p>
<table className="pad-top-sm">
<tbody>
<tr>
<td><code className="selectable">$.store.books[*].title</code>
</td>
<td>Get titles of all books in the store</td>
</tr>
<tr>
<td><code className="selectable">$.store.books[?(@.price &lt;
10)].title</code></td>
<td>Get books costing more than $10</td>
</tr>
<tr>
<td><code className="selectable">$.store.books[-1:]</code></td>
<td>Get the last book in the store</td>
</tr>
<tr>
<td><code className="selectable">$.store.books.length</code></td>
<td>Get the number of books in the store</td>
</tr>
</tbody>
</table>
</div>
)
})
}
componentDidUpdate () {
this._codemirrorSetOptions();
}
render () {
const {readOnly, fontSize, lightTheme} = this.props;
const {readOnly, fontSize, lightTheme, mode, filter} = this.props;
const classes = classnames(
'editor',
@ -214,6 +285,26 @@ class Editor extends Component {
}
);
let filterElement = null;
if (this.props.updateFilter && this._isJSON(mode)) {
filterElement = (
<div className="editor__filter">
<div className="form-control form-control--outlined">
<input
type="text"
defaultValue={filter || ''}
placeholder="$.store.book[*].author"
onChange={e => this._handleFilterChange(e.target.value)}
/>
</div>
<button className="btn btn--compact"
onClick={() => this._showFilterHelp()}>
<i className="fa fa-question-circle"></i>
</button>
</div>
)
}
return (
<div className={classes} style={{fontSize: `${fontSize || 12}px`}}>
<textarea
@ -221,6 +312,7 @@ class Editor extends Component {
readOnly={readOnly}
autoComplete='off'>
</textarea>
{filterElement}
</div>
);
}
@ -236,7 +328,9 @@ Editor.propTypes = {
value: PropTypes.string,
prettify: PropTypes.bool,
className: PropTypes.any,
lightTheme: PropTypes.bool
lightTheme: PropTypes.bool,
updateFilter: PropTypes.func,
filter: PropTypes.string
};
export default Editor;

View File

@ -21,9 +21,11 @@ class ResponseViewer extends Component {
render () {
const {
previewMode,
filter,
contentType,
editorLineWrapping,
editorFontSize,
updateFilter,
body,
url,
error
@ -31,10 +33,7 @@ class ResponseViewer extends Component {
if (error) {
return (
<ResponseError
url={url}
error={body}
/>
<ResponseError url={url} error={body}/>
)
}
@ -51,6 +50,8 @@ class ResponseViewer extends Component {
return (
<Editor
value={body || ''}
updateFilter={updateFilter}
filter={filter}
prettify={true}
mode={contentType}
readOnly={true}
@ -73,11 +74,13 @@ class ResponseViewer extends Component {
ResponseViewer.propTypes = {
body: PropTypes.string.isRequired,
previewMode: PropTypes.string.isRequired,
filter: PropTypes.string.isRequired,
editorFontSize: PropTypes.number.isRequired,
editorLineWrapping: PropTypes.bool.isRequired,
url: PropTypes.string.isRequired,
// Optional
updateFilter: PropTypes.func,
contentType: PropTypes.string,
error: PropTypes.bool
};

View File

@ -513,7 +513,9 @@ class App extends Component {
editorFontSize={settings.editorFontSize}
editorLineWrapping={settings.editorLineWrapping}
previewMode={activeRequest ? activeRequest.metaPreviewMode : PREVIEW_MODE_FRIENDLY}
responseFilter={activeRequest ? activeRequest.metaResponseFilter : ''}
updatePreviewMode={metaPreviewMode => db.requestUpdate(activeRequest, {metaPreviewMode})}
updateResponseFilter={metaResponseFilter => db.requestUpdate(activeRequest, {metaResponseFilter})}
loadingRequests={requests.loadingRequests}
showCookiesModal={() => getModal(CookiesModal).show(workspace)}
/>

View File

@ -6,6 +6,24 @@
box-sizing: border-box;
height: 100% !important;
width: 100%;
display: grid;
grid-template-rows: 1fr auto;
grid-template-columns: 100%;
.editor__filter {
display: flex;
flex-direction: row;
align-items: center;
.form-control {
margin-right: 0;
width: 100%;
}
button {
color: @hl;
}
}
.CodeMirror {
height: 100% !important;

View File

@ -2,8 +2,8 @@
@import '../constants/dimensions';
::-webkit-scrollbar {
width: @padding-sm;
height: @padding-sm;
width: @scrollbar-width;
height: @scrollbar-width;
}
::-webkit-scrollbar-track {

View File

@ -27,6 +27,9 @@
/* Sidebar */
@sidebar-width: 19rem;
/* Scrollbars */
@scrollbar-width: 0.8rem;
/* Borders */
@radius-sm: 0.15rem;
@radius-md: 0.3rem;

View File

@ -25,7 +25,7 @@ describe('requestCreate()', () => {
};
return db.requestCreate(patch).then(r => {
expect(Object.keys(r).length).toBe(14);
expect(Object.keys(r).length).toBe(15);
expect(r._id).toMatch(/^req_[a-zA-Z0-9]{24}$/);
expect(r.created).toBeGreaterThanOrEqual(now);

View File

@ -85,6 +85,7 @@ export const MODEL_DEFAULTS = {
headers: [],
authentication: {},
metaPreviewMode: PREVIEW_MODE_SOURCE,
metaResponseFilter: '',
metaSortKey: -1 * Date.now()
}),
[TYPE_RESPONSE]: () => ({

View File

@ -62,6 +62,7 @@
"electron-squirrel-startup": "^1.0.0",
"httpsnippet": "git@github.com:gschier/httpsnippet.git#39d2fb0449f33711e5cc71a4d42b0e5b808426b4",
"json-lint": "^0.1.0",
"jsonpath-plus": "^0.15.0",
"jsonschema": "^1.1.0",
"mousetrap": "^1.6.0",
"nedb": "^1.8.0",