This commit is contained in:
2018-08-22 23:44:26 +02:00
parent 7e635595f3
commit c8f41d45ea
24 changed files with 352 additions and 163 deletions

View File

@@ -14,10 +14,12 @@ import {
log, log,
network, network,
show, show,
context,
} from 'react-native-debug-console'; } from 'react-native-debug-console';
network.attach(); network.attach();
log.attach(); log.attach();
context.hello = () => 'earth';
console.log('fooo'); console.log('fooo');
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
@@ -105,11 +107,11 @@ export default class App extends React.Component {
/> />
<View style={{ height: 10 }} /> <View style={{ height: 10 }} />
</View> </View>
<DevTool {/* <DevTool
style={{ style={{
flex: 1, flex: 1,
}} }}
/> /> */}
<DevToolModal /> <DevToolModal />
</KeyboardAvoidingView> </KeyboardAvoidingView>
</SafeAreaView> </SafeAreaView>

View File

@@ -1,6 +1,6 @@
{ {
"name": "react-native-debug-console", "name": "react-native-debug-console",
"version": "1.0.0-beta.12", "version": "1.0.0-beta.13",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
"url": "https://github.com/morten-olsen/react-native-debugger" "url": "https://github.com/morten-olsen/react-native-debugger"

View File

@@ -7,6 +7,7 @@ import {
} from 'react-native'; } from 'react-native';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import State from '../../data/State'; import State from '../../data/State';
import { context } from '../../../console';
import log from '../../../log'; import log from '../../../log';
import Icon from '../../base/Icon'; import Icon from '../../base/Icon';
@@ -86,8 +87,8 @@ const Input = ({
<Button <Button
onPress={() => { onPress={() => {
const newHistory = [...history, text]; const newHistory = [...history, text];
const contextKeys = Object.keys(log.context); const contextKeys = Object.keys(context);
const contextValues = Object.values(log.context); const contextValues = Object.values(context);
const fn = new Function(...contextKeys, text); const fn = new Function(...contextKeys, text);
try { try {
fn(...contextValues); fn(...contextValues);

View File

@@ -101,6 +101,7 @@ const OutputList = ({
const Console = ({ const Console = ({
logs, logs,
includeStackTrace, includeStackTrace,
filter = [],
}) => ( }) => (
<ScrollView <ScrollView
ref={ref => this.scrollView = ref} ref={ref => this.scrollView = ref}
@@ -109,7 +110,7 @@ const Console = ({
}} }}
> >
<Wrapper> <Wrapper>
{logs.map((log, i) => ( {logs.filter(l => filter.includes(l.type)).map((log, i) => (
<Row key={i}> <Row key={i}>
<Emphasis color={getColor(log.type)}> <Emphasis color={getColor(log.type)}>
{log.type} {log.type}

View File

@@ -1,13 +1,16 @@
import React from 'react'; import React from 'react';
import { import {
StyleSheet, StyleSheet,
Clipboard,
Alert,
View, View,
} from 'react-native'; } from 'react-native';
import log from '../../../log'; import log from '../../../log';
import Log from '../../data/Log'; import Log from '../../data/Log';
import Toolbar from '../../base/Toolbar'; import State from '../../data/State';
import Toolbar, {
Button,
Selector,
Seperator,
} from '../../base/Toolbar';
import Output from './Output'; import Output from './Output';
import Input from './Input'; import Input from './Input';
@@ -17,31 +20,53 @@ const styles = StyleSheet.create({
}, },
}); });
const initFilters = [
'error',
'warn',
'info',
'debug',
].map(i => ({
name: i,
value: i,
selected: true,
}))
const Console = ({ const Console = ({
includeStackTrace, includeStackTrace,
}) => ( }) => (
<Log> <Log>
{({ logs }) => ( {({ logs }) => (
<View style={styles.container}> <State
<Toolbar initState={{
items={[{ filters: initFilters,
name: 'Download', }}
icon: 'download', >
onPress: () => { {({ filters }, setState) => (
Clipboard.setString(JSON.stringify(logs, null, ' ')); <View style={styles.container}>
Alert.alert( <Toolbar>
'Copied to clipboard', <Selector
); name="Filter"
}, icon="filter"
}, { options={filters}
name: 'Clear', multiSelect
icon: 'trash', onSelect={(selected) => {
onPress: () => log.clear(), setState({
}]} filters: selected,
/> });
<Output logs={logs} includeStackTrace={includeStackTrace} /> }}
<Input /> />
</View> <Seperator />
<Button
name="Clear"
icon="trash"
onPress={() => log.clear()}
/>
</Toolbar>
<Output filter={filters.filter(f => f.selected).map(f => f.name)} logs={logs} includeStackTrace={includeStackTrace} />
<Input />
</View>
)}
</State>
)} )}
</Log> </Log>
); );

View File

@@ -14,7 +14,6 @@ import CellHeader from '../../base/CellHeader';
import Tab from '../Tab'; import Tab from '../Tab';
const WebView = styled.WebView` const WebView = styled.WebView`
background: red;
`; `;
const theme = { const theme = {
@@ -42,11 +41,10 @@ const Indented = styled.View`
margin: 0 25px; margin: 0 25px;
`; `;
const getResponse = (request) => { const getResponse = (contentType, request) => {
if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') { if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') {
return <Emphasis>🤖 Binary</Emphasis> return <Emphasis>🤖 Binary</Emphasis>
} }
const contentType = request.getResponseHeader('content-type') || '';
const contentTypes = contentType.split(';').map(c => c.trim()); const contentTypes = contentType.split(';').map(c => c.trim());
if (contentTypes.includes('application/json')) { if (contentTypes.includes('application/json')) {
@@ -57,12 +55,13 @@ const getResponse = (request) => {
return <Fixed selectable={true}>{request.responseText}</Fixed>; return <Fixed selectable={true}>{request.responseText}</Fixed>;
} }
let i = 0;
const Data = ({ const Data = ({
url, url,
method, method,
status, status,
headers, headers,
request, requestHeaders,
args = [], args = [],
}) => { }) => {
const headerInfo = Object.keys(headers).map(key => `${key}: ${headers[key]}`).join('\n'); const headerInfo = Object.keys(headers).map(key => `${key}: ${headers[key]}`).join('\n');
@@ -73,7 +72,7 @@ const Data = ({
<Cell left="Method" right={method} /> <Cell left="Method" right={method} />
<Cell left="Url" right={url} /> <Cell left="Url" right={url} />
<CellHeader>Response Headers</CellHeader> <CellHeader>Response Headers</CellHeader>
<Indented><Fixed>{request.getAllResponseHeaders()}</Fixed></Indented> <Indented><Fixed>{requestHeaders}</Fixed></Indented>
{headerInfo && ( {headerInfo && (
<Fragment> <Fragment>
<CellHeader>Request Headers</CellHeader> <CellHeader>Request Headers</CellHeader>
@@ -92,21 +91,21 @@ const Data = ({
}; };
const Response = ({ const Response = ({
contentType,
request, request,
}) => ( }) => (
<ScrollView> <ScrollView>
<View> <View>
{getResponse(request)} {getResponse(contentType, request)}
</View> </View>
</ScrollView> </ScrollView>
); );
const getPreview = (request, url) => { const getPreview = (contentType, request, url) => {
if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') { if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') {
return []; return [];
} }
const contentType = request.getResponseHeader('content-type') || '';
const contentTypes = contentType.split(';').map(c => c.trim()); const contentTypes = contentType.split(';').map(c => c.trim());
if (contentTypes.includes('text/html')) { if (contentTypes.includes('text/html')) {
@@ -134,7 +133,7 @@ const RequestDetails = (props) => (
}, { }, {
name: 'Response', name: 'Response',
view: <Response {...props} /> view: <Response {...props} />
}, ...getPreview(props.request, props.url)]} }, ...getPreview(props.contentType, props.request, props.url)]}
/> />
); );

View File

@@ -2,15 +2,14 @@ import React from 'react';
import { import {
StyleSheet, StyleSheet,
View, View,
Clipboard,
Alert,
Text,
TouchableOpacity,
} from 'react-native'; } from 'react-native';
import network from '../../../network'; import network from '../../../network';
import State from '../../data/State'; import State from '../../data/State';
import Network from '../../data/Network'; import Network from '../../data/Network';
import Toolbar from '../../base/Toolbar'; import Toolbar, {
Button,
Seperator,
} from '../../base/Toolbar';
import Details from './Details'; import Details from './Details';
import List from './List'; import List from './List';
@@ -27,7 +26,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
}); });
let i = 0;
const Console = () => ( const Console = () => (
<State> <State>
{({ {({
@@ -38,22 +37,14 @@ const Console = () => (
const selected = active >= 0 ? requests[active] : undefined; const selected = active >= 0 ? requests[active] : undefined;
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Toolbar <Toolbar>
items={[{ <Seperator />
name: 'Download', <Button
icon: 'download', name="Clear"
onPress: () => { icon="trash"
Clipboard.setString(JSON.stringify(requests, null, ' ')); onPress={() => network.clear()}
Alert.alert( />
'Copied to clipboard', </Toolbar>
);
},
}, {
name: 'Clear',
icon: 'trash',
onPress: () => network.clear(),
}]}
/>
<List <List
selected={selected ? selected.id : undefined} selected={selected ? selected.id : undefined}
requests={requests} requests={requests}

View File

@@ -24,7 +24,7 @@ const Keys = ({
<Wrapper> <Wrapper>
{keys.map(key => ( {keys.map(key => (
<Button <Button
key={key} key={key || '[unknown]'}
onPress={() => onSelect(key)} onPress={() => onSelect(key)}
> >
<Row <Row

View File

@@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import {
Clipboard,
Alert,
} from 'react-native';
import Storage from '../../data/Storage'; import Storage from '../../data/Storage';
import State from '../../data/State'; import State from '../../data/State';
import Toolbar from '../../base/Toolbar'; import Toolbar, {
Button,
Seperator,
} from '../../base/Toolbar';
import Keys from './Keys'; import Keys from './Keys';
import Value from './Value'; import Value from './Value';
@@ -20,31 +19,25 @@ const StorageView = () => (
<Storage> <Storage>
{(data, update, removeItem, clear) => ( {(data, update, removeItem, clear) => (
<Wrapper> <Wrapper>
<Toolbar <Toolbar>
items={[{ <Seperator />
name: 'Download', <Button
icon: 'download', name="Refresh"
onPress: () => { icon="reload"
Clipboard.setString(JSON.stringify(data, null, ' ')); onPress={update}
Alert.alert( />
'Copied to clipboard', <Button
); name="Clear"
}, icon="trash"
}, { onPress={clear}
name: 'Refresh', />
icon: 'reload', <Button
onPress: update, name="Delete"
}, { icon="remove"
name: 'Clear', disabled={!selected}
icon: 'trash', onPress={() => removeItem(selected)}
onPress: clear, />
}, { </Toolbar>
name: 'Delete',
icon: 'remove',
disabled: !selected,
onPress: () => removeItem(selected),
}]}
/>
<Keys <Keys
selected={selected} selected={selected}
onSelect={(key) => setState({ selected: key })} onSelect={(key) => setState({ selected: key })}

View File

@@ -45,6 +45,7 @@ const Console = ({
tabs, tabs,
buttons = [], buttons = [],
onClose, onClose,
onDownload,
}) => ( }) => (
<View style={styles.container}> <View style={styles.container}>
<State <State
@@ -66,7 +67,13 @@ const Console = ({
<Text>{name}</Text> <Text>{name}</Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
{onDownload && (
<Button
onPress={onDownload}
>
<Icon name="download" />
</Button>
)}
{onClose && ( {onClose && (
<Button <Button
onPress={onClose} onPress={onClose}

View File

@@ -2,11 +2,15 @@ import React from 'react';
import { import {
StyleSheet, StyleSheet,
View, View,
Alert,
Clipboard,
} from 'react-native'; } from 'react-native';
import Tab from './Tab'; import Tab from './Tab';
import Console from './Console'; import Console from './Console';
import Requests from './Requests'; import Requests from './Requests';
import Storage from './Storage'; import Storage from './Storage';
import log from '../../log';
import network from '../../network';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@@ -18,22 +22,65 @@ const DevTool = ({
style, style,
includeStackTrace, includeStackTrace,
onClose, onClose,
}) => ( showNetwork = true,
<View style={style || styles.container}> showStorage = true,
<Tab showConsole = true,
tabs={[{ additionTools = [],
name: 'Console', }) => {
view: <Console includeStackTrace={includeStackTrace} />, const views = [];
}, {
name: 'Network', if (showConsole) {
view: <Requests />, views.push({
}, { name: 'Console',
name: 'Storage', view: <Console includeStackTrace={includeStackTrace} />,
view: <Storage />, getData: log.get,
}]} });
onClose={onClose} }
/>
</View> if (showNetwork) {
); views.push({
name: 'Network',
view: <Requests />,
getData: network.get,
});
}
if (showStorage) {
views.push({
name: 'Storage',
view: <Storage />,
});
}
additionTools.forEach(tool => {
views.push(tool);
});
const getData = async () => {
const result = {};
for (let i = 0; i < views.length; i++) {
const view = views[i];
if (view.getData) {
result[view.name] = await view.getData();
}
};
Clipboard.setString(JSON.stringify(result, null, ' '));
Alert.alert(
'Copied to clipboard',
);
}
return (
<View style={style || styles.container}>
<Tab
tabs={views}
onClose={onClose}
onDownload={getData}
/>
</View>
);
};
export default DevTool; export default DevTool;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -9,6 +9,9 @@ import left from './left.png';
import play from './play.png'; import play from './play.png';
import download from './download.png'; import download from './download.png';
import close from './close.png'; import close from './close.png';
import filter from './filter.png';
import square from './square.png';
import check from './check.png';
const icons = { const icons = {
reload, reload,
@@ -19,6 +22,9 @@ const icons = {
play, play,
download, download,
close, close,
filter,
square,
check,
} }
const Image = styled.Image` const Image = styled.Image`

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,47 +0,0 @@
import React from 'react';
import styled from 'styled-components/native';
import Icon from './Icon';
import {
Body,
} from './text';
const Wrapper = styled.View`
flex-direction: row;
justify-content: flex-end;
align-items: center;
border-bottom-width: 1px;
border-color: #ccc;
padding: 0 10px;
`;
const Item = styled.TouchableOpacity`
padding: 10px 10px;
opacity: ${({ disabled }) => disabled ? 0.3 : 1};
`
const Toolbar = ({
items = [],
}) => (
<Wrapper>
{items.map(({
name,
icon,
onPress,
disabled,
}) => (
<Item
key={name}
onPress={disabled ? undefined : onPress}
disabled={disabled}
>
{icon ? (
<Icon name={icon} />
) : (
<Body color={disabled ? '#ccc' : undefined}>{name}</Body>
)}
</Item>
))}
</Wrapper>
)
export default Toolbar;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import styled from 'styled-components/native';
import Icon from '../Icon';
import {
Body,
} from '../text';
const Item = styled.TouchableOpacity`
padding: 10px 10px;
opacity: ${({ disabled }) => disabled ? 0.3 : 1};
`;
const Button = ({
name,
icon,
onPress,
disabled,
}) => (
<Item
onPress={disabled ? undefined : onPress}
disabled={disabled}
>
{icon ? (
<Icon name={icon} />
) : (
<Body color={disabled ? '#ccc' : undefined}>{name}</Body>
)}
</Item>
);
export default Button;

View File

@@ -0,0 +1,78 @@
import React, { Fragment } from 'react';
import {
SafeAreaView,
TouchableOpacity,
} from 'react-native';
import styled from 'styled-components/native';
import State from '../../data/State';
import Button from './Button';
import Row from '../Row';
import Icon from '../Icon';
import {
Body,
} from '../text';
const Modal = styled.Modal`
`;
const Selector = ({
...others,
multiSelect = false,
onSelect,
options = [],
}) => (
<State>
{({ open }, setState) => (
<Fragment>
<Button
{...others}
onPress={() => {
setState({ open: true })
}}
/>
<Modal
animationType="slide"
transparent={false}
visible={!!open}
onRequestClose={() => {
setState({ open: false })
}}
>
<SafeAreaView
forceInset={{ top: 'always', vertical: 'always' }}
style={{flex: 1}}
>
<Button
name="Close"
icon="close"
onPress={() => {
setState({ open: false })
}}
/>
{options.map((option) => (
<TouchableOpacity
onPress={() => {
if (!multiSelect) {
options.forEach(o => o.selected = false);
}
option.selected = !option.selected
onSelect(options);
}}
>
<Row
left={(
<Icon name={option.selected ? 'check' : 'square'} />
)}
>
<Body>{option.name}</Body>
</Row>
</TouchableOpacity>
))}
</SafeAreaView>
</Modal>
</Fragment>
)}
</State>
);
export default Selector;

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components/native';
const Seperator = styled.View`
flex: 1;
`;
export default Seperator;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import styled from 'styled-components/native';
import Button from './Button';
import Seperator from './Seperator';
import Selector from './Selector';
export {
Button,
Seperator,
Selector,
}
const Wrapper = styled.View`
flex-direction: row;
align-items: center;
border-bottom-width: 1px;
border-color: #ccc;
padding: 0 10px;
`;
const Toolbar = ({
children,
}) => (
<Wrapper>
{children}
</Wrapper>
)
export default Toolbar;

8
lib/src/console.js Normal file
View File

@@ -0,0 +1,8 @@
import log from './log';
import { AsyncStorage } from 'react-native';
export const context = {
storage: AsyncStorage,
log: (...args) => console.log(...args),
clear: log.clear,
};

View File

@@ -3,6 +3,7 @@ import DevToolModal from './components/DevTool/Modal';
import log from './log'; import log from './log';
import network from './network'; import network from './network';
import events from './events'; import events from './events';
import { context } from './console';
const show = () => events.publish('SHOW_DEVTOOLS'); const show = () => events.publish('SHOW_DEVTOOLS');
const hide = () => events.publish('HIDE_DEVTOOLS'); const hide = () => events.publish('HIDE_DEVTOOLS');
@@ -12,6 +13,7 @@ export {
DevToolModal, DevToolModal,
log, log,
network, network,
context,
show, show,
hide, hide,
}; };

View File

@@ -1,16 +1,11 @@
import { AsyncStorage } from 'react-native';
export const proxyConsole = window.console; export const proxyConsole = window.console;
class Log { class Log {
constructor() { constructor() {
this.logs = []; this.logs = [];
this.listeners = []; this.listeners = [];
this.context = { this.clear = this.clear.bind(this);
storage: AsyncStorage, this.get = this.get.bind(this);
log: (...args) => console.log(...args),
clear: this.clear.bind(this),
};
} }
listen(fn) { listen(fn) {
@@ -27,6 +22,10 @@ class Log {
this.listeners.forEach(l => l(this.logs)); this.listeners.forEach(l => l(this.logs));
} }
get() {
return this.logs;
}
log(type, data, keep) { log(type, data, keep) {
const entry = { const entry = {
type, type,
@@ -67,6 +66,7 @@ class Log {
info: (...data) => this.info(data, keep), info: (...data) => this.info(data, keep),
log: (...data) => this.info(data, keep), log: (...data) => this.info(data, keep),
debug: (...data) => this.debug(data, keep), debug: (...data) => this.debug(data, keep),
verbose: (...data) => this.debug(data, keep),
}; };
ErrorUtils.setGlobalHandler((err, fatal) => { ErrorUtils.setGlobalHandler((err, fatal) => {
this.error([err], keep); this.error([err], keep);

View File

@@ -5,6 +5,8 @@ class Network {
this.requests = []; this.requests = [];
this.listeners = []; this.listeners = [];
this.currentId = 0; this.currentId = 0;
// this.clear = this.clear.bind(this);
this.get = this.get.bind(this);
} }
listen(fn) { listen(fn) {
@@ -12,6 +14,10 @@ class Network {
fn(this.requests); fn(this.requests);
} }
get() {
return this.requests;
}
unlisten(fn) { unlisten(fn) {
this.listeners = this.listeners.filter(l => l !== fn); this.listeners = this.listeners.filter(l => l !== fn);
} }
@@ -44,6 +50,8 @@ class Network {
method, method,
args: sendArgs, args: sendArgs,
headers, headers,
requestHeaders: this.getAllResponseHeaders(),
contentType: this.getResponseHeader('content-type') || '',
request: this, request: this,
status: this.status, status: this.status,
}); });
@@ -55,6 +63,7 @@ class Network {
error, error,
args: sendArgs, args: sendArgs,
headers, headers,
requestHeaders: this.getAllResponseHeaders(),
request: this, request: this,
status: this.status || 'CONN ERR', status: this.status || 'CONN ERR',
}); });