diff --git a/src/common/compute/interactive/message.js b/src/common/compute/interactive/message.js
index 5fee7dc2e..53c68604d 100644
--- a/src/common/compute/interactive/message.js
+++ b/src/common/compute/interactive/message.js
@@ -10,7 +10,7 @@
}(this, function() {
const Constants = makeEnum('STDOUT', 'STDERR', 'RUN', 'ADD_ARTIFACT', 'KILL',
'ADD_FILE', 'REMOVE_FILE', 'ADD_USER_DATA', 'COMPLETE', 'ERROR', 'SET_ENV',
- 'SAVE_ARTIFACT');
+ 'SAVE_ARTIFACT', 'STATUS');
function makeEnum() {
const names = Array.prototype.slice.call(arguments);
diff --git a/src/common/compute/interactive/session-with-queue.js b/src/common/compute/interactive/session-with-queue.js
index c402900a3..40fe6e3cd 100644
--- a/src/common/compute/interactive/session-with-queue.js
+++ b/src/common/compute/interactive/session-with-queue.js
@@ -63,8 +63,8 @@ define([
}
}
- static async new(id, config) {
- return await Session.new(id, config, SessionWithQueue);
+ static new(id, config) {
+ return Session.new(id, config, SessionWithQueue);
}
}
diff --git a/src/common/compute/interactive/session.js b/src/common/compute/interactive/session.js
index e8d9d2b06..1f8580efc 100644
--- a/src/common/compute/interactive/session.js
+++ b/src/common/compute/interactive/session.js
@@ -163,38 +163,60 @@ define([
return new Session(this.channel);
}
- static async new(computeID, config={}, SessionClass=InteractiveSession) {
- const channel = await createMessageChannel(computeID, config);
- const session = new SessionClass(channel);
- return session;
- }
- }
-
- async function createMessageChannel(computeID, config) {
- const address = gmeConfig.extensions.InteractiveComputeHost ||
- getDefaultServerURL();
-
- const connectedWs = await new Promise((resolve, reject) => {
- const ws = new WebSocket(address);
- ws.onopen = () => {
- ws.send(JSON.stringify([computeID, config, getGMEToken()]));
- ws.onmessage = async (wsMsg) => {
- const data = await Task.getMessageData(wsMsg);
-
- const msg = Message.decode(data);
- if (msg.type === Message.COMPLETE) {
- const err = msg.data;
- if (err) {
+ static new(computeID, config={}, SessionClass=InteractiveSession) {
+ const address = gmeConfig.extensions.InteractiveComputeHost ||
+ getDefaultServerURL();
+
+ let createSession;
+ createSession = new PromiseEvents(function(resolve, reject) {
+ const ws = new WebSocket(address);
+ ws.onopen = () => {
+ ws.send(JSON.stringify([computeID, config, getGMEToken()]));
+ ws.onmessage = async (wsMsg) => {
+ const data = await Task.getMessageData(wsMsg);
+
+ const msg = Message.decode(data);
+ if (msg.type === Message.COMPLETE) {
+ const err = msg.data;
+ if (err) {
+ reject(err);
+ } else {
+ const channel = new MessageChannel(ws);
+ const session = new SessionClass(channel);
+ resolve(session);
+ }
+ } else if (msg.type === Message.ERROR) {
+ const err = msg.data;
reject(err);
- } else {
- resolve(ws);
+ } else if (msg.type === Message.STATUS) {
+ createSession.emit('update', msg.data);
}
- }
+ };
};
- };
- });
+ });
+
+ return createSession;
+ }
+ }
- return new MessageChannel(connectedWs);
+ class PromiseEvents extends Promise {
+ constructor(fn) {
+ super(fn);
+ this._handlers = {};
+ }
+
+ on(event, fn) {
+ if (!this._handlers[event]) {
+ this._handlers[event] = [];
+ }
+ this._handlers[event].push(fn);
+ }
+
+ emit(event) {
+ const handlers = this._handlers[event] || [];
+ const args = Array.prototype.slice.call(arguments, 1);
+ handlers.forEach(fn => fn.apply(null, args));
+ }
}
function getDefaultServerURL() {
diff --git a/src/routers/InteractiveCompute/Session.js b/src/routers/InteractiveCompute/Session.js
index 3513aa714..05f3a3d5f 100644
--- a/src/routers/InteractiveCompute/Session.js
+++ b/src/routers/InteractiveCompute/Session.js
@@ -40,11 +40,14 @@ class Session extends EventEmitter {
const name = 'DeepForge Interactive Session';
const computeJob = new ComputeJob(hash, name);
this.jobInfo = this.compute.startJob(computeJob);
+ this.compute.on('update', async (id, status) =>
+ this.clientSocket.send(Message.encode(-1, Message.STATUS, status))
+ );
this.compute.on('end', async (id, info) => {
const isError = this.clientSocket.readyState === WebSocket.OPEN &&
info.status !== ComputeClient.SUCCESS;
if (isError) {
- this.clientSocket.send(Message.encode(Message.ERROR, info));
+ this.clientSocket.send(Message.encode(-1, Message.ERROR, info));
}
await this.compute.purgeJob(computeJob);
});
diff --git a/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js b/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js
index 7dd40392e..ec8b6cb57 100644
--- a/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js
+++ b/src/visualizers/widgets/InteractiveEditor/InteractiveEditorWidget.js
@@ -14,6 +14,8 @@ define([
DeepForge,
) {
const COMPUTE_MESSAGE = 'Compute Required. Click to configure.';
+ const COMPUTE_LOADING_MESSAGE = 'Connecting to Compute Instance...';
+ const LoaderHTML = '
';
class InteractiveEditorWidget {
constructor(container) {
this.showComputeShield(container);
@@ -22,13 +24,21 @@ define([
showComputeShield(container) {
const overlay = $('', {class: 'compute-shield'});
container.append(overlay);
- const msg = $('
');
- msg.text(COMPUTE_MESSAGE);
+ overlay.append($('', {class: 'filler'}));
+ const loader = $(LoaderHTML);
+ overlay.append(loader);
+ const msg = $('', {class: 'title'});
overlay.append(msg);
+ const subtitle = $('', {class: 'subtitle'});
+ overlay.append(subtitle);
+ msg.text(COMPUTE_MESSAGE);
+ loader.addClass('hidden');
+ subtitle.addClass('hidden');
+
overlay.on('click', async () => {
const {id, config} = await this.promptComputeConfig();
try {
- this.session = await this.createInteractiveSession(id, config);
+ this.session = await this.createInteractiveSession(id, config, overlay);
const features = this.getCapabilities();
if (features.save) {
DeepForge.registerAction('Save', 'save', 10, () => this.save());
@@ -38,6 +48,10 @@ define([
} catch (err) {
const title = 'Compute Creation Error';
const body = 'Unable to create compute. Please verify the credentials are correct.';
+ msg.text(COMPUTE_MESSAGE);
+ loader.addClass('hidden');
+ subtitle.addClass('hidden');
+
// TODO: Detect authorization errors...
const dialog = new InformDialog(title, body);
dialog.show();
@@ -94,8 +108,35 @@ define([
return {id, config};
}
- async createInteractiveSession(computeId, config) {
- return await Session.new(computeId, config);
+ showComputeLoadingStatus(status, overlay) {
+ const msg = overlay.find('.subtitle');
+ const loader = overlay.find('.lds-ripple');
+ const title = overlay.find('.title');
+
+ title.text(COMPUTE_LOADING_MESSAGE);
+ loader.removeClass('hidden');
+ msg.removeClass('hidden');
+ return msg;
+ }
+
+ updateComputeLoadingStatus(status, subtitle) {
+ const displayText = status === 'running' ?
+ 'Configuring environment' :
+ status.substring(0, 1).toUpperCase() + status.substring(1);
+ subtitle.text(`${displayText}...`);
+ }
+
+ async createInteractiveSession(computeId, config, overlay) {
+ const createSession = Session.new(computeId, config);
+
+ const msg = this.showComputeLoadingStatus(status, overlay);
+ this.updateComputeLoadingStatus('Connecting', msg);
+ createSession.on(
+ 'update',
+ status => this.updateComputeLoadingStatus(status, msg)
+ );
+ const session = await createSession;
+ return session;
}
destroy() {
@@ -103,6 +144,7 @@ define([
if (features.save) {
DeepForge.unregisterAction('Save');
}
+ this.session.close();
}
updateNode(/*desc*/) {
diff --git a/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css
index 397cb14e3..85adc98d7 100644
--- a/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css
+++ b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.css
@@ -13,10 +13,46 @@
top: 0;
z-index: 100; }
-.compute-shield span {
+.compute-shield .invisible {
+ visibility: hidden; }
+.compute-shield .filler {
+ margin-top: 25%; }
+.compute-shield .title {
color: whitesmoke;
display: block;
font-size: 2em;
margin: auto;
- padding-top: 25%;
text-align: center; }
+.compute-shield .subtitle {
+ color: whitesmoke;
+ display: block;
+ font-size: 1.5em;
+ margin: auto;
+ text-align: center; }
+.compute-shield .lds-ripple {
+ margin: auto;
+ display: block;
+ position: relative;
+ width: 80px;
+ height: 80px; }
+.compute-shield .lds-ripple div {
+ position: absolute;
+ border: 4px solid #fff;
+ opacity: 1;
+ border-radius: 50%;
+ animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; }
+.compute-shield .lds-ripple div:nth-child(2) {
+ animation-delay: -0.5s; }
+@keyframes lds-ripple {
+ 0% {
+ top: 36px;
+ left: 36px;
+ width: 0;
+ height: 0;
+ opacity: 1; }
+ 100% {
+ top: 0px;
+ left: 0px;
+ width: 72px;
+ height: 72px;
+ opacity: 0; } }
diff --git a/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss
index 47ca20ac9..a43eee1a6 100644
--- a/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss
+++ b/src/visualizers/widgets/InteractiveEditor/styles/InteractiveEditorWidget.scss
@@ -16,11 +16,62 @@
z-index: 100;
}
-.compute-shield span {
- color: whitesmoke;
- display: block;
- font-size: 2em;
- margin: auto;
- padding-top: 25%;
- text-align: center;
+.compute-shield {
+ .hidden {
+ display: none;
+ }
+
+ .filler {
+ margin-top: 25%;
+ }
+
+ .title {
+ color: whitesmoke;
+ display: block;
+ font-size: 2em;
+ margin: auto;
+ text-align: center;
+ }
+
+ .subtitle {
+ color: whitesmoke;
+ display: block;
+ font-size: 1.5em;
+ margin: auto;
+ text-align: center;
+ }
+
+ .lds-ripple {
+ margin: auto;
+ display: block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+ }
+ .lds-ripple div {
+ position: absolute;
+ border: 4px solid #fff;
+ opacity: 1;
+ border-radius: 50%;
+ animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
+ }
+ .lds-ripple div:nth-child(2) {
+ animation-delay: -0.5s;
+ }
+ @keyframes lds-ripple {
+ 0% {
+ top: 36px;
+ left: 36px;
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+ 100% {
+ top: 0px;
+ left: 0px;
+ width: 72px;
+ height: 72px;
+ opacity: 0;
+ }
+ }
}
diff --git a/src/visualizers/widgets/TensorPlotter/TensorPlotterWidget.js b/src/visualizers/widgets/TensorPlotter/TensorPlotterWidget.js
index d23d7f8d0..ab5241290 100644
--- a/src/visualizers/widgets/TensorPlotter/TensorPlotterWidget.js
+++ b/src/visualizers/widgets/TensorPlotter/TensorPlotterWidget.js
@@ -64,13 +64,6 @@ define([
this._logger.debug('ctor finished');
}
- async createInteractiveSession(computeId, config) {
- const session = await Session.new(computeId, config);
- this.initSession(session);
- this.artifactLoader.session = session;
- return session;
- }
-
async getAuthenticationConfig (dataInfo) {
const {backend} = dataInfo;
const metadata = Storage.getStorageMetadata(backend);
@@ -87,8 +80,8 @@ define([
}
}
- async initSession (session) {
- await session.whenConnected();
+ async onComputeInitialized (session) {
+ this.artifactLoader.session = session;
const initCode = await this.getInitializationCode();
await session.addFile('utils/init.py', initCode);
await session.addFile('utils/explorer_helpers.py', HELPERS_PY);