Rework js-app with func. components (#1651)
This commit is contained in:
parent
84a46ef76f
commit
f33b4b7f91
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user