mirror of
https://github.com/morten-olsen/react-native-debug-console.git
synced 2026-02-08 00:36:26 +01:00
V2 (#1)
This commit is contained in:
114
packages/lib/src/components/DevTool/Console/Input.js
Normal file
114
packages/lib/src/components/DevTool/Console/Input.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
TextInput,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { createContext } from '../../../console';
|
||||
import Icon from '../../base/Icon';
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
padding: 12px 8px;
|
||||
`;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderColor: '#ccc',
|
||||
borderTopWidth: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontFamily: 'Menlo-Regular',
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 5,
|
||||
margin: 10,
|
||||
padding: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const Input = ({
|
||||
provider,
|
||||
context: baseContext,
|
||||
}) => {
|
||||
const [text, setText] = useState('');
|
||||
const [history, setHistory] = useState([]);
|
||||
const [historyOffset, setHistoryOffset] = useState();
|
||||
|
||||
const send = () => {
|
||||
const newHistory = [...history, text];
|
||||
const context = createContext({
|
||||
logProvider: provider,
|
||||
}, baseContext);
|
||||
const contextKeys = Object.keys(context);
|
||||
const contextValues = Object.values(context);
|
||||
const fn = new Function(...contextKeys, text);
|
||||
try {
|
||||
fn(...contextValues);
|
||||
setText('');
|
||||
setHistoryOffset(undefined);
|
||||
setHistory(newHistory);
|
||||
} catch (err) {
|
||||
provider.error([err]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button
|
||||
onPress={() => {
|
||||
let currentOffset = typeof historyOffset === 'undefined' ? -1 : historyOffset;
|
||||
currentOffset += 1;
|
||||
const index = history.length - 1 - currentOffset;
|
||||
if (history[index]) {
|
||||
setText(history[index]);
|
||||
setHistoryOffset(currentOffset);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon name="left" />
|
||||
</Button>
|
||||
<Button
|
||||
title=">"
|
||||
onPress={() => {
|
||||
let currentOffset = typeof historyOffset === 'undefined' ? -1 : historyOffset;
|
||||
currentOffset -= 1;
|
||||
const index = history.length - 1 - currentOffset;
|
||||
if (history[index]) {
|
||||
setText(history[index]);
|
||||
setHistoryOffset(currentOffset);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon name="right" />
|
||||
</Button>
|
||||
<TextInput
|
||||
multiline
|
||||
placeholder="{your code here}"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={styles.input}
|
||||
value={text}
|
||||
onChangeText={text => setText(text)}
|
||||
onKeyPress={(evt) => {
|
||||
global.proxyConsole.log(Platform.OS === 'web' && evt.key === 'Enter' && evt.shiftKey);
|
||||
if (Platform.OS === 'web' && evt.key === 'Enter' && !evt.shiftKey) {
|
||||
send();
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onPress={() => send()}
|
||||
>
|
||||
<Icon name="play" />
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
133
packages/lib/src/components/DevTool/Console/Output.js
Normal file
133
packages/lib/src/components/DevTool/Console/Output.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { Fragment, createRef } from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import JSONTree from 'react-native-json-tree';
|
||||
import {
|
||||
ScrollView
|
||||
} from 'react-native';
|
||||
import {
|
||||
Body,
|
||||
Emphasis,
|
||||
Fixed,
|
||||
} from '../../base/text';
|
||||
|
||||
const theme = {
|
||||
scheme: 'bright',
|
||||
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 Wrapper = styled.View``;
|
||||
export const List = styled.View`
|
||||
padding-left: 10px;
|
||||
border-left-width: 10px;
|
||||
border-color: ${props => props.color || 'black' }
|
||||
`;
|
||||
export 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') {
|
||||
return <Fixed>undefined</Fixed>;
|
||||
}
|
||||
if (data instanceof Error) {
|
||||
if (includeStackTrace) {
|
||||
return (
|
||||
<JSONTree
|
||||
theme={theme}
|
||||
data={{
|
||||
message: data.toString(),
|
||||
stackTrace: data.stack ? data.stack.toString() : undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <Fixed selectable={true}>{data.toString()}</Fixed>;
|
||||
}
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
return <JSONTree data={data} />
|
||||
}
|
||||
return <Fixed selectable={true}>{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 = ({
|
||||
logs,
|
||||
includeStackTrace,
|
||||
filter = [],
|
||||
}) => {
|
||||
// const ref = createRef();
|
||||
return (
|
||||
<ScrollView
|
||||
onContentSizeChange={(contentWidth, contentHeight)=>{
|
||||
/*if (ref.current) {
|
||||
ref.current.scrollView.scrollToEnd({animated: true});
|
||||
}*/
|
||||
}}
|
||||
>
|
||||
<Wrapper>
|
||||
{logs.filter(l => filter.includes(l.type)).map((log, i) => (
|
||||
<Row key={i}>
|
||||
<Emphasis color={getColor(log.type)}>
|
||||
{log.type}
|
||||
</Emphasis>
|
||||
<OutputList
|
||||
items={log.data}
|
||||
includeStackTrace={includeStackTrace}
|
||||
color={getColor(log.type)}
|
||||
/>
|
||||
</Row>
|
||||
))}
|
||||
</Wrapper>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Console;
|
||||
64
packages/lib/src/components/DevTool/Console/index.js
Normal file
64
packages/lib/src/components/DevTool/Console/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import useLog from '../../data/log';
|
||||
import Toolbar, {
|
||||
Button,
|
||||
Selector,
|
||||
Seperator,
|
||||
} from '../../base/Toolbar';
|
||||
import Output from './Output';
|
||||
import Input from './Input';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const initFilters = [
|
||||
'error',
|
||||
'warn',
|
||||
'info',
|
||||
'debug',
|
||||
].map(i => ({
|
||||
name: i,
|
||||
value: i,
|
||||
selected: true,
|
||||
}))
|
||||
|
||||
const Console = ({
|
||||
includeStackTrace,
|
||||
provider,
|
||||
context,
|
||||
}) => {
|
||||
const logs = useLog(provider);
|
||||
const [filters, setFilters] = useState(initFilters);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Toolbar>
|
||||
<Selector
|
||||
name="Filter"
|
||||
icon="filter"
|
||||
options={filters}
|
||||
multiSelect
|
||||
onSelect={(selected) => {
|
||||
setFilters([...selected]);
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
name="Clear"
|
||||
icon="trash"
|
||||
onPress={() => provider.clear()}
|
||||
/>
|
||||
</Toolbar>
|
||||
<Output filter={filters.filter(f => f.selected).map(f => f.name)} logs={logs} includeStackTrace={includeStackTrace} />
|
||||
<Input provider={provider} context={context} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Console;
|
||||
94
packages/lib/src/components/DevTool/Console/tools.js
Normal file
94
packages/lib/src/components/DevTool/Console/tools.js
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
|
||||
var DEFAULT_MAX_DEPTH = 3;
|
||||
var DEFAULT_ARRAY_MAX_LENGTH = 50;
|
||||
var seen; // Same variable used for all stringifications
|
||||
|
||||
Date.prototype.toPrunedJSON = Date.prototype.toJSON;
|
||||
String.prototype.toPrunedJSON = String.prototype.toJSON;
|
||||
|
||||
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
meta = { // table of character substitutions
|
||||
'\b': '\\b',
|
||||
'\t': '\\t',
|
||||
'\n': '\\n',
|
||||
'\f': '\\f',
|
||||
'\r': '\\r',
|
||||
'"' : '\\"',
|
||||
'\\': '\\\\'
|
||||
};
|
||||
|
||||
function quote(string) {
|
||||
escapable.lastIndex = 0;
|
||||
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
|
||||
var c = meta[a];
|
||||
return typeof c === 'string'
|
||||
? c
|
||||
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + '"' : '"' + string + '"';
|
||||
}
|
||||
|
||||
function str(key, holder, depthDecr, arrayMaxLength) {
|
||||
var i, // The loop counter.
|
||||
k, // The member key.
|
||||
v, // The member value.
|
||||
length,
|
||||
partial,
|
||||
value = holder[key];
|
||||
if (value && typeof value === 'object' && typeof value.toPrunedJSON === 'function') {
|
||||
value = value.toPrunedJSON(key);
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return quote(value);
|
||||
case 'number':
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
return String(value);
|
||||
case 'object':
|
||||
if (!value) {
|
||||
return 'null';
|
||||
}
|
||||
if (depthDecr<=0 || seen.indexOf(value)!==-1) {
|
||||
return '"-pruned-"';
|
||||
}
|
||||
seen.push(value);
|
||||
partial = [];
|
||||
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
||||
length = Math.min(value.length, arrayMaxLength);
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value, depthDecr-1, arrayMaxLength) || 'null';
|
||||
}
|
||||
v = partial.length === 0
|
||||
? '[]'
|
||||
: '[' + partial.join(',') + ']';
|
||||
return v;
|
||||
}
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
try {
|
||||
v = str(k, value, depthDecr-1, arrayMaxLength);
|
||||
if (v) partial.push(quote(k) + ':' + v);
|
||||
} catch (e) {
|
||||
// this try/catch due to some "Accessing selectionEnd on an input element that cannot have a selection." on Chrome
|
||||
}
|
||||
}
|
||||
}
|
||||
v = partial.length === 0
|
||||
? '{}'
|
||||
: '{' + partial.join(',') + '}';
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
export default function (value, depthDecr, arrayMaxLength) {
|
||||
seen = [];
|
||||
depthDecr = depthDecr || DEFAULT_MAX_DEPTH;
|
||||
arrayMaxLength = arrayMaxLength || DEFAULT_ARRAY_MAX_LENGTH;
|
||||
const raw = str('', {'': value}, depthDecr, arrayMaxLength);
|
||||
// return JSON.stringify(JSON.parse(raw.root), null, ' ');
|
||||
return raw;
|
||||
};
|
||||
77
packages/lib/src/components/DevTool/Modal.js
Normal file
77
packages/lib/src/components/DevTool/Modal.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
KeyboardAvoidingView,
|
||||
} from 'react-native';
|
||||
import events from '../../events';
|
||||
import DevTool from './index';
|
||||
import Modal from '../base/Modal';
|
||||
|
||||
|
||||
class Events extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
visible: false,
|
||||
};
|
||||
this.listen = this.listen.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
events.listen(this.listen);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
events.unlisten(this.listen);
|
||||
}
|
||||
|
||||
listen(type, data) {
|
||||
if (type === 'SHOW_DEVTOOLS') {
|
||||
return this.setState({
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'HIDE_DEVTOOLS') {
|
||||
return this.setState({
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
...others
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={false}
|
||||
visible={this.state.visible}
|
||||
onRequestClose={() => {
|
||||
}}
|
||||
>
|
||||
<SafeAreaView
|
||||
forceInset={{ top: 'always', vertical: 'always', bottom: 'always' }}
|
||||
style={{flex: 1}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={{flex: 1}}
|
||||
behavior="padding"
|
||||
enabled
|
||||
>
|
||||
<DevTool
|
||||
{...others}
|
||||
onClose={() => {
|
||||
events.publish('HIDE_DEVTOOLS');
|
||||
}}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Events;
|
||||
149
packages/lib/src/components/DevTool/Requests/Details.js
Normal file
149
packages/lib/src/components/DevTool/Requests/Details.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
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';
|
||||
|
||||
const theme = {
|
||||
scheme: 'bright',
|
||||
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 = (contentType, request) => {
|
||||
if (!contentType) {
|
||||
return null;
|
||||
}
|
||||
if (request.responseType == 'blob' || request.responseType == 'ArrayBuffer') {
|
||||
return <Emphasis>🤖 Binary</Emphasis>
|
||||
}
|
||||
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 selectable={true}>{request.responseText}</Fixed>;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
const Data = ({
|
||||
url,
|
||||
method,
|
||||
status,
|
||||
headers,
|
||||
requestHeaders,
|
||||
args = [],
|
||||
}) => {
|
||||
const headerInfo = Object.keys(headers).map(key => `${key}: ${headers[key]}`).join('\n');
|
||||
return (
|
||||
<ScrollView>
|
||||
<View>
|
||||
<Cell left="Status" right={status} />
|
||||
<Cell left="Method" right={method} />
|
||||
<Cell left="Url" right={url} />
|
||||
<CellHeader>Response Headers</CellHeader>
|
||||
<Indented>
|
||||
<Fixed>
|
||||
{requestHeaders}
|
||||
</Fixed>
|
||||
</Indented>
|
||||
{headerInfo.length > 0 && (
|
||||
<Fragment>
|
||||
<CellHeader>Request Headers</CellHeader>
|
||||
<Indented>
|
||||
<Fixed selectable={true}>
|
||||
{headerInfo}
|
||||
</Fixed>
|
||||
</Indented>
|
||||
</Fragment>
|
||||
)}
|
||||
{args[0] && (
|
||||
<Fragment>
|
||||
<CellHeader>Request Body</CellHeader>
|
||||
<Fixed selectable={true}>{args[0].toString()}</Fixed>
|
||||
</Fragment>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const Response = ({
|
||||
contentType,
|
||||
request,
|
||||
}) => (
|
||||
<ScrollView>
|
||||
<View>
|
||||
{getResponse(contentType, request)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
|
||||
const getPreview = (contentType, request, url) => {
|
||||
if (!contentType || request.responseType == 'blob' || request.responseType == 'ArrayBuffer' || !contentType) {
|
||||
return [];
|
||||
}
|
||||
const contentTypes = contentType.split(';').map(c => c.trim());
|
||||
|
||||
if (contentTypes.includes('text/html')) {
|
||||
return [{
|
||||
name: 'Preview',
|
||||
view: (
|
||||
<WebView
|
||||
source={{
|
||||
html: request.responseText,
|
||||
baseUrl: url,
|
||||
}}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
),
|
||||
}]
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const RequestDetails = (props) => (
|
||||
<Tab
|
||||
tabs={[{
|
||||
name: 'Details',
|
||||
view: <Data {...props} />
|
||||
}, {
|
||||
name: 'Response',
|
||||
view: <Response {...props} />
|
||||
}, ...getPreview(props.contentType, props.request, props.url)]}
|
||||
/>
|
||||
);
|
||||
|
||||
export default RequestDetails;
|
||||
54
packages/lib/src/components/DevTool/Requests/List.js
Normal file
54
packages/lib/src/components/DevTool/Requests/List.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import Status from '../../base/Status';
|
||||
import Row from '../../base/Row';
|
||||
import {
|
||||
Body,
|
||||
} from '../../base/text';
|
||||
|
||||
const ScrollView = styled.ScrollView`
|
||||
flex: 1;
|
||||
border-bottom-width: 1px;
|
||||
border-color: #ccc;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.View`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const TouchableOpacity = styled.TouchableOpacity`
|
||||
`;
|
||||
|
||||
const RequestDetails = ({
|
||||
requests,
|
||||
onSelect,
|
||||
selected,
|
||||
}) => (
|
||||
<ScrollView>
|
||||
<Wrapper>
|
||||
{requests.map(({
|
||||
id,
|
||||
status,
|
||||
method,
|
||||
url,
|
||||
}, i) => (
|
||||
<TouchableOpacity
|
||||
key={i}
|
||||
onPress={() => onSelect(i)}
|
||||
>
|
||||
<Row
|
||||
selected={selected === id}
|
||||
left={<Body>{method}</Body>}
|
||||
right={(
|
||||
<Status code={status} />
|
||||
)}
|
||||
>
|
||||
<Body>{url}</Body>
|
||||
</Row>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</Wrapper>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
export default RequestDetails;
|
||||
55
packages/lib/src/components/DevTool/Requests/index.js
Normal file
55
packages/lib/src/components/DevTool/Requests/index.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import network from '../../../network';
|
||||
import useRequests from '../../data/requests';
|
||||
import Toolbar, {
|
||||
Button,
|
||||
Seperator,
|
||||
} from '../../base/Toolbar';
|
||||
import Details from './Details';
|
||||
import List from './List';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
borderColor: '#ccc',
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
details: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
let i = 0;
|
||||
const Console = ({
|
||||
provider,
|
||||
}) => {
|
||||
const requests = useRequests(provider);
|
||||
const [active, setActive] = useState();
|
||||
const selected = active >= 0 ? requests[active] : undefined;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Toolbar>
|
||||
<Seperator />
|
||||
<Button
|
||||
name="Clear"
|
||||
icon="trash"
|
||||
onPress={() => network.clear()}
|
||||
/>
|
||||
</Toolbar>
|
||||
<List
|
||||
selected={selected ? selected.id : undefined}
|
||||
requests={requests}
|
||||
onSelect={(i) => setActive(i)}
|
||||
/>
|
||||
{selected && <Details {...selected} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Console;
|
||||
41
packages/lib/src/components/DevTool/Storage/Keys.js
Normal file
41
packages/lib/src/components/DevTool/Storage/Keys.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import Row from '../../base/Row';
|
||||
import {
|
||||
Body,
|
||||
} from '../../base/text';
|
||||
|
||||
const Scroll = styled.ScrollView`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.View`
|
||||
`;
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
`;
|
||||
|
||||
const Keys = ({
|
||||
keys,
|
||||
selected,
|
||||
onSelect,
|
||||
}) => (
|
||||
<Scroll>
|
||||
<Wrapper>
|
||||
{keys.map(key => (
|
||||
<Button
|
||||
key={key || '[unknown]'}
|
||||
onPress={() => onSelect(key)}
|
||||
>
|
||||
<Row
|
||||
selected={selected === key}
|
||||
>
|
||||
<Body>{key}</Body>
|
||||
</Row>
|
||||
</Button>
|
||||
))}
|
||||
</Wrapper>
|
||||
</Scroll>
|
||||
)
|
||||
|
||||
export default Keys;
|
||||
27
packages/lib/src/components/DevTool/Storage/Value.js
Normal file
27
packages/lib/src/components/DevTool/Storage/Value.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import {
|
||||
Fixed,
|
||||
} from '../../base/text';
|
||||
|
||||
const Scroll = styled.ScrollView`
|
||||
flex: 1;
|
||||
border-top-width: 1px;
|
||||
border-color: #ccc;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.View`
|
||||
padding: 8px 16px;
|
||||
`;
|
||||
|
||||
const Value = ({
|
||||
value,
|
||||
}) => (
|
||||
<Scroll>
|
||||
<Wrapper>
|
||||
<Fixed selectable={true}>{value}</Fixed>
|
||||
</Wrapper>
|
||||
</Scroll>
|
||||
)
|
||||
|
||||
export default Value;
|
||||
60
packages/lib/src/components/DevTool/Storage/index.js
Normal file
60
packages/lib/src/components/DevTool/Storage/index.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import useStorage from '../../data/storage';
|
||||
import Toolbar, {
|
||||
Button,
|
||||
Seperator,
|
||||
} from '../../base/Toolbar';
|
||||
import Keys from './Keys';
|
||||
import Value from './Value';
|
||||
|
||||
const Wrapper = styled.View`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const StorageView = ({
|
||||
provider,
|
||||
}) => {
|
||||
const [selected, setSelected] = useState();
|
||||
const {
|
||||
data,
|
||||
update,
|
||||
removeItem,
|
||||
clear,
|
||||
} = useStorage(provider);
|
||||
return (
|
||||
<Wrapper>
|
||||
<Toolbar>
|
||||
<Seperator />
|
||||
<Button
|
||||
name="Refresh"
|
||||
icon="reload"
|
||||
onPress={update}
|
||||
/>
|
||||
<Button
|
||||
name="Clear"
|
||||
icon="trash"
|
||||
onPress={clear}
|
||||
/>
|
||||
<Button
|
||||
name="Delete"
|
||||
icon="remove"
|
||||
disabled={!selected}
|
||||
onPress={() => removeItem(selected)}
|
||||
/>
|
||||
</Toolbar>
|
||||
<Keys
|
||||
selected={selected}
|
||||
onSelect={(key) => {
|
||||
setSelected(key);
|
||||
}}
|
||||
keys={Object.keys(data)}
|
||||
/>
|
||||
{selected && data[selected] && (
|
||||
<Value value={data[selected]} />
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default StorageView;
|
||||
118
packages/lib/src/components/DevTool/Tab.js
Normal file
118
packages/lib/src/components/DevTool/Tab.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from '../base/Icon';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
},
|
||||
tabInactive: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
tabActive: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
borderBottomWidth: 4,
|
||||
borderColor: '#2980b9',
|
||||
},
|
||||
});
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
padding: 10px 20px 10px 0;
|
||||
`;
|
||||
|
||||
const TabScroll = styled.ScrollView`
|
||||
flex: 1;
|
||||
border-right-width: 1px;
|
||||
border-color: #ccc;
|
||||
margin-right: 20px;
|
||||
`;
|
||||
|
||||
const TabWrapper = styled.View`
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Header = styled.View`
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Tab = styled.TouchableOpacity`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 20px;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const Console = ({
|
||||
tabs,
|
||||
onClose,
|
||||
onDownload,
|
||||
}) => {
|
||||
const [active, setActive] = useState(0);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Header>
|
||||
<TabScroll horizontal>
|
||||
<TabWrapper>
|
||||
{tabs.map(({ name }, i) => (
|
||||
<Tab
|
||||
key={name}
|
||||
style={active === i ? styles.tabActive : styles.tabInactive}
|
||||
onPress={() => {
|
||||
setActive(i);
|
||||
}}
|
||||
>
|
||||
<Text>{name}</Text>
|
||||
</Tab>
|
||||
))}
|
||||
</TabWrapper>
|
||||
</TabScroll>
|
||||
{onDownload && (
|
||||
<Button
|
||||
onPress={onDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
)}
|
||||
{onClose && (
|
||||
<Button
|
||||
onPress={onClose}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
)}
|
||||
</Header>
|
||||
{tabs[active] && tabs[active].view}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Console.propTypes = {
|
||||
tabs: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
Console.defaultProps = {
|
||||
tabs: [],
|
||||
};
|
||||
|
||||
export default Console;
|
||||
93
packages/lib/src/components/DevTool/index.js
Normal file
93
packages/lib/src/components/DevTool/index.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Alert,
|
||||
Clipboard,
|
||||
} from 'react-native';
|
||||
import Tab from './Tab';
|
||||
import Console from './Console';
|
||||
import Requests from './Requests';
|
||||
import Storage from './Storage';
|
||||
import log from '../../log';
|
||||
import network from '../../network';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const DevTool = ({
|
||||
style,
|
||||
includeStackTrace,
|
||||
onClose,
|
||||
logProvider = log,
|
||||
storageProvider,
|
||||
context = {},
|
||||
requestProvider = network,
|
||||
additionalTools = [],
|
||||
}) => {
|
||||
const views = [];
|
||||
|
||||
if (logProvider) {
|
||||
views.push({
|
||||
name: 'Console',
|
||||
view: (
|
||||
<Console
|
||||
includeStackTrace={includeStackTrace}
|
||||
provider={logProvider}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
getData: log.get,
|
||||
});
|
||||
}
|
||||
|
||||
if (requestProvider) {
|
||||
views.push({
|
||||
name: 'Network',
|
||||
view: <Requests provider={requestProvider} />,
|
||||
getData: network.get,
|
||||
});
|
||||
}
|
||||
|
||||
if (storageProvider) {
|
||||
views.push({
|
||||
name: 'Storage',
|
||||
view: <Storage provider={storageProvider} />,
|
||||
});
|
||||
}
|
||||
|
||||
additionalTools.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;
|
||||
Reference in New Issue
Block a user