Merge branch 'tezos-client-command' into 'dev'

Tezos client command

See merge request ligolang/ligo!497
This commit is contained in:
Jev Björsell 2020-03-12 20:26:13 +00:00
commit 6203427d3f
22 changed files with 567 additions and 353 deletions

View File

@ -25,6 +25,7 @@
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-devtools": "^3.5.0", "redux-devtools": "^3.5.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"slugify": "^1.4.0",
"styled-components": "^4.4.0", "styled-components": "^4.4.0",
"typescript": "3.6.4" "typescript": "3.6.4"
}, },

View File

@ -7,15 +7,17 @@ import { DeployAction } from '../../redux/actions/deploy';
import { DryRunAction } from '../../redux/actions/dry-run'; import { DryRunAction } from '../../redux/actions/dry-run';
import { EvaluateFunctionAction } from '../../redux/actions/evaluate-function'; import { EvaluateFunctionAction } from '../../redux/actions/evaluate-function';
import { EvaluateValueAction } from '../../redux/actions/evaluate-value'; import { EvaluateValueAction } from '../../redux/actions/evaluate-value';
import { GenerateCommandAction } from '../../redux/actions/generate-command';
import { AppState } from '../../redux/app'; import { AppState } from '../../redux/app';
import { ChangeDispatchedAction, ChangeSelectedAction, CommandState } from '../../redux/command'; import { ChangeDispatchedAction, ChangeSelectedAction, CommandState } from '../../redux/command';
import { Command } from '../../redux/types'; import { Command } from '../../redux/types';
import { CommandSelectComponent } from './command-select'; import { Option, Select } from '../form/select';
import { CompilePaneComponent } from './compile-pane'; import { CompilePaneComponent } from './compile-pane';
import { DeployPaneComponent } from './deploy-pane'; import { DeployPaneComponent } from './deploy-pane';
import { DryRunPaneComponent } from './dry-run-pane'; import { DryRunPaneComponent } from './dry-run-pane';
import { EvaluateFunctionPaneComponent } from './evaluate-function-pane'; import { EvaluateFunctionPaneComponent } from './evaluate-function-pane';
import { EvaluateValuePaneComponent } from './evaluate-value-pane'; import { EvaluateValuePaneComponent } from './evaluate-value-pane';
import { GenerateCommandPaneComponent } from './generate-command-pane';
const Container = styled.div<{ visible?: boolean }>` const Container = styled.div<{ visible?: boolean }>`
position: absolute; position: absolute;
@ -58,8 +60,8 @@ const RunButton = styled.div`
background-color: var(--orange); background-color: var(--orange);
`; `;
const CommandPaneContainer = styled.div` const SelectCommand = styled(Select)`
padding-top: 1em; flex: 2;
`; `;
function createAction(command: Command) { function createAction(command: Command) {
@ -74,6 +76,8 @@ function createAction(command: Command) {
return new EvaluateValueAction(); return new EvaluateValueAction();
case Command.EvaluateFunction: case Command.EvaluateFunction:
return new EvaluateFunctionAction(); return new EvaluateFunctionAction();
case Command.GenerateCommand:
return new GenerateCommandAction();
default: default:
throw new Error('Unsupported command'); throw new Error('Unsupported command');
} }
@ -97,12 +101,20 @@ export const ConfigureTabComponent = (props: {
return ( return (
<Container visible={props.selected}> <Container visible={props.selected}>
<CommonActionsGroup> <CommonActionsGroup>
<CommandSelectComponent <SelectCommand
selected={command} id="command-select"
value={command}
onChange={command => { onChange={command => {
dispatch({ ...new ChangeSelectedAction(command) }); dispatch({ ...new ChangeSelectedAction(command) });
}} }}
></CommandSelectComponent> >
<Option value={Command.Compile}>Compile</Option>
<Option value={Command.Deploy}>Deploy</Option>
<Option value={Command.DryRun}>Dry Run</Option>
<Option value={Command.EvaluateFunction}>Evaluate Function</Option>
<Option value={Command.EvaluateValue}>Evaluate Value</Option>
<Option value={Command.GenerateCommand}>Generate Command</Option>
</SelectCommand>
<RunButton <RunButton
id="run" id="run"
onClick={() => { onClick={() => {
@ -120,7 +132,6 @@ export const ConfigureTabComponent = (props: {
Run Run
</RunButton> </RunButton>
</CommonActionsGroup> </CommonActionsGroup>
<CommandPaneContainer>
{(command === Command.Compile && ( {(command === Command.Compile && (
<CompilePaneComponent></CompilePaneComponent> <CompilePaneComponent></CompilePaneComponent>
)) || )) ||
@ -135,8 +146,10 @@ export const ConfigureTabComponent = (props: {
)) || )) ||
(command === Command.EvaluateValue && ( (command === Command.EvaluateValue && (
<EvaluateValuePaneComponent></EvaluateValuePaneComponent> <EvaluateValuePaneComponent></EvaluateValuePaneComponent>
)) ||
(command === Command.GenerateCommand && (
<GenerateCommandPaneComponent></GenerateCommandPaneComponent>
))} ))}
</CommandPaneContainer>
</Container> </Container>
); );
}; };

View File

@ -0,0 +1,85 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../../redux/app';
import {
ChangeCommandAction,
ChangeEntrypointAction,
ChangeStorageAction,
ChangeToolAction,
GenerateCommandState,
} from '../../redux/generate-command';
import { Tool, ToolCommand } from '../../redux/types';
import { AccessFunctionLabel, Group, Input, Label, Textarea } from '../form/inputs';
import { Option, Select } from '../form/select';
const Container = styled.div`
overflow: auto;
`;
export const GenerateCommandPaneComponent = () => {
const dispatch = useDispatch();
const tool = useSelector<AppState, GenerateCommandState['tool']>(
state => state.generateCommand.tool
);
const command = useSelector<AppState, GenerateCommandState['command']>(
state => state.generateCommand.command
);
const entrypoint = useSelector<AppState, GenerateCommandState['entrypoint']>(
state => state.generateCommand.entrypoint
);
const storage = useSelector<AppState, GenerateCommandState['storage']>(
state => state.generateCommand.storage
);
return (
<Container>
<Group>
<Label>Tool</Label>
<Select
id="tool"
value={tool}
onChange={value => dispatch({ ...new ChangeToolAction(value) })}
>
<Option value={Tool.TezosClient}>Tezos Client</Option>
</Select>
</Group>
<Group>
<Label>Command</Label>
<Select
id="tool-command"
value={command}
onChange={value => dispatch({ ...new ChangeCommandAction(value) })}
>
<Option value={ToolCommand.Originate}>Originate</Option>
</Select>
</Group>
<Group>
<AccessFunctionLabel htmlFor="entrypoint"></AccessFunctionLabel>
<Input
id="entrypoint"
value={entrypoint}
onChange={ev =>
dispatch({ ...new ChangeEntrypointAction(ev.target.value) })
}
></Input>
</Group>
<Group>
<Label htmlFor="storage">Storage</Label>
<Textarea
id="storage"
rows={9}
value={storage}
onChange={ev =>
dispatch({ ...new ChangeStorageAction(ev.target.value) })
}
></Textarea>
</Group>
</Container>
);
};

View File

@ -3,11 +3,12 @@ import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppState } from '../../redux/app'; import { AppState } from '../../redux/app';
import { ChangeTitleAction } from '../../redux/editor'; import { ChangeLanguageAction, ChangeTitleAction, EditorState } from '../../redux/editor';
import { Language } from '../../redux/types';
import { Option, Select } from '../form/select';
import { ShareComponent } from '../share'; import { ShareComponent } from '../share';
import { EditableTitleComponent } from './editable-title'; import { EditableTitleComponent } from './editable-title';
import { MonacoComponent } from './monaco'; import { MonacoComponent } from './monaco';
import { SyntaxSelectComponent } from './syntax-select';
const Container = styled.div` const Container = styled.div`
flex: 2; flex: 2;
@ -35,6 +36,9 @@ const StyledEditableTitleComponent = styled(EditableTitleComponent)`
export const EditorComponent = () => { export const EditorComponent = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const title = useSelector<AppState, string>(state => state.editor.title); const title = useSelector<AppState, string>(state => state.editor.title);
const language = useSelector<AppState, EditorState['language']>(
state => state.editor.language
);
return ( return (
<Container> <Container>
@ -49,7 +53,17 @@ export const EditorComponent = () => {
}} }}
></StyledEditableTitleComponent> ></StyledEditableTitleComponent>
</LeftActions> </LeftActions>
<SyntaxSelectComponent></SyntaxSelectComponent> <Select
id="syntax-select"
value={language}
onChange={language => {
dispatch({ ...new ChangeLanguageAction(language) });
}}
>
<Option value={Language.PascaLigo}>PascaLIGO</Option>
<Option value={Language.CameLigo}>CameLIGO</Option>
<Option value={Language.ReasonLIGO}>ReasonLIGO</Option>
</Select>
</Header> </Header>
<MonacoComponent></MonacoComponent> <MonacoComponent></MonacoComponent>
</Container> </Container>

View File

@ -1,157 +0,0 @@
import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler';
import { useDispatch, useSelector } from 'react-redux';
import styled, { css } from 'styled-components';
import { AppState } from '../../redux/app';
import { ChangeLanguageAction, EditorState } from '../../redux/editor';
import { Language } from '../../redux/types';
import { Tooltip } from '../tooltip';
const Container = styled.div`
display: flex;
position: relative;
z-index: 2;
min-width: 10em;
`;
const Header = styled.div`
cursor: pointer;
user-select: none;
flex: 1;
display: flex;
height: 2em;
padding: 0 0.5em;
border: 1px solid var(--blue_trans1);
`;
const Label = styled.div`
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
`;
const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
<FontAwesomeIcon {...props} icon={faCaretDown} size="lg"></FontAwesomeIcon>
);
const Arrow = styled(ArrowIcon)`
margin-left: 0.5em;
pointer-events: none;
color: var(--blue_trans1);
transition: transform 0.15s ease-in;
${(props: { rotate: boolean }) =>
props.rotate &&
css`
transform: rotate(180deg);
`};
`;
const List = styled.ul`
position: absolute;
list-style-type: none;
background-color: white;
width: 100%;
margin: 0;
padding: 0;
box-shadow: 1px 3px 10px 0px rgba(153, 153, 153, 0.4);
border-radius: 3px;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s ease-in;
${(props: { visible: boolean }) =>
props.visible &&
css`
visibility: visible;
opacity: 1;
`}
`;
const Option = styled.li`
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: space-between;
height: 2em;
padding: 0 0.5em;
&:first-child {
border-radius: 3px 3px 0 0;
}
&:last-child {
border-radius: 0 0 3px 3px;
}
&:hover {
background-color: var(--blue_trans2);
font-weight: 600;
}
`;
export const SyntaxSelectComponent = () => {
const OPTIONS = {
[Language.PascaLigo]: 'PascaLIGO',
[Language.CameLigo]: 'CameLIGO',
[Language.ReasonLIGO]: 'ReasonLIGO'
};
const moveOptionToTop = (option: Language) => {
return Object.keys(OPTIONS).reduce((list, entry) => {
if (entry === option) {
list.unshift(entry);
} else {
list.push(entry as Language);
}
return list;
}, [] as Language[]);
};
const language = useSelector<AppState, EditorState['language']>(
state => state.editor.language
);
const dispatch = useDispatch();
const [opened, open] = useState(false);
const selectOption = (option: Language) => {
if (language !== option) {
dispatch({ ...new ChangeLanguageAction(option) });
}
open(false);
};
return (
<Container>
<OutsideClickHandler onOutsideClick={() => open(false)}>
<List visible={opened}>
{moveOptionToTop(language).map(option => (
<Option
id={option}
key={option}
onClick={() => selectOption(option)}
>
<span>{OPTIONS[option]}</span>
</Option>
))}
</List>
</OutsideClickHandler>
<Header id="syntax-select" onClick={() => open(true)}>
<Label>
{OPTIONS[language]}
<Arrow rotate={opened}></Arrow>
</Label>
<Tooltip>Select syntax</Tooltip>
</Header>
</Container>
);
};

View File

@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useState } from 'react'; import React, { useState } from 'react';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
const Container = styled.div<{ checked: boolean }>` const Container = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -44,7 +44,6 @@ export const CheckboxComponent = (props: {
return ( return (
<Container <Container
className={props.className} className={props.className}
checked={isChecked}
onClick={() => { onClick={() => {
const newState = !isChecked; const newState = !isChecked;

View File

@ -4,17 +4,20 @@ import styled from 'styled-components';
export const Group = styled.div` export const Group = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0.7em 0 0.7em 0;
`; `;
export const HGroup = styled.div` export const HGroup = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0.4em 0 0.4em 0;
`; `;
export const Label = styled.label` export const Label = styled.label`
font-size: 1em; font-size: 1em;
color: var(--label_foreground); color: var(--label_foreground);
user-select: none; user-select: none;
margin: 0.3em 0 0.3em 0;
`; `;
export const Hint = styled.span` export const Hint = styled.span`
@ -25,7 +28,7 @@ export const Hint = styled.span`
export const AccessFunctionLabel = (props: any) => { export const AccessFunctionLabel = (props: any) => {
return ( return (
<Label {...props}> <Label {...props}>
Access Function Access function
<br /> <br />
<Hint>The function name from where your contract will start</Hint> <Hint>The function name from where your contract will start</Hint>
</Label> </Label>
@ -33,7 +36,7 @@ export const AccessFunctionLabel = (props: any) => {
}; };
export const Input = styled.input` export const Input = styled.input`
margin: 0.3em 0 0.7em 0; /* margin: 0.3em 0 0.7em 0; */
background-color: var(--input_background); background-color: var(--input_background);
border-style: none; border-style: none;
border-bottom: 5px solid #e1f1ff; border-bottom: 5px solid #e1f1ff;
@ -49,7 +52,7 @@ export const Input = styled.input`
export const Textarea = styled.textarea` export const Textarea = styled.textarea`
resize: vertical; resize: vertical;
margin: 0.3em 0 0.7em 0; /* margin: 0.3em 0 0.7em 0; */
background-color: var(--input_background); background-color: var(--input_background);
border-style: none; border-style: none;
border-bottom: 5px solid #e1f1ff; border-bottom: 5px solid #e1f1ff;

View File

@ -1,17 +1,13 @@
import { faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useState } from 'react'; import React, { FunctionComponentElement, useState } from 'react';
import OutsideClickHandler from 'react-outside-click-handler'; import OutsideClickHandler from 'react-outside-click-handler';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { Command } from '../../redux/types';
const Container = styled.div` const Container = styled.div`
flex: 2;
display: flex; display: flex;
position: relative; position: relative;
min-width: 8em; min-width: 8em;
z-index: 2;
`; `;
const Header = styled.div` const Header = styled.div`
@ -20,8 +16,8 @@ const Header = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
align-items: center;
min-height: 2em; min-height: 2em;
padding: 0 0.5em; padding: 0 0.5em;
@ -33,6 +29,7 @@ const ArrowIcon = ({ rotate, ...props }: { rotate: boolean }) => (
); );
const Arrow = styled(ArrowIcon)` const Arrow = styled(ArrowIcon)`
z-index: 1;
pointer-events: none; pointer-events: none;
color: var(--blue_trans1); color: var(--blue_trans1);
transition: transform 0.15s ease-in; transition: transform 0.15s ease-in;
@ -45,6 +42,7 @@ const Arrow = styled(ArrowIcon)`
`; `;
const List = styled.ul` const List = styled.ul`
z-index: 1;
position: absolute; position: absolute;
list-style-type: none; list-style-type: none;
background-color: white; background-color: white;
@ -66,7 +64,7 @@ const List = styled.ul`
`} `}
`; `;
const Option = styled.li` const OptionContainer = styled.li`
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -90,56 +88,73 @@ const Option = styled.li`
} }
`; `;
export const CommandSelectComponent = (props: { interface OptionProps {
selected: Command; value: string;
onChange?: (value: Command) => void; children: string;
}) => { }
const OPTIONS = {
[Command.Compile]: 'Compile',
[Command.Deploy]: 'Deploy',
[Command.DryRun]: 'Dry Run',
[Command.EvaluateFunction]: 'Evaluate Function',
[Command.EvaluateValue]: 'Evaluate Value'
};
const moveOptionToTop = (option: Command) => { type OptionElement = FunctionComponentElement<OptionProps>;
return Object.keys(OPTIONS).reduce((list, entry) => {
if (entry === option) { export const Option = (props: OptionProps) => {
// This is an empty component. It's used as a way to get option information into its parent. It is not inserted into the DOM.
return <></>;
};
export const Select = (props: {
id: string;
value: any;
children: OptionElement[] | OptionElement;
onChange?: (value: any) => void;
className?: string;
}) => {
const [isOpen, setOpen] = useState(false);
const options = Array.isArray(props.children)
? props.children
: [props.children];
const labelLookup = new Map(
options.map(
child => [child.props.value, child.props.children] as [string, string]
)
);
const moveOptionToTop = (value: string) => {
return options.reduce((list, entry) => {
if (entry.props.value === value) {
list.unshift(entry); list.unshift(entry);
} else { } else {
list.push(entry as Command); list.push(entry);
} }
return list; return list;
}, [] as Command[]); }, [] as OptionElement[]);
}; };
const [opened, open] = useState(false); const selectOption = (option: OptionElement) => {
if (props.value !== option.props.value && props.onChange) {
const selectOption = (option: Command) => { props.onChange(option.props.value);
if (props.selected !== option && props.onChange) {
props.onChange(option);
} }
open(false); setOpen(false);
}; };
return ( return (
<Container> <Container className={props.className}>
<OutsideClickHandler onOutsideClick={() => open(false)}> <OutsideClickHandler onOutsideClick={() => setOpen(false)}>
<List visible={opened}> <List visible={isOpen}>
{moveOptionToTop(props.selected).map(option => ( {moveOptionToTop(props.value).map((option: OptionElement) => (
<Option <OptionContainer
id={option} id={option.props.value}
key={option} key={option.props.value}
onClick={() => selectOption(option)} onClick={() => selectOption(option)}
> >
<span>{OPTIONS[option]}</span> <span>{option.props.children}</span>
</Option> </OptionContainer>
))} ))}
</List> </List>
</OutsideClickHandler> </OutsideClickHandler>
<Header id="command-select" onClick={() => open(true)}> <Header id={props.id} onClick={() => setOpen(true)}>
<span>{OPTIONS[props.selected]}</span> <span>{labelLookup.get(props.value)}</span>
<Arrow rotate={opened}></Arrow> <Arrow rotate={isOpen}></Arrow>
</Header> </Header>
</Container> </Container>
); );

View File

@ -1,83 +0,0 @@
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useState } from 'react';
import styled, { css } from 'styled-components';
const Container = styled.div<{ checked: boolean }>`
position: relative;
height: 2em;
width: 3.5em;
border-radius: 1em;
background-color: var(--blue_trans1);
border: 1px solid var(--blue);
transition: background-color 0.2s ease-in;
${props =>
props.checked &&
css`
background-color: var(--blue);
`};
`;
const Button = styled.div<{ checked: boolean }>`
display: flex;
justify-content: center;
align-items: center;
position: absolute;
height: 2em;
width: 2em;
background-color: white;
border-radius: 50%;
cursor: pointer;
right: calc(1.5em);
transition: right 0.2s ease-in;
${props =>
props.checked &&
css`
right: 0;
`};
`;
const CheckIcon = ({ visible, ...props }: { visible: boolean }) => (
<FontAwesomeIcon {...props} icon={faCheck}></FontAwesomeIcon>
);
const Check = styled(CheckIcon)`
position: absolute;
pointer-events: none;
opacity: 1;
transition: opacity 0.2s ease-in;
color: var(--blue);
${props =>
!props.visible &&
css`
opacity: 0;
`}
`;
export const ToggleComponent = (props: {
checked: boolean;
onChanged: (value: boolean) => void;
className?: string;
}) => {
const [isChecked, setChecked] = useState(props.checked);
return (
<Container className={props.className} checked={isChecked}>
<Button
checked={isChecked}
onClick={() => {
const newState = !isChecked;
setChecked(newState);
props.onChanged(newState);
}}
>
<Check visible={isChecked}></Check>
</Button>
</Container>
);
};

View File

@ -5,6 +5,7 @@ import styled from 'styled-components';
import { AppState } from '../../redux/app'; import { AppState } from '../../redux/app';
import { ResultState } from '../../redux/result'; import { ResultState } from '../../redux/result';
import { OutputToolbarComponent } from './output-toolbar'; import { OutputToolbarComponent } from './output-toolbar';
import { copyOutput, downloadOutput } from './utils';
const Container = styled.div<{ visible?: boolean }>` const Container = styled.div<{ visible?: boolean }>`
display: flex; display: flex;
@ -23,35 +24,6 @@ const Pre = styled.pre`
margin: 0; margin: 0;
`; `;
function copyOutput(el: HTMLElement | null) {
if (el) {
const range = document.createRange();
range.selectNodeContents(el);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
}
}
}
function downloadOutput(output: string) {
const anchor = document.createElement('a');
anchor.setAttribute(
'href',
`data:text/plain;charset=utf-8,${encodeURIComponent(output)}`
);
anchor.setAttribute('download', 'output.txt');
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
export const CompileOutputPane = () => { export const CompileOutputPane = () => {
const output = useSelector<AppState, ResultState['output']>( const output = useSelector<AppState, ResultState['output']>(
state => state.result.output state => state.result.output
@ -62,6 +34,7 @@ export const CompileOutputPane = () => {
return ( return (
<Container> <Container>
<OutputToolbarComponent <OutputToolbarComponent
showTryMichelson={true}
onCopy={() => copyOutput(preRef.current)} onCopy={() => copyOutput(preRef.current)}
onDownload={() => downloadOutput(output)} onDownload={() => downloadOutput(output)}
></OutputToolbarComponent> ></OutputToolbarComponent>

View File

@ -0,0 +1,45 @@
import React, { useRef } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { AppState } from '../../redux/app';
import { ResultState } from '../../redux/result';
import { OutputToolbarComponent } from './output-toolbar';
import { copyOutput, downloadOutput } from './utils';
const Container = styled.div<{ visible?: boolean }>`
display: flex;
flex-direction: column;
height: 100%;
`;
const Output = styled.div`
flex: 1;
padding: 0.5em;
display: flex;
overflow: auto;
`;
const Pre = styled.pre`
margin: 0;
`;
export const GenerateCommandOutputPane = () => {
const output = useSelector<AppState, ResultState['output']>(
state => state.result.output
);
const preRef = useRef<HTMLPreElement>(null);
return (
<Container>
<OutputToolbarComponent
onCopy={() => copyOutput(preRef.current)}
onDownload={() => downloadOutput(output)}
></OutputToolbarComponent>
<Output id="output">
<Pre ref={preRef}>{output}</Pre>
</Output>
</Container>
);
};

View File

@ -8,6 +8,7 @@ import { ResultState } from '../../redux/result';
import { Command } from '../../redux/types'; import { Command } from '../../redux/types';
import { CompileOutputPane } from './compile-output-pane'; import { CompileOutputPane } from './compile-output-pane';
import { DeployOutputPane } from './deploy-output-pane'; import { DeployOutputPane } from './deploy-output-pane';
import { GenerateCommandOutputPane } from './generate-command-output-pane';
import { Loading } from './loading'; import { Loading } from './loading';
import { OutputPane } from './output-pane'; import { OutputPane } from './output-pane';
@ -41,14 +42,21 @@ export const OutputTab = (props: {
const loading = useSelector<AppState, LoadingState['loading']>( const loading = useSelector<AppState, LoadingState['loading']>(
state => state.loading.loading state => state.loading.loading
); );
const output = useSelector<AppState, ResultState['output']>(
state => state.result.output
);
const renderResult = () => { const renderResult = () => {
if (loading) { if (loading) {
return <Loading onCancel={props.onCancel}></Loading>; return <Loading onCancel={props.onCancel}></Loading>;
} else if (!output) {
return <></>;
} else if (command === Command.Compile) { } else if (command === Command.Compile) {
return <CompileOutputPane></CompileOutputPane>; return <CompileOutputPane></CompileOutputPane>;
} else if (command === Command.Deploy) { } else if (command === Command.Deploy) {
return <DeployOutputPane></DeployOutputPane>; return <DeployOutputPane></DeployOutputPane>;
} else if (command === Command.GenerateCommand) {
return <GenerateCommandOutputPane></GenerateCommandOutputPane>;
} }
return <OutputPane></OutputPane>; return <OutputPane></OutputPane>;

View File

@ -24,6 +24,7 @@ const Link = styled.a`
`; `;
export const OutputToolbarComponent = (props: { export const OutputToolbarComponent = (props: {
showTryMichelson?: boolean;
onCopy?: () => void; onCopy?: () => void;
onDownload?: () => void; onDownload?: () => void;
}) => { }) => {
@ -41,7 +42,8 @@ export const OutputToolbarComponent = (props: {
<FontAwesomeIcon icon={faDownload}></FontAwesomeIcon> <FontAwesomeIcon icon={faDownload}></FontAwesomeIcon>
<Tooltip>Download</Tooltip> <Tooltip>Download</Tooltip>
</Item> </Item>
<Divider></Divider> {props.showTryMichelson && <Divider></Divider>}
{props.showTryMichelson && (
<Item> <Item>
<Link <Link
target="_blank" target="_blank"
@ -53,6 +55,7 @@ export const OutputToolbarComponent = (props: {
View in Try-Michelson IDE View in Try-Michelson IDE
</Link> </Link>
</Item> </Item>
)}
</Toolbar> </Toolbar>
); );
}; };

View File

@ -0,0 +1,28 @@
export function copyOutput(el: HTMLElement | null) {
if (el) {
const range = document.createRange();
range.selectNodeContents(el);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
}
}
}
export function downloadOutput(output: string) {
const anchor = document.createElement('a');
anchor.setAttribute(
'href',
`data:text/plain;charset=utf-8,${encodeURIComponent(output)}`
);
anchor.setAttribute('download', 'output.txt');
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}

View File

@ -0,0 +1,112 @@
import { Tezos } from '@taquito/taquito';
import { Dispatch } from 'redux';
import slugify from 'slugify';
import { compileContract, compileStorage, getErrorMessage } from '../../services/api';
import { AppState } from '../app';
import { MichelsonFormat } from '../compile';
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
import { ChangeOutputAction } from '../result';
import { Command } from '../types';
import { CancellableAction } from './cancellable';
const URL = 'https://api.tez.ie/keys/carthagenet/';
const AUTHORIZATION_HEADER = 'Bearer ligo-ide';
export async function fetchRandomPrivateKey(): Promise<string> {
const response = await fetch(URL, {
method: 'POST',
headers: { Authorization: AUTHORIZATION_HEADER }
});
return response.text();
}
export class GenerateCommandAction extends CancellableAction {
getAction() {
return async (dispatch: Dispatch, getState: () => AppState) => {
dispatch({ ...new UpdateLoadingAction('Compiling contract...') });
try {
const { editor, generateCommand } = getState();
const michelsonCodeJson = await compileContract(
editor.language,
editor.code,
generateCommand.entrypoint,
MichelsonFormat.Json
);
const michelsonCode = await compileContract(
editor.language,
editor.code,
generateCommand.entrypoint
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new UpdateLoadingAction('Compiling storage...') });
const michelsonStorageJson = await compileStorage(
editor.language,
editor.code,
generateCommand.entrypoint,
generateCommand.storage,
MichelsonFormat.Json
);
const michelsonStorage = await compileStorage(
editor.language,
editor.code,
generateCommand.entrypoint,
generateCommand.storage
);
if (this.isCancelled()) {
return;
}
dispatch({ ...new UpdateLoadingAction('Estimating burn cap...') });
await Tezos.importKey(await fetchRandomPrivateKey());
const estimate = await Tezos.estimate.originate({
code: JSON.parse(michelsonCodeJson.result),
init: JSON.parse(michelsonStorageJson.result)
});
if (this.isCancelled()) {
return;
}
const title = slugify(editor.title).toLowerCase() || 'untitled';
const output = `tezos-client \\
${generateCommand.command} \\
contract \\
${title} \\
transferring 0 \\
from $YOUR_SOURCE_ACCOUNT \\
running '${michelsonCode.result.trim()}' \\
--init '${michelsonStorage.result.trim()}' \\
--burn-cap ${estimate.burnFeeMutez / 1000000}`;
dispatch({
...new ChangeOutputAction(output, Command.GenerateCommand)
});
} catch (ex) {
if (this.isCancelled()) {
return;
}
dispatch({
...new ChangeOutputAction(
`Error: ${getErrorMessage(ex)}`,
Command.GenerateCommand
)
});
}
dispatch({ ...new DoneLoadingAction() });
};
}
}

View File

@ -8,6 +8,7 @@ import editor, { EditorState } from './editor';
import evaluateFunction, { EvaluateFunctionState } from './evaluate-function'; import evaluateFunction, { EvaluateFunctionState } from './evaluate-function';
import evaluateValue, { EvaluateValueState } from './evaluate-value'; import evaluateValue, { EvaluateValueState } from './evaluate-value';
import examples, { ExamplesState } from './examples'; import examples, { ExamplesState } from './examples';
import generateCommand, { GenerateCommandState } from './generate-command';
import loading, { LoadingState } from './loading'; import loading, { LoadingState } from './loading';
import result, { ResultState } from './result'; import result, { ResultState } from './result';
import share, { ShareState } from './share'; import share, { ShareState } from './share';
@ -20,6 +21,7 @@ export interface AppState {
deploy: DeployState; deploy: DeployState;
evaluateFunction: EvaluateFunctionState; evaluateFunction: EvaluateFunctionState;
evaluateValue: EvaluateValueState; evaluateValue: EvaluateValueState;
generateCommand: GenerateCommandState;
result: ResultState; result: ResultState;
command: CommandState; command: CommandState;
examples: ExamplesState; examples: ExamplesState;
@ -34,6 +36,7 @@ export default combineReducers({
deploy, deploy,
evaluateFunction, evaluateFunction,
evaluateValue, evaluateValue,
generateCommand,
result, result,
command, command,
examples, examples,

View File

@ -0,0 +1,81 @@
import { Tool, ToolCommand } from './types';
export enum ActionType {
ChangeTool = 'generate-command-change-tool',
ChangeCommand = 'generate-command-change-command',
ChangeEntrypoint = 'generate-command-change-entrypoint',
ChangeStorage = 'generate-command-change-storage'
}
export interface GenerateCommandState {
tool: Tool;
command: ToolCommand;
entrypoint: string;
originationAccount: string;
storage: string;
burnCap: number;
}
export class ChangeToolAction {
public readonly type = ActionType.ChangeTool;
constructor(public payload: GenerateCommandState['tool']) {}
}
export class ChangeCommandAction {
public readonly type = ActionType.ChangeCommand;
constructor(public payload: GenerateCommandState['command']) {}
}
export class ChangeEntrypointAction {
public readonly type = ActionType.ChangeEntrypoint;
constructor(public payload: GenerateCommandState['entrypoint']) {}
}
export class ChangeStorageAction {
public readonly type = ActionType.ChangeStorage;
constructor(public payload: GenerateCommandState['storage']) {}
}
type Action =
| ChangeToolAction
| ChangeCommandAction
| ChangeEntrypointAction
| ChangeStorageAction;
const DEFAULT_STATE: GenerateCommandState = {
tool: Tool.TezosClient,
command: ToolCommand.Originate,
entrypoint: '',
storage: '',
originationAccount: '',
burnCap: 0
};
export default (
state = DEFAULT_STATE,
action: Action
): GenerateCommandState => {
switch (action.type) {
case ActionType.ChangeTool:
return {
...state,
tool: action.payload
};
case ActionType.ChangeCommand:
return {
...state,
command: action.payload
};
case ActionType.ChangeEntrypoint:
return {
...state,
entrypoint: action.payload
};
case ActionType.ChangeStorage:
return {
...state,
storage: action.payload
};
}
return state;
};

View File

@ -9,5 +9,14 @@ export enum Command {
DryRun = 'dry-run', DryRun = 'dry-run',
EvaluateValue = 'evaluate-value', EvaluateValue = 'evaluate-value',
EvaluateFunction = 'evaluate-function', EvaluateFunction = 'evaluate-function',
Deploy = 'deploy' Deploy = 'deploy',
GenerateCommand = 'generate-command'
}
export enum Tool {
TezosClient = 'tezos-client'
}
export enum ToolCommand {
Originate = 'originate'
} }

View File

@ -43,6 +43,9 @@ export async function compileStorage(
storage: string, storage: string,
format?: string format?: string
) { ) {
// For whatever reason, storage set by examples is not treated as a string. So we convert it here.
storage = `${storage}`;
const response = await axios.post('/api/compile-storage', { const response = await axios.post('/api/compile-storage', {
syntax, syntax,
code, code,

View File

@ -0,0 +1,52 @@
import joi from '@hapi/joi';
import { Request, Response } from 'express';
import { CompilerError, LigoCompiler } from '../ligo-compiler';
import { logger } from '../logger';
interface CompileBody {
syntax: string;
code: string;
entrypoint: string;
storage: string;
format?: string;
}
const validateRequest = (body: any): { value: CompileBody; error: any } => {
return joi
.object({
syntax: joi.string().required(),
code: joi.string().required(),
entrypoint: joi.string().required(),
storage: joi.string().required(),
format: joi.string().optional()
})
.validate(body);
};
export async function compileStorageHandler(req: Request, res: Response) {
const { error, value: body } = validateRequest(req.body);
if (error) {
res.status(400).json({ error: error.message });
} else {
try {
const michelsonStorage = await new LigoCompiler().compileStorage(
body.syntax,
body.code,
body.entrypoint,
body.format || 'text',
body.storage
);
res.send({ result: michelsonStorage });
} catch (ex) {
if (ex instanceof CompilerError) {
res.status(400).json({ error: ex.message });
} else {
logger.error(ex);
res.sendStatus(500);
}
}
}
}

View File

@ -4,6 +4,7 @@ import { dirname, join } from 'path';
import { compileContractHandler } from './handlers/compile-contract'; import { compileContractHandler } from './handlers/compile-contract';
import { compileExpressionHandler } from './handlers/compile-expression'; import { compileExpressionHandler } from './handlers/compile-expression';
import { compileStorageHandler } from './handlers/compile-storage';
import { deployHandler } from './handlers/deploy'; import { deployHandler } from './handlers/deploy';
import { dryRunHandler } from './handlers/dry-run'; import { dryRunHandler } from './handlers/dry-run';
import { evaluateValueHandler } from './handlers/evaluate-value'; import { evaluateValueHandler } from './handlers/evaluate-value';
@ -51,6 +52,7 @@ app.get(
); );
app.post('/api/compile-contract', compileContractHandler); app.post('/api/compile-contract', compileContractHandler);
app.post('/api/compile-expression', compileExpressionHandler); app.post('/api/compile-expression', compileExpressionHandler);
app.post('/api/compile-storage', compileStorageHandler);
app.post('/api/dry-run', dryRunHandler); app.post('/api/dry-run', dryRunHandler);
app.post('/api/share', shareHandler); app.post('/api/share', shareHandler);
app.post('/api/evaluate-value', evaluateValueHandler); app.post('/api/evaluate-value', evaluateValueHandler);

View File

@ -10706,6 +10706,11 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0" astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
slugify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.4.0.tgz#c9557c653c54b0c7f7a8e786ef3431add676d2cb"
integrity sha512-FtLNsMGBSRB/0JOE2A0fxlqjI6fJsgHGS13iTuVT28kViI4JjUiNqp/vyis0ZXYcMnpR3fzGNkv+6vRlI2GwdQ==
snakeize@^0.1.0: snakeize@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d"