diff --git a/packages/app/scripts/start.js b/packages/app/scripts/start.js index 041f7e8692d..3eb1427ce4c 100644 --- a/packages/app/scripts/start.js +++ b/packages/app/scripts/start.js @@ -212,15 +212,6 @@ function addMiddleware(devServer, index) { changeOrigin: true, }) ); - - // devServer.use( - // '/socket.io', - // proxy({ - // target: 'https://sse.codesandbox.io', - // changeOrigin: true, - // secure: false, - // }) - // ); } if (process.env.VSCODE) { devServer.use( diff --git a/packages/app/src/app/components/Preview/DevTools/Tabs/index.tsx b/packages/app/src/app/components/Preview/DevTools/Tabs/index.tsx index 2bb70534e82..8ceb57b55cc 100644 --- a/packages/app/src/app/components/Preview/DevTools/Tabs/index.tsx +++ b/packages/app/src/app/components/Preview/DevTools/Tabs/index.tsx @@ -36,7 +36,7 @@ const DevToolTabs = ({ const currentPane = panes[currentPaneIndex]; const actions = typeof currentPane.actions === 'function' - ? currentPane.actions(owned) + ? currentPane.actions({ owned }) : currentPane.actions; const TypedTabDropZone = (TabDropZone as unknown) as React.SFC< @@ -89,21 +89,27 @@ const DevToolTabs = ({ - {actions.map(({ title, onClick, Icon }) => ( - + ); + })} ); diff --git a/packages/app/src/app/components/Preview/DevTools/Terminal/index.js b/packages/app/src/app/components/Preview/DevTools/Terminal/index.js index 021a832daed..7e538b93456 100644 --- a/packages/app/src/app/components/Preview/DevTools/Terminal/index.js +++ b/packages/app/src/app/components/Preview/DevTools/Terminal/index.js @@ -229,16 +229,17 @@ export default { id: 'codesandbox.terminal', title: 'Terminal', Content: withTheme(TerminalComponent), - actions: (owner: boolean) => + actions: ({ owned }) => [ - owner && { - title: 'Add Terminal', + { + title: owned ? 'Add Terminal' : 'Fork to add a terminal', onClick: () => { - if (createShell) { + if (createShell && owned) { createShell(); } }, Icon: PlusIcon, + disabled: !owned, }, ].filter(Boolean), }; diff --git a/packages/app/src/app/components/Preview/DevTools/index.tsx b/packages/app/src/app/components/Preview/DevTools/index.tsx index c250703819a..5f40889d1fb 100644 --- a/packages/app/src/app/components/Preview/DevTools/index.tsx +++ b/packages/app/src/app/components/Preview/DevTools/index.tsx @@ -46,13 +46,14 @@ export interface IViewAction { title: string; onClick: () => void; Icon: React.ComponentClass; + disabled?: boolean; } export interface IViewType { id: string; title: string; Content: React.ComponentClass; - actions: IViewAction[] | ((owner: boolean) => IViewAction[]); + actions: IViewAction[] | ((info: { owned: boolean }) => IViewAction[]); } export type StatusType = 'info' | 'warning' | 'error' | 'success' | 'clear'; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Content/Preview/index.js b/packages/app/src/app/pages/Sandbox/Editor/Content/Preview/index.js index 8d815ef0f8e..50878e4a206 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Content/Preview/index.js +++ b/packages/app/src/app/pages/Sandbox/Editor/Content/Preview/index.js @@ -217,9 +217,14 @@ class Preview extends Component { onToggleProjectView={() => signals.editor.projectViewToggled()} showDevtools={store.preferences.showDevtools} isResizing={store.editor.isResizing} - setServerStatus={(status: string) => { + setSSEManagerStatus={(status: string) => { signals.server.statusChanged({ status }); }} + setSSEContainerStatus={(status: string) => { + signals.server.containerStatusChanged({ status }); + }} + managerStatus={store.server.status} + containerStatus={store.server.containerStatus} syncSandbox={signals.files.syncSandbox} /> ) : ( diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/Status.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/Status.js deleted file mode 100644 index a3b5b30da95..00000000000 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/Status.js +++ /dev/null @@ -1,53 +0,0 @@ -// @flow -import React from 'react'; -import styled from 'styled-components'; - -type Props = { - status: - | 'connected' - | 'disconnected' - | 'initializing' - | 'hibernated' - | 'error', -}; - -const StatusCircle = styled.div` - border-radius: 50%; - background-color: ${props => props.color}; - width: 8px; - height: 8px; - margin-left: 0.75rem; /* Very precise to keep aligned with script icons */ - margin-right: 0.75rem; -`; - -const Container = styled.div` - display: flex; - align-items: center; - - color: ${props => - props.theme['editor.foreground'] || 'rgba(255, 255, 255, 0.8)'}; -`; - -const STATUS_MESSAGES = { - disconnected: 'Reconnecting to sandbox...', - connected: 'Connected to sandbox', - initializing: 'Initializing connection to sandbox...', - hibernated: 'Sandbox hibernated', - error: 'Unrecoverable sandbox error', -}; - -const STATUS_COLOR = { - disconnected: '#FD2439', - connected: '#4CFF00', - initializing: '#FFD399', - hibernated: '#FF662E', - error: '#FD2439', -}; - -export default ({ status }: Props) => ( - - - - {STATUS_MESSAGES[status]} - -); diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/Status.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/Status.tsx new file mode 100644 index 00000000000..bbba84098af --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/Status.tsx @@ -0,0 +1,94 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; + +import { + SSEContainerStatus, + SSEManagerStatus, +} from '@codesandbox/common/lib/types'; + +type Props = { + containerStatus: SSEContainerStatus; + managerStatus: SSEManagerStatus; +}; + +const StatusCircle = styled.div` + border-radius: 50%; + background-color: ${props => props.color}; + width: 8px; + height: 8px; + margin-left: 0.75rem; /* Very precise to keep aligned with script icons */ + margin-right: 0.75rem; +`; + +const Container = styled.div` + display: flex; + align-items: center; + + color: ${props => + props.theme['editor.foreground'] || 'rgba(255, 255, 255, 0.8)'}; +`; + +const STATUS_MESSAGES = { + disconnected: 'Reconnecting to sandbox...', + connected: 'Connected to sandbox', + initializing: 'Initializing connection to sandbox...', + hibernated: 'Sandbox hibernated', + error: 'Unrecoverable sandbox error', +}; + +const STATUS_COLOR = { + disconnected: '#FD2439', + connected: '#4CFF00', + initializing: '#FFD399', + hibernated: '#FF662E', + error: '#FD2439', +}; + +function getContainerStatusMessageAndColor( + containerStatus: SSEContainerStatus +) { + switch (containerStatus) { + case 'initializing': + return { color: '#FFD399', message: 'Container is starting...' }; + case 'container-started': + case 'stopped': + return { color: '#FFD399', message: 'Sandbox is starting...' }; + case 'sandbox-started': + return { color: '#4CFF00', message: 'Connected to sandbox' }; + case 'error': + return { color: '#FD2439', message: 'A sandbox error occurred' }; + case 'hibernated': + return { color: '#FF662E', message: 'Sandbox hibernated' }; + default: + return undefined; + } +} + +function getManagerStatusMessageAndColor(managerStatus: SSEManagerStatus) { + switch (managerStatus) { + case 'connected': + case 'disconnected': + return undefined; + case 'initializing': + return { + message: 'Initializing connection to sandbox...', + color: '#FFD399', + }; + default: + return undefined; + } +} + +export default ({ containerStatus, managerStatus }: Props) => { + const { color, message } = + getManagerStatusMessageAndColor(managerStatus) || + getContainerStatusMessageAndColor(containerStatus); + return ( + + + + {message} + + ); +}; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/index.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/index.js index 77db1a31e56..b6ca65683f3 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/index.js +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/items/Server/index.js @@ -23,9 +23,8 @@ const SubTitle = styled.div` font-size: 0.875rem; `; -const Server = ({ store }) => { +function Server({ store }) { const { parsed } = store.editor.parsedConfigurations.package; - const disconnected = store.server.status !== 'connected'; return ( @@ -38,7 +37,10 @@ const Server = ({ store }) => { Status - + @@ -58,17 +60,6 @@ const Server = ({ store }) => { - - Secret Keys - - Secrets are available as environment variables. They are kept private - and will not be transferred between forks. - - - - - - Control Container @@ -80,10 +71,12 @@ const Server = ({ store }) => { }} small block - disabled={disconnected} - onClick={() => - dispatch({ type: 'socket:message', channel: 'sandbox:restart' }) + disabled={ + disconnected || store.server.containerStatus !== 'sandbox-started' } + onClick={() => { + dispatch({ type: 'socket:message', channel: 'sandbox:restart' }); + }} > { Restart Sandbox + + + + + + + Secret Keys + + Secrets are available as environment variables. They are kept private + and will not be transferred between forks. + + + + ); -}; +} -export default inject('store')(observer(Server)); +export default inject('store', 'signals')(observer(Server)); diff --git a/packages/app/src/app/store/modules/server/index.js b/packages/app/src/app/store/modules/server/index.js index 0b83a40a966..cacccb7244b 100644 --- a/packages/app/src/app/store/modules/server/index.js +++ b/packages/app/src/app/store/modules/server/index.js @@ -6,8 +6,10 @@ export default Module({ model, state: { status: 'initializing', + containerStatus: 'initializing', }, signals: { statusChanged: sequences.setStatus, + containerStatusChanged: sequences.setContainerStatus, }, }); diff --git a/packages/app/src/app/store/modules/server/model.js b/packages/app/src/app/store/modules/server/model.js index a13f5db7a84..ef60e8609d8 100644 --- a/packages/app/src/app/store/modules/server/model.js +++ b/packages/app/src/app/store/modules/server/model.js @@ -2,9 +2,15 @@ import { types } from 'mobx-state-tree'; export default { status: types.enumeration('status', [ + 'initializing', 'connected', 'disconnected', + ]), + containerStatus: types.enumeration('container-status', [ 'initializing', + 'container-started', + 'sandbox-started', + 'stopped', 'hibernated', 'error', ]), diff --git a/packages/app/src/app/store/modules/server/sequences.js b/packages/app/src/app/store/modules/server/sequences.js index 5ff7b59b8a5..28dcd3ff4b0 100644 --- a/packages/app/src/app/store/modules/server/sequences.js +++ b/packages/app/src/app/store/modules/server/sequences.js @@ -2,3 +2,7 @@ import { set } from 'cerebral/operators'; import { state, props } from 'cerebral/tags'; export const setStatus = [set(state`server.status`, props`status`)]; + +export const setContainerStatus = [ + set(state`server.containerStatus`, props`status`), +]; diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 169f3cf3b65..26fcb21cf54 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -5,6 +5,7 @@ "esModuleInterop": true, "baseUrl": "./", "paths": { + "types": ["../common/src/types/index"], "app/*": ["./src/app/*"], "embed/*": ["./src/embed/*"], "sandbox/*": ["./src/sandbox/*"] diff --git a/packages/common/src/components/Preview/elements.ts b/packages/common/src/components/Preview/elements.ts index 3ea2eb160b8..9912608a77d 100644 --- a/packages/common/src/components/Preview/elements.ts +++ b/packages/common/src/components/Preview/elements.ts @@ -36,7 +36,7 @@ export const Loading = styled.div` align-items: center; justify-content: center; - font-size: 2rem; + font-size: 1.5rem; font-weight: 300; color: white; line-height: 1.3; diff --git a/packages/common/src/components/Preview/index.tsx b/packages/common/src/components/Preview/index.tsx index de3f21f074b..f452e6b5bb6 100644 --- a/packages/common/src/components/Preview/index.tsx +++ b/packages/common/src/components/Preview/index.tsx @@ -1,6 +1,11 @@ // @flow import * as React from 'react'; -import { Sandbox, Module } from '../../types'; +import { + Sandbox, + Module, + SSEManagerStatus, + SSEContainerStatus, +} from '../../types'; import { listen, dispatch, @@ -44,7 +49,10 @@ export type Props = { noPreview?: boolean; alignDirection?: 'right' | 'bottom'; delay?: number; - setServerStatus?: (status: string) => void; + setSSEManagerStatus?: (status: SSEManagerStatus) => void; + setSSEContainerStatus?: (status: SSEContainerStatus) => void; + managerStatus?: SSEManagerStatus; + containerStatus?: SSEContainerStatus; syncSandbox?: (updates: any) => void; className?: string; }; @@ -231,10 +239,10 @@ class BasePreview extends React.Component { setupSSESockets = async () => { const hasInitialized = !!this.$socket; - function onTimeout(comp) { + function onTimeout(comp: BasePreview) { comp.connectTimeout = null; - if (comp.props.setServerStatus) { - comp.props.setServerStatus('disconnected'); + if (comp.props.setSSEManagerStatus) { + comp.props.setSSEManagerStatus('disconnected'); } } @@ -267,14 +275,12 @@ class BasePreview extends React.Component { return; } - if (this.props.setServerStatus) { - let status = 'disconnected'; - if (this.state.hibernated) { - status = 'hibernated'; - } else if (this.state.sseError) { - status = 'error'; - } - this.props.setServerStatus(status); + if ( + this.props.setSSEManagerStatus && + this.props.managerStatus === 'connected' && + this.props.containerStatus !== 'hibernated' + ) { + this.props.setSSEManagerStatus('disconnected'); dispatch({ type: 'codesandbox:sse:disconnect' }); } }); @@ -285,8 +291,8 @@ class BasePreview extends React.Component { this.connectTimeout = null; } - if (this.props.setServerStatus) { - this.props.setServerStatus('connected'); + if (this.props.setSSEManagerStatus) { + this.props.setSSEManagerStatus('connected'); } const { id } = this.props.sandbox; @@ -322,28 +328,49 @@ class BasePreview extends React.Component { } }); + socket.on('sandbox:status', message => { + if (this.props.setSSEContainerStatus) { + if (message.status === 'starting-container') { + this.props.setSSEContainerStatus('initializing'); + } else if (message.status === 'installing-packages') { + this.props.setSSEContainerStatus('container-started'); + } + } + }); + socket.on('sandbox:start', () => { sseTerminalMessage(`sandbox ${this.props.sandbox.id} started.`); if (!this.state.frameInitialized && this.props.onInitialized) { this.disposeInitializer = this.props.onInitialized(this); } - this.handleRefresh(); + this.setState({ frameInitialized: true, overlayMessage: null, }); + if (this.props.setSSEContainerStatus) { + this.props.setSSEContainerStatus('sandbox-started'); + } + + setTimeout(() => { + this.executeCodeImmediately(true); + this.handleRefresh(); + }); }); socket.on('sandbox:hibernate', () => { sseTerminalMessage(`sandbox ${this.props.sandbox.id} hibernated.`); + if (this.props.setSSEContainerStatus) { + this.props.setSSEContainerStatus('hibernated'); + } + this.setState( { frameInitialized: false, overlayMessage: 'The sandbox was hibernated because of inactivity. Refresh the page to restart it.', - hibernated: true, }, () => this.$socket.close() ); @@ -352,6 +379,10 @@ class BasePreview extends React.Component { socket.on('sandbox:stop', () => { sseTerminalMessage(`sandbox ${this.props.sandbox.id} restarting...`); + if (this.props.setSSEContainerStatus) { + this.props.setSSEContainerStatus('stopped'); + } + this.setState({ frameInitialized: false, overlayMessage: 'Restarting the sandbox...', @@ -607,8 +638,11 @@ class BasePreview extends React.Component { const modulesToSend = this.getModulesToSend(); if (this.serverPreview) { const diff = getDiff(this.lastSent.modules, modulesToSend); - - this.lastSent.modules = modulesToSend; + if (this.props.containerStatus === 'sandbox-started') { + // Only mark the last modules if we're sure that the container has been able + // to process the last diff + this.lastSent.modules = modulesToSend; + } if (Object.keys(diff).length > 0 && this.$socket) { this.$socket.emit('sandbox:update', diff); diff --git a/packages/common/src/global.d.ts b/packages/common/src/types/global.d.ts similarity index 100% rename from packages/common/src/global.d.ts rename to packages/common/src/types/global.d.ts diff --git a/packages/common/src/types.ts b/packages/common/src/types/index.ts similarity index 94% rename from packages/common/src/types.ts rename to packages/common/src/types/index.ts index 76adacaed79..e329e61dda2 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types/index.ts @@ -1,5 +1,14 @@ import * as React from 'react'; -import { TemplateType } from './templates'; +import { TemplateType } from '../templates'; + +export type SSEContainerStatus = + | 'initializing' + | 'container-started' + | 'sandbox-started' + | 'stopped' + | 'hibernated' + | 'error'; +export type SSEManagerStatus = 'connected' | 'disconnected' | 'initializing'; export type ModuleError = { message: string; diff --git a/standalone-packages/sse-loading-screen/src/Cube.js b/standalone-packages/sse-loading-screen/src/Cube.js index d7195d786e5..4d153c96552 100644 --- a/standalone-packages/sse-loading-screen/src/Cube.js +++ b/standalone-packages/sse-loading-screen/src/Cube.js @@ -15,14 +15,13 @@ const SHADOW_SIZE = (() => { } if (isSafari) { - return 100; + return 50; } - return 100; + return 50; })(); -const getContainerAnimation = (offset: number) => { - return keyframes` +const getContainerAnimation = () => keyframes` 0% { transform: translateY(-5px); } @@ -33,7 +32,6 @@ const getContainerAnimation = (offset: number) => { transform: translateY(-5px); } `; -}; const Container = styled.div` animation: ${() => getContainerAnimation(0)} 8 s ease infinite;