Skip to content

Commit c1fef80

Browse files
Merge branch 'master' into add-socketio-events-for-conversations
2 parents 4cc9fab + bfb683d commit c1fef80

File tree

7 files changed

+302
-94
lines changed

7 files changed

+302
-94
lines changed

packages/pytest-simcore/src/pytest_simcore/simcore_services.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"whoami",
4141
"sto-worker",
4242
"sto-worker-cpu-bound",
43+
"traefik-configuration-placeholder",
4344
}
4445
# TODO: unify healthcheck policies see https://github.com/ITISFoundation/osparc-simcore/pull/2281
4546
DEFAULT_SERVICE_HEALTHCHECK_ENTRYPOINT: Final[str] = "/v0/"

services/docker-compose.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ services:
5757
- traefik.http.services.${SWARM_STACK_NAME}_api-server.loadbalancer.healthcheck.path=/
5858
- traefik.http.services.${SWARM_STACK_NAME}_api-server.loadbalancer.healthcheck.interval=2000ms
5959
- traefik.http.services.${SWARM_STACK_NAME}_api-server.loadbalancer.healthcheck.timeout=1000ms
60+
# NOTE: keep in sync with fallback router (rule and entrypoint)
6061
- traefik.http.routers.${SWARM_STACK_NAME}_api-server.rule=(Path(`/`) || Path(`/v0`) || PathPrefix(`/v0/`) || Path(`/api/v0/openapi.json`))
6162
- traefik.http.routers.${SWARM_STACK_NAME}_api-server.entrypoints=simcore_api
6263
- traefik.http.routers.${SWARM_STACK_NAME}_api-server.priority=3
@@ -628,6 +629,7 @@ services:
628629
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver.loadbalancer.healthcheck.interval=2000ms
629630
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver.loadbalancer.healthcheck.timeout=1000ms
630631
- traefik.http.middlewares.${SWARM_STACK_NAME}_static_webserver_retry.retry.attempts=2
632+
# NOTE: keep in sync with fallback router (rule and entrypoint)
631633
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.rule=(Path(`/osparc`) || Path(`/s4l`) || Path(`/s4llite`) || Path(`/s4lacad`) || Path(`/s4lengine`) || Path(`/s4ldesktop`) || Path(`/s4ldesktopacad`) || Path(`/tis`) || Path(`/tiplite`) || Path(`/transpiled`) || Path(`/resource`) || PathPrefix(`/osparc/`) || PathPrefix(`/s4l/`) || PathPrefix(`/s4llite/`) || PathPrefix(`/s4lacad/`) || PathPrefix(`/s4lengine/`) || PathPrefix(`/s4ldesktop/`) || PathPrefix(`/s4ldesktopacad/`) || PathPrefix(`/tis/`) || PathPrefix(`/tiplite/`) || PathPrefix(`/transpiled/`) || PathPrefix(`/resource/`))
632634
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.service=${SWARM_STACK_NAME}_static_webserver
633635
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.entrypoints=http
@@ -871,6 +873,7 @@ services:
871873
# NOTE: stickyness must remain only for specific endpoints, see https://github.com/ITISFoundation/osparc-simcore/pull/4180
872874
- traefik.http.middlewares.${SWARM_STACK_NAME}_webserver_retry.retry.attempts=2
873875
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.service=${SWARM_STACK_NAME}_webserver
876+
# NOTE: keep in sync with fallback router (rule and entrypoint)
874877
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.rule=(Path(`/`) || Path(`/v0`) || Path(`/socket.io/`) || Path(`/static-frontend-data.json`) || PathRegexp(`^/study/(?P<study_uuid>\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)`) || Path(`/view`) || Path(`/#/view`) || Path(`/#/error`) || PathPrefix(`/v0/`))
875878
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.entrypoints=http
876879
- traefik.http.routers.${SWARM_STACK_NAME}_webserver.priority=6
@@ -1456,6 +1459,62 @@ services:
14561459
- default
14571460
- interactive_services_subnet # for legacy dynamic services
14581461

1462+
# use to define fallback routes for simcore services
1463+
# if docker healthcheck fails, container's traefik configuration is removed
1464+
# leading to 404 https://github.com/traefik/traefik/issues/7842
1465+
#
1466+
# use fallback routes to return proper 503 (instead of 404)
1467+
# this service must be running at all times
1468+
traefik-configuration-placeholder:
1469+
image: busybox:1.35.0
1470+
command: sleep infinity
1471+
networks:
1472+
- default
1473+
deploy:
1474+
labels:
1475+
# route to internal traefik
1476+
- traefik.enable=true
1477+
- io.simcore.zone=${TRAEFIK_SIMCORE_ZONE}
1478+
1479+
### Fallback for api-server
1480+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.rule=(Path(`/`) || Path(`/v0`) || PathPrefix(`/v0/`) || Path(`/api/v0/openapi.json`))
1481+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.service=${SWARM_STACK_NAME}_api-server_fallback
1482+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.entrypoints=simcore_api
1483+
- traefik.http.routers.${SWARM_STACK_NAME}_api-server_fallback.priority=1
1484+
# always fail and return 503 via unhealthy loadbalancer healthcheck
1485+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.server.port=0 # port is required (otherwise traefik service is not created)
1486+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.healthcheck.path=/some/invalid/path/to/generate/a/503
1487+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.healthcheck.interval=10s
1488+
- traefik.http.services.${SWARM_STACK_NAME}_api-server_fallback.loadbalancer.healthcheck.timeout=1ms
1489+
1490+
### Fallback for webserver
1491+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.service=${SWARM_STACK_NAME}_webserver_fallback
1492+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.rule=(Path(`/`) || Path(`/v0`) || Path(`/socket.io/`) || Path(`/static-frontend-data.json`) || PathRegexp(`^/study/(?P<study_uuid>\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)`) || Path(`/view`) || Path(`/#/view`) || Path(`/#/error`) || PathPrefix(`/v0/`))
1493+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.entrypoints=http
1494+
- traefik.http.routers.${SWARM_STACK_NAME}_webserver_fallback.priority=1
1495+
# always fail and return 503 via unhealthy loadbalancer healthcheck
1496+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.server.port=0
1497+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.healthcheck.path=/v0/
1498+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.healthcheck.interval=10s
1499+
- traefik.http.services.${SWARM_STACK_NAME}_webserver_fallback.loadbalancer.healthcheck.timeout=1ms
1500+
1501+
### Fallback for static-webserver
1502+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.rule=(Path(`/osparc`) || Path(`/s4l`) || Path(`/s4llite`) || Path(`/s4lacad`) || Path(`/s4lengine`) || Path(`/s4ldesktop`) || Path(`/s4ldesktopacad`) || Path(`/tis`) || Path(`/tiplite`) || Path(`/transpiled`) || Path(`/resource`) || PathPrefix(`/osparc/`) || PathPrefix(`/s4l/`) || PathPrefix(`/s4llite/`) || PathPrefix(`/s4lacad/`) || PathPrefix(`/s4lengine/`) || PathPrefix(`/s4ldesktop/`) || PathPrefix(`/s4ldesktopacad/`) || PathPrefix(`/tis/`) || PathPrefix(`/tiplite/`) || PathPrefix(`/transpiled/`) || PathPrefix(`/resource/`))
1503+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.service=${SWARM_STACK_NAME}_static_webserver_fallback
1504+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.entrypoints=http
1505+
- traefik.http.routers.${SWARM_STACK_NAME}_static_webserver_fallback.priority=1
1506+
# always fail and return 503 via unhealthy loadbalancer healthcheck
1507+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.server.port=0
1508+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.healthcheck.path=/some/invalid/path/to/generate/a/503
1509+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.healthcheck.interval=10s
1510+
- traefik.http.services.${SWARM_STACK_NAME}_static_webserver_fallback.loadbalancer.healthcheck.timeout=1ms
1511+
healthcheck:
1512+
test: command -v sleep
1513+
interval: 10s
1514+
timeout: 1s
1515+
start_period: 1s
1516+
retries: 3
1517+
14591518
volumes:
14601519
postgres_data:
14611520
name: ${SWARM_STACK_NAME}_postgres_data

services/static-webserver/client/source/class/osparc/conversation/AddMessage.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ qx.Class.define("osparc.conversation.AddMessage", {
3636
},
3737

3838
events: {
39-
"commentAdded": "qx.event.type.Data",
40-
"messageEdited": "qx.event.type.Data",
39+
"messageAdded": "qx.event.type.Data",
40+
"messageUpdated": "qx.event.type.Data",
4141
},
4242

4343
members: {
@@ -227,7 +227,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
227227
if (content) {
228228
osparc.study.Conversations.addMessage(this.__studyData["uuid"], this.__conversationId, content)
229229
.then(data => {
230-
this.fireDataEvent("commentAdded", data);
230+
this.fireDataEvent("messageAdded", data);
231231
commentField.getChildControl("text-area").setValue("");
232232
});
233233
}
@@ -239,7 +239,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
239239
if (content) {
240240
osparc.study.Conversations.editMessage(this.__studyData["uuid"], this.__conversationId, this.__message["messageId"], content)
241241
.then(data => {
242-
this.fireDataEvent("messageEdited", data);
242+
this.fireDataEvent("messageUpdated", data);
243243
commentField.getChildControl("text-area").setValue("");
244244
});
245245
}
@@ -249,7 +249,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
249249
if (userGid) {
250250
osparc.study.Conversations.notifyUser(this.__studyData["uuid"], this.__conversationId, userGid)
251251
.then(data => {
252-
this.fireDataEvent("commentAdded", data);
252+
this.fireDataEvent("messageAdded", data);
253253
const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators();
254254
if (userGid in potentialCollaborators) {
255255
if ("getUserId" in potentialCollaborators[userGid]) {

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

Lines changed: 108 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ qx.Class.define("osparc.conversation.Conversation", {
2727
this.base(arguments);
2828

2929
this.__studyData = studyData;
30+
this.__messages = [];
3031

3132
if (conversationId) {
3233
this.setConversationId(conversationId);
@@ -46,7 +47,7 @@ qx.Class.define("osparc.conversation.Conversation", {
4647

4748
this.__buildLayout();
4849

49-
this.fetchMessages();
50+
this.__reloadMessages();
5051
},
5152

5253
properties: {
@@ -64,6 +65,7 @@ qx.Class.define("osparc.conversation.Conversation", {
6465

6566
members: {
6667
__studyData: null,
68+
__messages: null,
6769
__nextRequestParams: null,
6870
__messagesTitle: null,
6971
__messagesList: null,
@@ -159,24 +161,44 @@ qx.Class.define("osparc.conversation.Conversation", {
159161
});
160162

161163
this.__loadMoreMessages = new osparc.ui.form.FetchButton(this.tr("Load more messages..."));
162-
this.__loadMoreMessages.addListener("execute", () => this.fetchMessages(false));
164+
this.__loadMoreMessages.addListener("execute", () => this.__reloadMessages(false));
163165
this._add(this.__loadMoreMessages);
164166

165167
if (osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])) {
166168
const addMessages = new osparc.conversation.AddMessage(this.__studyData, this.getConversationId());
167169
addMessages.setPaddingLeft(10);
168-
addMessages.addListener("commentAdded", e => {
170+
addMessages.addListener("messageAdded", e => {
169171
const data = e.getData();
170172
if (data["conversationId"]) {
171173
this.setConversationId(data["conversationId"]);
174+
this.addMessage(data);
172175
}
173-
this.fetchMessages();
174176
});
175177
this._add(addMessages);
176178
}
177179
},
178180

179-
fetchMessages: function(removeMessages = true) {
181+
__getNextRequest: function() {
182+
const params = {
183+
url: {
184+
studyId: this.__studyData["uuid"],
185+
conversationId: this.getConversationId(),
186+
offset: 0,
187+
limit: 42
188+
}
189+
};
190+
const nextRequestParams = this.__nextRequestParams;
191+
if (nextRequestParams) {
192+
params.url.offset = nextRequestParams.offset;
193+
params.url.limit = nextRequestParams.limit;
194+
}
195+
const options = {
196+
resolveWResponse: true
197+
};
198+
return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options);
199+
},
200+
201+
__reloadMessages: function(removeMessages = true) {
180202
if (this.getConversationId() === null) {
181203
this.__messagesTitle.setValue(this.tr("No messages yet"));
182204
this.__messagesList.hide();
@@ -189,15 +211,14 @@ qx.Class.define("osparc.conversation.Conversation", {
189211
this.__loadMoreMessages.setFetching(true);
190212

191213
if (removeMessages) {
214+
this.__messages = [];
192215
this.__messagesList.removeAll();
193216
}
194217

195218
this.__getNextRequest()
196219
.then(resp => {
197220
const messages = resp["data"];
198-
// it's not provided by the backend
199-
messages.forEach(message => message["studyId"] = this.__studyData["uuid"]);
200-
this.__addMessages(messages);
221+
messages.forEach(message => this.addMessage(message));
201222
this.__nextRequestParams = resp["_links"]["next"];
202223
if (this.__nextRequestParams === null) {
203224
this.__loadMoreMessages.exclude();
@@ -206,48 +227,91 @@ qx.Class.define("osparc.conversation.Conversation", {
206227
.finally(() => this.__loadMoreMessages.setFetching(false));
207228
},
208229

209-
__getNextRequest: function() {
210-
const params = {
211-
url: {
212-
studyId: this.__studyData["uuid"],
213-
conversationId: this.getConversationId(),
214-
offset: 0,
215-
limit: 42
216-
}
217-
};
218-
const nextRequestParams = this.__nextRequestParams;
219-
if (nextRequestParams) {
220-
params.url.offset = nextRequestParams.offset;
221-
params.url.limit = nextRequestParams.limit;
222-
}
223-
const options = {
224-
resolveWResponse: true
225-
};
226-
return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options);
227-
},
228-
229-
__addMessages: function(messages) {
230-
const nMessages = messages.filter(msg => msg["type"] === "MESSAGE").length;
230+
__updateMessagesNumber: function() {
231+
const nMessages = this.__messages.filter(msg => msg["type"] === "MESSAGE").length;
231232
if (nMessages === 1) {
232233
this.__messagesTitle.setValue(this.tr("1 Message"));
233234
} else if (nMessages > 1) {
234235
this.__messagesTitle.setValue(nMessages + this.tr(" Messages"));
235236
}
237+
},
236238

237-
messages.forEach(message => {
238-
let control = null;
239-
switch (message["type"]) {
240-
case "MESSAGE":
241-
control = new osparc.conversation.MessageUI(message, this.__studyData);
242-
control.addListener("messageEdited", () => this.fetchMessages());
243-
control.addListener("messageDeleted", () => this.fetchMessages());
244-
break;
245-
case "NOTIFICATION":
246-
control = new osparc.conversation.NotificationUI(message);
247-
break;
248-
}
249-
if (control) {
250-
this.__messagesList.add(control);
239+
addMessage: function(message) {
240+
// backend doesn't provide the projectId
241+
message["projectId"] = this.__studyData["uuid"];
242+
243+
// ignore it if it was already there
244+
const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]);
245+
if (messageIndex !== -1) {
246+
return;
247+
}
248+
249+
// determine insertion index for most‐recent‐first order
250+
const newTime = new Date(message["created"]);
251+
let insertAt = this.__messages.findIndex(m => new Date(m["created"]) < newTime);
252+
if (insertAt === -1) {
253+
insertAt = this.__messages.length;
254+
}
255+
256+
// Insert the message in the messages array
257+
this.__messages.splice(insertAt, 0, message);
258+
259+
// Add the UI element to the messages list
260+
let control = null;
261+
switch (message["type"]) {
262+
case "MESSAGE":
263+
control = new osparc.conversation.MessageUI(message, this.__studyData);
264+
control.addListener("messageUpdated", e => this.updateMessage(e.getData()));
265+
control.addListener("messageDeleted", e => this.deleteMessage(e.getData()));
266+
break;
267+
case "NOTIFICATION":
268+
control = new osparc.conversation.NotificationUI(message);
269+
break;
270+
}
271+
if (control) {
272+
// insert into the UI at the same position
273+
this.__messagesList.addAt(control, insertAt);
274+
}
275+
276+
this.__updateMessagesNumber();
277+
},
278+
279+
deleteMessage: function(message) {
280+
// remove it from the messages array
281+
const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]);
282+
if (messageIndex === -1) {
283+
return;
284+
}
285+
this.__messages.splice(messageIndex, 1);
286+
287+
// Remove the UI element from the messages list
288+
const children = this.__messagesList.getChildren();
289+
const controlIndex = children.findIndex(
290+
ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"])
291+
);
292+
if (controlIndex > -1) {
293+
this.__messagesList.remove(children[controlIndex]);
294+
}
295+
296+
this.__updateMessagesNumber();
297+
},
298+
299+
updateMessage: function(message) {
300+
// backend doesn't provide the projectId
301+
message["projectId"] = this.__studyData["uuid"];
302+
303+
// Replace the message in the messages array
304+
const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]);
305+
if (messageIndex === -1) {
306+
return;
307+
}
308+
this.__messages[messageIndex] = message;
309+
310+
// Update the UI element from the messages list
311+
this.__messagesList.getChildren().forEach(control => {
312+
if ("getMessage" in control && control.getMessage()["messageId"] === message["messageId"]) {
313+
control.setMessage(message);
314+
return;
251315
}
252316
});
253317
},

0 commit comments

Comments
 (0)