This commit is contained in:
2019-08-23 00:12:49 +02:00
committed by GitHub
parent c30e9f27a5
commit fd3a2bc24f
94 changed files with 13855 additions and 9269 deletions

View 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;

View 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;

View 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;

View 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;
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;