Add experimental transform

This commit is contained in:
Chen Asraf
2018-01-27 18:35:04 +02:00
parent a91942d659
commit c4e90b7355
9 changed files with 243 additions and 69 deletions

View File

@@ -4,6 +4,7 @@ import * as Immutable from 'immutable'
import axios, { AxiosResponse } from 'axios'
import * as Headers from 'common/Headers'
import * as Payload from 'common/Payload'
import { compileCode } from 'common/Trasformer'
const ActionTypes = {
SEND_REQUEST: 'SEND_REQUEST',
@@ -14,6 +15,8 @@ const ActionTypes = {
UPDATE_REQ_METHOD: 'UPDATE_REQ_METHOD',
UPDATE_REQ_HEADERS: 'UPDATE_REQ_HEADERS',
UPDATE_REQ_URL: 'UPDATE_REQ_URL',
UPDATE_RES_TRANSFORM: 'UPDATE_RES_TRANSFORM',
UPDATE_RES_TRANSFORM_ERROR: 'UPDATE_RES_TRANSFORM_ERROR',
}
const StoreKeys = {
@@ -24,10 +27,13 @@ const StoreKeys = {
RequestMethod: 'REQ_METHOD',
RequestHeaders: 'REQ_HEADERS',
RequestURL: 'REQ_URL',
ResponseTransform: 'RES_TRANSFORM',
ResponseTransformError: 'RES_TRANSFORM.ERROR',
}
export type TActionName =
'UPDATE_RESPONSE' | 'UPDATE_TABLE' | 'UPDATE_COLUMNS' | 'UPDATE_VIEWKEY' | 'UPDATE_REQ_TYPE' | 'SEND_REQUEST'
export type TActionName =
'SEND_REQUEST' | 'UPDATE_RESPONSE' | 'UPDATE_VIEWKEY' | 'UPDATE_REQ_TYPE' | 'UPDATE_REQ_PAYLOAD' |
'UPDATE_REQ_METHOD' | 'UPDATE_REQ_HEADERS' | 'UPDATE_REQ_URL' | 'UPDATE_RES_TRANSFORM'
export interface IAction<T = any> {
name: TActionName | string,
@@ -55,6 +61,7 @@ class AppStore extends ReduceStore<IState, IAction> {
[StoreKeys.RequestPayload, localStorage.lastPayload || '""'],
[StoreKeys.RequestURL, localStorage.lastURL || ''],
[StoreKeys.RequestHeaders, Headers.parseHeaderList(localStorage.lastHeaders || '')],
[StoreKeys.ResponseTransform, localStorage.lastResTransform || ''],
])
}
@@ -62,11 +69,15 @@ class AppStore extends ReduceStore<IState, IAction> {
switch (action.name) {
case ActionTypes.UPDATE_RESPONSE:
let viewKey = state.get(StoreKeys.ViewKey)
if (localStorage.lastViewKey !== '' && action.payload && !action.payload.hasOwnProperty(viewKey)) {
viewKey = this.getViewKey(action.payload)
let response = action.payload
state = state.set(StoreKeys.ResponseTransformError, '')
if (localStorage.lastViewKey !== '' && response && !response.hasOwnProperty(viewKey)) {
viewKey = this.getViewKey(response)
state = state.set(StoreKeys.ViewKey, viewKey)
}
return state.set(StoreKeys.Response, action.payload)
return state.set(StoreKeys.Response, response)
case ActionTypes.UPDATE_VIEWKEY:
localStorage.lastViewKey = action.payload
return state.set(StoreKeys.ViewKey, action.payload)
@@ -82,6 +93,11 @@ class AppStore extends ReduceStore<IState, IAction> {
case ActionTypes.UPDATE_REQ_PAYLOAD:
localStorage.lastPayload = action.payload
return state.set(StoreKeys.RequestPayload, action.payload)
case ActionTypes.UPDATE_RES_TRANSFORM:
localStorage.lastResTransform = action.payload
return state.set(StoreKeys.ResponseTransform, action.payload)
case ActionTypes.UPDATE_RES_TRANSFORM_ERROR:
return state.set(StoreKeys.ResponseTransformError, action.payload)
case ActionTypes.SEND_REQUEST:
if (!action.payload) {
action.payload = {
@@ -93,8 +109,8 @@ class AppStore extends ReduceStore<IState, IAction> {
}
}
axios.request(action.payload)
.then((response: AxiosResponse) => {
dispatch(ActionTypes.UPDATE_RESPONSE, response.data)
.then((resp: AxiosResponse) => {
dispatch(ActionTypes.UPDATE_RESPONSE, resp.data)
})
return state
default:

27
src/common/Trasformer.ts Normal file
View File

@@ -0,0 +1,27 @@
const sandboxProxies = new WeakMap()
export default function compileCode (src: string) {
src = 'with (sandbox) {' + src + '}'
const code = new Function('sandbox', src)
return function (sandbox: any) {
if (!sandboxProxies.has(sandbox)) {
const sandboxProxy = new Proxy(sandbox, {has, get})
sandboxProxies.set(sandbox, sandboxProxy)
}
return code(sandboxProxies.get(sandbox))
}
}
export { compileCode }
function has (target: any, key: string) {
return true
}
function get (target: any, key: string | symbol) {
if (key === Symbol.unscopables) {
return undefined
}
return target[key]
}

View File

@@ -11,6 +11,7 @@ import { dispatch, register, ActionTypes, StoreKeys, Store } from 'common/Dispat
import * as classNames from 'classnames'
import NavBar from 'components/NavBar/NavBar'
import RequestHeaders from 'components/RequestHeaders/RequestHeaders'
import Transformer from 'components/Transformer/Transformer'
const logo = require('../../../public/android-chrome-192x192.png')
class Header extends React.Component<I.IProps, I.IState> {
@@ -26,11 +27,14 @@ class Header extends React.Component<I.IProps, I.IState> {
<img src={logo} />
<NavBar store={this.props.store} />
</div>
<TabContainer collapsible={true}>
<TabContainer rememberAs="mainTabs" collapsible={true}>
<Tab label="Data" className={css.requestDataContainer}>
<RequestPayload store={this.props.store} />
<RequestHeaders store={this.props.store} />
</Tab>
<Tab label="Transform Response">
<Transformer store={this.props.store} />
</Tab>
</TabContainer>
</div>
)

View File

@@ -10,7 +10,7 @@
border: 1px solid #ccc;
overflow-x: hidden;
min-height: 50px;
font-family: "Lucida Grande", "Courier New", monospace, serif;
font-family: "Courier", monospace, serif;
}
}

View File

@@ -5,6 +5,8 @@ import * as D from 'common/Dispatcher'
import RObject from 'components/RObject/RObject'
import * as classNames from 'classnames'
import { parse } from '../../filter-parser/filter.pegjs'
import { Store } from 'common/Dispatcher'
import { compileCode } from 'common/Trasformer'
class ResponseRepr extends React.Component<I.IProps, I.IState> {
private listeners: string[]
@@ -22,8 +24,9 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
componentDidMount() {
this.listeners = [
D.register(D.ActionTypes.UPDATE_RESPONSE, (response) => {
const viewKey = this.props.store.get(D.StoreKeys.ViewKey)
D.register(D.ActionTypes.UPDATE_RESPONSE, () => {
const viewKey = Store.getState().get(D.StoreKeys.ViewKey)
let response = Store.getState().get(D.StoreKeys.Response)
if (viewKey) {
response = this.objectByPath(response, viewKey)
}
@@ -31,9 +34,9 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
}),
D.register(D.ActionTypes.UPDATE_VIEWKEY, (viewKey) => {
let response = this.props.store.get(D.StoreKeys.Response, {})
let response = Store.getState().get(D.StoreKeys.Response, {})
response = response && viewKey ? this.objectByPath(response, viewKey) : response
this.setState({ response: response })
this.setState({ response })
}),
]
}
@@ -63,6 +66,8 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
let response = this.state.response || []
const keys = Object.keys(response)
const isArray = response.constructor === Array
const transform = this.props.store.get(D.StoreKeys.ResponseTransform)
if (!keys.length) {
return [{ key: this.props.store.get(D.StoreKeys.ViewKey), value: JSON.stringify(response) }]
}
@@ -71,68 +76,82 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
return [{ type: typeof response, value: JSON.stringify(response) }]
}
if (!isArray) {
let flag = false
keys.forEach((key) => {
const row = response[key]
if (!row || !Object.keys(row || {}).length) {
flag = true
}
})
response = keys.map((key) => {
let row = response[key]
let oldVal: any
if (typeof row !== 'string' && typeof row !== 'number' && row) {
oldVal = row
if (flag) {
row = { key: row._id || row.id || key, value: row }
} else {
row = { key: row._id || row.id || key, ...row }
try {
if (!isArray) {
let flag = false
keys.forEach((key) => {
const row = response[key]
if (!row || !Object.keys(row || {}).length) {
flag = true
}
})
response = keys.map((key) => {
let row = response[key]
let oldVal: any
if (typeof row !== 'string' && typeof row !== 'number' && row) {
oldVal = row
if (flag) {
row = { key: row._id || row.id || key, value: row }
} else {
row = { key: row._id || row.id || key, ...row }
}
} else {
flag = true
row = { key: key, value: row }
}
if (Object.keys(row).length < 2) {
row = { key: row.key, value: JSON.stringify(oldVal) }
}
return row
})
}
if (transform) {
const oldResponse = response
const transformed = compileCode(transform)({
console: console,
response
})
if (transformed) {
response = transformed
console.debug('transformed:', transformed)
} else {
flag = true
row = { key: key, value: row }
throw new Error('response returned a falsy value')
}
if (Object.keys(row).length < 2) {
row = { key: row.key, value: JSON.stringify(oldVal) }
}
return row
})
}
}
if (response[0].hasOwnProperty(this.state.sortKey)) {
const desc = this.state.sortDesc
const key = this.state.sortKey
if (response[0].hasOwnProperty(this.state.sortKey)) {
const desc = this.state.sortDesc
const key = this.state.sortKey
response = response.sort((a, b) => {
// numbers are matching
if (typeof a[key] === 'number' && typeof b === 'number' ||
isFinite(a[key]) && isFinite(b[key])) {
const result = parseFloat(a[key]) - parseFloat(b[key])
return desc ? -result : result
}
response = response.sort((a, b) => {
// numbers are matching
if (typeof a[key] === 'number' && typeof b === 'number' ||
isFinite(a[key]) && isFinite(b[key])) {
const result = parseFloat(a[key]) - parseFloat(b[key])
return desc ? -result : result
}
if (a[key] > b[key]) {
return desc ? -1 : 1
// tslint:disable-next-line:triple-equals
} else if (a[key] == b[key]) {
return 0
} else if (a[key] < b[key]) {
return desc ? 1 : -1
}
if (a[key] > b[key]) {
return desc ? -1 : 1
// tslint:disable-next-line:triple-equals
} else if (a[key] == b[key]) {
return 0
} else if (a[key] < b[key]) {
return desc ? 1 : -1
}
if (!a && !b) {
return 0
}
if (!a && !b) {
return 0
}
return a.valueOf() - b.valueOf()
})
}
if (this.state.filter.trim().length) {
const filter = this.state.filter.trim().replace(/\s{2,}/g, ' ')
try {
return a.valueOf() - b.valueOf()
})
}
if (this.state.filter.trim().length) {
const filter = this.state.filter.trim().replace(/\s{2,}/g, ' ')
const filterOps = parse(filter)
response = response.filter((row) => {
@@ -161,9 +180,9 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
}
}
})
} catch (e) {
console.warn(e)
}
} catch (e) {
console.warn(e)
}
return response

View File

@@ -0,0 +1,35 @@
@import "../../_variables.css";
.Transformer {
& .textarea {
width: 100%;
min-height: 80px;
font-family: 'Courier', monospace;
border: 1px solid #ccc;
&.has-error {
border: 1px solid #cc9;
background: #ffc;
}
}
& pre {
display: inline-block;
font-family: 'Courier', monospace;
font-size: 0.95em;
}
}
.title {
text-transform: uppercase;
color: #555;
margin-bottom: 5px;
font-weight: bold;
font-size: 0.8em;
}
.info {
font-size: 0.8em;
color: #666;
margin-bottom: 5px;
}

View File

@@ -0,0 +1,6 @@
export const Transformer: string;
export const transformer: string;
export const textarea: string;
export const hasError: string;
export const title: string;
export const info: string;

View File

@@ -0,0 +1,9 @@
export interface IProps {
className?: string
store: any
}
export interface IState {
transform: string
hasError: boolean
}

View File

@@ -0,0 +1,58 @@
import * as React from 'react'
import * as css from './Transformer.css'
import * as I from './Transformer.module'
import * as classNames from 'classnames'
import Dispatcher, { StoreKeys, ActionTypes, dispatch, register, Store } from 'common/Dispatcher'
class Transformer extends React.Component<I.IProps, I.IState> {
private listeners: string[]
constructor(props: I.IProps) {
super(props)
this.state = {
transform: Store.getState().get(StoreKeys.ResponseTransform),
hasError: Store.getState().get(StoreKeys.ResponseTransformError),
}
}
private onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
this.setState({ transform: e.currentTarget.value }, () => {
dispatch(ActionTypes.UPDATE_RES_TRANSFORM, this.state.transform)
})
}
public componentWillMount() {
this.listeners = [
register(ActionTypes.UPDATE_RES_TRANSFORM_ERROR, (error: any) => {
this.setState({ hasError: Boolean(error) })
}),
]
}
public componentWillUnmount() {
this.listeners.forEach(l => Dispatcher.unregister(l))
}
render() {
const className = classNames(css.Transformer, this.props.className)
const textAreaCls = classNames(css.textarea, {
[css.hasError]: this.state.hasError
})
return (
<div className={className}>
<h3 className={css.title}>Transform Response (experimental)</h3>
<div className={css.info}>
Use <pre>response</pre> to get the response data,
and return the final output you want to parse as a table.
</div>
<textarea className={textAreaCls}
onChange={(e) => this.onChange(e)}
placeholder="e.g. return response.map(row => ...)"
value={this.state.transform} />
</div>
)
}
}
export default Transformer