Extract request headers from Header component + cleanup

This commit is contained in:
Chen Asraf
2018-01-23 23:22:15 +02:00
parent 71dec9bf7a
commit 0e953efe9a
15 changed files with 238 additions and 96 deletions

View File

@@ -34,11 +34,11 @@ export interface IAction<T = any> {
export const AppDispatcher = new Dispatcher<IAction>()
function innerObjFromKey<T = any>(obj: T, k: string): Immutable.OrderedMap<string, T> {
return Immutable.OrderedMap<string, any>(obj || {}).get(k, {})
function innerObjFromKey<T = any>(obj: T, k: string): Immutable.Map<string, T> {
return Immutable.Map<string, any>(obj || {}).get(k, {})
}
export type IState = Immutable.OrderedMap<string, any>
export type IState = Immutable.Map<string, any>
class AppStore extends ReduceStore<IState, IAction> {
constructor() {
@@ -46,12 +46,13 @@ class AppStore extends ReduceStore<IState, IAction> {
}
getInitialState() {
const t = Immutable.OrderedMap<string, any>([
return Immutable.Map<string, any>([
[StoreKeys.ViewKey, localStorage.lastViewKey || ''],
[StoreKeys.RequestType, localStorage.lastRequestType || 'JSON'],
[StoreKeys.RequestPayload, localStorage.lastRequestPayload || ''],
[StoreKeys.RequestPayload, localStorage.lastPayload || ''],
[StoreKeys.RequestURL, localStorage.lastURL || ''],
[StoreKeys.RequestHeaders, localStorage.lastHeaders || ''],
])
return t
}
reduce(state: IState, action: IAction) {
@@ -66,6 +67,18 @@ class AppStore extends ReduceStore<IState, IAction> {
case ActionTypes.UPDATE_VIEWKEY:
localStorage.lastViewKey = action.payload
return state.set(StoreKeys.ViewKey, action.payload)
case ActionTypes.UPDATE_REQ_HEADERS:
localStorage.lastHeaders = action.payload
return state.set(StoreKeys.RequestHeaders, action.payload)
case ActionTypes.UPDATE_REQ_URL:
localStorage.lastURL = action.payload
return state.set(StoreKeys.RequestURL, action.payload)
case ActionTypes.UPDATE_REQ_METHOD:
localStorage.lastMethod = action.payload
return state.set(StoreKeys.RequestMethod, action.payload)
case ActionTypes.UPDATE_REQ_PAYLOAD:
localStorage.lastPayload = action.payload
return state.set(StoreKeys.RequestPayload, action.payload)
case ActionTypes.SEND_REQUEST:
axios.request(action.payload)
.then((response: AxiosResponse) => {

View File

@@ -1,6 +1,7 @@
export interface IProps {
url?: string
handleChange?(value: string, e: React.ChangeEvent<HTMLInputElement>): void
store: any
url: string
onChange?(value: string, e: React.ChangeEvent<HTMLInputElement>): void
}
export interface IState {

View File

@@ -1,21 +1,23 @@
import * as React from 'react'
import * as css from './AddressBar.css'
import * as I from './AddressBar.module'
import Dispatcher from 'src/common/Dispatcher'
import Dispatcher, { StoreKeys } from 'src/common/Dispatcher'
class AddressBar extends React.Component<I.IProps, I.IState> {
constructor(props: I.IProps) {
super(props)
this.state = {
url: props.url || ''
url: props.url
}
}
private handleChange(e: React.ChangeEvent<HTMLInputElement>) {
private onChange(e: React.ChangeEvent<HTMLInputElement>) {
const { value } = e.target as HTMLInputElement
if (typeof this.props.handleChange === 'function') {
this.props.handleChange(value, e)
}
this.setState({ url: value }, () => {
if (this.props.onChange) {
this.props.onChange(value, e)
}
})
}
public componentWillReceiveProps(nextProps: I.IProps) {
@@ -29,7 +31,7 @@ class AddressBar extends React.Component<I.IProps, I.IState> {
<div className={css.addressBar}>
<input placeholder="https://www.example.com/request"
value={this.state.url}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.handleChange(e)}
onChange={(e) => this.onChange(e)}
/>
</div>
)

View File

@@ -14,30 +14,4 @@
grid-template-columns: [payload] 2fr [headers] 1fr;
grid-column-gap: 14px;
width: 100%;
& .title {
text-transform: uppercase;
color: #555;
margin-bottom: 5px;
font-weight: bold;
font-size: 0.8em;
}
}
.payload {
grid-area: payload;
}
.headers {
grid-area: headers;
}
.payload, .headers {
& textarea {
width: 100%;
border: 1px solid #ccc;
overflow-x: hidden;
min-height: 50px;
font-family: "Lucida Grande", "Courier New", monospace, serif;
}
}

View File

@@ -1,5 +1,2 @@
export const header: string;
export const requestDataContainer: string;
export const title: string;
export const payload: string;
export const headers: string;

View File

@@ -9,6 +9,7 @@ import axios, { AxiosResponse } from 'axios'
import { dispatch, register, ActionTypes, StoreKeys } from 'common/Dispatcher'
import * as classNames from 'classnames'
import NavBar from 'components/NavBar/NavBar'
import RequestHeaders from 'components/RequestHeaders/RequestHeaders'
class Header extends React.Component<I.IProps, I.IState> {
constructor(props: I.IProps) {
@@ -16,7 +17,7 @@ class Header extends React.Component<I.IProps, I.IState> {
this.state = {
url: localStorage.lastUrl || '',
method: localStorage.lastMethod || 'GET',
requestPayload: localStorage.lastRequestPayload || '',
requestPayload: localStorage.lastPayload || '',
headers: localStorage.lastHeaders || '',
}
}
@@ -27,44 +28,13 @@ class Header extends React.Component<I.IProps, I.IState> {
}
}
private changeURL(url: string) {
this.setState({ url })
localStorage.lastUrl = url
}
private changeMethod(method: string) {
this.setState({ method })
localStorage.lastMethod = method
}
private changeRequestPayload(requestPayload: string) {
this.setState({ requestPayload })
localStorage.lastRequestPayload = requestPayload
}
private changeHeaders(headers: string) {
this.setState({ headers })
localStorage.lastHeaders = headers
}
private get requestHeaders() {
const headers = this.state.headers.split(`\n`).reduce((accu, cur) => {
const [key, val] = cur.split(':').map(s => s.trim())
if (key.length) {
accu[key] = val
}
return accu
}, {})
return headers
}
private go() {
const { method, url: url } = this.state
dispatch(ActionTypes.SEND_REQUEST, {
method, url,
data: this.state.requestPayload,
headers: this.requestHeaders
method: this.props.store.get(StoreKeys.RequestMethod),
url: this.props.store.get(StoreKeys.RequestURL),
data: this.props.store.get(StoreKeys.RequestPayload),
headers: this.props.store.get(StoreKeys.RequestHeaders),
})
}
@@ -73,16 +43,8 @@ class Header extends React.Component<I.IProps, I.IState> {
<div className={classNames(css.header, this.props.className)}>
<NavBar store={this.props.store} />
<div className={css.requestDataContainer}>
<RequestType className={css.payload}
onChange={(payload) => this.setState({ requestPayload: payload })}
store={this.props.store} />
<div className={css.headers}>
<h4 className={css.title}>Headers</h4>
<textarea name="headers"
value={this.state.headers}
placeholder="Headers"
onChange={(e) => this.changeHeaders(e.target.value)} />
</div>
<RequestType store={this.props.store} />
<RequestHeaders store={this.props.store} />
</div>
</div>
)

View File

@@ -7,6 +7,8 @@ export interface IProps {
}
export interface IState {
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
method: Methods
url: string
}
export type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE'

View File

@@ -5,7 +5,7 @@ import * as classNames from 'classnames'
import SelectBox, { Option, styles as selectBoxStyle } from 'components/SelectBox/SelectBox'
import AddressBar from 'components/AddressBar/AddressBar'
import Button from 'components/Button/Button'
import { StoreKeys } from 'common/Dispatcher'
import { StoreKeys, dispatch, ActionTypes } from 'common/Dispatcher'
class NavBar extends React.Component<I.IProps, I.IState> {
private httpMethods = [
@@ -23,13 +23,19 @@ class NavBar extends React.Component<I.IProps, I.IState> {
}
}
private changeMethod(value: string) {
private changeMethod(value: I.Methods) {
dispatch(ActionTypes.UPDATE_REQ_METHOD, value)
this.setState({ method: value })
if (this.props.onChangeMethod) {
this.props.onChangeMethod(value)
}
}
private changeURL(value: string) {
dispatch(ActionTypes.UPDATE_REQ_URL, value)
this.setState({ url: value })
if (this.props.onChangeURL) {
this.props.onChangeURL(value)
}
@@ -53,12 +59,13 @@ class NavBar extends React.Component<I.IProps, I.IState> {
value={this.state.method}
options={this.httpMethods}
placeholder="METHOD"
onChange={(value: Option<string>) => this.changeMethod(value.value!)}
onChange={(value: Option<I.Methods>) => this.changeMethod(value.value!)}
/>
</div>
<div className={css.address}>
<AddressBar url={this.state.url}
handleChange={(value) => this.changeURL(value)}/>
store={this.props.store}
onChange={(value) => this.changeURL(value)}/>
</div>
<div className={css.go}>
<Button onClick={() => this.go()}>Go</Button>

View File

@@ -0,0 +1,29 @@
@import "../../_variables.css";
.RequestHeaders {
display: grid;
grid-row-gap: 5px;
& input {
display: block;
width: 100%;
border: 1px solid #ccc;
font-family: "Lucida Grande", "Courier New", monospace, serif;
padding: 6px 3px;
}
}
.title {
text-transform: uppercase;
color: #555;
font-weight: bold;
font-size: 0.8em;
}
.pair {
display: grid;
width: 100%;
grid-column-gap: 3px;
grid-template-columns: repeat(2, 50%);
min-width: 50%;
}

View File

@@ -0,0 +1,4 @@
export const RequestHeaders: string;
export const requestHeaders: string;
export const title: string;
export const pair: string;

View File

@@ -0,0 +1,12 @@
import * as Immutable from 'immutable'
export interface IProps {
className?: string
store: any
}
export type HeaderSet = Immutable.List<[string, string]>
export interface IState {
headers: HeaderSet
}

View File

@@ -0,0 +1,118 @@
import * as React from 'react'
import * as css from './RequestHeaders.css'
import * as I from './RequestHeaders.module'
import * as classNames from 'classnames'
import * as Immutable from 'immutable'
import { StoreKeys, ActionTypes, dispatch } from 'common/Dispatcher'
class RequestHeaders extends React.Component<I.IProps, I.IState> {
constructor(props: I.IProps) {
super(props)
this.state = {
headers: this.parseStringHeaders(props.store.get(StoreKeys.RequestHeaders, ''))
}
}
private parseStringHeaders(headers: string) {
let headerMap = Immutable.List<[string, string]>()
headers.split('\n').forEach((header, idx) => {
const [ name, value ] = header.split(':').map(s => s.trim())
const finalName = name.split('-')
.map(s => this.capitalize(s))
.join('-')
if (finalName) {
headerMap = headerMap.set(idx, [finalName, value || ''])
}
})
return headerMap
}
private get stringHeaders() {
const seq = this.state.headers.entrySeq()
return seq
.map((header) => {
if (!header) {
return
}
let [ name, value ] = header[1]
name = name.split('-').map(s => this.capitalize(s)).join('-')
return name ?
`${name}: ${value}`
: undefined
})
.filter(s => Boolean(s))
.join('\n')
}
private capitalize(str: string) {
if (!str) {
return ''
}
return str[0].toUpperCase() + str.slice(1).toLowerCase()
}
private updateHeaderName(idx: number, name: string) {
const value = this.state.headers.get(idx, [name, ''])
this.setState({
headers: this.state.headers.set(idx, [name, value[1]])
}, () => {
dispatch(ActionTypes.UPDATE_REQ_HEADERS, this.stringHeaders)
})
}
private updateHeaderValue(idx: number, value: string) {
const header = this.state.headers.get(idx, ['', ''])
this.setState({
headers: this.state.headers.set(idx, [header[0], value || ''])
}, () => {
dispatch(ActionTypes.UPDATE_REQ_HEADERS, this.stringHeaders)
})
}
private getHeaderName(idx: number) {
return this.state.headers.get(idx, ['', ''])[0]
}
private getHeaderValue(idx: number) {
return this.state.headers.get(idx, ['', ''])[1]
}
render() {
const className = classNames(css.RequestHeaders, this.props.className)
const inputRowRange = Immutable.Range(0, this.state.headers.count() + 1)
return (
<div className={className}>
<h4 className={css.title}>Headers</h4>
{inputRowRange.map((i: number) => {
return (
<div key={`header-${i}`}
className={css.pair}>
<div>
<input type="text"
placeholder="Name"
value={this.getHeaderName(i)}
onChange={(e) => this.updateHeaderName(i, (e.target as HTMLInputElement).value)}
/>
</div>
<div>
<input type="text"
placeholder="Value"
value={this.getHeaderValue(i)}
onChange={(e) => this.updateHeaderValue(i, (e.target as HTMLInputElement).value)}
/>
</div>
</div>
)
})}
</div>
)
}
}
export default RequestHeaders

View File

@@ -14,6 +14,7 @@
text-transform: uppercase;
color: $text-light;
font-size: 0.9em;
cursor: pointer;
}
}
@@ -43,3 +44,14 @@
padding: 5px 7px;
font-size: 1em;
}
.sort-icon {
vertical-align: middle;
height: 1rem;
line-height: 1rem;
opacity: 0.5;
&.active {
opacity: 1;
}
}

View File

@@ -5,3 +5,5 @@ export const cell: string;
export const rowStart: string;
export const rowEnd: string;
export const filterInput: string;
export const sortIcon: string;
export const active: string;

View File

@@ -52,10 +52,17 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
private columns() {
const keys = Object.keys(this.state.response[0] || {})
return keys.map((key) => {
const cls = classNames(css.sortIcon, 'material-icons', {
[css.active]: this.state.sortKey === key
})
return (
<h3 key={`col-th-${key}`}
onClick={() => this.sortBy(key)}>
{key}
<i className={cls}>
{this.state.sortKey === key && this.state.sortDesc ? 'arrow_drop_down' : 'arrow_drop_up'}
</i>
</h3>
)
})
@@ -163,7 +170,7 @@ class ResponseRepr extends React.Component<I.IProps, I.IState> {
return (
<div className={css.table}
style={{gridTemplateRows: `repeat(${colAmt}, auto)`}}>}
style={{gridTemplateRows: `repeat(${colAmt}, auto)`}}>
<RObject data={response} />
</div>
)