Rework js-app with func. components (#1651)

This commit is contained in:
Jaco Greeff 2019-09-24 13:33:49 +02:00 committed by GitHub
parent 84a46ef76f
commit f33b4b7f91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 525 additions and 611 deletions

View File

@ -149,7 +149,18 @@ class Create extends React.PureComponent<Props, State> {
public render (): React.ReactNode {
const { className, t } = this.props;
const { address, isValid, showWarning } = this.state;
const { address, deriveError, derivePath, isNameValid, isPassValid, isSeedValid, isValid, name, pairType, password, seed, seedOptions, seedType, showWarning } = this.state;
const isDevSeed = seedType === 'dev';
const seedLabel = ((): string => {
switch (seedType) {
case 'bip':
return t('mnemonic seed');
case 'dev':
return t('development seed');
default:
return t('seed (hex or string)');
}
})();
return (
<Modal
@ -166,7 +177,75 @@ class Create extends React.PureComponent<Props, State> {
onHideWarning={this.onHideWarning}
/>
)}
{this.renderInput()}
<Modal.Content>
<AddressRow
defaultName={name}
value={isSeedValid ? address : ''}
>
<Input
autoFocus
className='full'
help={t('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
isError={!isNameValid}
label={t('name')}
onChange={this.onChangeName}
onEnter={this.onCommit}
value={name}
/>
<Input
className='full'
help={t('The private key for your account is derived from this seed. This seed must be kept secret as anyone in its possession has access to the funds of this account. If you validate, use the seed of the session account as the "--key" parameter of your node.')}
isAction
isError={!isSeedValid}
isReadOnly={isDevSeed}
label={seedLabel}
onChange={this.onChangeSeed}
onEnter={this.onCommit}
value={seed}
>
<Dropdown
isButton
defaultValue={seedType}
onChange={this.selectSeedType}
options={seedOptions}
/>
</Input>
<Password
className='full'
help={t('This password is used to encrypt your private key. It must be strong and unique! You will need it to sign transactions with this account. You can recover this account using this password together with the backup file (generated in the next step).')}
isError={!isPassValid}
label={t('password')}
onChange={this.onChangePass}
onEnter={this.onCommit}
value={password}
/>
<details
className='accounts--Creator-advanced'
open
>
<summary>{t('Advanced creation options')}</summary>
<Dropdown
defaultValue={pairType}
help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
label={t('keypair crypto type')}
onChange={this.onChangePairType}
options={uiSettings.availableCryptos}
/>
<Input
className='full'
help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`.')}
isError={!!deriveError}
label={t('secret derivation path')}
onChange={this.onChangeDerive}
onEnter={this.onCommit}
value={derivePath}
/>
{deriveError && (
<Labelled label=''><article className='error'>{deriveError}</article></Labelled>
)}
</details>
</AddressRow>
</Modal.Content>
<Modal.Actions>
<Button.Group>
<Button
@ -189,94 +268,6 @@ class Create extends React.PureComponent<Props, State> {
);
}
private renderInput (): React.ReactNode {
const { t } = this.props;
const { address, deriveError, derivePath, isNameValid, isPassValid, isSeedValid, name, pairType, password, seed, seedOptions, seedType } = this.state;
const isDevSeed = seedType === 'dev';
const seedLabel = ((): string => {
switch (seedType) {
case 'bip':
return t('mnemonic seed');
case 'dev':
return t('development seed');
default:
return t('seed (hex or string)');
}
})();
return (
<Modal.Content>
<AddressRow
defaultName={name}
value={isSeedValid ? address : ''}
>
<Input
autoFocus
className='full'
help={t('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
isError={!isNameValid}
label={t('name')}
onChange={this.onChangeName}
onEnter={this.onCommit}
value={name}
/>
<Input
className='full'
help={t('The private key for your account is derived from this seed. This seed must be kept secret as anyone in its possession has access to the funds of this account. If you validate, use the seed of the session account as the "--key" parameter of your node.')}
isAction
isError={!isSeedValid}
isReadOnly={isDevSeed}
label={seedLabel}
onChange={this.onChangeSeed}
onEnter={this.onCommit}
value={seed}
>
<Dropdown
isButton
defaultValue={seedType}
onChange={this.selectSeedType}
options={seedOptions}
/>
</Input>
<Password
className='full'
help={t('This password is used to encrypt your private key. It must be strong and unique! You will need it to sign transactions with this account. You can recover this account using this password together with the backup file (generated in the next step).')}
isError={!isPassValid}
label={t('password')}
onChange={this.onChangePass}
onEnter={this.onCommit}
value={password}
/>
<details
className='accounts--Creator-advanced'
open
>
<summary>{t('Advanced creation options')}</summary>
<Dropdown
defaultValue={pairType}
help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
label={t('keypair crypto type')}
onChange={this.onChangePairType}
options={uiSettings.availableCryptos}
/>
<Input
className='full'
help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`.')}
isError={!!deriveError}
label={t('secret derivation path')}
onChange={this.onChangeDerive}
onEnter={this.onCommit}
value={derivePath}
/>
{deriveError && (
<Labelled label=''><article className='error'>{deriveError}</article></Labelled>
)}
</details>
</AddressRow>
</Modal.Content>
);
}
private nextState (newState: Partial<State>): void {
this.setState(
(prevState: State): State => {
@ -389,10 +380,9 @@ class Create extends React.PureComponent<Props, State> {
return;
}
this.setState(({ derivePath, pairType }: State): State => ({
...(generateSeed(null, derivePath, seedType, pairType) as State),
seedType
}));
this.setState(({ derivePath, pairType }: State): State =>
(generateSeed(null, derivePath, seedType, pairType) as State)
);
}
}

View File

@ -13,7 +13,6 @@ import translate from './translate';
interface Props extends BareProps, I18nProps {
isCustomExample: boolean;
isRunning: boolean;
generateLink: () => void;
removeSnippet: () => void;
runJs: () => void;
@ -22,7 +21,8 @@ interface Props extends BareProps, I18nProps {
stopJs: () => void;
}
function ActionButtons ({ className, generateLink, isCustomExample, isRunning, removeSnippet, runJs, saveSnippet, stopJs, t }: Props): React.ReactElement<Props> {
function ActionButtons ({ className, generateLink, isCustomExample, removeSnippet, runJs, saveSnippet, stopJs, t }: Props): React.ReactElement<Props> {
const [isRunning, setIsRunning] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [shareText, setShareText] = useState(t('Generate link to share code example'));
const [snippetName, setSnippetName] = useState('');
@ -48,6 +48,14 @@ function ActionButtons ({ className, generateLink, isCustomExample, isRunning, r
saveSnippet(snippetName);
_onPopupClose();
};
const _runJs = (): void => {
setIsRunning(true);
runJs();
};
const _stopJs = (): void => {
stopJs();
setIsRunning(false);
};
return (
<div className={`${className} action-button`}>
@ -115,20 +123,25 @@ function ActionButtons ({ className, generateLink, isCustomExample, isRunning, r
/>
</Popup>
)}
<Button
className='play-button'
icon='play'
isCircular
isPrimary
onClick={runJs}
/>
<Button
icon='close'
isCircular
isDisabled={!isRunning}
isNegative
onClick={stopJs}
/>
{isRunning
? (
<Button
icon='close'
isCircular
isNegative
onClick={_stopJs}
/>
)
: (
<Button
className='play-button'
icon='play'
isCircular
isPrimary
onClick={_runJs}
/>
)
}
</div>
);
}

View File

@ -6,6 +6,7 @@ import { BareProps } from '@polkadot/react-components/types';
import { Log } from './types';
import React from 'react';
import styled from 'styled-components';
import { isError, isNull, isUndefined } from '@polkadot/util';
interface Props extends BareProps {
@ -35,9 +36,9 @@ const renderEntry = ({ args, type }: Log, index: number): React.ReactNode => (
</div>
);
export default function Output ({ children, logs }: Props): React.ReactElement<Props> {
function Output ({ children, className, logs }: Props): React.ReactElement<Props> {
return (
<article className='container js--Output'>
<article className={`container ${className}`}>
<div className='logs-wrapper'>
<div className='logs-container'>
<pre className='logs-content'>
@ -49,3 +50,43 @@ export default function Output ({ children, logs }: Props): React.ReactElement<P
</article>
);
}
export default styled(Output)`
background-color: #4e4e4e;
color: #ffffff;
display: flex;
flex-direction: column;
flex-grow: 1;
font-family: monospace;
font-size: 12px;
font-variant-ligatures: common-ligatures;
line-height: 18px;
padding: 50px 10px 10px;
position: relative;
width: 40%;
.logs-wrapper {
display: flex;
flex: 1;
min-height: 0px;
}
.logs-container {
flex: 1;
overflow: auto;;
}
.logs-content {
height: auto;
}
.js--Log {
animation: fadein 0.2s;
margin: 0 0 5px 0;
word-break: break-all;
&.error {
color: #f88;
}
}
`;

View File

@ -4,24 +4,23 @@
import { ApiPromise } from '@polkadot/api';
import { KeyringInstance } from '@polkadot/keyring/types';
import { ApiProps } from '@polkadot/react-api/types';
import { AppProps, I18nProps } from '@polkadot/react-components/types';
import { Log, LogType, Snippet } from './types';
import React from 'react';
import React, { useContext, useRef, useState, useEffect } from 'react';
import { withRouter, RouteComponentProps } from 'react-router';
import { Transition } from 'semantic-ui-react';
import snappy from 'snappyjs';
import { withApi, withMulti } from '@polkadot/react-api';
import styled from 'styled-components';
import { ApiContext } from '@polkadot/react-api';
import { Button, Dropdown, Editor } from '@polkadot/react-components';
import uiKeyring from '@polkadot/ui-keyring';
import * as types from '@polkadot/types';
import * as util from '@polkadot/util';
import * as hashing from '@polkadot/util-crypto';
import './index.css';
import makeWrapper from './snippets/wrapping';
import snippets from './snippets';
import allSnippets from './snippets';
import translate from './translate';
import { STORE_EXAMPLES, STORE_SELECTED, CUSTOM_LABEL } from './constants';
@ -42,7 +41,7 @@ interface Injected {
window: null;
}
interface Props extends ApiProps, AppProps, I18nProps, RouteComponentProps<{}> {
interface Props extends AppProps, I18nProps, RouteComponentProps<{}> {
match: {
isExact: boolean;
params: {
@ -55,309 +54,57 @@ interface Props extends ApiProps, AppProps, I18nProps, RouteComponentProps<{}> {
history: any;
}
interface State {
animated: boolean;
customExamples: Snippet[];
isCustomExample: boolean;
isRunning: boolean;
logs: Log[];
options: Snippet[];
sharedExample?: Snippet;
selected: Snippet;
const snippets: Snippet[] = JSON.parse(JSON.stringify(allSnippets));
function decodeBase64 (base64: string): Snippet {
const sharedExample: Snippet = {
code: '',
label: { basic: true, children: 'URL', size: 'tiny' },
text: 'Shared code example (unsaved)',
type: 'shared',
value: `custom-${Date.now()}`
};
try {
const compStr = atob(base64);
const compU8a = new Uint8Array(compStr.length);
compU8a.forEach((_, i): void => {
compU8a[i] = compStr.charCodeAt(i);
});
const u8a = snappy.uncompress(compU8a);
const code = util.u8aToString(u8a);
sharedExample.code = code;
} catch (error) {
const errorMessage = 'ERROR: Unable to decode code example from URL';
console.error(`${errorMessage}: \n${error}`);
sharedExample.code = `// ${errorMessage}`;
}
return sharedExample;
}
class Playground extends React.PureComponent<Props, State> {
private injected: Injected | null = null;
// FIXME This... ladies & gentlemen, is a mess that should be untangled
function Playground ({ className, history, match: { params: { base64 } }, t }: Props): React.ReactElement<Props> {
const { api, isDevelopment } = useContext(ApiContext);
const injectedRef = useRef<Injected | null>(null);
const [isAnimated, setIsAnimated] = useState(true);
const [isCustomExample, setIsCustomExample] = useState(false);
const [customExamples, setCustomExamples] = useState<Snippet[]>([]);
const [logs, setLogs] = useState<Log[]>([]);
const [options, setOptions] = useState<Snippet[]>([]);
const [selected, setSelected] = useState(snippets[0]);
const [sharedExample, setSharedExample] = useState<Snippet | undefined>();
private snippets: Snippet[] = JSON.parse(JSON.stringify(snippets));
public constructor (props: Props) {
super(props);
this.snippets.forEach((snippet): void => {
snippet.code = `${makeWrapper(this.props.isDevelopment)}${snippet.code}`;
});
this.state = {
animated: true,
customExamples: [],
isCustomExample: false,
isRunning: false,
logs: [],
options: [],
selected: this.snippets[0],
sharedExample: undefined
};
}
// FIXME: Semantic UI is still using Reacts old lifecycle methods that are considered as unsafe (eg. componentWillMount).
// There's a ticket and an ongoing process of updating SUI to the new lifecycle methods (here: componentDidMount).
// Please check https://github.com/Semantic-Org/Semantic-UI-React/issues/2732 for details
// This needs to change to componentDidMount() as soon as the original MUI component got updated
public UNSAFE_componentWillMount (): void {
const { match: { params: { base64 } } } = this.props;
const sharedExample = base64 ? this.decodeBase64(base64) : undefined;
const localData = {
examples: localStorage.getItem(STORE_EXAMPLES),
selectedValue: localStorage.getItem(STORE_SELECTED)
};
const customExamples = localData.examples ? JSON.parse(localData.examples) : [];
const options: Snippet[] = sharedExample
? [sharedExample, ...customExamples, ...this.snippets]
: [...customExamples, ...this.snippets];
const selected = options.find((option): boolean => option.value === localData.selectedValue);
this.setState({
customExamples,
isCustomExample: (selected && selected.type === 'custom') || false,
options,
selected: sharedExample || selected || this.snippets[0],
sharedExample
});
}
public render (): React.ReactNode {
const { t } = this.props;
const { animated, isCustomExample, isRunning, logs, options, selected } = this.state;
const snippetName = selected.type === 'custom' ? selected.text : undefined;
return (
<main className='js--App'>
<header className='container'>
<Dropdown
className='js--Dropdown'
defaultValue={selected.value}
onChange={this.selectExample}
options={options}
label={t('Select example')}
/>
</header>
<section className='js--Content'>
<Transition animation='glow' duration={700} visible={animated}>
<article className='container js--Editor'>
<ActionButtons
generateLink={this.generateLink}
isCustomExample={isCustomExample}
isRunning={isRunning}
removeSnippet={this.removeSnippet}
runJs={this.runJs}
saveSnippet={this.saveSnippet}
snippetName={snippetName}
stopJs={this.stopJs}
/>
<Editor
code={selected.code}
onEdit={this.onEdit}
/>
</article>
</Transition>
<Output logs={logs} >
<Button
className='action-button'
icon='erase'
isCircular
isNegative
onClick={this.clearConsole}
/>
</Output>
</section>
</main>
);
}
private runJs = async (): Promise<void> => {
const { api, isDevelopment } = this.props;
const { code } = this.state.selected;
this.stopJs();
this.clearConsole();
this.injected = {
api: api.clone(),
console: {
error: this.hookConsole('error'),
log: this.hookConsole('log')
},
global: null,
hashing,
keyring: isDevelopment
? uiKeyring.keyring
: null,
types,
util,
window: null
};
await this.injected.api.isReady;
// squash into a single line so exceptions (with linenumbers) maps to the same line/origin
// as we have in the editor view (TODO: Make the console.error here actually return the full stack)
const exec = `(async ({${Object.keys(this.injected).join(',')}}) => { try { ${code} \n } catch (error) { console.error(error); } })(injected);`;
// eslint-disable-next-line no-new-func
new Function('injected', exec)(this.injected);
this.setState({ isRunning: true });
}
private stopJs = (): void => {
if (!this.injected) {
return;
}
this.injected.api.disconnect();
this.injected = null;
this.setState({ isRunning: false });
}
private selectExample = (value: string): void => {
if (value.length) {
const { options } = this.state;
const option = options.find((option): boolean => option.value === value);
if (option) {
localStorage.setItem(STORE_SELECTED, value);
this.setState({
logs: [],
isCustomExample: option.type === 'custom',
selected: option
});
}
}
}
private saveSnippet = (snippetName: string): void => {
const { customExamples, selected: { code, type }, sharedExample } = this.state;
// The <Dropdown> component doesn't take boolean custom props and no
// camelCase keys, that's why 'custom' is passed as a string here
const snapshot: Snippet = {
code,
label: CUSTOM_LABEL,
text: snippetName,
type: 'custom',
value: `custom-${Date.now()}`
};
const nextOptions = [snapshot, ...customExamples, ...this.snippets];
const options = (type === 'shared')
? nextOptions
: sharedExample
? [sharedExample, ...nextOptions]
: nextOptions;
localStorage.setItem(STORE_EXAMPLES, JSON.stringify([snapshot, ...customExamples]));
this.setState((prevState: State): Pick<State, never> => ({
...prevState,
customExamples: [snapshot, ...prevState.customExamples],
isCustomExample: true,
options,
selected: snapshot,
sharedExample: type === 'shared' ? undefined : prevState.sharedExample
}));
}
private removeSnippet = (): void => {
const { customExamples, selected } = this.state;
const filtered = customExamples.filter((value): boolean => value.value !== selected.value);
const nextOptions = [...filtered, ...this.snippets];
this.setState((prevState: State): Pick<State, never> => ({
...prevState,
customExamples: filtered,
isCustomExample: nextOptions[0].type === 'custom' || false,
options: prevState.sharedExample ? [prevState.sharedExample, ...nextOptions] : nextOptions
}));
this.selectExample(nextOptions[0].value);
localStorage.setItem(STORE_EXAMPLES, JSON.stringify(filtered));
}
private onEdit = (code: string): void => {
if (code !== this.state.selected.code) {
this.setState((prevState: State): Pick<State, never> => ({
selected: { ...prevState.selected, code },
isCustomExample: false
}));
}
}
private clearConsole = (): void => {
this.setState({ logs: [] });
}
private hookConsole = (type: LogType): (...args: any[]) => void => {
return (...args: any[]): void => {
this.setState(({ logs }: State): Pick<State, never> => {
logs.push({ args, type });
return { logs: logs.slice(0) };
});
};
}
private decodeBase64 = (base64: string): Snippet => {
const sharedExample: Snippet = {
code: '',
label: { basic: true, children: 'URL', size: 'tiny' },
text: 'Shared code example (unsaved)',
type: 'shared',
value: `custom-${Date.now()}`
};
try {
const compStr = atob(base64);
const compU8a = new Uint8Array(compStr.length);
compU8a.forEach((_, i): void => {
compU8a[i] = compStr.charCodeAt(i);
});
const u8a = snappy.uncompress(compU8a);
const code = util.u8aToString(u8a);
sharedExample.code = code;
} catch (error) {
const errorMessage = this.props.t('ERROR: Unable to decode code example from URL');
console.error(`${errorMessage}: \n${error}`);
sharedExample.code = `// ${errorMessage}`;
}
return sharedExample;
}
private generateLink = (): void => {
const {
props: { history, match: { params: { base64 } } },
state: { selected: { code } }
} = this;
const u8a = util.stringToU8a(code);
const compU8a = snappy.compress(u8a);
const compStr = compU8a.reduce((str: string, ch: number): string => {
return str + String.fromCharCode(ch);
}, '');
const base64code = btoa(compStr);
const path = `/js/share/${base64code}`;
if (base64code !== base64) {
history.push(path);
}
const basePath = window.location.pathname.replace('/', '').length > 0
? `${window.location.origin}/${window.location.pathname.replace('/', '')}`
: `${window.location.origin}`;
this.copyToClipboard(`${basePath}/#${path}`);
}
private copyToClipboard = (link: string): void => {
const _clearConsole = (): void => setLogs([]);
const _hookConsole = (type: LogType, args: any[]): void => {
logs.push({ args, type });
setLogs(logs.slice(0));
};
const _copyToClipboard = (link: string): void => {
// See https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
const el = document.createElement('textarea');
@ -381,12 +128,294 @@ class Playground extends React.PureComponent<Props, State> {
existingSelection.addRange(selected);
}
this.setState({ animated: !this.state.animated });
}
setIsAnimated(!isAnimated);
};
const _stopJs = (): void => {
if (injectedRef.current) {
injectedRef.current.api.disconnect();
injectedRef.current = null;
}
};
const _runJs = async (): Promise<void> => {
_stopJs();
_clearConsole();
injectedRef.current = {
api: api.clone(),
console: {
error: (...args: any[]): void => _hookConsole('error', args),
log: (...args: any[]): void => _hookConsole('log', args)
},
global: null,
hashing,
keyring: isDevelopment
? uiKeyring.keyring
: null,
types,
util,
window: null
};
await injectedRef.current.api.isReady;
// squash into a single line so exceptions (with linenumbers) maps to the same line/origin
// as we have in the editor view (TODO: Make the console.error here actually return the full stack)
const exec = `(async ({${Object.keys(injectedRef.current).join(',')}}) => { try { ${selected.code} \n } catch (error) { console.error(error); } })(injected);`;
// eslint-disable-next-line no-new-func
new Function('injected', exec)(injectedRef.current);
};
const _selectExample = (value: string): void => {
if (value.length) {
const option = options.find((option): boolean => option.value === value);
if (option) {
localStorage.setItem(STORE_SELECTED, value);
setLogs([]);
setIsCustomExample(option.type === 'custom');
setSelected(option);
}
}
};
const _generateLink = (): void => {
const u8a = util.stringToU8a(selected.code);
const compU8a = snappy.compress(u8a);
const compStr = compU8a.reduce((str: string, ch: number): string => {
return str + String.fromCharCode(ch);
}, '');
const base64code = btoa(compStr);
const path = `/js/share/${base64code}`;
if (base64code !== base64) {
history.push(path);
}
const basePath = window.location.pathname.replace('/', '').length > 0
? `${window.location.origin}/${window.location.pathname.replace('/', '')}`
: `${window.location.origin}`;
_copyToClipboard(`${basePath}/#${path}`);
};
const _onEdit = (code: string): void => {
if (code !== selected.code) {
setIsCustomExample(false);
setSelected({ ...selected, code });
}
};
const _removeSnippet = (): void => {
const filtered = customExamples.filter((value): boolean => value.value !== selected.value);
const nextOptions = [...filtered, ...snippets];
setCustomExamples(filtered);
setIsCustomExample(nextOptions[0].type === 'custom');
setOptions(sharedExample ? [sharedExample, ...nextOptions] : nextOptions);
_selectExample(nextOptions[0].value);
localStorage.setItem(STORE_EXAMPLES, JSON.stringify(filtered));
};
const _saveSnippet = (snippetName: string): void => {
// The <Dropdown> component doesn't take boolean custom props and no
// camelCase keys, that's why 'custom' is passed as a string here
const snapshot: Snippet = {
code: selected.code,
label: CUSTOM_LABEL,
text: snippetName,
type: 'custom',
value: `custom-${Date.now()}`
};
const nextOptions = [snapshot, ...customExamples, ...snippets];
const options = selected.type === 'shared'
? nextOptions
: sharedExample
? [sharedExample, ...nextOptions]
: nextOptions;
localStorage.setItem(STORE_EXAMPLES, JSON.stringify([snapshot, ...customExamples]));
setCustomExamples([snapshot, ...customExamples]);
setIsCustomExample(true);
setOptions(options);
setSelected(snapshot);
setSharedExample(selected.type === 'shared' ? undefined : sharedExample);
};
useEffect((): void => {
snippets.forEach((snippet): void => {
snippet.code = `${makeWrapper(isDevelopment)}${snippet.code}`;
});
const sharedExample = base64 ? decodeBase64(base64) : undefined;
const localData = {
examples: localStorage.getItem(STORE_EXAMPLES),
selectedValue: localStorage.getItem(STORE_SELECTED)
};
const customExamples = localData.examples ? JSON.parse(localData.examples) : [];
const options: Snippet[] = sharedExample
? [sharedExample, ...customExamples, ...snippets]
: [...customExamples, ...snippets];
const selected = options.find((option): boolean => option.value === localData.selectedValue);
setCustomExamples(customExamples);
setIsCustomExample((selected && selected.type === 'custom') || false);
setOptions(options);
setSelected(sharedExample || selected || snippets[0]);
setSharedExample(sharedExample);
}, []);
const snippetName = selected.type === 'custom' ? selected.text : undefined;
return (
<main className={`js--App ${className}`}>
<header className='container'>
<Dropdown
className='js--Dropdown'
defaultValue={selected.value}
onChange={_selectExample}
options={options}
label={t('Select example')}
/>
</header>
<section className='js--Content'>
<Transition
animation='glow'
duration={700}
visible={isAnimated}
>
<article className='container js--Editor'>
<ActionButtons
generateLink={_generateLink}
isCustomExample={isCustomExample}
removeSnippet={_removeSnippet}
runJs={_runJs}
saveSnippet={_saveSnippet}
snippetName={snippetName}
stopJs={_stopJs}
/>
<Editor
code={selected.code}
onEdit={_onEdit}
/>
</article>
</Transition>
<Output
className='js--Output'
logs={logs}
>
<Button
className='action-button'
icon='erase'
isCircular
isNegative
onClick={_clearConsole}
/>
</Output>
</section>
</main>
);
}
export default withMulti(
withRouter(Playground),
translate,
withApi
const Routed = withRouter(Playground);
export default translate(
styled(Routed)`
display: flex;
flex-direction: column;
height: 100vh;
padding: 1rem 0 0;
.js--Content {
align-content: stretch;
align-items: stretch;
display: flex;
height: 100%;
justify-content: space-between;
margin-bottom: 0;
}
.js--Dropdown {
margin-right: 100px;
position: relative;
z-index: 200;
.dropdown .menu > .item {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
}
.js--Editor,
.js--Output {
min-width: 200px;
.action-button {
position: absolute;
right: 0.25rem;
top: 0.5rem;
z-index: 100;
}
}
.js--Editor {
flex-grow: 1;
overflow: auto;
padding: 0;
position: relative;
resize: horizontal;
width: 60%;
textarea {
outline: 0;
}
.codeflask {
background: transparent;
}
.codeflask--has-line-numbers {
z-index: 0;
}
.codeflask--has-line-numbers .codeflask__flatten {
font-size: 12px;
line-height: 18px;
min-width: calc(100% - 40px);
padding-top: 50px;
width: auto;
}
.codeflask__lines {
background: #fafafa;
line-height: 18px;
padding-top: 50px;
z-index: 100;
}
&::after {
bottom: 0;
content: '↔';
cursor: col-resize;
font-size: 20px;
height: 20px;
line-height: 18px;
position: absolute;
right: 0;
width: 22px;
z-index: 1;
}
}
.ui.popup.popup-local {
display: flex;
flex: 1 1 100%;
max-width: 300px;
.button {
margin: 0;
}
}
`
);

View File

@ -1,142 +0,0 @@
/* Copyright 2017-2019 @polkadot/app-js authors & contributors
/* This software may be modified and distributed under the terms
/* of the Apache-2.0 license. See the LICENSE file for details. */
.js--App {
display: flex;
flex-direction: column;
height: 100vh;
padding: 1rem 0 0;
}
.js--Dropdown {
margin-right: 100px;
position: relative;
z-index: 200;
.dropdown .menu > .item {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
}
.js--Content {
align-content: stretch;
align-items: stretch;
display: flex;
height: 100%;
justify-content: space-between;
margin-bottom: 0;
}
.js--Editor,
.js--Output {
min-width: 200px;
.action-button {
position: absolute;
right: 0.25rem;
top: 0.5rem;
z-index: 100;
}
}
.js--Editor {
flex-grow: 1;
overflow: auto;
padding: 0;
position: relative;
resize: horizontal;
width: 60%;
textarea {
outline: 0;
}
.codeflask {
background: transparent;
}
.codeflask--has-line-numbers {
z-index: 0;
}
.codeflask--has-line-numbers .codeflask__flatten {
font-size: 12px;
line-height: 18px;
min-width: calc(100% - 40px);
padding-top: 50px;
width: auto;
}
.codeflask__lines {
background: #fafafa;
line-height: 18px;
padding-top: 50px;
z-index: 100;
}
&::after {
bottom: 0;
content: '↔';
cursor: col-resize;
font-size: 20px;
height: 20px;
line-height: 18px;
position: absolute;
right: 0;
width: 22px;
z-index: 1;
}
}
.js--Output {
background-color: #4e4e4e;
color: #ffffff;
display: flex;
flex-direction: column;
flex-grow: 1;
font-family: monospace;
font-size: 12px;
font-variant-ligatures: common-ligatures;
line-height: 18px;
padding: 50px 10px 10px;
position: relative;
width: 40%;
.logs-wrapper {
display: flex;
flex: 1;
min-height: 0px;
}
.logs-container {
flex: 1;
overflow: auto;;
}
.logs-content {
height: auto;
}
.js--Log {
animation: fadein 0.2s;
margin: 0 0 5px 0;
word-break: break-all;
&.error {
color: #f88;
}
}
}
.ui.popup.popup-local {
display: flex;
flex: 1 1 100%;
max-width: 300px;
.button {
margin: 0;
}
}

View File

@ -7,7 +7,6 @@ import { AppProps, BareProps } from '@polkadot/react-components/types';
import React from 'react';
import { Route, Switch } from 'react-router';
import './index.css';
import Playground from './Playground';
interface Props extends AppProps, BareProps {}

View File

@ -25,7 +25,7 @@ export const rpcNewHead: Snippet = {
code: `// subscribe to new headers, printing the full info for 5 Blocks
let count = 0;
const unsub = await api.rpc.chain.subscribeNewHeads((header) => {
console.log(\`#\${header.blockNumber}:\`, header);
console.log(\`#\${header.number}:\`, header);
if (++count === 5) {
console.log('5 headers retrieved, unsubscribing');

View File

@ -8,15 +8,11 @@ import React from 'react';
import { classes } from '../util';
export default class ButtonDivider extends React.Component<DividerProps> {
public render (): React.ReactNode {
const { className, style } = this.props;
return (
<div
className={classes('ui button compact mini basic', className)}
style={style}
/>
);
}
export default function ButtonDivider ({ className, style }: DividerProps): React.ReactElement<DividerProps> {
return (
<div
className={classes('ui button compact mini basic', className)}
style={style}
/>
);
}

View File

@ -5,7 +5,7 @@
import { BareProps } from '@polkadot/react-components/types';
import CodeFlask from 'codeflask';
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { classes } from '@polkadot/react-components/util';
@ -34,46 +34,34 @@ interface Props extends BareProps {
* />
* ```
*/
class Editor extends React.Component<Props> {
private id = `flask-${Date.now()}`;
function Editor ({ className, code, isValid, onEdit }: Props): React.ReactElement<Props> {
const [editorId] = useState(`flask-${Date.now()}`);
const editorRef = useRef<typeof CodeFlask | null>(null);
private editor: any;
public componentDidMount (): void {
const { onEdit } = this.props;
this.editor = new CodeFlask(`#${this.id}`, {
useEffect((): void => {
const editor = new CodeFlask(`#${editorId}`, {
language: 'js',
lineNumbers: true
});
this.editor.updateCode(this.props.code);
this.editor.editorRoot.addEventListener('keydown', (): void => {
this.editor.onUpdate(onEdit);
editor.updateCode(code);
editor.editorRoot.addEventListener('keydown', (): void => {
editor.onUpdate(onEdit);
});
}
public shouldComponentUpdate (nextProps: Props): boolean {
return (
nextProps.code !== this.props.code
);
}
editorRef.current = editor;
}, []);
public componentDidUpdate (): void {
this.editor.updateCode(this.props.code);
}
useEffect((): void => {
editorRef.current && editorRef.current.updateCode(code);
}, [code]);
public render (): React.ReactNode {
const { className, isValid } = this.props;
return (
<div
className={classes('ui-Editor', className, isValid === false ? 'invalid' : '')}
id={this.id}
/>
);
}
return (
<div
className={classes('ui-Editor', className, isValid === false ? 'invalid' : '')}
id={editorId}
/>
);
}
export default styled(Editor)`