diff --git a/tools/webide/packages/client/src/components/output-tab.tsx b/tools/webide/packages/client/src/components/output-tab.tsx index 0f6cad87e..147942910 100644 --- a/tools/webide/packages/client/src/components/output-tab.tsx +++ b/tools/webide/packages/client/src/components/output-tab.tsx @@ -7,6 +7,7 @@ import { AppState } from '../redux/app'; import { CommandState } from '../redux/command'; import { DoneLoadingAction, LoadingState } from '../redux/loading'; import { ResultState } from '../redux/result'; +import { Command } from '../redux/types'; import { OutputToolbarComponent } from './output-toolbar'; const Container = styled.div<{ visible?: boolean }>` @@ -43,7 +44,7 @@ const CancelButton = styled.div` const Output = styled.div` flex: 1; - padding: 0 0.5em 0.5em 0.5em; + padding: 0.5em; display: flex; overflow: scroll; /* This font size is used to calcuate spinner size */ @@ -81,20 +82,18 @@ function copyOutput(el: HTMLElement | null) { } } -function downloadOutput(el: HTMLElement | null) { - if (el) { - const anchor = document.createElement('a'); - anchor.setAttribute( - 'href', - 'data:text/plain;charset=utf-8,' + encodeURIComponent(el.innerHTML) - ); - anchor.setAttribute('download', 'output.txt'); +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); - } + anchor.style.display = 'none'; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); } export const OutputTabComponent = (props: { @@ -107,6 +106,9 @@ export const OutputTabComponent = (props: { const contract = useSelector( state => state.result.contract ); + const command = useSelector( + state => state.result.command + ); const loading = useSelector(state => state.loading); @@ -132,10 +134,14 @@ export const OutputTabComponent = (props: { return ( - {output.length !== 0 && ( + {!( + loading.loading || + output.length === 0 || + command !== Command.Compile + ) && ( copyOutput(preRef.current)} - onDownload={() => downloadOutput(preRef.current)} + onDownload={() => downloadOutput(output)} > )} diff --git a/tools/webide/packages/client/src/components/output-toolbar.tsx b/tools/webide/packages/client/src/components/output-toolbar.tsx index a258352ce..871dcb873 100644 --- a/tools/webide/packages/client/src/components/output-toolbar.tsx +++ b/tools/webide/packages/client/src/components/output-toolbar.tsx @@ -1,78 +1,58 @@ import { faCopy, faDownload } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import { AppState } from '../redux/app'; +import { ResultState } from '../redux/result'; +import { Item, Toolbar } from './toolbar'; import { Tooltip } from './tooltip'; -const Container = styled.div` - display: flex; - justify-content: flex-start; - padding: 0.2em 0.5em; - z-index: 3; +const Divider = styled.div` + display: block; + background-color: rgba(0, 0, 0, 0.12); + height: 20px; + width: 1px; + margin: 0 3px; `; -const Action = styled.div` - z-index: 3; - position: relative; - margin: 4px 6px; - cursor: pointer; - - opacity: 0.5; - color: #444; - - ::before { - content: ''; - display: block; - position: absolute; - z-index: -1; - bottom: -4px; - left: -4px; - right: -4px; - top: -4px; - border-radius: 4px; - background: none; - box-sizing: border-box; - opacity: 0; - transform: scale(0); - transition-property: transform, opacity; - transition-duration: 0.15s; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - } - - :hover::before { - background-color: rgba(32, 33, 36, 0.059); - opacity: 1; - transform: scale(1); - } - - :hover { - opacity: 1; - } - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } +const Link = styled.a` + font-size: 0.8em; + color: var(--blue); + opacity: 1; `; export const OutputToolbarComponent = (props: { onCopy?: () => void; onDownload?: () => void; }) => { + const output = useSelector( + state => state.result.output + ); + return ( - - props.onCopy && props.onCopy()}> + + props.onCopy && props.onCopy()}> Copy - - props.onDownload && props.onDownload()}> + + props.onDownload && props.onDownload()}> Download - - + + + + + View in Try-Michelson IDE + + + ); }; diff --git a/tools/webide/packages/client/src/components/toolbar.tsx b/tools/webide/packages/client/src/components/toolbar.tsx new file mode 100644 index 000000000..ef3386fab --- /dev/null +++ b/tools/webide/packages/client/src/components/toolbar.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + align-items: center; + padding: 0.2em 0.5em; + z-index: 3; +`; + +export const Group = styled.div` + display: flex; + align-items: center; +`; + +export const Item = styled.div` + z-index: 3; + position: relative; + margin: 4px 6px; + cursor: pointer; + + opacity: 0.5; + color: #444; + + ::before { + content: ''; + display: block; + position: absolute; + z-index: -1; + bottom: -4px; + left: -4px; + right: -4px; + top: -4px; + border-radius: 4px; + background: none; + box-sizing: border-box; + opacity: 0; + transform: scale(0); + transition-property: transform, opacity; + transition-duration: 0.15s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + + :hover::before { + background-color: rgba(32, 33, 36, 0.059); + opacity: 1; + transform: scale(1); + } + + :hover { + opacity: 1; + } + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } +`; + +export const Toolbar = (props: any) => { + return {props.children}; +}; diff --git a/tools/webide/packages/client/src/index.css b/tools/webide/packages/client/src/index.css index 1f3296c3f..adb1df287 100644 --- a/tools/webide/packages/client/src/index.css +++ b/tools/webide/packages/client/src/index.css @@ -48,7 +48,7 @@ --font_ghost_weight: 700; --font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */ - --content_height: 85vh; + --content_height: 84vh; --tooltip_foreground: white; --tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/; diff --git a/tools/webide/packages/client/src/redux/actions/compile.ts b/tools/webide/packages/client/src/redux/actions/compile.ts index 2c3f21a29..cd2ed98db 100644 --- a/tools/webide/packages/client/src/redux/actions/compile.ts +++ b/tools/webide/packages/client/src/redux/actions/compile.ts @@ -4,6 +4,7 @@ import { compileContract, getErrorMessage } from '../../services/api'; import { AppState } from '../app'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { ChangeOutputAction } from '../result'; +import { Command } from '../types'; import { CancellableAction } from './cancellable'; export class CompileAction extends CancellableAction { @@ -24,13 +25,18 @@ export class CompileAction extends CancellableAction { return; } - dispatch({ ...new ChangeOutputAction(michelsonCode.result) }); + dispatch({ + ...new ChangeOutputAction(michelsonCode.result, Command.Compile) + }); } catch (ex) { if (this.isCancelled()) { return; } dispatch({ - ...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) + ...new ChangeOutputAction( + `Error: ${getErrorMessage(ex)}`, + Command.Compile + ) }); } diff --git a/tools/webide/packages/client/src/redux/actions/deploy.ts b/tools/webide/packages/client/src/redux/actions/deploy.ts index e8b8c2d9e..5fc9dcf1d 100644 --- a/tools/webide/packages/client/src/redux/actions/deploy.ts +++ b/tools/webide/packages/client/src/redux/actions/deploy.ts @@ -2,11 +2,12 @@ import { Tezos } from '@taquito/taquito'; import { TezBridgeSigner } from '@taquito/tezbridge-signer'; import { Dispatch } from 'redux'; -import { compileContract, compileExpression, deploy, getErrorMessage } from '../../services/api'; +import { compileContract, compileStorage, deploy, getErrorMessage } from '../../services/api'; import { AppState } from '../app'; import { MichelsonFormat } from '../compile'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { ChangeContractAction, ChangeOutputAction } from '../result'; +import { Command } from '../types'; import { CancellableAction } from './cancellable'; Tezos.setProvider({ @@ -32,8 +33,10 @@ export class DeployAction extends CancellableAction { } dispatch({ ...new UpdateLoadingAction('Compiling storage...') }); - const michelsonStorage = await compileExpression( + const michelsonStorage = await compileStorage( editorState.language, + editorState.code, + deployState.entrypoint, deployState.storage, MichelsonFormat.Json ); @@ -83,13 +86,18 @@ export class DeployAction extends CancellableAction { return; } - dispatch({ ...new ChangeContractAction(contract.address) }); + dispatch({ + ...new ChangeContractAction(contract.address, Command.Deploy) + }); } catch (ex) { if (this.isCancelled()) { return; } dispatch({ - ...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) + ...new ChangeOutputAction( + `Error: ${getErrorMessage(ex)}`, + Command.Deploy + ) }); } diff --git a/tools/webide/packages/client/src/redux/actions/dry-run.ts b/tools/webide/packages/client/src/redux/actions/dry-run.ts index 9b51a58ff..515435647 100644 --- a/tools/webide/packages/client/src/redux/actions/dry-run.ts +++ b/tools/webide/packages/client/src/redux/actions/dry-run.ts @@ -4,6 +4,7 @@ import { dryRun, getErrorMessage } from '../../services/api'; import { AppState } from '../app'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { ChangeOutputAction } from '../result'; +import { Command } from '../types'; import { CancellableAction } from './cancellable'; export class DryRunAction extends CancellableAction { @@ -25,13 +26,16 @@ export class DryRunAction extends CancellableAction { if (this.isCancelled()) { return; } - dispatch({ ...new ChangeOutputAction(result.output) }); + dispatch({ ...new ChangeOutputAction(result.output, Command.DryRun) }); } catch (ex) { if (this.isCancelled()) { return; } dispatch({ - ...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) + ...new ChangeOutputAction( + `Error: ${getErrorMessage(ex)}`, + Command.DryRun + ) }); } diff --git a/tools/webide/packages/client/src/redux/actions/evaluate-function.ts b/tools/webide/packages/client/src/redux/actions/evaluate-function.ts index 85871ebda..ab7b0053f 100644 --- a/tools/webide/packages/client/src/redux/actions/evaluate-function.ts +++ b/tools/webide/packages/client/src/redux/actions/evaluate-function.ts @@ -4,6 +4,7 @@ import { getErrorMessage, runFunction } from '../../services/api'; import { AppState } from '../app'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { ChangeOutputAction } from '../result'; +import { Command } from '../types'; import { CancellableAction } from './cancellable'; export class EvaluateFunctionAction extends CancellableAction { @@ -27,13 +28,18 @@ export class EvaluateFunctionAction extends CancellableAction { if (this.isCancelled()) { return; } - dispatch({ ...new ChangeOutputAction(result.output) }); + dispatch({ + ...new ChangeOutputAction(result.output, Command.EvaluateFunction) + }); } catch (ex) { if (this.isCancelled()) { return; } dispatch({ - ...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) + ...new ChangeOutputAction( + `Error: ${getErrorMessage(ex)}`, + Command.EvaluateFunction + ) }); } diff --git a/tools/webide/packages/client/src/redux/actions/evaluate-value.ts b/tools/webide/packages/client/src/redux/actions/evaluate-value.ts index 16f46696d..765476884 100644 --- a/tools/webide/packages/client/src/redux/actions/evaluate-value.ts +++ b/tools/webide/packages/client/src/redux/actions/evaluate-value.ts @@ -4,6 +4,7 @@ import { evaluateValue, getErrorMessage } from '../../services/api'; import { AppState } from '../app'; import { DoneLoadingAction, UpdateLoadingAction } from '../loading'; import { ChangeOutputAction } from '../result'; +import { Command } from '../types'; import { CancellableAction } from './cancellable'; export class EvaluateValueAction extends CancellableAction { @@ -28,13 +29,18 @@ export class EvaluateValueAction extends CancellableAction { return; } - dispatch({ ...new ChangeOutputAction(result.code) }); + dispatch({ + ...new ChangeOutputAction(result.code, Command.EvaluateValue) + }); } catch (ex) { if (this.isCancelled()) { return; } dispatch({ - ...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`) + ...new ChangeOutputAction( + `Error: ${getErrorMessage(ex)}`, + Command.EvaluateValue + ) }); } diff --git a/tools/webide/packages/client/src/redux/result.ts b/tools/webide/packages/client/src/redux/result.ts index 54315a948..3fcca74c7 100644 --- a/tools/webide/packages/client/src/redux/result.ts +++ b/tools/webide/packages/client/src/redux/result.ts @@ -1,26 +1,36 @@ +import { Command } from './types'; + export enum ActionType { ChangeOutput = 'result-change-output', ChangeContract = 'result-change-contract' } export interface ResultState { + command: Command; output: string; contract: string; } export class ChangeOutputAction { public readonly type = ActionType.ChangeOutput; - constructor(public payload: ResultState['output']) {} + constructor( + public output: ResultState['output'], + public command: ResultState['command'] + ) {} } export class ChangeContractAction { public readonly type = ActionType.ChangeContract; - constructor(public payload: ResultState['contract']) {} + constructor( + public contract: ResultState['contract'], + public command: ResultState['command'] + ) {} } type Action = ChangeOutputAction | ChangeContractAction; const DEFAULT_STATE: ResultState = { + command: Command.Compile, output: '', contract: '' }; @@ -30,13 +40,15 @@ export default (state = DEFAULT_STATE, action: Action): ResultState => { case ActionType.ChangeOutput: return { ...state, - output: action.payload + output: action.output, + command: action.command }; case ActionType.ChangeContract: return { ...state, output: DEFAULT_STATE.output, - contract: action.payload + contract: action.contract, + command: action.command }; } return state; diff --git a/tools/webide/packages/client/src/services/api.ts b/tools/webide/packages/client/src/services/api.ts index 14d89b391..39defca87 100644 --- a/tools/webide/packages/client/src/services/api.ts +++ b/tools/webide/packages/client/src/services/api.ts @@ -36,6 +36,23 @@ export async function compileExpression( return response.data; } +export async function compileStorage( + syntax: Language, + code: string, + entrypoint: string, + storage: string, + format?: string +) { + const response = await axios.post('/api/compile-storage', { + syntax, + code, + entrypoint, + storage, + format + }); + return response.data; +} + export async function dryRun( syntax: Language, code: string, diff --git a/tools/webide/packages/e2e/test/common-utils.js b/tools/webide/packages/e2e/test/common-utils.js index 4b6c53cc1..68cddbadc 100644 --- a/tools/webide/packages/e2e/test/common-utils.js +++ b/tools/webide/packages/e2e/test/common-utils.js @@ -79,6 +79,19 @@ exports.verifyAllExamples = async (action, done) => { done(); }; +exports.verifyWithParameter = async (command, parameter, value, action, done) => { + await page.click('#command-select'); + await page.click(`#${command}`); + + await page.click(`#${parameter}`); + await exports.clearText(page.keyboard); + await page.keyboard.type(value); + + expect(await action()).toEqual(`Error: "${parameter}" is not allowed to be empty`); + + done(); +} + exports.verifyWithBlankParameter = async (command, parameter, action, done) => { await page.click('#command-select'); await page.click(`#${command}`); diff --git a/tools/webide/packages/e2e/test/deploy.spec.js b/tools/webide/packages/e2e/test/deploy.spec.js new file mode 100644 index 000000000..ba0424565 --- /dev/null +++ b/tools/webide/packages/e2e/test/deploy.spec.js @@ -0,0 +1,38 @@ +const commonUtils = require('./common-utils'); + +const API_HOST = commonUtils.API_HOST; + +const runCommandAndGetOutputFor = commonUtils.runCommandAndGetOutputFor; +const clearText = commonUtils.clearText; + +const COMMAND = 'deploy'; +const COMMAND_ENDPOINT = 'deploy'; + +async function deploy() { + return await runCommandAndGetOutputFor(COMMAND, COMMAND_ENDPOINT); +} + +describe('Deploy contract', () => { + beforeAll(() => jest.setTimeout(60000)); + + beforeEach(async () => await page.goto(API_HOST)); + + it('should deploy', async done => { + expect(await deploy()).toContain('The contract was successfully deployed to the babylonnet test network.'); + + done(); + }); + + it('should fail to deploy contract with invalid storage', async done => { + await page.click('#command-select'); + await page.click(`#deploy`); + + await page.click(`#storage`); + await clearText(page.keyboard); + await page.keyboard.type('asdf'); + + expect(await deploy()).toContain('Error: '); + + done(); + }); +}); diff --git a/tools/webide/packages/server/src/handlers/deploy.ts b/tools/webide/packages/server/src/handlers/deploy.ts index 0fabd925f..2d24dfd3e 100644 --- a/tools/webide/packages/server/src/handlers/deploy.ts +++ b/tools/webide/packages/server/src/handlers/deploy.ts @@ -40,10 +40,12 @@ export async function deployHandler(req: Request, res: Response) { 'json' ); - const michelsonStorage = await new LigoCompiler().compileExpression( + const michelsonStorage = await new LigoCompiler().compileStorage( body.syntax, - body.storage, - 'json' + body.code, + body.entrypoint, + 'json', + body.storage ); await Tezos.importKey(await fetchRandomPrivateKey()); diff --git a/tools/webide/packages/server/src/ligo-compiler.ts b/tools/webide/packages/server/src/ligo-compiler.ts index b239cab73..53b575b3c 100644 --- a/tools/webide/packages/server/src/ligo-compiler.ts +++ b/tools/webide/packages/server/src/ligo-compiler.ts @@ -120,6 +120,7 @@ export class LigoCompiler { format: string ) { const { name, remove } = await this.createTemporaryFile(code); + try { const result = await this.execPromise(this.ligoCmd, [ 'compile-contract', @@ -148,6 +149,33 @@ export class LigoCompiler { return result; } + async compileStorage( + syntax: string, + code: string, + entrypoint: string, + format: string, + storage: string + ) { + const { name, remove } = await this.createTemporaryFile(code); + + try { + const result = await this.execPromise(this.ligoCmd, [ + 'compile-storage', + '--michelson-format', + format, + '-s', + syntax, + name, + entrypoint, + storage + ]); + + return result; + } finally { + remove(); + } + } + async dryRun( syntax: string, code: string, diff --git a/tools/webide/packages/server/test/ligo-compiler.spec.ts b/tools/webide/packages/server/test/ligo-compiler.spec.ts new file mode 100644 index 000000000..f5dbd0109 --- /dev/null +++ b/tools/webide/packages/server/test/ligo-compiler.spec.ts @@ -0,0 +1,37 @@ +import { LigoCompiler } from '../src/ligo-compiler'; + +const PASCALIGO_CODE = ` +type action is +| Increment of int +| Decrement of int + +function add (const a : int ; const b : int) : int is + block { skip } with a + b + +function subtract (const a : int ; const b : int) : int is + block { skip } with a - b + +function main (const p : action ; const s : int) : + (list(operation) * int) is + block { skip } with ((nil : list(operation)), + case p of + | Increment(n) -> add(s, n) + | Decrement(n) -> subtract(s, n) + end) +`; + +describe('Ligo compiler', () => { + it('should compile storage', async done => { + const michelsonCode = await new LigoCompiler().compileStorage( + 'pascaligo', + PASCALIGO_CODE, + 'main', + 'json', + '0' + ); + + expect(michelsonCode.trim()).toEqual('{ "int": "0" }'); + + done(); + }); +});