From 2bfc4516ac00c6f695b07c00e690b9fd8b75143a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 8 Sep 2016 17:52:14 -0700 Subject: [PATCH] JSONPath filtering (#37) * POC JSONPath support * Add help for JSONPath * Now save response filter on request * Fixed test --- app/app.json | 3 +- app/components/ResponsePane.js | 6 ++ app/components/base/Editor.js | 118 ++++++++++++++++++++--- app/components/viewers/ResponseViewer.js | 11 ++- app/containers/App.js | 2 + app/css/components/editor.less | 18 ++++ app/css/components/scrollbars.less | 4 +- app/css/constants/dimensions.less | 3 + app/database/__tests__/index.test.js | 2 +- app/database/index.js | 1 + package.json | 1 + 11 files changed, 149 insertions(+), 20 deletions(-) diff --git a/app/app.json b/app/app.json index 602e2e3293..2fa89f3284 100644 --- a/app/app.json +++ b/app/app.json @@ -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", diff --git a/app/components/ResponsePane.js b/app/components/ResponsePane.js index 2e84b54714..4ede6887f3 100644 --- a/app/components/ResponsePane.js +++ b/app/components/ResponsePane.js @@ -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, diff --git a/app/components/base/Editor.js b/app/components/base/Editor.js index d807410192..fd7bb53ff9 100644 --- a/app/components/base/Editor.js +++ b/app/components/base/Editor.js @@ -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: ( +
+

+ Use JSONPath to filter the + response body. Here are some examples that you might use on a + book store API. +

+ + + + + + + + + + + + + + + + + + + +
$.store.books[*].title + Get titles of all books in the store
$.store.books[?(@.price < + 10)].titleGet books costing more than $10
$.store.books[-1:]Get the last book in the store
$.store.books.lengthGet the number of books in the store
+
+ ) + }) + } + componentDidUpdate () { this._codemirrorSetOptions(); } render () { - const {readOnly, fontSize, lightTheme} = this.props; + const {readOnly, fontSize, lightTheme, mode, filter} = this.props; const classes = classnames( 'editor', @@ -214,13 +285,34 @@ class Editor extends Component { } ); + let filterElement = null; + if (this.props.updateFilter && this._isJSON(mode)) { + filterElement = ( +
+
+ this._handleFilterChange(e.target.value)} + /> +
+ +
+ ) + } + return (
- + + {filterElement}
); } @@ -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; diff --git a/app/components/viewers/ResponseViewer.js b/app/components/viewers/ResponseViewer.js index 409ef954af..9e2c74077c 100644 --- a/app/components/viewers/ResponseViewer.js +++ b/app/components/viewers/ResponseViewer.js @@ -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 ( - + ) } @@ -51,6 +50,8 @@ class ResponseViewer extends Component { return ( db.requestUpdate(activeRequest, {metaPreviewMode})} + updateResponseFilter={metaResponseFilter => db.requestUpdate(activeRequest, {metaResponseFilter})} loadingRequests={requests.loadingRequests} showCookiesModal={() => getModal(CookiesModal).show(workspace)} /> diff --git a/app/css/components/editor.less b/app/css/components/editor.less index 39d4451106..c2be88667c 100644 --- a/app/css/components/editor.less +++ b/app/css/components/editor.less @@ -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; diff --git a/app/css/components/scrollbars.less b/app/css/components/scrollbars.less index 2dbfdd203a..6caccc30d9 100644 --- a/app/css/components/scrollbars.less +++ b/app/css/components/scrollbars.less @@ -2,8 +2,8 @@ @import '../constants/dimensions'; ::-webkit-scrollbar { - width: @padding-sm; - height: @padding-sm; + width: @scrollbar-width; + height: @scrollbar-width; } ::-webkit-scrollbar-track { diff --git a/app/css/constants/dimensions.less b/app/css/constants/dimensions.less index f93c1d7b26..f69b0a83b1 100644 --- a/app/css/constants/dimensions.less +++ b/app/css/constants/dimensions.less @@ -27,6 +27,9 @@ /* Sidebar */ @sidebar-width: 19rem; +/* Scrollbars */ +@scrollbar-width: 0.8rem; + /* Borders */ @radius-sm: 0.15rem; @radius-md: 0.3rem; diff --git a/app/database/__tests__/index.test.js b/app/database/__tests__/index.test.js index 117f6587c7..088b5149bf 100644 --- a/app/database/__tests__/index.test.js +++ b/app/database/__tests__/index.test.js @@ -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); diff --git a/app/database/index.js b/app/database/index.js index a015701c88..d25ea84d6e 100644 --- a/app/database/index.js +++ b/app/database/index.js @@ -85,6 +85,7 @@ export const MODEL_DEFAULTS = { headers: [], authentication: {}, metaPreviewMode: PREVIEW_MODE_SOURCE, + metaResponseFilter: '', metaSortKey: -1 * Date.now() }), [TYPE_RESPONSE]: () => ({ diff --git a/package.json b/package.json index 8ecfd151d8..f6e2c47a92 100644 --- a/package.json +++ b/package.json @@ -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",