Skip to content

Commit 8896135

Browse files
authored
⚗️ [Frontend] PoC: Rocket preview (#8378)
1 parent 18249a4 commit 8896135

File tree

8 files changed

+280
-5
lines changed

8 files changed

+280
-5
lines changed

services/static-webserver/client/source/class/osparc/auth/ui/Login2FAValidationCodeView.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ qx.Class.define("osparc.auth.ui.Login2FAValidationCodeView", {
175175
this.__validateCodeBtn.setFetching(true);
176176

177177
const validationCodeTF = this._form.getItems()["validationCode"];
178-
const validationCode = validationCodeTF.getValue();
178+
const validationCode = validationCodeTF.getValue().trim();
179179

180180
const loginFun = log => {
181181
this.__validateCodeBtn.setFetching(false);

services/static-webserver/client/source/class/osparc/navigation/UserMenu.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ qx.Class.define("osparc.navigation.UserMenu", {
9898
control.addListener("execute", () => osparc.vipMarket.MarketWindow.openWindow());
9999
this.add(control);
100100
break;
101+
case "rocket-preview":
102+
control = new qx.ui.menu.Button(this.tr("Rocket Preview"));
103+
control.addListener("execute", () => osparc.wrapper.RocketPreview.openWindow());
104+
this.add(control);
105+
break;
101106
case "about":
102107
control = new qx.ui.menu.Button(this.tr("About oSPARC"));
103108
osparc.utils.Utils.setIdToWidget(control, "userMenuAboutBtn");
@@ -177,6 +182,10 @@ qx.Class.define("osparc.navigation.UserMenu", {
177182
this.getChildControl("market");
178183
}
179184

185+
if (osparc.utils.Utils.isDevelopmentPlatform() && osparc.wrapper.RocketPreview.existsBuild()) {
186+
this.getChildControl("rocket-preview");
187+
}
188+
180189
this.getChildControl("about");
181190
if (osparc.product.Utils.showAboutProduct()) {
182191
this.getChildControl("about-product");

services/static-webserver/client/source/class/osparc/support/Conversation.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ qx.Class.define("osparc.support.Conversation", {
5858
SYSTEM_MESSAGE_TYPE: {
5959
ASK_A_QUESTION: "askAQuestion",
6060
BOOK_A_CALL: "bookACall",
61+
ESCALATE_TO_SUPPORT: "escalateToSupport",
6162
REPORT_OEC: "reportOEC",
6263
FOLLOW_UP: "followUp",
6364
},
@@ -306,6 +307,9 @@ qx.Class.define("osparc.support.Conversation", {
306307
case osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.BOOK_A_CALL:
307308
msg = greet + "Let us know what your availability is and we will get back to you shortly to schedule a meeting.";
308309
break;
310+
case osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.ESCALATE_TO_SUPPORT:
311+
msg = greet + "Our support team will take it from here — please confirm or edit your question below to get started.";
312+
break;
309313
case osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.FOLLOW_UP:
310314
msg = "A support ticket has been created.\nOur team will review your request and contact you soon.";
311315
break;

services/static-webserver/client/source/class/osparc/support/ConversationPage.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ qx.Class.define("osparc.support.ConversationPage", {
160160
return control || this.base(arguments, id);
161161
},
162162

163-
proposeConversation: function(type) {
163+
proposeConversation: function(type, prefillText) {
164164
type = type || osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.ASK_A_QUESTION;
165165
this.setConversation(null);
166166

@@ -174,11 +174,18 @@ qx.Class.define("osparc.support.ConversationPage", {
174174
case osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.BOOK_A_CALL:
175175
title.setValue(this.tr("Book a Call"));
176176
break;
177+
case osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.ESCALATE_TO_SUPPORT:
178+
title.setValue(this.tr("Ask a Question"));
179+
break;
177180
case osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.REPORT_OEC:
178181
title.setValue(this.tr("Report an Error"));
179182
break;
180183
}
181184
conversationContent.addSystemMessage(type);
185+
186+
if (prefillText) {
187+
this.getChildControl("conversation-content").getChildControl("add-message").getChildControl("comment-field").setText(prefillText);
188+
}
182189
},
183190

184191
__applyConversation: function(conversation) {

services/static-webserver/client/source/class/osparc/support/SupportCenter.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ qx.Class.define("osparc.support.SupportCenter", {
196196
}
197197
},
198198

199-
createConversation: function(type) {
199+
createConversation: function(type, prefillText) {
200200
const conversationPage = this.getChildControl("conversation-page");
201-
conversationPage.proposeConversation(type);
201+
conversationPage.proposeConversation(type, prefillText);
202202
this.__showConversation();
203203
},
204204
}

services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,16 @@ qx.Class.define("osparc.widget.PersistentIframe", {
358358
}
359359
break;
360360
}
361+
// { type: "openSupport", message: {question: "", answer: ""} }
361362
case "openSupport": {
362-
osparc.support.SupportCenter.openWindow();
363+
const supportCenterWindow = osparc.support.SupportCenter.openWindow();
364+
// for now prefill the text box with the question
365+
if (data["message"] && data["message"]["question"]) {
366+
supportCenterWindow.proposeConversation(
367+
osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.ESCALATE_TO_SUPPORT,
368+
`From your last question: "${data["message"]["question"]}"`
369+
);
370+
}
363371
break;
364372
}
365373
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/* ************************************************************************
2+
3+
osparc - the simcore frontend
4+
5+
https://osparc.io
6+
7+
Copyright:
8+
2025 IT'IS Foundation, https://itis.swiss
9+
10+
License:
11+
MIT: https://opensource.org/licenses/MIT
12+
13+
Authors:
14+
* Odei Maiz (odeimaiz)
15+
16+
************************************************************************ */
17+
18+
/**
19+
* @asset(rocketPreview/build/index.html)
20+
* @asset(rocketPreview/build/**)
21+
* @asset(rocketPreview/osparc-bridge.js) // index.html needs to include it
22+
*/
23+
24+
/**
25+
* A qooxdoo wrapper for The Rocket Preview
26+
* It loads the app in an iframe and communicates via postMessage.
27+
* NOTES
28+
* In order to make this work, the Rocket Preview build needs to include the osparc-bridge.js script.
29+
* Add the following to the index.html
30+
* <script src="../osparc-bridge.js"></script>
31+
* Also, the include paths in the index.html need to be adjusted, so that are relative to the index.html.
32+
*/
33+
34+
qx.Class.define("osparc.wrapper.RocketPreview", {
35+
extend: qx.ui.core.Widget,
36+
37+
construct: function() {
38+
this.base(arguments);
39+
40+
this._setLayout(new qx.ui.layout.Grow());
41+
42+
this.__messageQueue = [];
43+
44+
// force creation of the iframe child control
45+
this._createChildControl("iframe");
46+
47+
window.addEventListener("message", this.__onMessage.bind(this));
48+
},
49+
50+
statics: {
51+
INDEX_HTML: "rocketPreview/build/index.html",
52+
53+
/**
54+
* Returns true if the RocketPreview build folder is available as a resource.
55+
*/
56+
existsBuild: function() {
57+
const rm = qx.util.ResourceManager.getInstance();
58+
// index.html is a good proxy for the whole build
59+
const resourceId = this.INDEX_HTML;
60+
return rm.has(resourceId);
61+
},
62+
63+
openWindow: function() {
64+
const win = new osparc.ui.window.Window();
65+
win.set({
66+
caption: "Rocket Preview",
67+
width: 800,
68+
height: 600,
69+
minWidth: 400,
70+
minHeight: 300,
71+
showMinimize: false,
72+
showMaximize: false,
73+
resizable: true,
74+
modal: true,
75+
allowClose: true,
76+
contentPadding: 0,
77+
layout: new qx.ui.layout.Grow()
78+
});
79+
80+
const rocketPreview = new osparc.wrapper.RocketPreview();
81+
win.add(rocketPreview);
82+
win.center();
83+
win.open();
84+
return win;
85+
}
86+
},
87+
88+
properties: {
89+
/**
90+
* True once the iframe signals it's ready (osparc:ready).
91+
*/
92+
rocketReady: {
93+
check: "Boolean",
94+
init: false,
95+
event: "changeReady"
96+
},
97+
},
98+
99+
members: {
100+
__messageQueue: null,
101+
__iframeEl: null,
102+
103+
_createChildControlImpl: function(id) {
104+
let control;
105+
switch (id) {
106+
case "iframe":
107+
const src = qx.util.ResourceManager.getInstance().toUri(this.self().INDEX_HTML);
108+
control = new qx.ui.embed.Html("<iframe></iframe>");
109+
control.set({
110+
allowGrowX: true,
111+
allowGrowY: true
112+
});
113+
114+
// configure the real DOM iframe element
115+
control.addListenerOnce("appear", () => {
116+
const el = control.getContentElement().getDomElement().querySelector("iframe");
117+
el.src = src;
118+
el.style.width = "100%";
119+
el.style.height = "100%";
120+
el.style.border = "0";
121+
this.__iframeEl = el;
122+
});
123+
124+
this._add(control);
125+
break;
126+
}
127+
return control || this.base(arguments, id);
128+
},
129+
130+
// ---- Public API ----
131+
setTreeData: function(data) {
132+
this.__send({type: "setTreeData", payload: data});
133+
},
134+
135+
setExtraData: function(data) {
136+
this.__send({type: "setExtraData", payload: data});
137+
},
138+
139+
setImage: function(img) {
140+
this.__send({type: "setImage", payload: img});
141+
},
142+
// --------------------
143+
144+
__send: function(msg) {
145+
if (!this.isRocketReady()) {
146+
this.__messageQueue.push(msg);
147+
return;
148+
}
149+
this.__postMessage(msg);
150+
},
151+
152+
__onMessage: function(ev) {
153+
const data = ev.data;
154+
if (data && data.type === "osparc:ready") {
155+
this.setRocketReady(true);
156+
while (this.__messageQueue.length) {
157+
this.__postMessage(this.__messageQueue.shift());
158+
}
159+
}
160+
},
161+
162+
__postMessage: function(msg) {
163+
if (this.__iframeEl && this.__iframeEl.contentWindow) {
164+
this.__iframeEl.contentWindow.postMessage(msg, "*");
165+
}
166+
},
167+
}
168+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// rocket-bridge.js
2+
(function () {
3+
const parentWin = window.parent;
4+
5+
const handlers = {
6+
setTreeData: async (payload) => {
7+
console.log("[rocketPreview] setTreeData", payload);
8+
// TODO
9+
return {
10+
ok: true
11+
};
12+
},
13+
setExtraData: async (payload) => {
14+
console.log("[rocketPreview] setExtraData", payload);
15+
// TODO
16+
return {
17+
ok: true
18+
};
19+
},
20+
setImage: async (payload) => {
21+
console.log("[rocketPreview] setImage", payload);
22+
// TODO
23+
return {
24+
ok: true
25+
};
26+
},
27+
getState: async () => {
28+
return {
29+
status: "ready"
30+
};
31+
},
32+
ping: async (payload) => {
33+
return {
34+
pong: true,
35+
t: payload?.t
36+
};
37+
}
38+
};
39+
40+
function reply(id, ok, resultOrError) {
41+
parentWin.postMessage({
42+
type: "osparc:rpc:result",
43+
id,
44+
ok,
45+
result: ok ? resultOrError : undefined,
46+
error: ok ? undefined : String(resultOrError)
47+
}, "*");
48+
}
49+
50+
window.addEventListener("message", async (ev) => {
51+
const data = ev.data;
52+
if (!data || data.type !== "osparc:rpc") {
53+
return;
54+
}
55+
const { id, action, payload, expectReply } = data;
56+
try {
57+
const fn = handlers[action];
58+
if (typeof fn !== "function") {
59+
throw new Error(`Unknown action '${action}'`);
60+
}
61+
const result = await fn(payload);
62+
if (expectReply) {
63+
reply(id, true, result);
64+
}
65+
} catch (err) {
66+
if (expectReply) {
67+
reply(id, false, err);
68+
}
69+
}
70+
});
71+
72+
// Tell osparc we’re ready
73+
window.addEventListener("DOMContentLoaded", () => {
74+
parentWin.postMessage({
75+
type: "osparc:ready",
76+
version: "1.0.0"
77+
}, "*");
78+
});
79+
})();

0 commit comments

Comments
 (0)