mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
JSONPath filtering (#37)
* POC JSONPath support * Add help for JSONPath * Now save response filter on request * Fixed test
This commit is contained in:
parent
c9766e0768
commit
2bfc4516ac
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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 <
|
||||
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;
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
@ -2,8 +2,8 @@
|
||||
@import '../constants/dimensions';
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: @padding-sm;
|
||||
height: @padding-sm;
|
||||
width: @scrollbar-width;
|
||||
height: @scrollbar-width;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
@ -27,6 +27,9 @@
|
||||
/* Sidebar */
|
||||
@sidebar-width: 19rem;
|
||||
|
||||
/* Scrollbars */
|
||||
@scrollbar-width: 0.8rem;
|
||||
|
||||
/* Borders */
|
||||
@radius-sm: 0.15rem;
|
||||
@radius-md: 0.3rem;
|
||||
|
@ -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);
|
||||
|
@ -85,6 +85,7 @@ export const MODEL_DEFAULTS = {
|
||||
headers: [],
|
||||
authentication: {},
|
||||
metaPreviewMode: PREVIEW_MODE_SOURCE,
|
||||
metaResponseFilter: '',
|
||||
metaSortKey: -1 * Date.now()
|
||||
}),
|
||||
[TYPE_RESPONSE]: () => ({
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user