mirror of
https://github.com/chenasraf/redar-browser.git
synced 2026-05-18 01:59:00 +00:00
Add experimental transform
This commit is contained in:
@@ -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
27
src/common/Trasformer.ts
Normal 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]
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
35
src/components/Transformer/Transformer.css
Normal file
35
src/components/Transformer/Transformer.css
Normal 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;
|
||||
}
|
||||
6
src/components/Transformer/Transformer.css.d.ts
vendored
Normal file
6
src/components/Transformer/Transformer.css.d.ts
vendored
Normal 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;
|
||||
9
src/components/Transformer/Transformer.module.d.ts
vendored
Normal file
9
src/components/Transformer/Transformer.module.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface IProps {
|
||||
className?: string
|
||||
store: any
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
transform: string
|
||||
hasError: boolean
|
||||
}
|
||||
58
src/components/Transformer/Transformer.tsx
Normal file
58
src/components/Transformer/Transformer.tsx
Normal 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
|
||||
Reference in New Issue
Block a user