Lots of UI updates

This commit is contained in:
2018-08-16 00:13:06 +02:00
parent 38b440e2f6
commit d92bd0c586
24 changed files with 637 additions and 225 deletions

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# React Native Debug Console
A network and console debug component and modal for `react native` purely in JavaScript
[[demo]](https://expo.io/@mortenolsen/demo)
![screen](docs/assets/screen1.png)
## Installation
```
npm install react-native-debug-console
```
## Usage
It comes as a ready to use modal, as well as a standalone component
```javascript
import {
DevToolModal,
log,
network,
show,
} from 'react-native-debug-console';
network.attach();
log.attach();
const App = () => (
<View>
<MyOtherContent />
<DevToolModal />
<Button
onPress={show}
/>
</View>
)
```
```javascript
import {
DevTool,
log,
network,
} from 'react-native-debug-console';
network.attach();
log.attach();
const App = () => (
<View>
<MyOtherContent />
<DevTool />
</View>
)
```

View File

@@ -19,22 +19,35 @@ network.attach();
log.attach(); log.attach();
console.log('fooo'); console.log('fooo');
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://google.com');
xhr.send();
xhr = new XMLHttpRequest();
xhr.open('GET', 'https://google.com/sdfsfsdfsfdf');
xhr.send();
const t = new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error('everything is broken');
}, 1000);
});
export default class App extends React.Component { export default class App extends React.Component {
render() { render() {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<KeyboardAvoidingView style={styles.wrapper} behavior="padding"> <KeyboardAvoidingView style={styles.wrapper} behavior="padding">
<View>
<Button <Button
style={styles.button} style={styles.button}
title="Console log" title="console.log('hello')"
onPress={() => { onPress={() => {
console.log('hello'); console.log('hello');
}} }}
/> />
<View style={{ height: 10 }} /> <View style={{ height: 10 }} />
<Button <Button
title="GET XHR" title="GET XHR (html)"
style={styles.button} style={styles.button}
onPress={() => { onPress={() => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
@@ -43,6 +56,17 @@ export default class App extends React.Component {
}} }}
/> />
<View style={{ height: 10 }} /> <View style={{ height: 10 }} />
<View style={{ height: 10 }} />
<Button
title="GET XHR (json)"
style={styles.button}
onPress={() => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com');
xhr.send();
}}
/>
<View style={{ height: 10 }} />
<Button <Button
title="POST XHR" title="POST XHR"
style={styles.button} style={styles.button}
@@ -57,7 +81,7 @@ export default class App extends React.Component {
/> />
<View style={{ height: 10 }} /> <View style={{ height: 10 }} />
<Button <Button
title="Break" title="Unhandled error"
style={styles.button} style={styles.button}
onPress={() => { onPress={() => {
const err = new Error('some error'); const err = new Error('some error');
@@ -66,16 +90,17 @@ export default class App extends React.Component {
/> />
<View style={{ height: 30 }} /> <View style={{ height: 30 }} />
<Button <Button
title="Show" title="Show modal"
style={styles.button} style={styles.button}
onPress={() => { onPress={() => {
show(); show();
}} }}
/> />
<View style={{ flex: 1 }} /> <View style={{ height: 10 }} />
</View>
<DevTool <DevTool
style={{ style={{
height: 300, flex: 1,
}} }}
/> />
<DevToolModal /> <DevToolModal />

View File

@@ -24,6 +24,7 @@
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "16.3.1", "react": "16.3.1",
"react-native": "~0.55.2", "react-native": "~0.55.2",
"react-native-json-tree": "^1.2.0" "react-native-json-tree": "^1.2.0",
"styled-components": "^3.4.2"
} }
} }

View File

@@ -10,6 +10,7 @@ module.exports = {
'react-native', 'react-native',
'react', 'react',
'prop-types', 'prop-types',
'styled-components',
'react-native-json-tree', 'react-native-json-tree',
]; ];
}, },

View File

@@ -1667,7 +1667,7 @@ base64-js@1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
base64-js@^1.1.2, base64-js@^1.2.0: base64-js@^1.0.2, base64-js@^1.1.2, base64-js@^1.2.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
@@ -1814,6 +1814,13 @@ buffer-from@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
buffer@^5.0.3:
version "5.2.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.0.tgz#53cf98241100099e9eeae20ee6d51d21b16e541e"
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
builtin-modules@^1.0.0: builtin-modules@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -2210,6 +2217,18 @@ crypto-token@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/crypto-token/-/crypto-token-1.0.1.tgz#27c6482faf3b63c2f5da11577f8304346fe797a5" resolved "https://registry.yarnpkg.com/crypto-token/-/crypto-token-1.0.1.tgz#27c6482faf3b63c2f5da11577f8304346fe797a5"
css-color-keywords@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
css-to-react-native@^2.0.3:
version "2.2.1"
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.2.1.tgz#7f3f4c95de65501b8720c87bf0caf1f39073b88e"
dependencies:
css-color-keywords "^1.0.0"
fbjs "^0.8.5"
postcss-value-parser "^3.3.0"
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
@@ -2771,7 +2790,7 @@ fbjs-scripts@^0.8.1:
semver "^5.1.0" semver "^5.1.0"
through2 "^2.0.0" through2 "^2.0.0"
fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: fbjs@^0.8.14, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.5, fbjs@^0.8.9:
version "0.8.17" version "0.8.17"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
dependencies: dependencies:
@@ -3325,6 +3344,10 @@ idx@^2.1.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/idx/-/idx-2.4.0.tgz#e89e6650c889a44bf889f79d47f40fe09b4eeaa3" resolved "https://registry.yarnpkg.com/idx/-/idx-2.4.0.tgz#e89e6650c889a44bf889f79d47f40fe09b4eeaa3"
ieee754@^1.1.4:
version "1.1.12"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
ignore-walk@^3.0.1: ignore-walk@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
@@ -5327,6 +5350,10 @@ posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
postcss-value-parser@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
pouchdb-collections@^1.0.1: pouchdb-collections@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz#fe63a17da977611abef7cb8026cb1a9553fd8359" resolved "https://registry.yarnpkg.com/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz#fe63a17da977611abef7cb8026cb1a9553fd8359"
@@ -5400,7 +5427,7 @@ prop-types@15.5.8:
dependencies: dependencies:
fbjs "^0.8.9" fbjs "^0.8.9"
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2: prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies: dependencies:
@@ -6552,6 +6579,28 @@ strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
styled-components@^3.4.2:
version "3.4.2"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.2.tgz#8f518419932327e47fe9144824e3184b3e2da95d"
dependencies:
buffer "^5.0.3"
css-to-react-native "^2.0.3"
fbjs "^0.8.16"
hoist-non-react-statics "^2.5.0"
prop-types "^15.5.4"
react-is "^16.3.1"
stylis "^3.5.0"
stylis-rule-sheet "^0.0.10"
supports-color "^3.2.3"
stylis-rule-sheet@^0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
stylis@^3.5.0:
version "3.5.3"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.3.tgz#99fdc46afba6af4deff570825994181a5e6ce546"
superagent-proxy@^1.0.2: superagent-proxy@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/superagent-proxy/-/superagent-proxy-1.0.3.tgz#acfa776672f11c24a90ad575e855def8be44f741" resolved "https://registry.yarnpkg.com/superagent-proxy/-/superagent-proxy-1.0.3.tgz#acfa776672f11c24a90ad575e855def8be44f741"
@@ -6582,7 +6631,7 @@ supports-color@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
supports-color@^3.1.2: supports-color@^3.1.2, supports-color@^3.2.3:
version "3.2.3" version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies: dependencies:

BIN
docs/assets/screen1.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

View File

@@ -17,6 +17,7 @@ const styles = StyleSheet.create({
}, },
input: { input: {
flex: 1, flex: 1,
fontFamily: 'Menlo-Regular',
}, },
}); });
@@ -39,13 +40,13 @@ const Input = ({
onPress={() => { onPress={() => {
const fn = new Function(text); const fn = new Function(text);
try { try {
const response = eval(text); const response = fn();
log.info(response); log.info([response]);
setState({ setState({
text: '', text: '',
}); });
} catch (err) { } catch (err) {
log.error(err); log.error([err]);
} }
}} }}
/> />

View File

@@ -1,59 +1,127 @@
import React from 'react'; import React, { Fragment } from 'react';
import styled from 'styled-components/native';
import JSONTree from 'react-native-json-tree';
import { import {
StyleSheet, ScrollView
ScrollView,
View,
Text,
} from 'react-native'; } from 'react-native';
import JSONTree from 'react-native-json-tree' import {
import prune from './tools'; Body,
Emphasis,
Fixed,
} from '../../base/text';
const styles = StyleSheet.create({ const theme = {
container: { scheme: 'bright',
flex: 1, author: 'chris kempson (http://chriskempson.com)',
}, base00: '#000000',
}); base01: '#303030',
base02: '#505050',
const getCircularReplacer = () => { base03: '#b0b0b0',
const seen = []; base04: '#d0d0d0',
return (key, val) => { base05: '#e0e0e0',
if (val != null && typeof val == "object") { base06: '#f5f5f5',
if (seen.indexOf(val) >= 0) { base07: '#ffffff',
return; base08: '#fb0120',
} base09: '#fc6d24',
seen.push(val); base0A: '#fda331',
} base0B: '#a1c659',
return val; base0C: '#76c7b7',
} base0D: '#6fb3d2',
base0E: '#d381c3',
base0F: '#be643c'
}; };
const formatData = (data) => { const Wrapper = styled.View``;
const List = styled.View`
padding-left: 10px;
border-left-width: 10px;
border-color: ${props => props.color || 'black' }
`;
const Row = styled.View`
margin: 10px;
`;
const getColor = (type) => {
if (type === 'error') {
return 'red';
}
if (type === 'warn') {
return 'yellow';
}
if (type === 'verbose') {
return 'gray';
}
return;
}
const formatData = (data, options) => {
const {
includeStackTrace
} = options;
if (typeof data === 'undefined') { if (typeof data === 'undefined') {
return <Text>undefined</Text>; return <Fixed>undefined</Fixed>;
} }
if (data instanceof Error) { if (data instanceof Error) {
return <Text>Error {data.toString()} {data.stack.toString()}</Text>; if (includeStackTrace) {
return (
<JSONTree
theme={theme}
data={{
message: data.toString(),
stackTrace: data.stack.toString(),
}}
/>
);
} else {
return <Fixed>{data.toString()}</Fixed>;
}
} }
if (typeof data === 'object') { if (typeof data === 'object') {
return <JSONTree data={prune(data, null, ' ')} /> return <JSONTree data={data} />
} }
return <Text>{data.toString()}</Text>; return <Fixed>{data.toString()}</Fixed>;
} }
const OutputList = ({
items,
color,
includeStackTrace,
}) => (
<List color={color}>
{items.map((data, i) => (
<Fragment key={i}>
{formatData(data, {
includeStackTrace,
})}
</Fragment>
))}
</List>
)
const Console = ({ const Console = ({
logs, logs,
includeStackTrace,
}) => ( }) => (
<ScrollView style={styles.container}> <ScrollView
<View> ref={ref => this.scrollView = ref}
onContentSizeChange={(contentWidth, contentHeight)=>{
this.scrollView.scrollToEnd({animated: true});
}}
>
<Wrapper>
{logs.map((log, i) => ( {logs.map((log, i) => (
<View key={i}> <Row key={i}>
<Text> <Emphasis color={getColor(log.type)}>
{log.type} {log.type}
</Text> </Emphasis>
{formatData(log.data)} <OutputList
</View> items={log.data}
includeStackTrace={includeStackTrace}
color={getColor(log.type)}
/>
</Row>
))} ))}
</View> </Wrapper>
</ScrollView> </ScrollView>
); );

View File

@@ -14,11 +14,13 @@ const styles = StyleSheet.create({
}, },
}); });
const Console = () => ( const Console = ({
includeStackTrace,
}) => (
<Log> <Log>
{({ logs }) => ( {({ logs }) => (
<View style={styles.container}> <View style={styles.container}>
<Output logs={logs} /> <Output logs={logs} includeStackTrace={includeStackTrace} />
<Input /> <Input />
</View> </View>
)} )}

View File

@@ -1,7 +1,9 @@
import React, { Component, Fragment } from 'react'; import React, { Component } from 'react';
import { import {
Button, Button,
Modal, Modal,
SafeAreaView,
KeyboardAvoidingView,
} from 'react-native'; } from 'react-native';
import events from '../../events'; import events from '../../events';
import DevTool from './index'; import DevTool from './index';
@@ -39,6 +41,10 @@ class Events extends Component {
} }
render() { render() {
const {
includeStackTrace
} = this.props;
return ( return (
<Modal <Modal
animationType="slide" animationType="slide"
@@ -47,15 +53,24 @@ class Events extends Component {
onRequestClose={() => { onRequestClose={() => {
}} }}
> >
<Fragment> <SafeAreaView
<DevTool /> forceInset={{ top: 'always', vertical: 'always' }}
style={{flex: 1}}
>
<KeyboardAvoidingView
style={{flex: 1}}
behavior="padding"
enabled
>
<DevTool includeStackTrace={includeStackTrace} />
<Button <Button
title="close" title="close"
onPress={() => { onPress={() => {
events.publish('HIDE_DEVTOOLS'); events.publish('HIDE_DEVTOOLS');
}} }}
/> />
</Fragment> </KeyboardAvoidingView>
</SafeAreaView>
</Modal> </Modal>
) )
} }

View File

@@ -1,43 +1,98 @@
import React from 'react'; import React, { Fragment } from 'react';
import { import {
StyleSheet,
ScrollView, ScrollView,
View, View,
Text,
} from 'react-native'; } from 'react-native';
import styled from 'styled-components/native';
import {
Emphasis,
Fixed,
} from '../../base/text';
import JSONTree from 'react-native-json-tree';
import Cell from '../../base/Cell';
import CellHeader from '../../base/CellHeader';
import Tab from '../Tab'; import Tab from '../Tab';
const styles = StyleSheet.create({ const theme = {
container: { scheme: 'bright',
flex: 1, author: 'chris kempson (http://chriskempson.com)',
}, base00: '#000000',
}); base01: '#303030',
base02: '#505050',
base03: '#b0b0b0',
base04: '#d0d0d0',
base05: '#e0e0e0',
base06: '#f5f5f5',
base07: '#ffffff',
base08: '#fb0120',
base09: '#fc6d24',
base0A: '#fda331',
base0B: '#a1c659',
base0C: '#76c7b7',
base0D: '#6fb3d2',
base0E: '#d381c3',
base0F: '#be643c'
};
const Indented = styled.View`
margin: 0 25px;
`;
const getResponse = (request) => {
if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') {
return <Emphasis>🤖 Binary</Emphasis>
}
const contentType = request.getResponseHeader('content-type');
const contentTypes = contentType.split(';').map(c => c.trim());
if (contentTypes.includes('application/json')) {
const data = JSON.parse(request.responseText);
return <JSONTree theme={theme} data={data} />
}
return <Fixed>{request.responseText}</Fixed>;
}
const Data = ({ const Data = ({
url, url,
method, method,
status, status,
headers,
request,
args = [], args = [],
}) => ( }) => {
<ScrollView style={styles.container}> const headerInfo = Object.keys(headers).map(key => `${key}: ${headers[key]}`).join('\n');
return (
<ScrollView>
<View> <View>
<Text>Status: {status}</Text> <Cell left="Status" right={status} />
<Text>Method: {method}</Text> <Cell left="Method" right={method} />
<Text>Url: {url}</Text> <Cell left="Url" right={url} />
<Text>Request Body:</Text> <CellHeader>Response Headers</CellHeader>
<Indented><Fixed>{request.getAllResponseHeaders()}</Fixed></Indented>
{headerInfo && (
<Fragment>
<CellHeader>Request Headers</CellHeader>
<Indented><Fixed>{headerInfo}</Fixed></Indented>
</Fragment>
)}
{args[0] && ( {args[0] && (
<Text>{args[0].toString()}</Text> <Fragment>
<CellHeader>Request Body</CellHeader>
<Fixed>{args[0].toString()}</Fixed>
</Fragment>
)} )}
</View> </View>
</ScrollView> </ScrollView>
); );
};
const Response = ({ const Response = ({
request, request,
}) => ( }) => (
<ScrollView style={styles.container}> <ScrollView>
<View> <View>
<Text>Response: {request.responseText}</Text> {getResponse(request)}
</View> </View>
</ScrollView> </ScrollView>
); );
@@ -46,10 +101,10 @@ const Response = ({
const RequestDetails = (props) => ( const RequestDetails = (props) => (
<Tab <Tab
tabs={[{ tabs={[{
name: 'overview', name: 'Details',
view: <Data {...props} /> view: <Data {...props} />
}, { }, {
name: 'response', name: 'Response',
view: <Response {...props} /> view: <Response {...props} />
}]} }]}
/> />

View File

@@ -1,41 +1,31 @@
import React from 'react'; import React, { Fragment } from 'react';
import styled from 'styled-components/native';
import Status from '../../base/Status';
import Row from '../../base/Row';
import { import {
StyleSheet, Body,
View, } from '../../base/text';
Text,
ScrollView,
TouchableOpacity,
} from 'react-native';
const styles = StyleSheet.create({ const ScrollView = styled.ScrollView`
container: { flex: 1;
flex: 1, `;
borderColor: '#ccc',
borderBottomWidth: 1, const Wrapper = styled.View`
}, flex: 1;
row: { `;
flexDirection: 'row',
padding: 10, const TouchableOpacity = styled.TouchableOpacity`
}, `;
method: {
padding: 5,
},
url: {
flex: 1,
padding: 5,
},
status: {
padding: 5,
},
});
const RequestDetails = ({ const RequestDetails = ({
requests, requests,
onSelect, onSelect,
selected,
}) => ( }) => (
<ScrollView style={styles.container}> <ScrollView>
<View> <Wrapper>
{requests.map(({ {requests.map(({
id,
status, status,
method, method,
url, url,
@@ -44,14 +34,18 @@ const RequestDetails = ({
key={i} key={i}
onPress={() => onSelect(i)} onPress={() => onSelect(i)}
> >
<View style={styles.row}> <Row
<Text style={styles.method}>{method}</Text> selected={selected === id}
<Text style={styles.url}>{url}</Text> left={<Body>{method}</Body>}
<Text style={styles.status}>{status}</Text> right={(
</View> <Status code={status} />
)}
>
<Body>{url}</Body>
</Row>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </Wrapper>
</ScrollView> </ScrollView>
); );

View File

@@ -35,6 +35,7 @@ const Console = () => (
return ( return (
<View style={styles.container}> <View style={styles.container}>
<List <List
selected={selected ? selected.id : undefined}
requests={requests} requests={requests}
onSelect={(i) => setState({ active: i })} onSelect={(i) => setState({ active: i })}
/> />

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Text, Text,
@@ -30,20 +30,21 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
padding: 10, padding: 10,
borderBottomWidth: 4, borderBottomWidth: 4,
borderColor: 'red', borderColor: '#2980b9',
}, },
}); });
const Console = ({ const Console = ({
tabs, tabs,
}) => ( }) => (
<View style={styles.container}>
<State <State
initState={{ initState={{
active: 0, active: 0,
}} }}
> >
{({ active }, setState) => ( {({ active }, setState) => (
<View style={styles.container}> <Fragment>
<View style={styles.tabs}> <View style={styles.tabs}>
{tabs.map(({ name }, i) => ( {tabs.map(({ name }, i) => (
<TouchableOpacity <TouchableOpacity
@@ -58,9 +59,10 @@ const Console = ({
))} ))}
</View> </View>
{tabs[active] && tabs[active].view} {tabs[active] && tabs[active].view}
</View> </Fragment>
)} )}
</State> </State>
</View>
); );
Console.propTypes = { Console.propTypes = {

View File

@@ -15,14 +15,15 @@ const styles = StyleSheet.create({
const DevTool = ({ const DevTool = ({
style, style,
includeStackTrace
}) => ( }) => (
<View style={style || styles.container}> <View style={style || styles.container}>
<Tab <Tab
tabs={[{ tabs={[{
name: 'console', name: 'Console',
view: <Console />, view: <Console includeStackTrace={includeStackTrace} />,
}, { }, {
name: 'network', name: 'Network',
view: <Requests />, view: <Requests />,
}]} }]}
/> />

View File

@@ -0,0 +1,29 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Body,
Emphasis,
} from './text';
const Wrapper = styled.View`
flex-direction: row;
padding: 10px;
`;
const Left = styled.View`
width: 100px;
`;
const Row = ({
left,
right,
}) => (
<Wrapper>
<Left>
<Emphasis>{left}:</Emphasis>
</Left>
<Body>{right}</Body>
</Wrapper>
)
export default Row;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Emphasis,
} from './text';
const Wrapper = styled.View`
flex-direction: row;
padding: 10px;
`;
const Row = ({
children,
}) => (
<Wrapper>
<Emphasis>{children}:</Emphasis>
</Wrapper>
)
export default Row;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import styled from 'styled-components/native';
const Wrapper = styled.View`
flex-direction: row;
padding: 10px;
border-left-width: ${props => props.selected ? '10px' : '0'};
border-color: #2980b9;
`;
const Left = styled.View`
`;
const Right = styled.View`
`;
const Center = styled.View`
flex: 1;
margin: 0 10px;
`;
const Row = ({
left,
right,
children,
selected,
}) => (
<Wrapper selected={selected}>
{left && <Left>{left}</Left>}
<Center>{children}</Center>
{right && <Right>{right}</Right>}
</Wrapper>
)
export default Row;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import styled from 'styled-components/native';
import {
Body,
} from '../base/text';
const getColor = (code) => {
if (code >= 500) {
return '#c0392b';
}
if (code >= 400) {
return '#f1c40f';
}
if (code >= 300) {
return '#2980b9';
}
return '#2ecc71';
}
const Wrapper = styled.View`
flex-direction: row;
`;
const Icon = styled.View`
width: 16px;
height: 16px;
border-radius: 8px;
background: ${props => getColor(props.code)};
margin-left: 5px;
`;
const Status = ({
code,
}) => (
<Wrapper>
<Body>{code}</Body>
<Icon code={code} />
</Wrapper>
)
export default Status;

View File

@@ -0,0 +1,14 @@
import styled from 'styled-components/native';
export const Body = styled.Text`
color: ${props => props.color || 'black'};
`;
export const Emphasis = styled.Text`
font-weight: bold;
color: ${props => props.color || 'black'};
`;
export const Fixed = styled.Text`
font-family: Menlo-Regular;
`;

View File

@@ -7,23 +7,18 @@ class Log extends Component {
this.state = { this.state = {
logs: [], logs: [],
}; };
this.addLog = this.addLog.bind(this); this.setLogs = this.setLogs.bind(this);
} }
componentDidMount() { componentDidMount() {
log.listen(this.addLog); log.listen(this.setLogs);
} }
componentWillUnmount() { componentWillUnmount() {
log.unlisten(this.addLog); log.unlisten(this.setLogs);
} }
addLog(entry) { setLogs(logs) {
entry = Array.isArray(entry) ? entry : [entry];
const logs = [
...this.state.logs,
...entry,
];
this.setState({ this.setState({
logs, logs,
}); });

View File

@@ -7,23 +7,18 @@ class Network extends Component {
this.state = { this.state = {
requests: [], requests: [],
}; };
this.addRequest = this.addRequest.bind(this); this.setRequests = this.setRequests.bind(this);
} }
componentDidMount() { componentDidMount() {
network.listen(this.addRequest); network.listen(this.setRequests);
} }
componentWillUnmount() { componentWillUnmount() {
network.unlisten(this.addRequest); network.unlisten(this.setRequests);
} }
addRequest(request) { setRequests(requests) {
request = Array.isArray(request) ? request : [request];
const requests = [
...this.state.requests,
...request,
];
this.setState({ this.setState({
requests, requests,
}); });

View File

@@ -1,12 +1,3 @@
/* const overrides = [
'log',
];
const proxies = overrides.reduce((output, key) => ({
...output,
[key]: window.console[key],
}), {}); */
const proxyConsole = window.console; const proxyConsole = window.console;
class Log { class Log {
@@ -24,41 +15,49 @@ class Log {
this.listeners = this.listeners.filter(l => l !== fn); this.listeners = this.listeners.filter(l => l !== fn);
} }
log(type, data) { log(type, data, keep) {
const entry = { const entry = {
type, type,
data, data,
}; };
this.logs.push(entry); this.logs.push(entry);
this.listeners.forEach(l => l(entry)); this.listeners.forEach(l => l(this.logs));
if (keep) {
proxyConsole[type](...data);
}
} }
info(data) { info(data, keep) {
this.log('info', data); this.log('info', data, keep);
} }
error(error) { error(data, keep) {
this.log('error', error); this.log('error', data, keep);
} }
warn(data) { warn(data, keep) {
this.log('warn', data); this.log('warn', data, keep);
} }
debug(data) { debug(data, keep) {
this.log('debug', data); this.log('debug', data, keep);
} }
attach(keep) { attach(keep) {
const redirected = Object.keys(proxyConsole).reduce((output, key) => ({
...output,
[key]: keep ? (...args) => proxyConsole[key](...args) : () => {},
}), {});
window.console = { window.console = {
error: (...data) => this.error(...data), ...redirected,
warn: (data) => this.warn(data), error: (...data) => this.error(data, keep),
info: (data) => this.info(data), warn: (...data) => this.warn(data, keep),
log: (data) => this.info(data), info: (...data) => this.info(data, keep),
debug: (data) => this.debug(data), log: (...data) => this.info(data, keep),
debug: (...data) => this.debug(data, keep),
}; };
ErrorUtils.setGlobalHandler((err, fatal) => { ErrorUtils.setGlobalHandler((err, fatal) => {
this.error(err); this.error([err], keep);
}); });
} }

View File

@@ -4,6 +4,7 @@ class Network {
constructor() { constructor() {
this.requests = []; this.requests = [];
this.listeners = []; this.listeners = [];
this.currentId = 0;
} }
listen(fn) { listen(fn) {
@@ -16,12 +17,16 @@ class Network {
} }
addRequest(request) { addRequest(request) {
this.requests.push(request); this.requests.push({
this.listeners.forEach(l => l(request)); id: this.currentId++,
...request,
});
this.listeners.forEach(l => l(this.requests));
} }
attach() { attach() {
const me = this; const me = this;
const headers = {};
window.XMLHttpRequest.prototype.open = function proxyOpen (...args) { window.XMLHttpRequest.prototype.open = function proxyOpen (...args) {
let sendArgs; let sendArgs;
const [ const [
@@ -33,6 +38,7 @@ class Network {
url, url,
method, method,
args: sendArgs, args: sendArgs,
headers,
request: this, request: this,
status: this.status, status: this.status,
}); });
@@ -43,15 +49,21 @@ class Network {
method, method,
error, error,
args: sendArgs, args: sendArgs,
headers,
request: this, request: this,
status: this.status || 'CONN ERR', status: this.status || 'CONN ERR',
}); });
}) })
const proxiedSend = this.send; const proxiedSend = this.send;
const proxiedSetRequestHeader = this.setRequestHeader;
this.send = function proxySend (...sendargs) { this.send = function proxySend (...sendargs) {
sendArgs = sendargs; sendArgs = sendargs;
return proxiedSend.apply(this, [].slice.call(arguments)); return proxiedSend.apply(this, [].slice.call(arguments));
} }
this.setRequestHeader = function (name, value) {
headers[name] = value;
return proxiedSetRequestHeader.apply(this, [].slice.call(arguments));
}
return proxied.apply(this, [].slice.call(arguments)); return proxied.apply(this, [].slice.call(arguments));
}; };
} }